mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
Merge commit '076fba1' into release/20261103-manager
This commit is contained in:
commit
5697a1d36e
34 files changed
+790
-52
No files matched your search
Generated
+3
-3
@@ -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
@@ -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.
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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,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
@@ -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.
|
||||
@@ -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 在回复前关闭连接,数据库关闭操作也能正确完成"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1293,6 +1293,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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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 }),
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in new issue
Block a user