Prepare Manager 0.9.8 with isolated validation and SQUID module controls

This commit is contained in:
Дмитрий committed 2026-10-31 20:00:11 +05:00
1 parent dc3fcab7e1
commit 23cbb108ad
34 files changed
+791 -52

No files matched your search

Generated
+3 -3
View File
@@ -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",
+1 -1
View File
@@ -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"
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
+27
View File
@@ -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<serde_json::Value> = 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")
+1
View File
@@ -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;
+47 -3
View File
@@ -197,7 +197,13 @@ fn list_in(cat: &[Entry], root: &Path) -> Vec<ModuleView> {
.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> {
};
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();
+165
View File
@@ -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<String> {
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());
}
}
+4
View File
@@ -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 {
+158 -19
View File
@@ -41,6 +41,24 @@ struct Saved {
sha256: Option<String>,
}
fn bot_files() -> Vec<String> {
["", ".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<serde_json::Value> = 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<String, Saved> = 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<String, Saved> = 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<String, Saved> = 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);
}
}
+18
View File
@@ -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",
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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": {
+15
View File
@@ -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,
+1 -1
View File
@@ -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",
+7 -2
View File
@@ -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.
+70 -10
View File
@@ -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 在回复前关闭连接,数据库关闭操作也能正确完成"
}
}
}
+132
View File
@@ -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"
}
]
}
+7
View File
@@ -1083,6 +1083,13 @@ const de: Partial<Record<keyof typeof en, string>> = {
"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;
+8
View File
@@ -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;
+7
View File
@@ -1083,6 +1083,13 @@ const es: Partial<Record<keyof typeof en, string>> = {
"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;
+7
View File
@@ -1083,6 +1083,13 @@ const fr: Partial<Record<keyof typeof en, string>> = {
"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;
+7
View File
@@ -1276,6 +1276,13 @@ const ru: Partial<Record<keyof typeof en, string>> = {
"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;
+7
View File
@@ -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;
+2 -1
View File
@@ -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<DriverOutcome>("force_stop_server", { id }),
stop: (id: string) => invoke<DriverOutcome>("stop_server", { id }),
settings: (id: string, scope: Scope) => invoke<SettingsView>("get_settings", { id, scope }),
serverLoggingPreset: (id: string, preset: "normal" | "errors" | "off") => invoke<void>("server_logging_preset", { id, preset }),
save: (id: string, scope: Scope, changes: Record<string, JsonValue>) => invoke<SaveReport>("save_settings", { id, scope, changes }),
presets: (scope: Scope) => invoke<PresetInfo[]>("list_presets", { scope }),
backups: (id: string) => invoke<RecoveryPoint[]>("list_backups", { id }),
+2 -1
View File
@@ -215,6 +215,7 @@ export function ModulesPage({ serverId, onChanged, onRegisterGuard }: { serverId
</div>
<p className="mt-1.5 line-clamp-2 text-sm text-muted">{m.description[locale] ?? m.description.en}</p>
{m.compatibility === "unsupported" && <p className="mt-2 text-xs text-warn">{t("realm.unsupported")}</p>}
{m.compatibility === "requires-squid" && <p className="mt-2 text-xs text-warn">{t("mod.requiresSquid")}</p>}
{m.compatibility === "experimental" && <p className="mt-2 text-xs text-warn">{t("realm.experimental")}</p>}
</div>
{m.installed && m.switchable && (
@@ -230,7 +231,7 @@ export function ModulesPage({ serverId, onChanged, onRegisterGuard }: { serverId
<span className={cn("absolute top-0.5 h-5 w-5 rounded-full bg-white transition-all", m.enabled ? "left-[22px]" : "left-0.5")} />
</button>
)}
{m.installed && !m.switchable && m.status !== "soon" && m.compatibility !== "unsupported" && <span className="mt-1.5 shrink-0 text-xs text-muted">{t("mod.alwaysOn")}</span>}
{m.installed && !m.switchable && m.status !== "soon" && m.compatibility !== "unsupported" && m.compatibility !== "requires-squid" && <span className="mt-1.5 shrink-0 text-xs text-muted">{t("mod.alwaysOn")}</span>}
{!m.installed && m.status !== "soon" && <span className="mt-1.5 shrink-0 text-xs text-muted">{t("mod.notHere")}</span>}
</div>
{toggling === m.id && <p role="status" className="flex items-center gap-2 text-sm text-gold"><Loader2 className="h-4 w-4 animate-spin" aria-hidden />{t("mod.switching")}</p>}
+26
View File
@@ -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 <Card className="mt-6 p-6">
<h2 className="font-semibold">{t("serverLogs.title")}</h2>
<p className="mt-2 text-sm text-muted">{t("serverLogs.description")}</p>
<div className="mt-4 flex flex-wrap gap-2">
{(["normal", "errors", "off"] as const).map(preset => <button key={preset} disabled={busy} onClick={() => apply(preset)} className="rounded-md border border-line px-3 py-2 text-sm hover:bg-white/5 disabled:opacity-50">{t(`serverLogs.${preset}`)}</button>)}
</div>
{saved && <p role="status" className="mt-3 text-sm text-ok">{t("serverLogs.saved")}</p>}
{error && <p role="alert" className="mt-3 text-sm text-bad">{error}</p>}
</Card>;
}
+2 -1
View File
@@ -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<void> }) {
@@ -51,7 +52,7 @@ export function SettingsHome({ serverId, path, onForget }: { serverId: string; s
</div>
<div hidden={section !== "startup"} className="max-w-3xl"><RealmStartupSettings serverId={serverId} /><AutoRestartSettings serverId={serverId} /></div>
<div hidden={section !== "client"} className="max-w-3xl"><ClientCard serverId={serverId} /><GameDataCard serverId={serverId} /></div>
<div hidden={section !== "service"} className="max-w-3xl"><DiagnosticsCard serverId={serverId} /><RepairCard serverId={serverId} /><RemoveServerCard serverId={serverId} path={path} onForget={onForget} /></div>
<div hidden={section !== "service"} className="max-w-3xl"><ServerLoggingSettings serverId={serverId} /><DiagnosticsCard serverId={serverId} /><RepairCard serverId={serverId} /><RemoveServerCard serverId={serverId} path={path} onForget={onForget} /></div>
<div hidden={section !== "interface"} className="max-w-3xl"><Card className="mt-6 p-6"><h2 className="font-semibold">{t("settings.language")}</h2><p className="mt-1 text-sm text-muted">{t("settings.languageHint")}</p><LanguagePicker className="mt-4" /></Card><WindowSettings /><PublicServerSettings /></div>
</div>;
}
+8
View File
@@ -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}`);
}
});
+10
View File
@@ -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"
]
}
+11 -7
View File
@@ -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"
}