From 23cbb108adb4f9c61c0ffff24a60c134e5a0dede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 31 Oct 2026 20:00:11 +0500 Subject: [PATCH] Prepare Manager 0.9.8 with isolated validation and SQUID module controls --- Cargo.lock | 6 +- Cargo.toml | 2 +- .../20261031-dungeon-clear-dependency.md | 7 + .../20261031-launcher-diagnostics-export.md | 7 + .../20261031-server-logging-controls.md | 7 + .../20261031-validation-bot-population.md | 7 + .../20261031-validation-mysql-reuse.md | 7 + crates/coa-core/src/diag.rs | 27 +++ crates/coa-core/src/lib.rs | 1 + crates/coa-core/src/modules.rs | 50 ++++- crates/coa-core/src/server_logging.rs | 165 ++++++++++++++++ crates/coa-core/src/update.rs | 4 + crates/coa-core/src/update_isolation.rs | 177 ++++++++++++++++-- modules/catalog.json | 18 ++ package-lock.json | 4 +- package.json | 2 +- src-tauri/src/lib.rs | 15 ++ src-tauri/tauri.conf.json | 2 +- src/data/README.md | 9 +- src/data/latest-note-translations.json | 80 +++++++- src/data/server-history.json | 132 +++++++++++++ src/i18n/locales/de.ts | 7 + src/i18n/locales/en.ts | 8 + src/i18n/locales/es.ts | 7 + src/i18n/locales/fr.ts | 7 + src/i18n/locales/ru.ts | 7 + src/i18n/locales/zh.ts | 7 + src/lib/api.ts | 3 +- src/screens/ModulesPage.tsx | 3 +- src/screens/ServerLoggingSettings.tsx | 26 +++ src/screens/SettingsHome.tsx | 3 +- tools/release-history.test.mjs | 8 + tools/reviewed-manager-notes.json | 10 + tools/reviewed-server-notes.json | 18 +- 34 files changed, 791 insertions(+), 52 deletions(-) create mode 100644 changelog.d/20261031-dungeon-clear-dependency.md create mode 100644 changelog.d/20261031-launcher-diagnostics-export.md create mode 100644 changelog.d/20261031-server-logging-controls.md create mode 100644 changelog.d/20261031-validation-bot-population.md create mode 100644 changelog.d/20261031-validation-mysql-reuse.md create mode 100644 crates/coa-core/src/server_logging.rs create mode 100644 src/screens/ServerLoggingSettings.tsx create mode 100644 tools/reviewed-manager-notes.json diff --git a/Cargo.lock b/Cargo.lock index 9419429..fa6b2ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,7 +586,7 @@ dependencies = [ [[package]] name = "coa-core" -version = "0.9.7" +version = "0.9.8" dependencies = [ "base64 0.22.1", "chrono", @@ -634,7 +634,7 @@ dependencies = [ [[package]] name = "coa-release" -version = "0.9.7" +version = "0.9.8" dependencies = [ "chrono", "coa-core", @@ -643,7 +643,7 @@ dependencies = [ [[package]] name = "coa-server-manager" -version = "0.9.7" +version = "0.9.8" dependencies = [ "coa-control-proto", "coa-core", diff --git a/Cargo.toml b/Cargo.toml index 334f896..9fc04c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/coa-core", "crates/coa-release", "crates/coa-registry-proto", exclude = ["services/coa-registry", "services/coa-registry-e2e", "services/coa-coordinator", "services/coa-relay"] [workspace.package] -version = "0.9.7" +version = "0.9.8" edition = "2021" license = "AGPL-3.0-only" diff --git a/changelog.d/20261031-dungeon-clear-dependency.md b/changelog.d/20261031-dungeon-clear-dependency.md new file mode 100644 index 0000000..a69a6c0 --- /dev/null +++ b/changelog.d/20261031-dungeon-clear-dependency.md @@ -0,0 +1,7 @@ +--- +area: manager +type: added +audience: admins +title: Dungeon Clear can be enabled when SQUID bots are active +--- +Dungeon Clear starts disabled. Switching SQUID bots off also switches Dungeon Clear off; enabling SQUID again leaves Dungeon Clear off until you choose to enable it. diff --git a/changelog.d/20261031-launcher-diagnostics-export.md b/changelog.d/20261031-launcher-diagnostics-export.md new file mode 100644 index 0000000..1796158 --- /dev/null +++ b/changelog.d/20261031-launcher-diagnostics-export.md @@ -0,0 +1,7 @@ +--- +area: manager +type: added +audience: admins +title: Diagnostic exports include launcher stages and lock owner records +--- +Support packages include recent launcher diagnostics for each realm, with sensitive fields removed. Recorded lock owners help diagnose overlapping startup and shutdown actions. diff --git a/changelog.d/20261031-server-logging-controls.md b/changelog.d/20261031-server-logging-controls.md new file mode 100644 index 0000000..fb4a4f7 --- /dev/null +++ b/changelog.d/20261031-server-logging-controls.md @@ -0,0 +1,7 @@ +--- +area: manager +type: added +audience: admins +title: Choose normal logs, errors only or no server logs in maintenance settings +--- +The logging preset updates both the game world and login server, including their configuration templates. Previous settings are saved in configuration snapshots. Restart the server to apply the change. diff --git a/changelog.d/20261031-validation-bot-population.md b/changelog.d/20261031-validation-bot-population.md new file mode 100644 index 0000000..0530440 --- /dev/null +++ b/changelog.d/20261031-validation-bot-population.md @@ -0,0 +1,7 @@ +--- +area: manager +type: fixed +audience: admins +title: Update checks temporarily suspend bot population and restore your configuration +--- +Validation preserves the selected bot system and its population limits. Original configuration files are restored after validation, including recovery from interrupted updates. diff --git a/changelog.d/20261031-validation-mysql-reuse.md b/changelog.d/20261031-validation-mysql-reuse.md new file mode 100644 index 0000000..8efcd04 --- /dev/null +++ b/changelog.d/20261031-validation-mysql-reuse.md @@ -0,0 +1,7 @@ +--- +area: manager +type: fixed +audience: admins +title: Update validation keeps MySQL running between compatible checks +--- +Database checks and the temporary server startup share one MySQL session. The server still stops before files or database snapshots are replaced or restored. diff --git a/crates/coa-core/src/diag.rs b/crates/coa-core/src/diag.rs index 63c80ad..6204934 100644 --- a/crates/coa-core/src/diag.rs +++ b/crates/coa-core/src/diag.rs @@ -718,6 +718,24 @@ pub fn export_package( "maintenance.json", serde_json::to_string_pretty(&maintenance)?.as_bytes(), )?; + for (realm, prefix) in [("primary", ""), ("secondary", ".realms/secondary/"), ("reborn", ".realms/reborn/")] { + let state = root.join(format!("{prefix}.state")); + if let Ok(owner) = fs::read_to_string(state.join("control-owner.json")) { + add(&format!("launcher/{realm}/control-owner.json"), redact_json(&owner).as_bytes())?; + } + for (path, _, _) in newest_files(&state.join("diagnostics"), "jsonl", 20) + .into_iter() + .filter(|(path, _, _)| path.file_name().is_some_and(|name| name.to_string_lossy().starts_with("manage-"))) + .take(5) + { + let bytes = tail(&path, 512 * 1024); + let entries: Vec = String::from_utf8_lossy(&bytes).lines().map(|line| { + serde_json::from_str(&redact_json(line)).unwrap_or_else(|_| serde_json::Value::String(redact(line))) + }).collect(); + add(&format!("launcher/{realm}/{}.json", path.file_stem().unwrap_or_default().to_string_lossy()), + serde_json::to_string_pretty(&entries)?.as_bytes())?; + } + } // Logs. The newest update logs of the repack's own updater are added under their own names. let mut logs: Vec<(String, PathBuf)> = vec![ @@ -1257,6 +1275,12 @@ real error fs::create_dir_all(meta_dir.join("diagnostics")).unwrap(); fs::write(meta_dir.join("diagnostics/schema-validation-CoA-test.json"), r#"{"problems":[{"table":"creature_display_preset","column":"display_id","detail":"default mismatch"}],"password":"schema-secret"}"#).unwrap(); fs::write(meta_dir.join("diagnostics/database-recovery-test.json"), r#"{"objects":[{"database":"acore_characters","kind":"TRIGGER","name":"user_character_trigger"}],"password":"recovery-secret"}"#).unwrap(); + for prefix in ["", ".realms/secondary/", ".realms/reborn/"] { + let state = root.join(format!("{prefix}.state")); + fs::create_dir_all(state.join("diagnostics")).unwrap(); + fs::write(state.join("control-owner.json"), r#"{"pid":42,"action":"start-all","password":"launcher-owner-secret"}"#).unwrap(); + fs::write(state.join("diagnostics/manage-test.jsonl"), "{\"event\":\"launcher-stage-test\",\"password\":\"launcher-stage-secret\"}\n{\"event\":\"wait_ready.error\",\"port\":3307}\n").unwrap(); + } let meta = InstallMeta::new(InstallKind::Imported, &root); let report = run(&root, &meta); let zip_path = d.path().join("diag.zip"); @@ -1289,6 +1313,9 @@ real error assert!(all.contains("checking updates") && all.contains("1234")); assert!(all.contains("lockFilePresent") && all.contains("does not establish")); assert!(!all.contains("owner-secret")); + assert!(all.contains("launcher-stage-test")); + assert!(all.contains("wait_ready.error")); + assert!(!all.contains("launcher-stage-secret")); assert_eq!(fs::read(meta_dir.join("update.lock")).unwrap(), b""); assert!( !all.contains("hunter2") diff --git a/crates/coa-core/src/lib.rs b/crates/coa-core/src/lib.rs index 7857525..8918c16 100644 --- a/crates/coa-core/src/lib.rs +++ b/crates/coa-core/src/lib.rs @@ -37,6 +37,7 @@ pub mod health; pub mod install; pub mod layout; pub mod logging; +pub mod server_logging; pub mod manifest; pub mod migrations; pub mod modules; diff --git a/crates/coa-core/src/modules.rs b/crates/coa-core/src/modules.rs index 727b44c..058a396 100644 --- a/crates/coa-core/src/modules.rs +++ b/crates/coa-core/src/modules.rs @@ -197,7 +197,13 @@ fn list_in(cat: &[Entry], root: &Path) -> Vec { .and_then(|c| c.get(&e.enable_key).map(truthy)) .unwrap_or(e.default_on) }; - let blocked = wildcard && matches!(e.id.as_str(), "companions" | "playerbots"); + let dependency_missing = e.id == "dungeon-clear" + && entry_in(cat, "playerbots") + .and_then(|entry| bot_enabled(root, &entry)) + .map(|enabled| !enabled) + .unwrap_or(true); + let blocked = dependency_missing + || (wildcard && matches!(e.id.as_str(), "companions" | "playerbots" | "dungeon-clear")); let version = if e.id == "playerbots" { crate::squid::release(root).map(|release| { [ @@ -228,7 +234,9 @@ fn list_in(cat: &[Entry], root: &Path) -> Vec { }; ModuleView { version, - compatibility: if blocked { + compatibility: if dependency_missing { + "requires-squid" + } else if blocked { "unsupported" } else if wildcard && e.id != "client-compat" { "experimental" @@ -296,7 +304,7 @@ fn require_bot_server_stopped(root: &Path) -> Result<()> { pub fn set_enabled(root: &Path, meta: &Path, id: &str, on: bool) -> Result<()> { let _update_lock = crate::update::operation_lock(meta)?; crate::update::ensure_recovered(meta)?; - let _lock = if matches!(id, "companions" | "playerbots") { + let _lock = if matches!(id, "companions" | "playerbots" | "dungeon-clear") { require_bot_server_stopped(root)?; fs::create_dir_all(root.join(".state"))?; let lock = fs::OpenOptions::new() @@ -317,6 +325,9 @@ pub fn set_enabled(root: &Path, meta: &Path, id: &str, on: bool) -> Result<()> { }; if on { crate::realms::guard_module(root, id)?; + if id == "dungeon-clear" { + crate::realms::guard_module(root, "playerbots")?; + } } if id == "custom-races" { if let Ok(meta_content) = fs::read_to_string(meta.join("install.json")) { @@ -379,6 +390,9 @@ pub fn ensure_bot_exclusivity(root: &Path) -> Result<()> { fn set_enabled_in(cat: &[Entry], root: &Path, meta: &Path, id: &str, on: bool) -> Result<()> { let e = entry_in(cat, id)?; + if on && id == "dungeon-clear" && !bot_enabled(root, &entry_in(cat, "playerbots")?)? { + return Err(Error::Invalid("Enable SQUID bots before enabling Dungeon Clear.".into())); + } if !e.switchable || e.enable_key.is_empty() { return Err(Error::Invalid("This part cannot be switched off.".into())); } @@ -394,6 +408,15 @@ fn set_enabled_in(cat: &[Entry], root: &Path, meta: &Path, id: &str, on: bool) - } } } + if (id == "playerbots" && !on) || (id == "companions" && on) { + if let Some(dependency) = cat.iter().find(|entry| entry.id == "dungeon-clear") { + if dir(root).join(&dependency.conf).exists() + || dir(root).join(format!("{}.dist", dependency.conf)).exists() + { + set_enabled_in(cat, root, meta, "dungeon-clear", false)?; + } + } + } let conf = active_file(root, &e)?; let mut file = ConfFile::parse_bytes(&fs::read(&conf)?)?; if file.get(&e.enable_key).map(truthy).unwrap_or(e.default_on) == on @@ -818,6 +841,27 @@ mod tests { ); } + #[test] + fn dungeon_clear_requires_squid_and_stays_off_after_squid_is_reenabled() { + let d = tempfile::tempdir().unwrap(); + let root = d.path(); + fs::create_dir_all(dir(root)).unwrap(); + fs::write(dir(root).join("playerbots.conf"), "AiPlayerbot.Enabled = 0\n").unwrap(); + fs::write(dir(root).join("mod_coa_playerbots.conf"), "CoaBots.Enable = 0\n").unwrap(); + let dungeon = dir(root).join("mod_dungeon_clear.conf"); + fs::write(&dungeon, "DungeonClear.Enable = 0\n").unwrap(); + let cat = catalog(); + assert!(set_enabled_in(&cat, root, root, "dungeon-clear", true).is_err()); + assert!(!list_in(&cat, root).iter().find(|entry| entry.id == "dungeon-clear").unwrap().switchable); + set_enabled_in(&cat, root, root, "playerbots", true).unwrap(); + set_enabled_in(&cat, root, root, "dungeon-clear", true).unwrap(); + assert!(read(&dungeon).unwrap().get("DungeonClear.Enable").map(truthy).unwrap()); + set_enabled_in(&cat, root, root, "playerbots", false).unwrap(); + assert!(!read(&dungeon).unwrap().get("DungeonClear.Enable").map(truthy).unwrap()); + set_enabled_in(&cat, root, root, "playerbots", true).unwrap(); + assert!(!read(&dungeon).unwrap().get("DungeonClear.Enable").map(truthy).unwrap()); + } + #[test] fn bot_systems_are_mutually_exclusive_in_both_directions_and_manual_configs() { let d = tempfile::tempdir().unwrap(); diff --git a/crates/coa-core/src/server_logging.rs b/crates/coa-core/src/server_logging.rs new file mode 100644 index 0000000..b7e8ddc --- /dev/null +++ b/crates/coa-core/src/server_logging.rs @@ -0,0 +1,165 @@ +use std::{fs, path::Path}; + +use crate::{ + config::{parser::ConfFile, Scope}, + error::{Error, Result}, + fsx, +}; + +fn transform(bytes: &[u8], preset: &str) -> Result { + let level = match preset { + "normal" => 4, + "errors" => 2, + "off" => 0, + _ => return Err(Error::Invalid("Unknown server logging preset.".into())), + }; + let mut conf = ConfFile::parse_bytes(bytes)?; + let entries: Vec<_> = conf + .entries() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect(); + for (key, value) in entries { + if key.starts_with("Logger.") { + let Some((_, appenders)) = value.split_once(',') else { + continue; + }; + let value = if preset == "normal" + && matches!( + key.as_str(), + "Logger.network" | "Logger.sql.sql" | "Logger.entities.player" + ) { + 2 + } else { + level + }; + conf.set(&key, &format!("{value},{appenders}"), &[]); + } else if key.starts_with("Appender.") { + let mut fields: Vec<_> = value.split(',').map(str::to_string).collect(); + if fields.len() >= 3 { + fields[1] = if key == "Appender.Errors" && preset == "normal" { + "2".into() + } else { + level.to_string() + }; + conf.set(&key, &fields.join(","), &[]); + } + } + } + Ok(conf.to_text()) +} + +pub fn apply(root: &Path, meta: &Path, preset: &str) -> Result<()> { + let _operation = crate::update::operation_lock(meta)?; + crate::update::ensure_recovered(meta)?; + fs::create_dir_all(root.join(".state"))?; + let lock = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(root.join(".state/control.lock"))?; + if !fs4::fs_std::FileExt::try_lock_exclusive(&lock)? { + return Err(Error::Invalid( + "Another start/stop action is in progress.".into(), + )); + } + let mut originals = Vec::new(); + let mut texts = Vec::new(); + for prefix in ["", ".realms/secondary/", ".realms/reborn/"] { + for name in [ + "Settings/worldserver.conf.template", + "Settings/authserver.conf.template", + "Core/configs/worldserver.conf", + "Core/configs/authserver.conf", + ] { + let path = fsx::ensure_within(root, &root.join(format!("{prefix}{name}")))?; + if path.is_file() { + let bytes = fs::read(&path)?; + texts.push(transform(&bytes, preset)?); + originals.push((path, bytes)); + } + } + let marker = root.join(format!("{prefix}.state/debug-logs.json")); + if marker.is_file() { + originals.push((marker.clone(), fs::read(marker)?)); + texts.push("{\"enabled\":false}\n".into()); + } + } + if originals.is_empty() { + return Err(Error::Invalid( + "Server configuration files were not found.".into(), + )); + } + crate::config::take_snapshot( + meta, + Scope::Server, + "before changing server logging", + &originals, + )?; + for (index, (path, _)) in originals.iter().enumerate() { + if let Err(error) = fsx::atomic_write(path, texts[index].as_bytes()) { + for (path, bytes) in originals.iter().take(index) { + let _ = fsx::atomic_write(path, bytes); + } + return Err(error); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn changes_auth_world_and_templates_in_all_realms_with_restorable_snapshots() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("server"); + let meta = temp.path().join("meta"); + fs::create_dir_all(&root).unwrap(); + let mut files = Vec::new(); + for prefix in ["", ".realms/secondary/", ".realms/reborn/"] { + for name in [ + "Settings/worldserver.conf.template", + "Settings/authserver.conf.template", + "Core/configs/worldserver.conf", + "Core/configs/authserver.conf", + ] { + let path = root.join(format!("{prefix}{name}")); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let bytes = + b"Logger.root=6,Console\r\nAppender.Console=1,6,0\r\nKeep = unchanged\r\n" + .to_vec(); + fs::write(&path, &bytes).unwrap(); + files.push((path, bytes)); + } + } + apply(&root, &meta, "errors").unwrap(); + for (path, _) in &files { + assert!(fs::read_to_string(path) + .unwrap() + .contains("Logger.root=2,Console\r\n")); + } + let snapshots = crate::config::list_snapshots(&meta); + assert_eq!(snapshots.len(), 1); + crate::config::restore_snapshot(&meta, &snapshots[0].id).unwrap(); + for (path, bytes) in files { + assert_eq!(fs::read(path).unwrap(), bytes); + } + } + + #[test] + fn presets_control_dispatch_and_appenders_and_can_restore_useful_logs() { + let original = b"Logger.root=6,Console Server\r\nLogger.sql.sql=6,Console Server\r\nAppender.Console=1,6,0,\"1 9 3 6 5 8\"\r\nAppender.Errors=2,2,0,Errors.log,w\r\nOther = keep\r\n"; + let off = transform(original, "off").unwrap(); + assert!(off.contains("Logger.root=0,Console Server\r\n")); + assert!(off.contains("Appender.Errors=2,0,0,Errors.log,w\r\n")); + let normal = transform(off.as_bytes(), "normal").unwrap(); + assert!(normal.contains("Logger.root=4,Console Server\r\n")); + assert!(normal.contains("Logger.sql.sql=2,Console Server\r\n")); + assert!(normal.contains("Appender.Errors=2,2,0,Errors.log,w\r\n")); + assert!(normal.contains("Other = keep\r\n")); + assert_eq!(transform(normal.as_bytes(), "normal").unwrap(), normal); + assert!(transform(original, "unsupported").is_err()); + } +} diff --git a/crates/coa-core/src/update.rs b/crates/coa-core/src/update.rs index 76daf3e..a23b8cc 100644 --- a/crates/coa-core/src/update.rs +++ b/crates/coa-core/src/update.rs @@ -1888,6 +1888,10 @@ impl RepackEnv<'_> { }; let result = (|| -> Result<()> { crate::update_isolation::begin(self.root, self.meta_dir)?; + let database = crate::driver::run(self.root, crate::driver::Verb::StartMysql)?; + if !database.ok { + return Err(Error::Invalid(crate::driver::startup_failure(self.root, &database))); + } use crate::process::{observe, ServiceState}; let identities = crate::backup::with_database(self.root, |db| { if check_schema { diff --git a/crates/coa-core/src/update_isolation.rs b/crates/coa-core/src/update_isolation.rs index 0ac1d23..831f2af 100644 --- a/crates/coa-core/src/update_isolation.rs +++ b/crates/coa-core/src/update_isolation.rs @@ -41,6 +41,24 @@ struct Saved { sha256: Option, } +fn bot_files() -> Vec { + ["", ".realms/secondary/", ".realms/reborn/"] + .into_iter() + .flat_map(|prefix| { + [ + "mod_coa_playerbots.conf", + "mod_coa_playerbots.conf.dist", + "playerbots.conf", + "playerbots.conf.dist", + "mod_playerbots.conf", + "mod_playerbots.conf.dist", + ] + .into_iter() + .map(move |name| format!("{prefix}Core/configs/modules/{name}")) + }) + .collect() +} + pub(crate) fn pending(meta: &Path) -> bool { meta.join("validation-configs.json").exists() } @@ -52,15 +70,20 @@ pub(crate) fn begin(root: &Path, meta: &Path) -> Result<()> { )); } let mut saved = BTreeMap::new(); - for rel in FILES { - let path = fsx::ensure_within(root, &fsx::safe_join(root, rel)?)?; + let modules = bot_files(); + for rel in FILES + .iter() + .map(|rel| rel.to_string()) + .chain(modules.iter().cloned()) + { + let path = fsx::ensure_within(root, &fsx::safe_join(root, &rel)?)?; let bytes = if path.exists() { Some(fs::read(path)?) } else { None }; let sha256 = bytes.as_ref().map(|b| fsx::sha256_bytes(b)); - saved.insert(*rel, Saved { bytes, sha256 }); + saved.insert(rel, Saved { bytes, sha256 }); } let mut settings: serde_json::Value = fsx::read_json(&root.join("Settings/repack.json"))?; let mut realm: Option = if root.join("Settings/realm-profile.json").exists() @@ -101,14 +124,14 @@ pub(crate) fn begin(root: &Path, meta: &Path) -> Result<()> { } } let mut configs = Vec::new(); - for rel in [ - "Settings/worldserver.conf.template", - "Settings/authserver.conf.template", - "Core/configs/worldserver.conf", - "Core/configs/authserver.conf", - ] { + for rel in FILES.iter().copied().filter(|rel| { + rel.ends_with("worldserver.conf.template") + || rel.ends_with("authserver.conf.template") + || rel.ends_with("worldserver.conf") + || rel.ends_with("authserver.conf") + }) { let path = root.join(rel); - if !path.exists() && rel.ends_with(".template") { + if !path.exists() && rel.ends_with(".template") && !rel.starts_with(".realms/") { return Err(Error::Invalid(format!( "Cannot isolate validation without {rel}." ))); @@ -118,10 +141,34 @@ pub(crate) fn begin(root: &Path, meta: &Path) -> Result<()> { conf.set("BindIP", "\"127.0.0.1\"", &[]); if rel.contains("worldserver") { conf.set("Ra.IP", "\"127.0.0.1\"", &[]); + conf.set("CoA.ValidationMode", "1", &[]); } configs.push((path, conf.to_text())); } } + for rel in modules { + let path = fsx::ensure_within(root, &fsx::safe_join(root, &rel)?)?; + if !path.exists() { + continue; + } + let mut conf = ConfFile::parse_bytes(&fs::read(&path)?)?; + let keys: &[&str] = if rel.contains("mod_coa_playerbots.") { + &[ + "CoaBots.AutoLoginOnStartup", + "CoaBots.RandomSpawn.AutoLogin", + ] + } else { + &[ + "AiPlayerbot.RandomBotAutologin", + "AiPlayerbot.BotAutologin", + "AiPlayerbot.DeleteRandomBotAccounts", + ] + }; + for key in keys { + conf.set(key, "0", &[]); + } + configs.push((path, conf.to_text())); + } fsx::atomic_write_json(&meta.join("validation-configs.json"), &saved)?; fsx::atomic_write_json(&root.join("Settings/repack.json"), &settings)?; if let Some(state) = realm { @@ -139,16 +186,20 @@ pub(crate) fn restore(root: &Path, meta: &Path) -> Result<()> { } let journal = meta.join("validation-configs.json"); let saved: BTreeMap = fsx::read_json(&journal)?; - // Previous managers could journal only the first two profiles. Recover those - // journals too, while continuing to reject missing or unknown legacy entries. - let legacy = saved.len() == FILES.len() - 1 + let modules = bot_files(); + let complete_core = FILES.iter().all(|rel| saved.contains_key(*rel)); + let legacy_core = FILES + .iter() + .filter(|rel| **rel != "Settings/realm-profiles/reborn.json") + .all(|rel| saved.contains_key(*rel)); + let current = complete_core + && saved.len() == FILES.len() + modules.len() + && modules.iter().all(|rel| saved.contains_key(rel)); + let previous = complete_core && saved.len() == FILES.len(); + let legacy = legacy_core + && saved.len() == FILES.len() - 1 && !saved.contains_key("Settings/realm-profiles/reborn.json"); - let required = if legacy { - &FILES[..FILES.len() - 1] - } else { - FILES - }; - if saved.len() != required.len() || required.iter().any(|rel| !saved.contains_key(*rel)) { + if !(current || previous || legacy) { return Err(Error::Invalid( "Isolated validation configuration journal is incomplete.".into(), )); @@ -263,4 +314,92 @@ mod tests { assert!(!pending(&meta)); assert_eq!(fs::read(root.join(FILES[0])).unwrap(), before); } + + #[test] + fn bot_population_is_suppressed_in_all_realms_and_restored_exactly() { + let (_dir, root, meta) = fixture(); + let companion = b"[worldserver]\r\n# owner settings\r\nCoaBots.Enable = 1\r\nCoaBots.AutoLoginOnStartup = 1\r\nCoaBots.RandomSpawn.AutoLogin = 1\r\nCoaBots.AutoLogin.MaxCount = 2000\r\n"; + let squid = b"[worldserver]\nAiPlayerbot.Enabled = 1\nAiPlayerbot.RandomBotAutologin = 1\nAiPlayerbot.BotAutologin = 1\nAiPlayerbot.DeleteRandomBotAccounts = 1\n"; + for rel in bot_files() { + fsx::atomic_write( + &root.join(&rel), + if rel.contains("mod_coa_playerbots.") { + companion + } else { + squid + }, + ) + .unwrap(); + } + begin(&root, &meta).unwrap(); + for rel in bot_files() { + let conf = ConfFile::parse_bytes(&fs::read(root.join(&rel)).unwrap()).unwrap(); + if rel.contains("mod_coa_playerbots.") { + assert_eq!(conf.get("CoaBots.Enable"), Some("1")); + assert_eq!(conf.get("CoaBots.AutoLoginOnStartup"), Some("0")); + assert_eq!(conf.get("CoaBots.RandomSpawn.AutoLogin"), Some("0")); + assert_eq!(conf.get("CoaBots.AutoLogin.MaxCount"), Some("2000")); + } else { + assert_eq!(conf.get("AiPlayerbot.Enabled"), Some("1")); + assert_eq!(conf.get("AiPlayerbot.RandomBotAutologin"), Some("0")); + assert_eq!(conf.get("AiPlayerbot.BotAutologin"), Some("0")); + assert_eq!(conf.get("AiPlayerbot.DeleteRandomBotAccounts"), Some("0")); + } + } + restore(&root, &meta).unwrap(); + for rel in bot_files() { + assert_eq!( + fs::read(root.join(&rel)).unwrap(), + if rel.contains("mod_coa_playerbots.") { + companion.as_slice() + } else { + squid.as_slice() + } + ); + } + } + + #[test] + fn validation_removes_only_module_configs_created_during_the_check() { + let (_dir, root, meta) = fixture(); + let dist = root.join("Core/configs/modules/mod_coa_playerbots.conf.dist"); + let active = root.join("Core/configs/modules/mod_coa_playerbots.conf"); + let original = b"CoaBots.AutoLoginOnStartup = 1\n"; + fsx::atomic_write(&dist, original).unwrap(); + begin(&root, &meta).unwrap(); + fs::copy(&dist, &active).unwrap(); + restore(&root, &meta).unwrap(); + assert!(!active.exists()); + assert_eq!(fs::read(dist).unwrap(), original); + } + + #[test] + fn previous_manager_journals_remain_recoverable() { + for without_reborn in [false, true] { + let (_dir, root, meta) = fixture(); + begin(&root, &meta).unwrap(); + let journal = meta.join("validation-configs.json"); + let mut saved: BTreeMap = fsx::read_json(&journal).unwrap(); + saved.retain(|key, _| FILES.contains(&key.as_str())); + if without_reborn { + saved.remove("Settings/realm-profiles/reborn.json"); + } + fsx::atomic_write_json(&journal, &saved).unwrap(); + restore(&root, &meta).unwrap(); + assert!(!pending(&meta)); + } + } + + #[test] + fn incomplete_bot_journal_blocks_restoration_before_writing() { + let (_dir, root, meta) = fixture(); + begin(&root, &meta).unwrap(); + let journal = meta.join("validation-configs.json"); + let mut saved: BTreeMap = fsx::read_json(&journal).unwrap(); + saved.remove(&bot_files()[0]); + fsx::atomic_write_json(&journal, &saved).unwrap(); + let before = fs::read(root.join(FILES[2])).unwrap(); + assert!(restore(&root, &meta).is_err()); + assert_eq!(fs::read(root.join(FILES[2])).unwrap(), before); + } } diff --git a/modules/catalog.json b/modules/catalog.json index ec1d762..b5c780c 100644 --- a/modules/catalog.json +++ b/modules/catalog.json @@ -1,6 +1,24 @@ { "schema": 1, "modules": [ + { + "id": "dungeon-clear", + "name": "Dungeon Clear", + "description": { + "en": "Dungeon tactics for SQUID bots. Requires SQUID bots to be enabled.", + "ru": "Тактика подземелий для SQUID bots. Работает только при включённых SQUID bots.", + "de": "Dungeon-Taktiken für SQUID-Bots. Erfordert aktivierte SQUID-Bots.", + "fr": "Tactiques de donjon pour les bots SQUID. Nécessite les bots SQUID activés.", + "es": "Tácticas de mazmorra para bots SQUID. Requiere activar los bots SQUID.", + "zh": "为 SQUID 机器人提供地下城战术。需要启用 SQUID 机器人。" + }, + "repo": "https://github.com/Zyth45/mod-dungeon-clear", + "conf": "mod_dungeon_clear.conf", + "enable_key": "DungeonClear.Enable", + "default_on": false, + "status": "early", + "icon": "bot" + }, { "id": "companions", "name": "CoA Companions", diff --git a/package-lock.json b/package-lock.json index e6cba7e..e7eec5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coa-server-manager", - "version": "0.9.7", + "version": "0.9.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coa-server-manager", - "version": "0.9.7", + "version": "0.9.8", "dependencies": { "@tauri-apps/api": "^2.12.0", "@tauri-apps/plugin-dialog": "^2.8.0", diff --git a/package.json b/package.json index 36bdcec..6582c26 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coa-server-manager", "private": true, - "version": "0.9.7", + "version": "0.9.8", "license": "AGPL-3.0-only", "type": "module", "scripts": { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dbf81d0..38c262f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -723,6 +723,20 @@ async fn get_settings( blocking(move || config::load(&root, scope)).await } +#[tauri::command] +async fn server_logging_preset( + state: State<'_, AppState>, + id: String, + preset: String, +) -> std::result::Result<(), UiError> { + let root = path_of(&state, &id)?; + let _guard = BusyGuard::acquire(&state, &id)?; + blocking(move || { + let meta = meta_dir(&root)?; + coa_core::server_logging::apply(&root, &meta, &preset) + }).await +} + #[tauri::command] async fn save_settings( state: State<'_, AppState>, @@ -2921,6 +2935,7 @@ pub fn run() { .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) .invoke_handler(tauri::generate_handler![ + server_logging_preset, release_history, default_install_dir, game_data_folder, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cc97436..a0673c5 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.9.7", + "version": "0.9.8", "identifier": "com.coa.servermanager", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/data/README.md b/src/data/README.md index 5e1eb34..800fc4e 100644 --- a/src/data/README.md +++ b/src/data/README.md @@ -2,8 +2,8 @@ The Manager feed is imported from the canonical `changelog/history.json`. The server snapshot in this directory is copied from `Corfirean/coa-server-build` -commit `15687243bd3f4d60dfedf70b938b6076c1bd6393`, path -`changelog/history.json`, on 2026-10-30. Only schema, component, repository and +commit `e91ae8af4e506c9b8461c7eed6731945a34c3ad8`, path +`changelog/history.json`, on 2026-10-31. Only schema, component, repository and releases are retained. Do not independently edit historical records here. At runtime the native read-only command fetches the fixed canonical URL, bounds @@ -31,3 +31,8 @@ The reviewed upcoming server record is tested from tools/reviewed-server-notes.j bound to the exact core, companions and SQUID source commits. This includes the SQUID 2.8.2 update note. The plan is not a published history record. Manager 0.9.7 carries these translations; the unpublished 0.9.6 installer is retained as an older candidate. + +Manager 0.9.8 preparation refreshes the bundled server history through 0.261030.90. +The two reviewed upcoming records are in `tools/reviewed-manager-notes.json` and +`tools/reviewed-server-notes.json`; every new note has all five translations. +These files describe preparation and do not add unpublished releases to canonical history. diff --git a/src/data/latest-note-translations.json b/src/data/latest-note-translations.json index c00fc82..ee8b37f 100644 --- a/src/data/latest-note-translations.json +++ b/src/data/latest-note-translations.json @@ -31,7 +31,12 @@ "Keep deliberate stops and update shutdowns from triggering crash restart": "Ручная остановка и остановка для обновления не вызывают перезапуск после сбоя", "Server startup waits for a responsive console before reporting success": "Запуск сервера ожидает ответа консоли перед сообщением об успехе", "Restore the existing Manager window when launching it again": "Повторный запуск открывает уже существующее окно менеджера", - "Show available update notes even when release history is cached": "Показывать изменения доступного обновления даже при сохранённой истории" + "Show available update notes even when release history is cached": "Показывать изменения доступного обновления даже при сохранённой истории", + "Update checks temporarily suspend bot population and restore your configuration": "Проверка обновления временно приостанавливает загрузку ботов и восстанавливает ваши настройки", + "Dungeon Clear can be enabled when SQUID bots are active": "Dungeon Clear можно включить при активных SQUID bots", + "Update validation keeps MySQL running between compatible checks": "При проверке обновления MySQL продолжает работать между совместимыми проверками", + "Choose normal logs, errors only or no server logs in maintenance settings": "Выбирайте обычные логи, только ошибки или отключение логов в настройках обслуживания", + "Diagnostic exports include launcher stages and lock owner records": "В архив диагностики входят этапы лаунчера и записи о владельцах блокировок" }, "de": { "Choose Parallel or Global level scaling in module settings": "Parallele oder globale Levelskalierung in den Moduleinstellungen wählen", @@ -64,7 +69,12 @@ "Keep deliberate stops and update shutdowns from triggering crash restart": "Bewusstes Stoppen und Herunterfahren für Updates lösen keinen Absturzneustart aus", "Server startup waits for a responsive console before reporting success": "Der Serverstart wartet vor der Erfolgsmeldung auf eine reagierende Konsole", "Restore the existing Manager window when launching it again": "Erneutes Starten stellt das vorhandene Manager-Fenster wieder her", - "Show available update notes even when release history is cached": "Änderungen verfügbarer Updates auch bei zwischengespeichertem Verlauf anzeigen" + "Show available update notes even when release history is cached": "Änderungen verfügbarer Updates auch bei zwischengespeichertem Verlauf anzeigen", + "Update checks temporarily suspend bot population and restore your configuration": "Update-Prüfungen setzen die Bot-Population vorübergehend aus und stellen Ihre Konfiguration wieder her", + "Dungeon Clear can be enabled when SQUID bots are active": "Dungeon Clear lässt sich bei aktiven SQUID-Bots einschalten", + "Update validation keeps MySQL running between compatible checks": "Bei der Update-Validierung bleibt MySQL zwischen kompatiblen Prüfungen aktiv", + "Choose normal logs, errors only or no server logs in maintenance settings": "In den Wartungseinstellungen normale Protokolle, nur Fehler oder keine Serverprotokolle wählen", + "Diagnostic exports include launcher stages and lock owner records": "Diagnoseexporte enthalten Launcher-Schritte und aufgezeichnete Sperrbesitzer" }, "fr": { "Choose Parallel or Global level scaling in module settings": "Choisissez une progression parallèle ou globale des niveaux dans les paramètres du module", @@ -97,7 +107,12 @@ "Keep deliberate stops and update shutdowns from triggering crash restart": "Les arrêts volontaires et ceux des mises à jour ne déclenchent plus de redémarrage après plantage", "Server startup waits for a responsive console before reporting success": "Le démarrage du serveur attend une console réactive avant d’annoncer sa réussite", "Restore the existing Manager window when launching it again": "Un nouveau lancement restaure la fenêtre existante du gestionnaire", - "Show available update notes even when release history is cached": "Afficher les changements des mises à jour disponibles même si l’historique est en cache" + "Show available update notes even when release history is cached": "Afficher les changements des mises à jour disponibles même si l’historique est en cache", + "Update checks temporarily suspend bot population and restore your configuration": "Les vérifications de mise à jour suspendent temporairement la population des bots et restaurent votre configuration", + "Dungeon Clear can be enabled when SQUID bots are active": "Dungeon Clear peut être activé lorsque les bots SQUID sont actifs", + "Update validation keeps MySQL running between compatible checks": "La validation des mises à jour garde MySQL actif entre les vérifications compatibles", + "Choose normal logs, errors only or no server logs in maintenance settings": "Choisissez les journaux normaux, les erreurs uniquement ou aucun journal dans les paramètres de maintenance", + "Diagnostic exports include launcher stages and lock owner records": "Les exports de diagnostic incluent les étapes de lancement et les propriétaires des verrous" }, "es": { "Choose Parallel or Global level scaling in module settings": "Elige escalado de nivel paralelo o global en los ajustes del módulo", @@ -130,7 +145,12 @@ "Keep deliberate stops and update shutdowns from triggering crash restart": "Las paradas voluntarias y las de actualización no activan el reinicio por fallo", "Server startup waits for a responsive console before reporting success": "El arranque del servidor espera a que responda la consola antes de indicar que ha terminado", "Restore the existing Manager window when launching it again": "Al volver a iniciar el gestor se restaura su ventana existente", - "Show available update notes even when release history is cached": "Mostrar los cambios de las actualizaciones disponibles aunque el historial esté en caché" + "Show available update notes even when release history is cached": "Mostrar los cambios de las actualizaciones disponibles aunque el historial esté en caché", + "Update checks temporarily suspend bot population and restore your configuration": "Las comprobaciones de actualización suspenden temporalmente la población de bots y restauran tu configuración", + "Dungeon Clear can be enabled when SQUID bots are active": "Dungeon Clear se puede activar cuando los bots SQUID están activos", + "Update validation keeps MySQL running between compatible checks": "La validación de actualizaciones mantiene MySQL activo entre comprobaciones compatibles", + "Choose normal logs, errors only or no server logs in maintenance settings": "Elige registros normales, solo errores o ningún registro en los ajustes de mantenimiento", + "Diagnostic exports include launcher stages and lock owner records": "Los archivos de diagnóstico incluyen las etapas del lanzador y los propietarios de bloqueos" }, "zh": { "Choose Parallel or Global level scaling in module settings": "在模块设置中选择并行或全局等级缩放", @@ -163,7 +183,12 @@ "Keep deliberate stops and update shutdowns from triggering crash restart": "主动停止及更新时关闭不会触发崩溃自动重启", "Server startup waits for a responsive console before reporting success": "服务器启动会等待控制台响应后再报告成功", "Restore the existing Manager window when launching it again": "再次启动时恢复已有的管理器窗口", - "Show available update notes even when release history is cached": "即使历史记录已缓存,也显示可用更新的说明" + "Show available update notes even when release history is cached": "即使历史记录已缓存,也显示可用更新的说明", + "Update checks temporarily suspend bot population and restore your configuration": "更新检查会暂时停止生成和登录机器人,并恢复您的配置", + "Dungeon Clear can be enabled when SQUID bots are active": "启用 SQUID 机器人后即可开启 Dungeon Clear", + "Update validation keeps MySQL running between compatible checks": "更新验证会在兼容的检查步骤之间保持 MySQL 运行", + "Choose normal logs, errors only or no server logs in maintenance settings": "在维护设置中选择普通日志、仅错误或关闭服务器日志", + "Diagnostic exports include launcher stages and lock owner records": "诊断导出包含启动器阶段和锁所属进程记录" } }, "server": { @@ -205,7 +230,14 @@ "Thin Human and Leper Troll characters retain their starting data after updates": "Персонажи Thin Human и Leper Troll сохраняют стартовые данные после обновлений", "Companions avoid rejected names and recover from forced rename login failures": "Компаньоны получают допустимые имена и восстанавливают вход при требовании переименования", "Updated the reviewed CoA upstream snapshot": "Обновлена проверенная версия исходников CoA", - "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots обновлён до версии 2.8.2" + "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots обновлён до версии 2.8.2", + "Server launchers accept configuration paths in folders with spaces": "Сервер запускается с конфигурацией из папок с пробелами в пути", + "Database restarts recover from ownerless Windows port entries": "Перезапуск базы корректно обрабатывает записи портов Windows без владельца", + "Concurrent server actions wait briefly and identify the action still running": "Одновременные действия с сервером ожидают завершения текущего и показывают, что ещё выполняется", + "Dungeon Clear is available as an optional extension for SQUID bots": "Dungeon Clear доступен как дополнительный модуль для SQUID bots", + "Server validation skips creating and logging in SQUID bots": "При проверке сервера аккаунты SQUID bots не создаются и боты не входят в мир", + "Startup and shutdown keep bounded diagnostics of launcher stages": "При запуске и остановке сохраняется ограниченная по объёму диагностика этапов лаунчера", + "Database shutdown succeeds when MySQL closes its connection before replying": "Остановка базы завершается успешно, если MySQL закрыл соединение до отправки ответа" }, "de": { "Restrict new characters by custom race without changing existing characters": "Neue Charaktere nach zusätzlichem Volk beschränken, ohne bestehende Charaktere zu ändern", @@ -245,7 +277,14 @@ "Thin Human and Leper Troll characters retain their starting data after updates": "Charaktere der Völker Thin Human und Leper Troll behalten ihre Startdaten nach Updates", "Companions avoid rejected names and recover from forced rename login failures": "Begleiter vermeiden unzulässige Namen und können sich nach erzwungener Umbenennung wieder anmelden", "Updated the reviewed CoA upstream snapshot": "Geprüften CoA-Upstream-Stand aktualisiert", - "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots auf Version 2.8.2 aktualisiert" + "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots auf Version 2.8.2 aktualisiert", + "Server launchers accept configuration paths in folders with spaces": "Serverstarter akzeptieren Konfigurationspfade in Ordnern mit Leerzeichen", + "Database restarts recover from ownerless Windows port entries": "Datenbank-Neustarts behandeln Windows-Porteinträge ohne Besitzer korrekt", + "Concurrent server actions wait briefly and identify the action still running": "Gleichzeitige Serveraktionen warten kurz und zeigen die noch laufende Aktion an", + "Dungeon Clear is available as an optional extension for SQUID bots": "Dungeon Clear ist als optionale Erweiterung für SQUID-Bots verfügbar", + "Server validation skips creating and logging in SQUID bots": "Die Servervalidierung überspringt das Erstellen und Anmelden von SQUID-Bots", + "Startup and shutdown keep bounded diagnostics of launcher stages": "Start und Herunterfahren speichern begrenzte Diagnosedaten der Launcher-Schritte", + "Database shutdown succeeds when MySQL closes its connection before replying": "Das Herunterfahren der Datenbank gelingt auch, wenn MySQL die Verbindung vor der Antwort schließt" }, "fr": { "Restrict new characters by custom race without changing existing characters": "Restreignez les races supplémentaires des nouveaux personnages sans modifier les personnages existants", @@ -285,7 +324,14 @@ "Thin Human and Leper Troll characters retain their starting data after updates": "Les personnages Thin Human et Leper Troll conservent leurs données de départ après les mises à jour", "Companions avoid rejected names and recover from forced rename login failures": "Les compagnons évitent les noms refusés et récupèrent leur connexion après un renommage imposé", "Updated the reviewed CoA upstream snapshot": "Version amont vérifiée de CoA mise à jour", - "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots mis à jour vers la version 2.8.2" + "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots mis à jour vers la version 2.8.2", + "Server launchers accept configuration paths in folders with spaces": "Le lancement du serveur accepte les chemins de configuration contenant des espaces", + "Database restarts recover from ownerless Windows port entries": "Les redémarrages de la base gèrent les entrées de port Windows sans propriétaire", + "Concurrent server actions wait briefly and identify the action still running": "Les actions simultanées du serveur attendent brièvement et indiquent celle encore en cours", + "Dungeon Clear is available as an optional extension for SQUID bots": "Dungeon Clear est disponible comme extension facultative pour les bots SQUID", + "Server validation skips creating and logging in SQUID bots": "La validation du serveur ignore la création et la connexion des bots SQUID", + "Startup and shutdown keep bounded diagnostics of launcher stages": "Le démarrage et l’arrêt conservent un diagnostic limité des étapes de lancement", + "Database shutdown succeeds when MySQL closes its connection before replying": "L’arrêt de la base réussit même si MySQL ferme la connexion avant de répondre" }, "es": { "Restrict new characters by custom race without changing existing characters": "Restringe las razas adicionales de personajes nuevos sin modificar los existentes", @@ -325,7 +371,14 @@ "Thin Human and Leper Troll characters retain their starting data after updates": "Los personajes Thin Human y Leper Troll conservan sus datos iniciales tras actualizar", "Companions avoid rejected names and recover from forced rename login failures": "Los compañeros evitan nombres rechazados y recuperan el acceso tras un cambio de nombre obligatorio", "Updated the reviewed CoA upstream snapshot": "Actualizada la versión revisada del código original de CoA", - "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots actualizado a la versión 2.8.2" + "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots actualizado a la versión 2.8.2", + "Server launchers accept configuration paths in folders with spaces": "El servidor acepta rutas de configuración en carpetas con espacios", + "Database restarts recover from ownerless Windows port entries": "Los reinicios de la base de datos gestionan las entradas de puertos de Windows sin propietario", + "Concurrent server actions wait briefly and identify the action still running": "Las acciones simultáneas del servidor esperan brevemente e identifican la que sigue en curso", + "Dungeon Clear is available as an optional extension for SQUID bots": "Dungeon Clear está disponible como extensión opcional para los bots SQUID", + "Server validation skips creating and logging in SQUID bots": "La validación del servidor omite la creación y conexión de bots SQUID", + "Startup and shutdown keep bounded diagnostics of launcher stages": "El inicio y la parada guardan diagnósticos limitados de las etapas del lanzador", + "Database shutdown succeeds when MySQL closes its connection before replying": "La parada de la base de datos se completa aunque MySQL cierre la conexión antes de responder" }, "zh": { "Restrict new characters by custom race without changing existing characters": "限制新角色可选的自定义种族,不影响现有角色", @@ -365,7 +418,14 @@ "Thin Human and Leper Troll characters retain their starting data after updates": "Thin Human 和 Leper Troll 角色在更新后保留初始数据", "Companions avoid rejected names and recover from forced rename login failures": "同伴避免使用被拒绝的名称,并修复强制改名导致的登录失败", "Updated the reviewed CoA upstream snapshot": "更新已审核的 CoA 上游版本", - "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots 已更新至 2.8.2" + "Updated SQUID Playerbots to 2.8.2": "SQUID Playerbots 已更新至 2.8.2", + "Server launchers accept configuration paths in folders with spaces": "服务器启动器支持包含空格的配置文件路径", + "Database restarts recover from ownerless Windows port entries": "数据库重启可正确处理 Windows 中无所属进程的端口记录", + "Concurrent server actions wait briefly and identify the action still running": "并发服务器操作会短暂等待,并显示仍在执行的操作", + "Dungeon Clear is available as an optional extension for SQUID bots": "Dungeon Clear 现可作为 SQUID 机器人的可选扩展", + "Server validation skips creating and logging in SQUID bots": "服务器验证会跳过 SQUID 机器人的创建和登录", + "Startup and shutdown keep bounded diagnostics of launcher stages": "启动和关闭时会保留大小受限的启动器阶段诊断日志", + "Database shutdown succeeds when MySQL closes its connection before replying": "即使 MySQL 在回复前关闭连接,数据库关闭操作也能正确完成" } } } diff --git a/src/data/server-history.json b/src/data/server-history.json index 3ca3c59..7f443f4 100644 --- a/src/data/server-history.json +++ b/src/data/server-history.json @@ -1297,6 +1297,138 @@ "portable-characters" ], "compare": "https://github.com/Corfirean/coa-server-build/compare/server-0.261028.83...server-0.261029.85" + }, + { + "tag": "server-0.261030.90", + "version": "0.261030.90", + "date": "2026-10-30", + "url": "https://github.com/Corfirean/coa-server-build/releases/tag/server-0.261030.90", + "notes": [ + "Prevent crashes while players update Apex Hunt quest markers on different maps", + "Thin Human and Leper Troll characters retain their starting data after updates", + "Companions avoid rejected names and recover from forced rename login failures", + "Updated SQUID Playerbots to 2.8.2", + "Updated the reviewed CoA upstream snapshot" + ], + "snapshot": "1bf0b6dae2f39110c400cb04f361654b2523d2263e0256728840ecb163c2f484", + "components": { + "upstream": { + "repository": "jealous-sound/azerothcore-wotlk-coa", + "ref": "18b15e4f8bb2a28e975d578884727cd338c323af", + "sha": "18b15e4f8bb2a28e975d578884727cd338c323af" + }, + "core": { + "repository": "Corfirean/azerothcore-wotlk-coa", + "ref": "8b6bbd8610cd672d2823f40909f1c7ee95a83371", + "sha": "8b6bbd8610cd672d2823f40909f1c7ee95a83371" + }, + "bots": { + "repository": "Corfirean/mod-coa-playerbots", + "ref": "4f6cf99e6bf509ae2b178e993d1170cc7fe0cb0c", + "sha": "4f6cf99e6bf509ae2b178e993d1170cc7fe0cb0c" + }, + "scaling": { + "repository": "Corfirean/mod-coa-content-scaling", + "ref": "0898a3e976677cde470a60ba4e6f9bb9d0fe5e2c", + "sha": "0898a3e976677cde470a60ba4e6f9bb9d0fe5e2c" + }, + "manager": { + "repository": "Corfirean/coa-server-manager", + "ref": "642da8fdc1a0165aea74fbd1ab1b46933fc7e589", + "sha": "642da8fdc1a0165aea74fbd1ab1b46933fc7e589" + }, + "races": { + "repository": "ilusixn/azerothcore-wotlk-coa", + "ref": "coa-custom-1.9.2", + "sha": "31bd63d309b8b0fb4f1a4d9b793a343b8ac36f1e", + "assets": [ + { + "name": "CoA-Custom-1.9.2-Client-1.zip", + "size": 1513021586, + "digest": "sha256:a9dd51ed2d90fb048d549602f40f5429d56e2df0df5606a38781b358fa369568", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/CoA-Custom-1.9.2-Client-1.zip" + }, + { + "name": "CoA-Custom-1.9.2-Client-2.zip", + "size": 1198283005, + "digest": "sha256:ab084a55509ccbcc2977a06dd0c01fa879973a1892baaacaeb3ff52f3dfc750e", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/CoA-Custom-1.9.2-Client-2.zip" + }, + { + "name": "CoA-Custom-1.9.2-Client-3.zip", + "size": 1678829573, + "digest": "sha256:907e267f0ee7038807e626c3c1eca5e12db6c88b747137a23c5fb52eaa1a358e", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/CoA-Custom-1.9.2-Client-3.zip" + }, + { + "name": "CoA-Custom-1.9.2-Client-4.zip", + "size": 502385128, + "digest": "sha256:e0bd87172af989fa50a8e2b08ba4321c72b6265047482f051a1db3b80d55bcaa", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/CoA-Custom-1.9.2-Client-4.zip" + }, + { + "name": "CoA-Custom-1.9.2-Client-5.zip", + "size": 168079711, + "digest": "sha256:73364ad1b58c6342641a848a3b20a835472834f600416606be1470386774ca78", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/CoA-Custom-1.9.2-Client-5.zip" + }, + { + "name": "CoA-Custom-1.9.2-Server.zip", + "size": 53004560, + "digest": "sha256:b893813573a4d3f6a0003c1f174582d3def997f37b402cbca44d53055d2fc1d2", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/CoA-Custom-1.9.2-Server.zip" + }, + { + "name": "INSTALL-GUIDE.txt", + "size": 3006, + "digest": "sha256:f17e97fafa3225901d1caf8c3b8141293f98fbfb8c9243c0c4717278c60146aa", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/INSTALL-GUIDE.txt" + }, + { + "name": "PATCH-NOTES-1.9.2.txt", + "size": 2822, + "digest": "sha256:f2fd4693ba181959a572d3fb10a0050e9ff6c4f735b3ab0019c7d16712db28cb", + "url": "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.9.2/PATCH-NOTES-1.9.2.txt" + } + ] + }, + "squid": { + "repository": "Zyth45/mod-playerbots", + "ref": "v2.8.2", + "sha": "fa4cfd31997367b3e574a2b89b858c4669ff53fb" + }, + "build": { + "repository": "Corfirean/coa-server-build", + "ref": "9be61ebea6d56bc3bd0298423e7331c52e90fda6", + "sha": "9be61ebea6d56bc3bd0298423e7331c52e90fda6" + }, + "navigation": { + "repository": "Corfirean/azerothcore-wotlk-coa", + "ref": "8b44099f480af0b69c6dfc697e4d388e865e9ffc", + "sha": "8b44099f480af0b69c6dfc697e4d388e865e9ffc", + "sourceRelease": "navigation-20261012", + "manifestSha256": "7f28600735f115792e79a5e8d1c8d78a83a8f5ed14a090c6d87268e49c2bb683", + "archiveSha256": "aa1347a095f85af44b4f592270a0e9314954801fda4578f0147f847e32c24a60", + "assets": [ + { + "name": "navigation.zip.001", + "size": 1500000000, + "sha256": "e3a93903fc8155ee0d92db399ec426769c360bef87c44c07beb7e21cfd892c18", + "url": "https://github.com/Corfirean/coa-server-build/releases/download/navigation-20261012/navigation.zip.001" + }, + { + "name": "navigation.zip.002", + "size": 1377006470, + "sha256": "c1af26ba7f83676278613076c357c4da0a11cfa6ae1ff00003479057c4864471", + "url": "https://github.com/Corfirean/coa-server-build/releases/download/navigation-20261012/navigation.zip.002" + } + ] + } + }, + "experimentalFeatures": [ + "portable-characters" + ], + "compare": "https://github.com/Corfirean/coa-server-build/compare/server-0.261029.85...server-0.261030.90" } ] } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index fee3534..44c2f27 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -1083,6 +1083,13 @@ const de: Partial> = { "stop.forceWarning": "Ungespeicherter Spielfortschritt kann verloren gehen. Nur geprüfte Prozesse dieses Servers werden beendet. Die Datenbank läuft weiter, bis alle Welten beendet sind.", "stop.cancel": "Abbrechen", + "serverLogs.title": "Serverprotokolle", + "serverLogs.description": "Protokollierung für Spielwelt und Anmeldeserver wählen. Änderungen gelten nach einem Serverneustart. Die vorherige Konfiguration wird als Einstellungssicherung gespeichert.", + "serverLogs.normal": "Normal", + "serverLogs.errors": "Nur Fehler", + "serverLogs.off": "Aus", + "serverLogs.saved": "Gespeichert. Zum Anwenden den Server neu starten.", + "mod.requiresSquid": "Aktivieren Sie SQUID-Bots, um Dungeon Clear zu verwenden.", }; export default de; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 2b8e2e7..37339d6 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1293,6 +1293,14 @@ const en = { "stop.forceWarning": "Unsaved player progress may be lost. Only verified processes belonging to this server will be stopped. The database will remain running until every world has stopped.", "stop.cancel": "Cancel", + "serverLogs.title": "Server logs", + "serverLogs.description": "Choose logging for the game world and login server. Changes apply after a server restart. Previous configuration is saved in configuration snapshots.", + "serverLogs.normal": "Normal", + "serverLogs.errors": "Errors only", + "serverLogs.off": "Off", + "serverLogs.saved": "Saved. Restart the server to apply.", + "mod.requiresSquid": "Enable SQUID bots to use Dungeon Clear.", + "mod.requiresSquid": "Enable SQUID bots to use Dungeon Clear.", } as const; export default en; diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 3bd966c..7f9f2da 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -1083,6 +1083,13 @@ const es: Partial> = { "stop.forceWarning": "Puede perderse el progreso sin guardar. Solo se detendrán los procesos verificados de este servidor. La base de datos seguirá activa hasta que se detengan todos los mundos.", "stop.cancel": "Cancelar", + "serverLogs.title": "Registros del servidor", + "serverLogs.description": "Elige los registros del mundo y del servidor de acceso. Los cambios se aplican al reiniciar el servidor. La configuración anterior se guarda en las instantáneas de ajustes.", + "serverLogs.normal": "Normales", + "serverLogs.errors": "Solo errores", + "serverLogs.off": "Desactivados", + "serverLogs.saved": "Guardado. Reinicia el servidor para aplicar los cambios.", + "mod.requiresSquid": "Activa los bots SQUID para usar Dungeon Clear.", }; export default es; diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index a1a4f25..a00a770 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -1083,6 +1083,13 @@ const fr: Partial> = { "stop.forceWarning": "La progression non enregistrée peut être perdue. Seuls les processus vérifiés de ce serveur seront arrêtés. La base de données reste active jusqu’à l’arrêt de tous les mondes.", "stop.cancel": "Annuler", + "serverLogs.title": "Journaux du serveur", + "serverLogs.description": "Choisissez la journalisation du monde et du serveur de connexion. Les changements prennent effet après un redémarrage. La configuration précédente est sauvegardée dans les instantanés des paramètres.", + "serverLogs.normal": "Normaux", + "serverLogs.errors": "Erreurs uniquement", + "serverLogs.off": "Désactivés", + "serverLogs.saved": "Enregistré. Redémarrez le serveur pour appliquer.", + "mod.requiresSquid": "Activez les bots SQUID pour utiliser Dungeon Clear.", }; export default fr; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index f93a4d4..97d51ac 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -1276,6 +1276,13 @@ const ru: Partial> = { "stop.forceWarning": "Несохранённый прогресс игроков может быть потерян. Будут остановлены только проверенные процессы этого сервера. База данных останется работать до остановки всех миров.", "stop.cancel": "Отмена", + "serverLogs.title": "Логи сервера", + "serverLogs.description": "Выберите режим логов игрового мира и сервера входа. Изменения применятся после перезапуска сервера. Предыдущая конфигурация сохранится в снимках настроек.", + "serverLogs.normal": "Обычные", + "serverLogs.errors": "Только ошибки", + "serverLogs.off": "Выключены", + "serverLogs.saved": "Сохранено. Перезапустите сервер для применения.", + "mod.requiresSquid": "Для Dungeon Clear сначала включите SQUID bots.", }; export default ru; diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index dab7ba6..b42c05c 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -1085,6 +1085,13 @@ const zh = { "stop.forceWarning": "玩家未保存的进度可能丢失。只会停止经验证属于此服务器的进程。所有世界停止后才会关闭数据库。", "stop.cancel": "取消", + "serverLogs.title": "服务器日志", + "serverLogs.description": "选择游戏世界和登录服务器的日志模式。重启服务器后生效。原配置会保存在设置快照中。", + "serverLogs.normal": "普通", + "serverLogs.errors": "仅错误", + "serverLogs.off": "关闭", + "serverLogs.saved": "已保存。重启服务器后生效。", + "mod.requiresSquid": "请先启用 SQUID 机器人,再使用 Dungeon Clear。", } as const; export default zh; diff --git a/src/lib/api.ts b/src/lib/api.ts index b2d932f..e4c0ee3 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -229,7 +229,7 @@ export interface ModuleView { page: string | null; /** not shown on the Modules page */ hidden: boolean; - compatibility?: "compatible" | "experimental" | "unsupported"; + compatibility?: "compatible" | "experimental" | "unsupported" | "requires-squid"; } export interface ModuleSetting { @@ -499,6 +499,7 @@ export const api = { forceStop: (id: string) => invoke("force_stop_server", { id }), stop: (id: string) => invoke("stop_server", { id }), settings: (id: string, scope: Scope) => invoke("get_settings", { id, scope }), + serverLoggingPreset: (id: string, preset: "normal" | "errors" | "off") => invoke("server_logging_preset", { id, preset }), save: (id: string, scope: Scope, changes: Record) => invoke("save_settings", { id, scope, changes }), presets: (scope: Scope) => invoke("list_presets", { scope }), backups: (id: string) => invoke("list_backups", { id }), diff --git a/src/screens/ModulesPage.tsx b/src/screens/ModulesPage.tsx index b61fd12..ebc2fb6 100644 --- a/src/screens/ModulesPage.tsx +++ b/src/screens/ModulesPage.tsx @@ -215,6 +215,7 @@ export function ModulesPage({ serverId, onChanged, onRegisterGuard }: { serverId

{m.description[locale] ?? m.description.en}

{m.compatibility === "unsupported" &&

{t("realm.unsupported")}

} + {m.compatibility === "requires-squid" &&

{t("mod.requiresSquid")}

} {m.compatibility === "experimental" &&

{t("realm.experimental")}

} {m.installed && m.switchable && ( @@ -230,7 +231,7 @@ export function ModulesPage({ serverId, onChanged, onRegisterGuard }: { serverId )} - {m.installed && !m.switchable && m.status !== "soon" && m.compatibility !== "unsupported" && {t("mod.alwaysOn")}} + {m.installed && !m.switchable && m.status !== "soon" && m.compatibility !== "unsupported" && m.compatibility !== "requires-squid" && {t("mod.alwaysOn")}} {!m.installed && m.status !== "soon" && {t("mod.notHere")}} {toggling === m.id &&

{t("mod.switching")}

} diff --git a/src/screens/ServerLoggingSettings.tsx b/src/screens/ServerLoggingSettings.tsx new file mode 100644 index 0000000..00bfba2 --- /dev/null +++ b/src/screens/ServerLoggingSettings.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { Card } from "@/components/ui/card"; +import { api, asUiError } from "@/lib/api"; +import { useT } from "@/i18n"; + +export function ServerLoggingSettings({ serverId }: { serverId: string }) { + const t = useT(); + const [busy, setBusy] = useState(false); + const [saved, setSaved] = useState(false); + const [error, setError] = useState(""); + async function apply(preset: "normal" | "errors" | "off") { + setBusy(true); setSaved(false); setError(""); + try { await api.serverLoggingPreset(serverId, preset); setSaved(true); } + catch (error) { setError(asUiError(error).technical); } + finally { setBusy(false); } + } + return +

{t("serverLogs.title")}

+

{t("serverLogs.description")}

+
+ {(["normal", "errors", "off"] as const).map(preset => )} +
+ {saved &&

{t("serverLogs.saved")}

} + {error &&

{error}

} +
; +} diff --git a/src/screens/SettingsHome.tsx b/src/screens/SettingsHome.tsx index b2cc827..e8b21da 100644 --- a/src/screens/SettingsHome.tsx +++ b/src/screens/SettingsHome.tsx @@ -16,6 +16,7 @@ import { AutoRestartSettings } from "./AutoRestartSettings"; import { DiagnosticsCard } from "./DiagnosticsCard"; import { RepairCard } from "./RepairCard"; import { RemoveServerCard } from "./RemoveServerCard"; +import { ServerLoggingSettings } from "./ServerLoggingSettings"; const sections = [["updates", Download], ["startup", Power], ["client", FolderOpen], ["service", Wrench], ["interface", Languages]] as const; export function SettingsHome({ serverId, path, onForget }: { serverId: string; serverName?: string; path: string; onForget: () => Promise }) { @@ -51,7 +52,7 @@ export function SettingsHome({ serverId, path, onForget }: { serverId: string; s - + ; } diff --git a/tools/release-history.test.mjs b/tools/release-history.test.mjs index cdd5c42..c789829 100644 --- a/tools/release-history.test.mjs +++ b/tools/release-history.test.mjs @@ -33,3 +33,11 @@ test("reviewed upcoming server notes have all five translations before publicati assert.equal(localizeNotes(planned.notes, translations.server[locale]).fallback, false, `reviewed server/${locale}`); } }); + +test("reviewed upcoming Manager notes have all five translations before publication", () => { + const planned = JSON.parse(readFileSync(new URL("./reviewed-manager-notes.json", import.meta.url))); + const translations = JSON.parse(readFileSync(new URL("../src/data/latest-note-translations.json", import.meta.url))); + for (const locale of ["ru", "de", "fr", "es", "zh"]) { + assert.equal(localizeNotes(planned.notes, translations.manager[locale]).fallback, false, `reviewed manager/${locale}`); + } +}); diff --git a/tools/reviewed-manager-notes.json b/tools/reviewed-manager-notes.json new file mode 100644 index 0000000..22381e4 --- /dev/null +++ b/tools/reviewed-manager-notes.json @@ -0,0 +1,10 @@ +{ + "version": "0.9.8", + "notes": [ + "Update checks temporarily suspend bot population and restore your configuration", + "Dungeon Clear can be enabled when SQUID bots are active", + "Update validation keeps MySQL running between compatible checks", + "Choose normal logs, errors only or no server logs in maintenance settings", + "Diagnostic exports include launcher stages and lock owner records" + ] +} diff --git a/tools/reviewed-server-notes.json b/tools/reviewed-server-notes.json index f8b43bf..79c23e4 100644 --- a/tools/reviewed-server-notes.json +++ b/tools/reviewed-server-notes.json @@ -1,12 +1,16 @@ { - "core": "8b6bbd8610cd672d2823f40909f1c7ee95a83371", + "core": "66380c78147cbd01e22361df1df54dc87b0f1701", "bots": "4f6cf99e6bf509ae2b178e993d1170cc7fe0cb0c", "squid": "fa4cfd31997367b3e574a2b89b858c4669ff53fb", "notes": [ - "Prevent crashes while players update Apex Hunt quest markers on different maps", - "Thin Human and Leper Troll characters retain their starting data after updates", - "Companions avoid rejected names and recover from forced rename login failures", - "Updated SQUID Playerbots to 2.8.2", - "Updated the reviewed CoA upstream snapshot" - ] + "Server launchers accept configuration paths in folders with spaces", + "Database restarts recover from ownerless Windows port entries", + "Concurrent server actions wait briefly and identify the action still running", + "Dungeon Clear is available as an optional extension for SQUID bots", + "Server validation skips creating and logging in SQUID bots", + "Startup and shutdown keep bounded diagnostics of launcher stages", + "Updated the reviewed CoA upstream snapshot", + "Database shutdown succeeds when MySQL closes its connection before replying" + ], + "dungeon-clear": "9a9d09cf512e0a883af9cda6973445b81c123b13" }