Merge Manager 0.9.9 (master) into the Modules Preview

Stable packages from 0.261104.94 need Manager 0.9.8; the preview was 0.9.7.
Conflicts kept the preview's module screen and plugin-aware module logic, took
master's requires-squid compatibility value and version 0.9.9 under the
preview's own product name and identifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ДмитрийandClaude Opus 5.5 committed 2026-11-05 16:23:05 +05:00
commit 2bec907fa7
36 files changed
+927 -75

No files matched your search

+6
View File
@@ -8,6 +8,12 @@ Published releases and previews. Pre-release entries do not change the stable up
Dates follow GitHub publication (UTC); legacy records retain their archived dates. Newest releases first.
## 0.9.9 — 2026-11-04
- Update and recovery checks use the temporary login port even with a Windows override.
[Release](https://github.com/Corfirean/coa-server-manager/releases/tag/v0.9.9) · [Source changes](https://github.com/Corfirean/coa-server-manager/compare/v0.9.8...v0.9.9)
## 0.9.8 — 2026-11-01
- Dungeon Clear can be enabled when SQUID bots are active.
Generated
+3 -3
View File
@@ -586,7 +586,7 @@ dependencies = [
[[package]]
name = "coa-core"
version = "0.9.7"
version = "0.9.9"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -634,7 +634,7 @@ dependencies = [
[[package]]
name = "coa-release"
version = "0.9.7"
version = "0.9.9"
dependencies = [
"chrono",
"coa-core",
@@ -772,7 +772,7 @@ dependencies = [
[[package]]
name = "csm-modules-preview"
version = "0.9.7"
version = "0.9.9"
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.9"
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.
@@ -0,0 +1,6 @@
---
area: manager
type: fixed
audience: admins
title: Update and recovery checks use the temporary login port even with a Windows override
---
+13 -1
View File
@@ -174,7 +174,8 @@
"Corfirean/coa-server-manager:20261031-launcher-diagnostics-export.md",
"Corfirean/coa-server-manager:20261031-server-logging-controls.md",
"Corfirean/coa-server-manager:20261031-validation-bot-population.md",
"Corfirean/coa-server-manager:20261031-validation-mysql-reuse.md"
"Corfirean/coa-server-manager:20261031-validation-mysql-reuse.md",
"Corfirean/coa-server-manager:20261103-isolated-login-port.md"
],
"releases": [
{
@@ -888,6 +889,17 @@
],
"commit": "076fba185c4d96d74f7258d6ca0cae6a4ae4a613",
"compare": "https://github.com/Corfirean/coa-server-manager/compare/v0.9.7...v0.9.8"
},
{
"tag": "v0.9.9",
"version": "0.9.9",
"date": "2026-11-04",
"url": "https://github.com/Corfirean/coa-server-manager/releases/tag/v0.9.9",
"notes": [
"Update and recovery checks use the temporary login port even with a Windows override"
],
"commit": "173248efbe24da9b4c1b5ca12fa7580fcd1e8ea7",
"compare": "https://github.com/Corfirean/coa-server-manager/compare/v0.9.8...v0.9.9"
}
]
}
+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")
+4
View File
@@ -233,6 +233,10 @@ fn run_inner_mode(
recover_world_for_forced_stop(&root)?;
}
let mut cmd = Command::new(&python);
if validating_update {
// Isolated startup must use the port from its temporary auth config.
cmd.env_remove("AC_REALM_SERVER_PORT");
}
cmd.arg("-B")
.arg("-c")
.arg(LAUNCH_SCRIPT)
+1
View File
@@ -40,6 +40,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;
+21 -21
View File
@@ -1254,6 +1254,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();
@@ -1922,25 +1943,4 @@ mod tests {
let conf_content = fs::read_to_string(dir(&root).join("npc_buffer.conf")).unwrap();
assert!(conf_content.contains("NpcBuffer.Enable = 0"));
}
#[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());
}
}
+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
@@ -1915,6 +1915,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);
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "coa-server-manager",
"version": "0.9.7",
"version": "0.9.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "coa-server-manager",
"version": "0.9.7",
"version": "0.9.9",
"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.9",
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
+15
View File
@@ -761,6 +761,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>,
@@ -3406,6 +3420,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,
preview_toolchain_status,
preview_install_msvc,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CSM Modules Preview",
"version": "0.9.7",
"version": "0.9.9",
"identifier": "com.corfirean.csm-modules-preview",
"build": {
"beforeBuildCommand": "npm run build",
+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 `902ed8eed3017e3ee8c31e5dc5e4df6f551362bf`, path
`changelog/history.json`, on 2026-11-03. 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.
+90 -15
View File
@@ -31,7 +31,13 @@
"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": "В архив диагностики входят этапы лаунчера и записи о владельцах блокировок",
"Update and recovery checks use the temporary login port even with a Windows override": "Проверка обновления и восстановления использует временный порт входа даже при переопределении в Windows"
},
"de": {
"Choose Parallel or Global level scaling in module settings": "Parallele oder globale Levelskalierung in den Moduleinstellungen wählen",
@@ -64,7 +70,13 @@
"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",
"Update and recovery checks use the temporary login port even with a Windows override": "Update- und Wiederherstellungsprüfungen verwenden den temporären Anmeldeport auch bei einer Windows-Vorgabe"
},
"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 +109,13 @@
"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",
"Update and recovery checks use the temporary login port even with a Windows override": "Les contrôles de mise à jour et de restauration utilisent le port de connexion temporaire même si Windows en impose un autre"
},
"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 +148,13 @@
"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",
"Update and recovery checks use the temporary login port even with a Windows override": "Las comprobaciones de actualización y recuperación usan el puerto temporal de acceso aunque Windows indique otro"
},
"zh": {
"Choose Parallel or Global level scaling in module settings": "在模块设置中选择并行或全局等级缩放",
@@ -163,7 +187,13 @@
"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": "诊断导出包含启动器阶段和锁所属进程记录",
"Update and recovery checks use the temporary login port even with a Windows override": "即使 Windows 指定了其他端口,更新和恢复检查也会使用临时登录端口"
}
},
"server": {
@@ -197,7 +227,7 @@
"Imported 40 upstream commits with class, item and Pale Reach quest fixes, optional account-wide friends, and corrected Vanity purchases": "Включены 40 изменений апстрима: исправления классов, предметов и заданий Pale Reach, необязательные друзья на весь аккаунт и исправленные покупки Vanity",
"Server includes newer upstream class, quest and dungeon updates": "Сервер включает новые обновления классов, заданий и подземелий из апстрима",
"Dungeon scaling preserves the level differences between creatures": "Масштабирование подземелий сохраняет разницу уровней между существами",
"Normal Classic dungeons retain their authored damage at full party size": "Обычные подземелья классики сохраняют исходный урон при полной группе",
"Normal Classic dungeons retain their authored damage at full party size": "В обычных классических подземельях урон при полной группе соответствует исходной настройке",
"Interactive World Map displays allied bots, guild tasks and events": "Интерактивная карта мира показывает союзных ботов, задания гильдии и события",
"Database startup allows time for slower hard drives": "Запуск базы данных допускает более долгое ожидание на медленных дисках",
"Option to make low-health runners walk": "Добавлена возможность замедлить убегающих существ с низким здоровьем до шага",
@@ -205,7 +235,16 @@
"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 закрыл соединение до отправки ответа",
"Server startup waits for ports that Windows is still releasing": "При запуске сервер ждёт, пока Windows освободит порт",
"Companions avoid repeating unreachable routes in the open world": "В открытом мире спутники перестают повторять непроходимые маршруты"
},
"de": {
"Restrict new characters by custom race without changing existing characters": "Neue Charaktere nach zusätzlichem Volk beschränken, ohne bestehende Charaktere zu ändern",
@@ -237,7 +276,7 @@
"Imported 40 upstream commits with class, item and Pale Reach quest fixes, optional account-wide friends, and corrected Vanity purchases": "40 Upstream-Änderungen mit Klassen-, Gegenstands- und Pale-Reach-Questkorrekturen, optionalen accountweiten Freunden und korrigierten Vanity-Käufen übernommen",
"Server includes newer upstream class, quest and dungeon updates": "Der Server enthält neuere Upstream-Updates für Klassen, Quests und Dungeons",
"Dungeon scaling preserves the level differences between creatures": "Die Dungeon-Skalierung erhält die Stufenunterschiede zwischen Kreaturen",
"Normal Classic dungeons retain their authored damage at full party size": "Normale Classic-Dungeons behalten bei voller Gruppe ihren ursprünglichen Schaden",
"Normal Classic dungeons retain their authored damage at full party size": "Normale klassische Dungeons behalten bei voller Gruppe ihren vorgesehenen Schaden",
"Interactive World Map displays allied bots, guild tasks and events": "Die interaktive Weltkarte zeigt verbündete Bots, Gildenaufgaben und Ereignisse",
"Database startup allows time for slower hard drives": "Der Datenbankstart lässt langsameren Festplatten mehr Zeit",
"Option to make low-health runners walk": "Fliehende Kreaturen mit wenig Gesundheit können im Schritttempo laufen",
@@ -245,7 +284,16 @@
"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",
"Server startup waits for ports that Windows is still releasing": "Beim Start wartet der Server, bis Windows den Port freigibt",
"Companions avoid repeating unreachable routes in the open world": "Gefährten meiden in der offenen Welt wiederholte Versuche auf unpassierbaren Routen"
},
"fr": {
"Restrict new characters by custom race without changing existing characters": "Restreignez les races supplémentaires des nouveaux personnages sans modifier les personnages existants",
@@ -277,7 +325,7 @@
"Imported 40 upstream commits with class, item and Pale Reach quest fixes, optional account-wide friends, and corrected Vanity purchases": "Intégration de 40 changements amont : classes, objets et quêtes de Pale Reach corrigés, amis de compte facultatifs et achats Vanity corrigés",
"Server includes newer upstream class, quest and dungeon updates": "Le serveur intègre de nouvelles mises à jour des classes, quêtes et donjons provenant du projet amont",
"Dungeon scaling preserves the level differences between creatures": "La mise à l’échelle des donjons conserve les écarts de niveau entre les créatures",
"Normal Classic dungeons retain their authored damage at full party size": "Les donjons Classic normaux conservent leurs dégâts d’origine avec un groupe complet",
"Normal Classic dungeons retain their authored damage at full party size": "Les donjons classiques normaux conservent leurs dégâts prévus pour un groupe complet",
"Interactive World Map displays allied bots, guild tasks and events": "La carte du monde interactive affiche les bots alliés, les tâches de guilde et les événements",
"Database startup allows time for slower hard drives": "Le démarrage de la base de données laisse plus de temps aux disques lents",
"Option to make low-health runners walk": "Une option permet aux créatures affaiblies de fuir en marchant",
@@ -285,7 +333,16 @@
"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",
"Server startup waits for ports that Windows is still releasing": "Au démarrage, le serveur attend que Windows libère le port",
"Companions avoid repeating unreachable routes in the open world": "Les compagnons évitent de reprendre les chemins impraticables en monde ouvert"
},
"es": {
"Restrict new characters by custom race without changing existing characters": "Restringe las razas adicionales de personajes nuevos sin modificar los existentes",
@@ -317,7 +374,7 @@
"Imported 40 upstream commits with class, item and Pale Reach quest fixes, optional account-wide friends, and corrected Vanity purchases": "Integrados 40 cambios del proyecto original: correcciones de clases, objetos y misiones de Pale Reach, amigos opcionales de toda la cuenta y compras Vanity corregidas",
"Server includes newer upstream class, quest and dungeon updates": "El servidor incorpora nuevas actualizaciones de clases, misiones y mazmorras del proyecto original",
"Dungeon scaling preserves the level differences between creatures": "El escalado de las mazmorras conserva las diferencias de nivel entre criaturas",
"Normal Classic dungeons retain their authored damage at full party size": "Las mazmorras Classic normales conservan su daño original con un grupo completo",
"Normal Classic dungeons retain their authored damage at full party size": "Las mazmorras clásicas normales conservan el daño previsto para un grupo completo",
"Interactive World Map displays allied bots, guild tasks and events": "El mapa del mundo interactivo muestra bots aliados, tareas de hermandad y eventos",
"Database startup allows time for slower hard drives": "El inicio de la base de datos permite más tiempo en discos lentos",
"Option to make low-health runners walk": "Una opción permite que las criaturas con poca salud huyan caminando",
@@ -325,7 +382,16 @@
"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",
"Server startup waits for ports that Windows is still releasing": "Al iniciarse, el servidor espera a que Windows libere el puerto",
"Companions avoid repeating unreachable routes in the open world": "Los compañeros evitan repetir rutas intransitables en el mundo abierto"
},
"zh": {
"Restrict new characters by custom race without changing existing characters": "限制新角色可选的自定义种族,不影响现有角色",
@@ -357,7 +423,7 @@
"Imported 40 upstream commits with class, item and Pale Reach quest fixes, optional account-wide friends, and corrected Vanity purchases": "合入 40 项上游更改:职业、物品及 Pale Reach 任务修复,可选的账号共享好友,以及 Vanity 购买修复",
"Server includes newer upstream class, quest and dungeon updates": "服务器整合了上游新的职业、任务和地下城更新",
"Dungeon scaling preserves the level differences between creatures": "地下城缩放保留生物之间的等级差异",
"Normal Classic dungeons retain their authored damage at full party size": "普通经典地下城在满员队伍下保留原始伤害",
"Normal Classic dungeons retain their authored damage at full party size": "满队时普通经典地下城保留原定的伤害数值",
"Interactive World Map displays allied bots, guild tasks and events": "交互式世界地图显示友方机器人、公会任务和事件",
"Database startup allows time for slower hard drives": "数据库启动为较慢的硬盘预留更多等待时间",
"Option to make low-health runners walk": "新增选项,可让低生命值的逃跑生物改为步行",
@@ -365,7 +431,16 @@
"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 在回复前关闭连接,数据库关闭操作也能正确完成",
"Server startup waits for ports that Windows is still releasing": "服务器启动时会等待 Windows 释放端口",
"Companions avoid repeating unreachable routes in the open world": "伙伴在开放世界中会避免反复尝试无法通行的路线"
}
}
}
+274
View File
@@ -1297,6 +1297,280 @@
"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"
},
{
"tag": "server-0.261101.92",
"version": "0.261101.92",
"date": "2026-11-01",
"url": "https://github.com/Corfirean/coa-server-build/releases/tag/server-0.261101.92",
"notes": [
"Concurrent server actions wait briefly and identify the action still running",
"Dungeon Clear is available as an optional extension for SQUID bots",
"Server launchers accept configuration paths in folders with spaces",
"Startup and shutdown keep bounded diagnostics of launcher stages",
"Database shutdown succeeds when MySQL closes its connection before replying",
"Database restarts recover from ownerless Windows port entries",
"Server validation skips creating and logging in SQUID bots",
"Updated the reviewed CoA upstream snapshot"
],
"snapshot": "f2be62fe451dd635a27393b6289b7801c7103826597232a78633f49f564f858c",
"components": {
"upstream": {
"repository": "jealous-sound/azerothcore-wotlk-coa",
"ref": "2ee0f77eebed8b984fb151cf7fe1163f640efd3a",
"sha": "2ee0f77eebed8b984fb151cf7fe1163f640efd3a"
},
"core": {
"repository": "Corfirean/azerothcore-wotlk-coa",
"ref": "66380c78147cbd01e22361df1df54dc87b0f1701",
"sha": "66380c78147cbd01e22361df1df54dc87b0f1701"
},
"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": "23cbb108adb4f9c61c0ffff24a60c134e5a0dede",
"sha": "23cbb108adb4f9c61c0ffff24a60c134e5a0dede"
},
"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",
"patch": "patches/squid-coa-validation.patch",
"patchSha256": "9b101db17369fb5a6af3f7e99323f7770eb8650ba3b5e9b3970bf09a0ef5d32f"
},
"build": {
"repository": "Corfirean/coa-server-build",
"ref": "9705eb844085b72379bef517635968473cb43419",
"sha": "9705eb844085b72379bef517635968473cb43419"
},
"dungeon-clear": {
"repository": "Zyth45/mod-dungeon-clear",
"sha": "9a9d09cf512e0a883af9cda6973445b81c123b13",
"patch": "patches/dungeon-clear-coa-validation.patch",
"patchSha256": "956f278865e22ee75c984ca22ff8238150b5886c9934ddfe64dd9c832c912dcc",
"ref": "9a9d09cf512e0a883af9cda6973445b81c123b13"
},
"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": [],
"compare": "https://github.com/Corfirean/coa-server-build/compare/server-0.261030.90...server-0.261101.92"
}
]
}
+7
View File
@@ -1085,6 +1085,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;
+7
View File
@@ -1297,6 +1297,13 @@ 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.",
} as const;
export default en;
+7
View File
@@ -1085,6 +1085,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
@@ -1085,6 +1085,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
@@ -1281,6 +1281,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
@@ -1087,6 +1087,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
@@ -237,7 +237,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";
component_type?: ComponentType;
can_remove?: boolean;
source_commit?: string | null;
@@ -753,6 +753,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 }),
+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}`);
}
});
+6
View File
@@ -0,0 +1,6 @@
{
"version": "0.9.9",
"notes": [
"Update and recovery checks use the temporary login port even with a Windows override"
]
}
+7 -7
View File
@@ -1,12 +1,12 @@
{
"core": "8b6bbd8610cd672d2823f40909f1c7ee95a83371",
"bots": "4f6cf99e6bf509ae2b178e993d1170cc7fe0cb0c",
"core": "14cf375f1e006770b330d3fb89201ace54bfca24",
"bots": "447558bc27d6bb447aaf10eefcb2e85ac2b15c32",
"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",
"Server startup waits for ports that Windows is still releasing",
"Normal Classic dungeons retain their authored damage at full party size",
"Companions avoid repeating unreachable routes in the open world",
"Updated the reviewed CoA upstream snapshot"
]
],
"dungeon-clear": "9a9d09cf512e0a883af9cda6973445b81c123b13"
}