diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..03af862 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,459 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +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 = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mod_manager" +version = "0.1.1" +dependencies = [ + "env_logger", + "log", + "md5", + "rayon", + "serde", + "serde_json", + "sha1", + "sha2", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..18902e5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mod_manager" +version = "0.1.1" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +sha1 = "0.10" +md5 = "0.7" +rayon = "1.10" +log = "0.4" +env_logger = "0.11" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9b4aa8f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,364 @@ +use log::{error, info, warn}; +use md5; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use sha1::Sha1; +use sha2::{Digest, Sha256, Sha512}; +use std::collections::{HashMap, HashSet}; +use std::fs::{self, File}; +use std::io::{self, BufReader, BufWriter, Read}; +use std::path::PathBuf; + +const MODS_DIR: &str = "mods"; +const JSON_FILE: &str = "mods-list.json"; +const BUILD_CLIENT_DIR: &str = "build_client"; +const BUILD_SERVER_DIR: &str = "build_server"; +const CATEGORIES: [&str; 3] = ["client-only", "server-only", "universal"]; + +#[derive(Serialize, Deserialize, Clone)] +struct ModEntry { + name: String, + sha512: String, + sha256: String, + sha1: String, + md5: String, +} + +#[derive(Serialize, Deserialize)] +struct ModList { + #[serde(rename = "client-only")] + client_only: Vec, + #[serde(rename = "server-only")] + server_only: Vec, + #[serde(rename = "universal")] + universal: Vec, +} + +#[derive(Clone)] +struct Hashes { + sha512: String, + sha256: String, + sha1: String, + md5: String, +} + +fn main() { + env_logger::init(); + let mods_dir = PathBuf::from(MODS_DIR); + let build_client_dir = PathBuf::from(BUILD_CLIENT_DIR); + let build_server_dir = PathBuf::from(BUILD_SERVER_DIR); + + info!("Запуск менеджера модов"); + println!("Выберите действие:"); + println!("[1] Обновить моды"); + println!("[2] Создать сборки"); + println!("[3] Перевыбрать категории"); + + let mut input = String::new(); + io::stdin().read_line(&mut input).expect("Ошибка ввода"); + let choice = input.trim(); + + match choice { + "1" => update_mods(&mods_dir), + "2" => { + let data = load_json(); + build( + &data.client_only, + &data.universal, + &mods_dir, + &build_client_dir, + "Client", + ); + build( + &data.server_only, + &data.universal, + &mods_dir, + &build_server_dir, + "Server", + ); + } + "3" => reassign_categories(&mods_dir), + _ => error!("Некорректный выбор"), + } +} + +fn load_json() -> ModList { + match File::open(JSON_FILE) { + Ok(file) => serde_json::from_reader(BufReader::new(file)).unwrap_or_else(|e| { + warn!("Ошибка чтения JSON: {}. Создаём пустой.", e); + ModList { + client_only: vec![], + server_only: vec![], + universal: vec![], + } + }), + Err(_) => ModList { + client_only: vec![], + server_only: vec![], + universal: vec![], + }, + } +} + +fn save_json(data: &ModList) -> io::Result<()> { + let file = File::create(JSON_FILE)?; + serde_json::to_writer_pretty(BufWriter::new(file), data)?; + Ok(()) +} + +fn compute_hashes_for_file(path: &PathBuf) -> Hashes { + let file = File::open(path).expect("Не удалось открыть файл"); + let mut reader = BufReader::new(file); + let mut sha512 = Sha512::new(); + let mut sha256 = Sha256::new(); + let mut sha1 = Sha1::new(); + let mut md5_ctx = md5::Context::new(); + let mut buffer = [0u8; 8192]; + + loop { + match reader.read(&mut buffer) { + Ok(0) => break, + Ok(n) => { + sha512.update(&buffer[..n]); + sha256.update(&buffer[..n]); + sha1.update(&buffer[..n]); + md5_ctx.consume(&buffer[..n]); + } + Err(e) => { + eprintln!("Ошибка чтения файла {}: {}", path.display(), e); + break; + } + } + } + + Hashes { + sha512: format!("{:x}", sha512.finalize()), + sha256: format!("{:x}", sha256.finalize()), + sha1: format!("{:x}", sha1.finalize()), + md5: format!("{:x}", md5_ctx.compute()), + } +} + +fn compute_hashes(paths: &[PathBuf]) -> Vec<(PathBuf, Hashes)> { + paths + .par_iter() + .map(|p| (p.clone(), compute_hashes_for_file(p))) + .collect() +} + +fn ask_category(mod_name: &str) -> &'static str { + println!("\nМод: {}", mod_name); + println!("Выберите категорию:"); + for (i, cat) in CATEGORIES.iter().enumerate() { + println!("[{}] {}", i + 1, cat); + } + loop { + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_ok() { + if let Ok(n) = input.trim().parse::() { + if n > 0 && n <= CATEGORIES.len() { + return CATEGORIES[n - 1]; + } + } + } + println!("Некорректный ввод, попробуйте снова:"); + } +} + +fn get_jar_files(dir: &PathBuf) -> Vec { + match fs::read_dir(dir) { + Ok(entries) => entries + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.extension().map(|e| e == "jar").unwrap_or(false)) + .collect(), + Err(e) => { + eprintln!("Не удалось прочитать папку {}: {}", MODS_DIR, e); + vec![] + } + } +} + +fn process_new_mods( + paths: &[PathBuf], + existing_sha512: &HashSet, +) -> Vec<(ModEntry, &'static str)> { + let mut new_entries = vec![]; + let file_hashes = compute_hashes(paths); + for (path, hashes) in file_hashes { + if existing_sha512.contains(&hashes.sha512) { + continue; + } + let name = path + .file_name() + .expect("Нет имени файла") + .to_string_lossy() + .into_owned(); + info!("Новый мод: {}", name); + let cat = ask_category(&name); + let entry = ModEntry { + name, + sha512: hashes.sha512, + sha256: hashes.sha256, + sha1: hashes.sha1, + md5: hashes.md5, + }; + new_entries.push((entry, cat)); + } + new_entries +} + +fn build( + side_only: &[ModEntry], + universal: &[ModEntry], + mods_dir: &PathBuf, + build_dir: &PathBuf, + name: &str, +) { + if let Err(e) = fs::create_dir_all(build_dir) { + error!("Не удалось создать папку {}: {}", name, e); + return; + } + let mods: Vec<_> = side_only.iter().chain(universal).map(|e| &e.name).collect(); + let count = mods.len(); + for file_name in &mods { + let src = mods_dir.join(file_name); + let dst = build_dir.join(file_name); + if src.exists() { + if let Err(e) = fs::copy(&src, &dst) { + warn!("Не удалось скопировать {}: {}", file_name, e); + } + } + } + info!( + "{} сборка создана: {} модов в {}", + name, + count, + build_dir.display() + ); +} + +fn update_mods(mods_dir: &PathBuf) { + let mut data = load_json(); + let existing_sha512: HashSet = data + .client_only + .iter() + .chain(&data.server_only) + .chain(&data.universal) + .map(|e| e.sha512.clone()) + .collect(); + + if !mods_dir.exists() { + error!("Папка {} не найдена!", MODS_DIR); + return; + } + + let all_files = get_jar_files(mods_dir); + let current_names: HashSet<_> = all_files + .iter() + .filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) + .collect(); + + for cat in [ + &mut data.client_only, + &mut data.server_only, + &mut data.universal, + ] { + cat.retain(|e| { + if !current_names.contains(&e.name) { + info!("Удалён устаревший мод: {}", e.name); + false + } else { + true + } + }); + } + + let new_entries = process_new_mods(&all_files, &existing_sha512); + for (entry, cat) in new_entries { + match cat { + "client-only" => data.client_only.push(entry), + "server-only" => data.server_only.push(entry), + "universal" => data.universal.push(entry), + _ => {} + } + } + + if let Err(e) = save_json(&data) { + error!("Не удалось сохранить JSON: {}", e); + } else { + info!("Обновление завершено"); + } +} + +fn reassign_categories(mods_dir: &PathBuf) { + if !mods_dir.exists() { + error!("Папка {} не найдена!", MODS_DIR); + return; + } + + let all_files = get_jar_files(mods_dir); + + if PathBuf::from(JSON_FILE).exists() { + if let Err(e) = fs::remove_file(JSON_FILE) { + warn!("Не удалось удалить старый JSON: {}", e); + } else { + info!("Старый mods-list.json удалён"); + } + } + + let mut data = ModList { + client_only: vec![], + server_only: vec![], + universal: vec![], + }; + let empty_sha512: HashSet = HashSet::new(); + + // 1. Спрашиваем категории для всех модов + let mut temp_entries: Vec<(String, &'static str)> = vec![]; + for path in &all_files { + let name = path + .file_name() + .expect("Нет имени файла") + .to_string_lossy() + .into_owned(); + let cat = ask_category(&name); + temp_entries.push((name, cat)); + } + + // 2. Вычисляем хэши параллельно (в конце) + let file_hashes = compute_hashes(&all_files); + let mut hash_map: HashMap = HashMap::new(); + for (path, hashes) in file_hashes { + let name = path + .file_name() + .expect("Нет имени файла") + .to_string_lossy() + .into_owned(); + hash_map.insert(name, hashes); + } + + // 3. Собираем ModEntry + for (name, cat) in temp_entries { + if let Some(hashes) = hash_map.get(&name) { + let entry = ModEntry { + name: name.clone(), + sha512: hashes.sha512.clone(), + sha256: hashes.sha256.clone(), + sha1: hashes.sha1.clone(), + md5: hashes.md5.clone(), + }; + match cat { + "client-only" => data.client_only.push(entry), + "server-only" => data.server_only.push(entry), + "universal" => data.universal.push(entry), + _ => {} + } + } + } + + if let Err(e) = save_json(&data) { + error!("Не удалось сохранить JSON: {}", e); + } else { + info!("Все моды перераспределены"); + } +}