From 1bcef5f8c2e85b28eb466e720002d2fbf2b67161 Mon Sep 17 00:00:00 2001 From: Corfirean Date: Sat, 3 Oct 2026 11:12:04 +0500 Subject: [PATCH] Add CoA / Wildcard realm profiles for v0.5.0 (#10) * Add isolated Wildcard realm profiles and release v0.5.0 * Document realm validation and align npm lock version --- Cargo.lock | 6 +- Cargo.toml | 2 +- README.md | 6 + .../20261003-wildcard-realm-profiles.md | 7 + crates/coa-core/examples/realm_e2e.rs | 92 +++++ crates/coa-core/src/backup.rs | 28 +- crates/coa-core/src/config/mod.rs | 1 + crates/coa-core/src/db.rs | 98 ++++- crates/coa-core/src/driver.rs | 11 + crates/coa-core/src/friends.rs | 3 +- crates/coa-core/src/lib.rs | 1 + crates/coa-core/src/modules.rs | 10 +- crates/coa-core/src/realms.rs | 371 ++++++++++++++++++ crates/coa-core/src/update.rs | 21 +- docs/WILDCARD.md | 42 ++ docs/screenshots/wildcard-demo/modules.jpg | Bin 0 -> 88574 bytes docs/screenshots/wildcard-demo/overview.jpg | Bin 0 -> 58070 bytes package-lock.json | 4 +- package.json | 2 +- src-tauri/src/lib.rs | 36 +- src-tauri/tauri.conf.json | 2 +- src/dev/mock.ts | 11 +- src/i18n/locales/de.ts | 12 + src/i18n/locales/en.ts | 12 + src/i18n/locales/es.ts | 12 + src/i18n/locales/fr.ts | 12 + src/i18n/locales/ru.ts | 12 + src/lib/api.ts | 7 + src/screens/BackupsPage.tsx | 8 +- src/screens/ModulesPage.tsx | 6 +- src/screens/Overview.tsx | 9 +- src/screens/RealmPicker.tsx | 53 +++ src/screens/Shell.tsx | 4 +- tools/make-fixture.ps1 | 6 +- vite.config.ts | 2 +- 35 files changed, 872 insertions(+), 37 deletions(-) create mode 100644 changelog.d/20261003-wildcard-realm-profiles.md create mode 100644 crates/coa-core/examples/realm_e2e.rs create mode 100644 crates/coa-core/src/realms.rs create mode 100644 docs/WILDCARD.md create mode 100644 docs/screenshots/wildcard-demo/modules.jpg create mode 100644 docs/screenshots/wildcard-demo/overview.jpg create mode 100644 src/screens/RealmPicker.tsx diff --git a/Cargo.lock b/Cargo.lock index 630c791..86f504d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,7 +347,7 @@ dependencies = [ [[package]] name = "coa-core" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64 0.22.1", "chrono", @@ -375,7 +375,7 @@ dependencies = [ [[package]] name = "coa-release" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "coa-core", @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "coa-server-manager" -version = "0.4.0" +version = "0.5.0" dependencies = [ "coa-core", "serde", diff --git a/Cargo.toml b/Cargo.toml index f4cdd47..99b38df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/coa-core", "crates/coa-release", "src-tauri"] [workspace.package] -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "AGPL-3.0-only" diff --git a/README.md b/README.md index 5c8f804..d4890e5 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ no console, no config files, no database tools. Tauri 2 + Rust + React. into **UPDATE**. Updates are authenticated (Ed25519), back up first, keep files you changed and can be rolled back. * **Backups.** Characters, accounts and settings on demand, automatically before every update; verify and restore from the app. +* **CoA / Wildcard realms.** Choose the active realm on Overview. A server build with Wildcard support is required; + the first switch copies world data and creates an empty character database. Accounts are shared, characters and + module settings are separate, and only one world runs. Switching a running server saves characters and restarts it. + CoA companions are unavailable on Wildcard; other optional modules are experimental. Backups include both realms, + and database updates apply to both. + See [realm profiles and validation](docs/WILDCARD.md). * **Companions (bots).** Fill the world with level-appropriate bots, see progress, stop spawning, take them offline, despawn N to relieve the server, and choose how many log in at server start. * **Realmlist switcher.** Keep named realmlists (for example *Solo* and *PTR*) and switch the game client between them from a diff --git a/changelog.d/20261003-wildcard-realm-profiles.md b/changelog.d/20261003-wildcard-realm-profiles.md new file mode 100644 index 0000000..59e45c7 --- /dev/null +++ b/changelog.d/20261003-wildcard-realm-profiles.md @@ -0,0 +1,7 @@ +--- +area: manager +type: added +audience: admins +title: Switch between CoA and Wildcard while keeping separate characters and module settings +--- +Run one world at a time with shared accounts. Wildcard keeps its own progress; CoA companions are unavailable there, and other modules show experimental compatibility. Backups and server updates cover both realms. diff --git a/crates/coa-core/examples/realm_e2e.rs b/crates/coa-core/examples/realm_e2e.rs new file mode 100644 index 0000000..af08337 --- /dev/null +++ b/crates/coa-core/examples/realm_e2e.rs @@ -0,0 +1,92 @@ +//! Native smoke checks for the disposable fixture created by tools/make-fixture.ps1. +//! Never point these probes at an owned server. +use std::path::Path; + +use coa_core::db::{Account, Db}; +use coa_core::driver::{self, Verb}; +use coa_core::realms::{self, Mode}; +use coa_core::{Error, Result}; + +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + let root = Path::new("C:/games/coa-wildcard-fixture"); + let action = args.get(1).map(String::as_str).unwrap_or("view"); + match action { + "select" => { + let mode = if args.get(2).map(String::as_str) == Some("wildcard") { Mode::Wildcard } else { Mode::Coa }; + println!("{}", serde_json::to_string(&realms::select(root, mode)?)?); + } + "start" | "stop" => { + let out = driver::run(root, if action == "start" { Verb::StartAll } else { Verb::StopAll })?; + println!("{}", serde_json::to_string(&out)?); + if !out.ok { return Err(Error::Invalid("Fixture operation failed".into())); } + } + "probe" => { + let db = Db::from_repack(root, Account::Admin)?; + let s = realms::state(root)?; + println!("{}", db.query("SELECT * FROM acore_characters.manager_profile_probe;")?); + println!("active={} chars={} world={}", s.active.name(), s.active.schema("characters")?, s.active.schema("world")?); + } + "mark" => { + Db::from_repack(root, Account::Admin)?.query("INSERT INTO acore_characters.manager_profile_probe VALUES (22);")?; + } + "backup" => { + use coa_core::backup::{self, Kind, Trigger}; + let meta = root.join("fixture-manager"); + let point = backup::create(root, &meta, Kind::Full, Trigger::Manual, Some("realm isolation check".into()), &|step| println!("{step}"))?; + assert_eq!(point.realm, Mode::Wildcard); + for name in ["characters", "world", "auth", "coa-characters", "coa-world", "configs"] { + assert!(point.components.iter().any(|c| c.name == name), "Missing {name}"); + } + assert!(backup::verify(&meta, &point.id)?.ok); + println!("Verified both realm backups: {}", point.id); + } + "database-checks" => { + use coa_core::backup::{self, Kind, Trigger}; + use coa_core::manifest::{Manifest, Migration}; + use coa_core::update::{Env, RepackEnv}; + let meta = root.join("fixture-manager"); + let env = RepackEnv { root, meta_dir: &meta }; + env.ensure_stopped()?; + let staged = root.join("fixture-migrations"); + let mut manifest: Manifest = serde_json::from_value(serde_json::json!({ + "schema": 1, "kind": "update", "version": "0.5.0", "core": {"commit": null}, + "builtAt": "fixture", "minManagerVersion": "0.5.0" + }))?; + for kind in ["characters", "world", "auth"] { + let sql = format!("CREATE TABLE acore_{kind}.manager_migration_probe (value INT); INSERT INTO acore_{kind}.manager_migration_probe VALUES (42);"); + let path = staged.join(format!("{kind}/manager_realm_fixture.sql")); + std::fs::create_dir_all(path.parent().unwrap())?; + std::fs::write(&path, sql)?; + manifest.migrations.push(Migration { id: "manager_realm_fixture".into(), db: kind.into(), sha256: coa_core::fsx::sha256_file(&path)?, destructive: false }); + } + let result = env.migrate(&manifest, &staged)?; + assert!(result.failed.is_none()); + backup::with_database(root, |db| { + for mode in [Mode::Coa, Mode::Wildcard] { + let realm = db.clone().for_realm(mode); + for kind in ["characters", "world", "auth"] { + assert_eq!(realm.query(&format!("SELECT COUNT(*) FROM acore_{kind}.manager_migration_probe;"))?, "1"); + } + } + Ok(()) + })?; + assert!(env.migrate(&manifest, &staged)?.applied.is_empty()); + let point = backup::create(root, &meta, Kind::Quick, Trigger::Manual, None, &|_| {})?; + backup::with_database(root, |db| { + db.query("INSERT INTO acore_characters.manager_profile_probe VALUES (22);")?; + Ok(()) + })?; + backup::restore_database(root, &meta, &point.id, "characters")?; + backup::with_database(root, |db| { + assert_eq!(db.query("SELECT COUNT(*) FROM acore_characters.manager_profile_probe;")?, "0"); + assert_eq!(db.clone().for_realm(Mode::Coa).query("SELECT value FROM acore_characters.manager_profile_probe;")?, "11"); + Ok(()) + })?; + println!("Both realms migrated once; shared auth migrated once; Wildcard restore left CoA intact."); + } + "view" => println!("{}", serde_json::to_string(&realms::view(root)?)?), + _ => return Err(Error::Invalid("Unknown fixture action".into())), + } + Ok(()) +} diff --git a/crates/coa-core/src/backup.rs b/crates/coa-core/src/backup.rs index 8e71b67..bdb41bf 100644 --- a/crates/coa-core/src/backup.rs +++ b/crates/coa-core/src/backup.rs @@ -63,6 +63,8 @@ pub struct RecoveryPoint { pub created_at: String, pub manager_version: String, pub components: Vec, + #[serde(default)] + pub realm: crate::realms::Mode, } fn backups_dir(meta: &Path) -> PathBuf { @@ -127,6 +129,8 @@ pub fn config_files(root: &Path) -> Vec { if root.join("RELEASE.json").is_file() { out.push("RELEASE.json".into()); } + walk(root, &root.join("Settings/realm-profiles"), &mut out); + if root.join("Settings/realm-profile.json").is_file() { out.push("Settings/realm-profile.json".into()); } out.sort(); out } @@ -191,16 +195,24 @@ pub fn create(root: &Path, meta: &Path, kind: Kind, trigger: Trigger, label: Opt let build = || -> Result> { let mut components = Vec::new(); - for name in wanted_databases(kind) { - let schema = db::schema_of(name)?; + let realm = crate::realms::state(root)?; + let mut databases: Vec<(String, &str)> = wanted_databases(kind).iter() + .map(|name| Ok((name.to_string(), realm.active.schema(name)?))).collect::>()?; + if realm.wildcard_created && kind != Kind::Config { + let other = if realm.active == crate::realms::Mode::Coa { crate::realms::Mode::Wildcard } else { crate::realms::Mode::Coa }; + databases.push((format!("{}-characters", other.name()), other.schema("characters")?)); + if kind == Kind::Full { databases.push((format!("{}-world", other.name()), other.schema("world")?)); } + } + for (name, schema) in databases { progress(&format!("Backing up {name} database")); let component = with_database(root, |db| { + let db = db.clone().for_realm(crate::realms::Mode::Coa); let size = db.schema_bytes(schema)?; fsx::require_space(&partial, size / 2 + 32 * 1024 * 1024)?; let tables = db.tables(schema)?.len(); let file = format!("{name}.sql.zst"); let (bytes, sha) = db.dump_to(schema, &partial.join(&file))?; - Ok(Component { name: (*name).into(), path: file, bytes, sha256: Some(sha), tables: Some(tables), files: None }) + Ok(Component { name, path: file, bytes, sha256: Some(sha), tables: Some(tables), files: None }) })?; components.push(component); } @@ -219,6 +231,7 @@ pub fn create(root: &Path, meta: &Path, kind: Kind, trigger: Trigger, label: Opt } }; let point = RecoveryPoint { + realm: crate::realms::state(root)?.active, schema: 1, id: id.clone(), kind, @@ -289,6 +302,9 @@ pub fn delete(meta: &Path, id: &str) -> Result<()> { /// Put configuration files back. Files the backup does not contain are left alone. A safety point is taken first. pub fn restore_configs(root: &Path, meta: &Path, id: &str) -> Result { let point = get(meta, id)?; + if point.realm != crate::realms::state(root)?.active { + return Err(Error::Invalid("Select the realm this backup belongs to before restoring it.".into())); + } let comp = point.components.iter().find(|c| c.name == "configs").ok_or_else(|| Error::Invalid("this backup has no configuration".into()))?; if !verify(meta, id)?.ok { return Err(Error::Invalid("This backup is damaged and cannot be restored.".into())); @@ -314,6 +330,9 @@ pub struct DbRestore { /// import into a staging schema, sanity-check it, then swap tables atomically and keep the old schema. pub fn restore_database(root: &Path, meta: &Path, id: &str, name: &str) -> Result { let point = get(meta, id)?; + if point.realm != crate::realms::state(root)?.active { + return Err(Error::Invalid("Select the realm this backup belongs to before restoring it.".into())); + } let comp = point.components.iter().find(|c| c.name == name && c.sha256.is_some()).ok_or_else(|| Error::Invalid(format!("this backup has no {name} database")))?; if !verify(meta, id)?.ok { return Err(Error::Invalid("This backup is damaged and cannot be restored.".into())); @@ -322,7 +341,7 @@ pub fn restore_database(root: &Path, meta: &Path, id: &str, name: &str) -> Resul if now.world.state != ServiceState::Stopped || now.auth.state != ServiceState::Stopped { return Err(Error::Invalid("Stop the server before restoring a database.".into())); } - let live = db::schema_of(name)?; + let live = if name.contains('-') { db::schema_of(name)? } else { point.realm.schema(name)? }; let expected_tables = comp.tables.unwrap_or(0); let dump = point_dir(meta, id)?.join(&comp.path); let stamp = chrono::Utc::now().format("%Y%m%d%H%M%S").to_string(); @@ -331,6 +350,7 @@ pub fn restore_database(root: &Path, meta: &Path, id: &str, name: &str) -> Resul let safety = create(root, meta, Kind::Database, Trigger::BeforeRestore, Some(format!("before restoring {name} from {id}")), &|_| {})?; with_database(root, |db| { + let db = db.clone().for_realm(crate::realms::Mode::Coa); let staging = format!("{live}_restore_{stamp}"); let old = format!("{live}_before_restore_{stamp}"); if db.schema_exists(&staging)? || db.schema_exists(&old)? { diff --git a/crates/coa-core/src/config/mod.rs b/crates/coa-core/src/config/mod.rs index 5296d6a..4e680fe 100644 --- a/crates/coa-core/src/config/mod.rs +++ b/crates/coa-core/src/config/mod.rs @@ -436,6 +436,7 @@ fn quiet_debug_loggers(root: &Path) -> Result> { /// Apply validated `changes`. Nothing is written unless every change is valid; previous contents are snapshotted first. pub fn save(root: &Path, meta_dir: &Path, scope: Scope, changes: &BTreeMap) -> Result { + if scope == Scope::Bots { crate::realms::guard_module(root, "companions")?; } let raws = validate(root, scope, changes)?; let schema = scope.schema(); let t = targets(root, scope)?; diff --git a/crates/coa-core/src/db.rs b/crates/coa-core/src/db.rs index 83d0381..ff94438 100644 --- a/crates/coa-core/src/db.rs +++ b/crates/coa-core/src/db.rs @@ -44,6 +44,7 @@ pub struct Db { port: u16, user: &'static str, password: Secret, + realm: crate::realms::Mode, } #[derive(Deserialize)] @@ -67,6 +68,8 @@ fn no_window(cmd: &mut Command) { pub const SCHEMAS: [(&str, &str); 3] = [("characters", "acore_characters"), ("auth", "acore_auth"), ("world", "acore_world")]; pub fn schema_of(kind: &str) -> Result<&'static str> { + if let Some(kind) = kind.strip_prefix("wildcard-") { return crate::realms::Mode::Wildcard.schema(kind); } + if let Some(kind) = kind.strip_prefix("coa-") { return crate::realms::Mode::Coa.schema(kind); } SCHEMAS.iter().find(|(k, _)| *k == kind).map(|(_, s)| *s).ok_or_else(|| Error::Invalid(format!("unknown database {kind}"))) } @@ -87,7 +90,38 @@ impl Db { Account::Admin => ("root", creds.root_password), Account::App => ("acore", creds.app_password), }; - Ok(Db { bin, port: ports.mysql, user, password: Secret(password) }) + Ok(Db { bin, port: ports.mysql, user, password: Secret(password), realm: crate::realms::state(root)?.active }) + } + + pub fn for_realm(mut self, realm: crate::realms::Mode) -> Self { self.realm = realm; self } + + pub fn realm_schema<'a>(&self, name: &'a str) -> &'a str { + if self.realm == crate::realms::Mode::Wildcard { + match name { + "acore_world" => "acore_world_wildcard", + "acore_characters" => "acore_characters_wildcard", + _ => name, + } + } else { name } + } + + pub fn clone_structure(&self, source: &str, dest: &str, cache: &Path) -> Result<()> { + if !ident_ok(source) || !ident_ok(dest) { return Err(Error::Invalid("Invalid schema name.".into())); } + let path = cache.join("characters-structure.sql"); + self.dump_structure_to(source, &path)?; + self.run_sql_file(dest, &path)?; + std::fs::remove_file(path)?; + Ok(()) + } + + pub fn dump_structure_to(&self, source: &str, path: &Path) -> Result<()> { + if !ident_ok(source) { return Err(Error::Invalid("Invalid schema name.".into())); } + let mut c = self.command("mysqldump.exe"); + c.args(["--no-data", "--no-tablespaces", "--skip-comments", "--default-character-set=utf8mb4", source]); + c.stdout(Stdio::from(File::create(&path)?)); + let out = c.output()?; + if !out.status.success() { return Err(self.fail(&out.stderr)); } + Ok(()) } fn command(&self, tool: &str) -> Command { @@ -123,9 +157,10 @@ impl Db { pub fn query(&self, sql: &str) -> Result { let mut c = self.command("mysql.exe"); c.args(["--batch", "--skip-column-names", "--connect-timeout=5"]); + c.args(["--default-character-set=utf8mb4", "--max-allowed-packet=128M"]); c.stdin(Stdio::piped()); let mut child = c.spawn()?; - child.stdin.take().expect("piped").write_all(sql.as_bytes())?; + child.stdin.take().expect("piped").write_all(route_sql(sql, self.realm).as_bytes())?; let out = child.wait_with_output()?; if !out.status.success() { return Err(self.fail(&out.stderr)); @@ -134,6 +169,7 @@ impl Db { } pub fn schema_exists(&self, name: &str) -> Result { + let name = self.realm_schema(name); if !ident_ok(name) { return Err(Error::Invalid("bad schema name".into())); } @@ -141,6 +177,7 @@ impl Db { } pub fn tables(&self, schema: &str) -> Result> { + let schema = self.realm_schema(schema); if !ident_ok(schema) { return Err(Error::Invalid("bad schema name".into())); } @@ -150,6 +187,7 @@ impl Db { /// Bytes of data + index the schema occupies (used to estimate backup size). pub fn schema_bytes(&self, schema: &str) -> Result { + let schema = self.realm_schema(schema); if !ident_ok(schema) { return Err(Error::Invalid("bad schema name".into())); } @@ -159,6 +197,7 @@ impl Db { /// Stored routines / triggers / views make a table-level swap unsafe; count them. pub fn extra_objects(&self, schema: &str) -> Result { + let schema = self.realm_schema(schema); if !ident_ok(schema) { return Err(Error::Invalid("bad schema name".into())); } @@ -170,6 +209,7 @@ impl Db { /// Consistent dump of one schema, zstd-compressed to `out`. Returns (compressed bytes, sha256 of the file). pub fn dump_to(&self, schema: &str, out: &Path) -> Result<(u64, String)> { + let schema = self.realm_schema(schema); if !ident_ok(schema) { return Err(Error::Invalid("bad schema name".into())); } @@ -203,9 +243,15 @@ impl Db { /// Run a plain SQL file against `schema`; the first error aborts and is returned. pub fn run_sql_file(&self, schema: &str, file: &Path) -> Result<()> { + let schema = self.realm_schema(schema); if !ident_ok(schema) { return Err(Error::Invalid("bad schema name".into())); } + if self.realm == crate::realms::Mode::Wildcard { + let sql = std::fs::read_to_string(file)?; + self.query(&format!("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; USE `{schema}`;\n{sql}"))?; + return Ok(()); + } let mut c = self.command("mysql.exe"); c.args(["--default-character-set=utf8mb4", "--init-command=SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci", "--max-allowed-packet=128M"]); c.arg(schema); @@ -219,6 +265,7 @@ impl Db { /// Import a dump produced by `dump_to` into `schema` (which must already exist and be empty). pub fn import_from(&self, schema: &str, dump: &Path) -> Result<()> { + let schema = self.realm_schema(schema); if !ident_ok(schema) { return Err(Error::Invalid("bad schema name".into())); } @@ -278,3 +325,50 @@ pub fn write_console_credentials(root: &Path, password: &str) -> Result<()> { pub fn valid_identifier(name: &str) -> bool { ident_ok(name) } + +// Route only SQL identifiers. User names, values, comments and longer identifiers remain untouched. +fn route_sql(sql: &str, realm: crate::realms::Mode) -> String { + if realm == crate::realms::Mode::Coa { return sql.into(); } + let bytes = sql.as_bytes(); + let mut out = String::new(); + let mut i = 0; + while i < bytes.len() { + let start = i; + if matches!(bytes[i], b'\'' | b'"') { + let quote = bytes[i]; i += 1; + while i < bytes.len() { + if bytes[i] == b'\\' { i = (i + 2).min(bytes.len()); } + else if bytes[i] == quote { + i += 1; + if i < bytes.len() && bytes[i] == quote { i += 1; } else { break; } + } else { i += 1; } + } + out.push_str(&sql[start..i]); + } else if bytes[i] == b'#' || sql[i..].starts_with("--") || sql[i..].starts_with("/*") { + if sql[i..].starts_with("/*") { + i = sql[i + 2..].find("*/").map(|p| i + p + 4).unwrap_or(bytes.len()); + } else { i = sql[i..].find('\n').map(|p| i + p).unwrap_or(bytes.len()); } + out.push_str(&sql[start..i]); + } else if bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_' { + while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; } + out.push_str(match &sql[start..i] { + "acore_world" => "acore_world_wildcard", + "acore_characters" => "acore_characters_wildcard", + other => other, + }); + } else { + let c = sql[i..].chars().next().unwrap(); out.push(c); i += c.len_utf8(); + } + } + out +} + +#[cfg(test)] +mod realm_tests { + use super::*; + #[test] + fn routes_schema_identifiers_without_rewriting_account_values() { + let sql = "SELECT * FROM `acore_characters`.characters WHERE name='acore_world' AND x=\"acore_characters\"; -- acore_world\nSELECT * FROM acore_world.items;"; + assert_eq!(route_sql(sql, crate::realms::Mode::Wildcard), "SELECT * FROM `acore_characters_wildcard`.characters WHERE name='acore_world' AND x=\"acore_characters\"; -- acore_world\nSELECT * FROM acore_world_wildcard.items;"); + } +} diff --git a/crates/coa-core/src/driver.rs b/crates/coa-core/src/driver.rs index 7b9f808..992df31 100644 --- a/crates/coa-core/src/driver.rs +++ b/crates/coa-core/src/driver.rs @@ -80,6 +80,17 @@ fn launcher(root: &Path) -> Result<(PathBuf, PathBuf)> { /// Run a launcher verb to completion (blocking; call from a worker thread). pub fn run(root: &Path, verb: Verb) -> Result { let root = fsx::canonicalize_lenient(root)?; + if verb == Verb::StartAll { + crate::realms::before_start(&root)?; + if root.join("Settings/realm-profile.json").exists() { + let db = crate::db::Db::from_repack(&root, crate::db::Account::Admin)?; + if !db.ping() { + let out = run(&root, Verb::StartMysql)?; + if !out.ok { return Ok(out); } + } + crate::realms::setup_realmlist(&root)?; + } + } let (python, script) = launcher(&root)?; let mut cmd = Command::new(&python); cmd.arg("-B").arg(&script).arg(verb.arg()).current_dir(&root).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); diff --git a/crates/coa-core/src/friends.rs b/crates/coa-core/src/friends.rs index ba4ee6c..ec9cccc 100644 --- a/crates/coa-core/src/friends.rs +++ b/crates/coa-core/src/friends.rs @@ -117,7 +117,8 @@ pub fn apply_realm_address(root: &Path, host: &str) -> Result<()> { return Err(Error::Invalid("That address is not valid.".into())); } let db = Db::from_repack(root, Account::Admin)?; - db.query(&format!("UPDATE acore_auth.realmlist SET address='{host}', localAddress='{host}' WHERE id=1;"))?; + let realm = crate::realms::state(root)?.active.realm_id(); + db.query(&format!("UPDATE acore_auth.realmlist SET address='{host}', localAddress='{host}' WHERE id={realm};"))?; Ok(()) } diff --git a/crates/coa-core/src/lib.rs b/crates/coa-core/src/lib.rs index e2bf73a..1ec07a9 100644 --- a/crates/coa-core/src/lib.rs +++ b/crates/coa-core/src/lib.rs @@ -29,6 +29,7 @@ pub mod ra; pub mod allsettings; pub mod modules; pub mod realmlist; +pub mod realms; pub mod registry; pub mod release; pub mod update; diff --git a/crates/coa-core/src/modules.rs b/crates/coa-core/src/modules.rs index 9e9f76b..2b118c3 100644 --- a/crates/coa-core/src/modules.rs +++ b/crates/coa-core/src/modules.rs @@ -97,6 +97,7 @@ pub struct ModuleView { pub icon: String, pub page: Option, pub hidden: bool, + pub compatibility: String, } pub fn list(root: &Path) -> Vec { @@ -116,6 +117,7 @@ fn conf_name(root: &Path, e: &Entry) -> String { } fn list_in(cat: &[Entry], root: &Path) -> Vec { + let wildcard = crate::realms::state(root).map(|s| s.active == crate::realms::Mode::Wildcard).unwrap_or(true); cat.iter() .cloned() .map(|e| { @@ -126,14 +128,16 @@ fn list_in(cat: &[Entry], root: &Path) -> Vec { // the active file decides; without one the documented default is what the server will use let enabled = !e.switchable || read(&conf).or_else(|| read(&dist)).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"); ModuleView { + compatibility: if blocked { "unsupported" } else if wildcard && e.id != "client-compat" { "experimental" } else { "compatible" }.into(), id: e.id, name: e.name, description: e.description, repo: e.repo, installed, - enabled, - switchable: e.switchable, + enabled: enabled && !blocked, + switchable: e.switchable && !blocked, status: e.status, icon: e.icon, page: e.page, @@ -164,6 +168,7 @@ fn backup(meta: &Path, conf: &Path) -> Result<()> { } pub fn set_enabled(root: &Path, meta: &Path, id: &str, on: bool) -> Result<()> { + if on { crate::realms::guard_module(root, id)?; } set_enabled_in(&catalog(), root, meta, id, on) } @@ -217,6 +222,7 @@ fn settings_in(cat: &[Entry], root: &Path, id: &str) -> Result> { /// Change values of settings the module already has. A value is one line of at most 500 characters. pub fn save_settings(root: &Path, meta: &Path, id: &str, changes: &BTreeMap) -> Result> { + crate::realms::guard_module(root, id)?; save_settings_in(&catalog(), root, meta, id, changes) } diff --git a/crates/coa-core/src/realms.rs b/crates/coa-core/src/realms.rs new file mode 100644 index 0000000..db4c283 --- /dev/null +++ b/crates/coa-core/src/realms.rs @@ -0,0 +1,371 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::config::parser::ConfFile; +use crate::db::{Account, Db}; +use crate::error::{Error, Result}; +use crate::{fsx, layout, process}; + +const STATE: &str = "Settings/realm-profile.json"; +const JOURNAL: &str = "Settings/realm-switch.json"; + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Mode { + #[default] + Coa, + Wildcard, +} + +impl Mode { + pub fn name(self) -> &'static str { + match self { Self::Coa => "coa", Self::Wildcard => "wildcard" } + } + pub fn realm_id(self) -> u32 { if self == Self::Coa { 1 } else { 2 } } + pub fn schema(self, kind: &str) -> Result<&'static str> { + match (self, kind) { + (_, "auth") => Ok("acore_auth"), + (Self::Coa, "characters") => Ok("acore_characters"), + (Self::Coa, "world") => Ok("acore_world"), + (Self::Wildcard, "characters") => Ok("acore_characters_wildcard"), + (Self::Wildcard, "world") => Ok("acore_world_wildcard"), + _ => Err(Error::Invalid("Unknown realm database.".into())), + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RealmState { + pub active: Mode, + pub wildcard_created: bool, +} + +#[derive(Debug, Serialize)] +pub struct View { + pub active: Mode, + pub wildcard_created: bool, + pub supported: bool, + pub recovery_pending: bool, +} + +pub fn state(root: &Path) -> Result { + if !root.join(STATE).exists() { return Ok(RealmState::default()); } + fsx::read_json(&root.join(STATE)) +} + +pub fn view(root: &Path) -> Result { + let s = state(root)?; + // Inspect the binary itself; templates or an old log cannot prove installed support. + let supported = fs::read(root.join("Core/worldserver.exe"))? + .windows(b"Wildcard synergy settings".len()).any(|w| w == b"Wildcard synergy settings"); + Ok(View { active: s.active, wildcard_created: s.wildcard_created, supported, recovery_pending: root.join(JOURNAL).exists() }) +} + +pub fn guard_module(root: &Path, id: &str) -> Result<()> { + if state(root)?.active == Mode::Wildcard && matches!(id, "companions" | "playerbots") { + return Err(Error::Invalid("CoA companion class mechanics have not been validated for Wildcard. This module is unavailable on this realm.".into())); + } + Ok(()) +} + +type Files = BTreeMap>; + +fn configs(root: &Path) -> Result { + let mut files = Files::new(); + for rel in crate::backup::config_files(root) { + if rel.starts_with("Core/configs/") && rel.ends_with(".conf") + || rel.starts_with("Settings/") && rel.ends_with(".template") { + files.insert(rel.clone(), fs::read(fsx::safe_join(root, &rel)?)?); + } + } + Ok(files) +} + +fn snapshot(root: &Path, mode: Mode) -> std::path::PathBuf { + root.join(format!("Settings/realm-profiles/{}.json", mode.name())) +} + +fn write_configs(root: &Path, files: &Files) -> Result<()> { + for rel in configs(root)?.keys() { + if !files.contains_key(rel) { fs::remove_file(fsx::safe_join(root, rel)?)?; } + } + for (rel, bytes) in files { + if !(rel.starts_with("Core/configs/") && rel.ends_with(".conf") + || rel.starts_with("Settings/") && rel.ends_with(".template")) { + return Err(Error::Invalid("Invalid realm configuration path.".into())); + } + fsx::atomic_write(&fsx::ensure_within(root, &fsx::safe_join(root, rel)?)?, bytes)?; + } + Ok(()) +} + +fn set(files: &mut Files, path: &str, key: &str, value: &str) -> Result<()> { + let mut conf = ConfFile::parse_bytes(files.entry(path.into()).or_insert_with(|| b"[worldserver]\n".to_vec()))?; + conf.set(key, value, &[]); + files.insert(path.into(), conf.to_text().into_bytes()); + Ok(()) +} + +fn wildcard_configs(mut files: Files) -> Result { + for (key, value) in [ + ("CoA.ClassModel", "\"hero\""), ("CoA.GameModeMask", "64"), + ("CoA.MapClass10ToWarrior", "0"), ("CoA.RealmType", "\"live seasonal\""), + ("CoA.ClientBooleanConfigs", "\"CONFIG_LEGACY_CHARACTER_ADVANCEMENT_ENABLED=0\""), + ] { set(&mut files, "Core/configs/modules/coa.conf", key, value)?; } + set(&mut files, "Core/configs/modules/mod-coa-challenges.conf", "CoAChallenges.GameModes.Realm", "WildCard")?; + set(&mut files, "Core/configs/modules/mod-coa-challenges.conf", "CoAChallenges.GameModes.Enable", "1")?; + for (key, value) in [("ChancePercent", "65"), ("LinkWeight", "3"), ("RelatedWeight", "2"), ("SpecTagWeight", "2"), ("SchoolTagWeight", "1"), ("TooltipWeight", "3"), ("TalentsNeedTarget", "1"), ("LogRolls", "1")] { + set(&mut files, "Core/configs/modules/wildcard.conf", &format!("Wildcard.Synergy.{key}"), value)?; + } + for path in ["Settings/worldserver.conf.template", "Core/configs/worldserver.conf"] { + if files.contains_key(path) { + for (key, value) in [("RealmID", "2"), ("AlwaysMaxSkillForLevel", "1"), ("PlayerStart.CustomSpells", "1")] { + set(&mut files, path, key, value)?; + } + } + } + for (key, value) in [("CoaBots.Enable", "0"), ("CoaBots.AutoLoginOnStartup", "0")] { + set(&mut files, "Core/configs/modules/mod_coa_playerbots.conf", key, value)?; + } + Ok(files) +} + +#[derive(Serialize, Deserialize)] +struct Journal { before: RealmState, files: Files } + +fn require_world_stopped(root: &Path) -> Result<()> { + let observed = process::observe(root, &layout::read_ports(root)); + if observed.world.state != process::ServiceState::Stopped || observed.auth.state != process::ServiceState::Stopped { + return Err(Error::Invalid("Stop the server before switching realms. Your characters will be saved.".into())); + } + Ok(()) +} + +pub fn recover(root: &Path) -> Result<()> { + if !root.join(JOURNAL).exists() { return Ok(()); } + require_world_stopped(root)?; + let journal: Journal = fsx::read_json(&root.join(JOURNAL))?; + write_configs(root, &journal.files)?; + fsx::atomic_write_json(&root.join(STATE), &journal.before)?; + fs::remove_file(root.join(JOURNAL))?; + Ok(()) +} + +fn create_databases(root: &Path) -> Result<()> { + crate::backup::with_database(root, |_| { + let db = Db::from_repack(root, Account::Admin)?.for_realm(Mode::Coa); + if db.query("SELECT COUNT(*) FROM acore_auth.realmlist WHERE id=2;")? != "0" { + return Err(Error::Invalid("Realm ID 2 is already in use. Its settings were left intact.".into())); + } + for kind in ["world", "characters"] { + let source = Mode::Coa.schema(kind)?; + let dest = Mode::Wildcard.schema(kind)?; + if db.schema_exists(dest)? { + return Err(Error::Invalid(format!("Database {dest} already exists. It was left intact; inspect it before creating a realm."))); + } + if db.extra_objects(source)? != 0 { + return Err(Error::Invalid("Realm creation does not support databases with routines, triggers or views.".into())); + } + } + for (schema, tables) in [ + ("acore_world", &["ascension_wildcard_boss_marks", "ascension_wildcard_tooltip_links"][..]), + ("acore_characters", &["coa_wildcard_skill_card", "coa_wildcard_skill_card_pending", "coa_wildcard_skill_card_account", "coa_wildcard_skill_card_purchase", "coa_wildcard_specialization_cache"][..]), + ] { + let present = db.tables(schema)?; + if tables.iter().any(|table| !present.iter().any(|p| p == table)) { + return Err(Error::Invalid("Wildcard database migrations are missing. Update the server database before creating this realm.".into())); + } + } + let cache = root.join("Settings/realm-profiles/staging"); + fs::create_dir_all(&cache)?; + fsx::require_space(&cache, db.schema_bytes("acore_world")?.saturating_mul(2) + 512 * 1024 * 1024)?; + let dump = cache.join("world.sql.zst"); + db.dump_to("acore_world", &dump)?; + let stage_world = format!("coa_realm_world_{}", uuid::Uuid::new_v4().simple()); + let stage_chars = format!("coa_realm_chars_{}", uuid::Uuid::new_v4().simple()); + db.query(&format!("CREATE DATABASE `{stage_world}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE DATABASE `{stage_chars}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"))?; + db.import_from(&stage_world, &dump)?; + db.clone_structure("acore_characters", &stage_chars, &cache)?; + for table in ["active_arena_season", "addons", "updates", "updates_include", "warden_action"] { + db.query(&format!("INSERT INTO `{stage_chars}`.`{table}` SELECT * FROM acore_characters.`{table}`;"))?; + } + // The newly empty realm inherits schema migration history, never the owner's characters or cards. + if db.tables("acore_world")?.iter().any(|t| t == "coa_manager_migrations") { + db.query(&format!("CREATE TABLE IF NOT EXISTS `{stage_world}`.coa_manager_migrations LIKE acore_world.coa_manager_migrations;"))?; + } + db.query("CREATE DATABASE acore_world_wildcard CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE DATABASE acore_characters_wildcard CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;")?; + let mut renames = Vec::new(); + for (stage, dest) in [(&stage_world, "acore_world_wildcard"), (&stage_chars, "acore_characters_wildcard")] { + for table in db.tables(stage)? { + if !crate::db::valid_identifier(&table) { return Err(Error::Invalid("Unsupported table name.".into())); } + renames.push(format!("`{stage}`.`{table}` TO `{dest}`.`{table}`")); + } + } + db.query(&format!("RENAME TABLE {};", renames.join(", ")))?; + for host in db.query("SELECT host FROM mysql.user WHERE user='acore';")?.lines() { + let host = hex::encode(host.as_bytes()); + // Host names are trusted database data, but still quote them through a hex SQL literal. + db.query(&format!("SET @host=CONVERT(UNHEX('{host}') USING utf8mb4); SET @grant=CONCAT('GRANT ALL ON acore_world_wildcard.* TO ', QUOTE('acore'), '@', QUOTE(@host)); PREPARE realm_grant FROM @grant; EXECUTE realm_grant; DEALLOCATE PREPARE realm_grant; SET @grant=CONCAT('GRANT ALL ON acore_characters_wildcard.* TO ', QUOTE('acore'), '@', QUOTE(@host)); PREPARE realm_grant FROM @grant; EXECUTE realm_grant; DEALLOCATE PREPARE realm_grant;"))?; + } + db.query(&format!("DROP DATABASE `{stage_world}`; DROP DATABASE `{stage_chars}`;"))?; + fs::remove_file(&dump)?; + Ok(()) + }) +} + +pub fn select(root: &Path, mode: Mode) -> Result { + require_world_stopped(root)?; + recover(root)?; + let mut s = state(root)?; + if mode == s.active { return view(root); } + if mode == Mode::Wildcard && !view(root)?.supported { + return Err(Error::Invalid("Update the server to a build with Wildcard support first.".into())); + } + // Verify launcher support before creating databases or changing the selected realm. + prepare_launcher(root)?; + let before = configs(root)?; + fsx::atomic_write_json(&snapshot(root, s.active), &before)?; + if mode == Mode::Wildcard && !s.wildcard_created { + fsx::atomic_write_json(&snapshot(root, mode), &wildcard_configs(before.clone())?)?; + create_databases(root)?; + s.wildcard_created = true; + fsx::atomic_write_json(&root.join(STATE), &s)?; + } + let target: Files = fsx::read_json(&snapshot(root, mode))?; + // The launcher and batch files share this lock; recheck after the potentially long database copy. + 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"))?; + fs4::fs_std::FileExt::try_lock_exclusive(&lock).map_err(|_| Error::Invalid("Another start/stop action is in progress.".into()))?; + require_world_stopped(root)?; + let original = state(root)?; + fsx::atomic_write_json(&root.join(JOURNAL), &Journal { before: original, files: before })?; + let result = (|| { + write_configs(root, &target)?; + s.active = mode; + fsx::atomic_write_json(&root.join(STATE), &s)?; + Ok(()) + })(); + if let Err(e) = result { drop(lock); recover(root)?; return Err(e); } + fs::remove_file(root.join(JOURNAL))?; + view(root) +} + +/// Adapt the bundled launcher's canonical schema and realm choices without changing its supervision logic. +pub fn prepare_launcher(root: &Path) -> Result<()> { + let path = root.join("Scripts/manage.py"); + let source = fs::read_to_string(&path)?; + let patched = patch_launcher(&source)?; + if patched != source { fsx::atomic_write(&path, patched.as_bytes())?; } + Ok(()) +} + +fn patch_launcher(source: &str) -> Result { + if source.contains("# coa-manager-realm-profiles-v1") { return Ok(source.into()); } + let replacements = [ + ("(\"WorldDatabaseInfo\", \"acore_world\")", "(\"WorldDatabaseInfo\", \"acore_world_wildcard\" if coa_realm() == 'wildcard' else \"acore_world\")"), + ("(\"CharacterDatabaseInfo\", \"acore_characters\")", "(\"CharacterDatabaseInfo\", \"acore_characters_wildcard\" if coa_realm() == 'wildcard' else \"acore_characters\")"), + ("SET name='AzerothCore',address=", "SET name='{coa_realm_name()}',address="), + ("WHERE id=1;", "WHERE id={coa_realm_id()};"), + ("mysql(\"UPDATE acore_auth.realmlist SET flag=0", "mysql(f\"UPDATE acore_auth.realmlist SET flag=0"), + ]; + let mut out = source.to_string(); + for (from, to) in replacements { + if !out.contains(from) { return Err(Error::Invalid("This launcher does not support realm profiles. Update the server package first.".into())); } + out = out.replace(from, to); + } + let insert = "# coa-manager-realm-profiles-v1\ndef coa_realm():\n path = ROOT / 'Settings/realm-profile.json'\n return json.loads(path.read_text(encoding='utf-8')).get('active', 'coa') if path.exists() else 'coa'\n\ndef coa_realm_id():\n return 2 if coa_realm() == 'wildcard' else 1\n\ndef coa_realm_name():\n return 'Wildcard' if coa_realm() == 'wildcard' else 'Conquest of Azeroth'\n\n"; + // Insert before the entry point so ROOT, json and functions are all defined when called. + let marker = "if __name__ == \"__main__\":"; + if !out.contains(marker) { return Err(Error::Invalid("Unrecognised server launcher entry point.".into())); } + Ok(out.replace(marker, &format!("{insert}{marker}"))) +} + +pub fn before_start(root: &Path) -> Result<()> { + recover(root)?; + let s = state(root)?; + if !root.join(STATE).exists() { return Ok(()); } + prepare_launcher(root)?; + if s.active == Mode::Wildcard { + if !view(root)?.supported { return Err(Error::Invalid("The installed server build does not support Wildcard.".into())); } + let path = root.join("Core/configs/modules/mod_coa_playerbots.conf"); + let mut conf = ConfFile::parse_bytes(&fs::read(&path)?)?; + conf.set("CoaBots.Enable", "0", &[]); + fsx::atomic_write(&path, conf.to_text().as_bytes())?; + } + Ok(()) +} + +pub fn setup_realmlist(root: &Path) -> Result<()> { + let s = state(root)?; + if !root.join(STATE).exists() || !s.wildcard_created { return Ok(()); } + let db = Db::from_repack(root, Account::Admin)?; + let port = layout::read_ports(root).world; + db.query(&format!("INSERT INTO acore_auth.realmlist (id,name,address,localAddress,localSubnetMask,port,icon,flag,timezone,gamebuild) SELECT 2,'Wildcard',address,localAddress,localSubnetMask,{port},icon,2,timezone,gamebuild FROM acore_auth.realmlist WHERE id=1 ON DUPLICATE KEY UPDATE port={port}; UPDATE acore_auth.realmlist SET flag=2 WHERE id IN (1,2) AND id<>{};", s.active.realm_id()))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fresh_wildcard_disables_bots_and_preserves_scaling() { + let mut f = Files::new(); + f.insert("Core/configs/modules/mod-coa-content-scaling.conf".into(), b"[worldserver]\nCoAContentScaling.Enable = 1\n".to_vec()); + f.insert("Settings/worldserver.conf.template".into(), b"[worldserver]\nRealmID = 1\n".to_vec()); + let w = wildcard_configs(f.clone()).unwrap(); + assert_eq!(w["Core/configs/modules/mod-coa-content-scaling.conf"], f["Core/configs/modules/mod-coa-content-scaling.conf"]); + let c = ConfFile::parse_bytes(&w["Core/configs/modules/mod_coa_playerbots.conf"]).unwrap(); + assert_eq!(c.get("CoaBots.Enable"), Some("0")); + assert_eq!(ConfFile::parse_bytes(&w["Settings/worldserver.conf.template"]).unwrap().get("RealmID"), Some("2")); + } + + #[test] + fn corrupt_state_is_not_silently_treated_as_coa() { + let t = tempfile::tempdir().unwrap(); + fs::create_dir(t.path().join("Settings")).unwrap(); + fs::write(t.path().join(STATE), "broken").unwrap(); + assert!(state(t.path()).is_err()); + } + + #[test] + fn recovery_restores_original_settings() { + let t = tempfile::tempdir().unwrap(); + let mut files = Files::new(); + files.insert("Core/configs/modules/coa.conf".into(), b"CoA.ClassModel = coa\n".to_vec()); + fsx::atomic_write_json(&t.path().join(JOURNAL), &Journal { before: RealmState::default(), files }).unwrap(); + recover(t.path()).unwrap(); + assert_eq!(state(t.path()).unwrap().active, Mode::Coa); + assert_eq!(fs::read_to_string(t.path().join("Core/configs/modules/coa.conf")).unwrap(), "CoA.ClassModel = coa\n"); + assert!(!t.path().join(JOURNAL).exists()); + } + + #[test] + fn reverting_to_coa_removes_wildcard_only_config_files() { + let t = tempfile::tempdir().unwrap(); + let mut coa = Files::new(); + coa.insert("Core/configs/modules/coa.conf".into(), b"[worldserver]\nCoA.ClassModel = coa\n".to_vec()); + let wildcard = wildcard_configs(coa.clone()).unwrap(); + write_configs(t.path(), &wildcard).unwrap(); + write_configs(t.path(), &coa).unwrap(); + assert!(!t.path().join("Core/configs/modules/mod-coa-challenges.conf").exists()); + assert_eq!(configs(t.path()).unwrap(), coa); + } + + #[test] + fn wildcard_blocks_bot_settings_and_enable_calls() { + let t = tempfile::tempdir().unwrap(); + fsx::atomic_write_json(&t.path().join(STATE), &RealmState { active: Mode::Wildcard, wildcard_created: true }).unwrap(); + assert!(crate::modules::set_enabled(t.path(), t.path(), "companions", true).is_err()); + assert!(crate::config::save(t.path(), t.path(), crate::config::Scope::Bots, &BTreeMap::new()).is_err()); + assert!(guard_module(t.path(), "content-scaling").is_ok()); + } + + #[test] + fn unknown_launcher_is_refused_without_partial_patching() { + assert!(patch_launcher("print('custom launcher')").is_err()); + } +} diff --git a/crates/coa-core/src/update.rs b/crates/coa-core/src/update.rs index d1fe9fa..5028c10 100644 --- a/crates/coa-core/src/update.rs +++ b/crates/coa-core/src/update.rs @@ -476,9 +476,24 @@ impl Env for RepackEnv<'_> { fn migrate(&self, manifest: &Manifest, staged: &Path) -> Result { let (root, meta) = (self.root, self.meta_dir); crate::backup::with_database(root, |db| { - crate::migrations::apply_pending(db, &manifest.migrations, staged, &|| { - Ok(crate::backup::create(root, meta, crate::backup::Kind::Database, crate::backup::Trigger::BeforeMigration, None, &|_| {})?.id) - }) + let realms = crate::realms::state(root)?; + let mut result = crate::migrations::apply_pending(db, &manifest.migrations, staged, &|| { + Ok(crate::backup::create(root, meta, crate::backup::Kind::Full, crate::backup::Trigger::BeforeMigration, None, &|_| {})?.id) + })?; + if realms.wildcard_created && result.failed.is_none() { + let other = if realms.active == crate::realms::Mode::Coa { crate::realms::Mode::Wildcard } else { crate::realms::Mode::Coa }; + let other_db = db.clone().for_realm(other); + let shared_snapshot = result.snapshot.clone(); + let migrations: Vec<_> = manifest.migrations.iter().filter(|m| m.db != "auth").cloned().collect(); + let extra = crate::migrations::apply_pending(&other_db, &migrations, staged, &|| { + if let Some(id) = &shared_snapshot { return Ok(id.clone()); } + Ok(crate::backup::create(root, meta, crate::backup::Kind::Full, crate::backup::Trigger::BeforeMigration, None, &|_| {})?.id) + })?; + result.applied.extend(extra.applied); + result.failed = extra.failed; + if result.snapshot.is_none() { result.snapshot = extra.snapshot; } + } + Ok(result) }) } diff --git a/docs/WILDCARD.md b/docs/WILDCARD.md new file mode 100644 index 0000000..aa4bc72 --- /dev/null +++ b/docs/WILDCARD.md @@ -0,0 +1,42 @@ +# Realm profiles + +Overview offers CoA and Wildcard. The installed worldserver must contain Wildcard support and the database must +already have its Wildcard migrations. The Manager refuses an unsupported binary, an occupied realm ID 2 or an +existing destination database rather than overwrite them. + +The first switch copies the CoA world into `acore_world_wildcard` and creates `acore_characters_wildcard` from +the character schema and neutral server seed tables. Existing characters, card collections and progress are not +copied. Accounts remain in shared `acore_auth`. Realm IDs 1 and 2 share the world port; only the selected world +runs, and the inactive realm is listed offline. A switch while running stops the server gracefully and restarts it. + +Configuration profiles are saved under `Settings/realm-profiles`. An interrupted switch can restore its previous +configuration from the journal. CoA companion bots cannot be enabled on Wildcard. Content Scaling and the other +optional modules retain their settings but show experimental compatibility; class mechanics need separate gameplay +validation. Switching back restores the CoA module settings. + +Quick/database backups include both character databases and shared auth. Full backups also include both worlds. +Recovery points identify their active realm; select that realm before restoring them. Server migrations apply to +both realms, with auth applied once. The server updater can replace its launcher: the Manager reapplies the realm +adapter before the next start. + +![Wildcard overview](screenshots/wildcard-demo/overview.jpg) + +![Module compatibility](screenshots/wildcard-demo/modules.jpg) + +## Validation for v0.5.0 + +- `cargo test --workspace`: 178 core tests passed; command layer and examples compiled. +- `npm run build`, translation key checks and changelog validation passed. +- Native disposable fixture on offset ports: fresh Wildcard creation and startup, CoA startup after switching back, + separate probe values and restored CoA configuration. A second fresh creation verified the arena-season seed fix. +- Full backup verified both worlds, both character databases, shared auth and configurations. +- Native migration probe applied once to each realm and once to auth; repeating it did nothing. +- Restoring Wildcard characters removed a fixture-only change and preserved the CoA probe value. +- Core gameplay wrapper: `VERIFY ALL: PASSED`, gameplay stage only, real clock. First login (7 assertions), + level rolls/reroll (31), skill card rolls (13), stat path (12). The level-roll and skill-card scenarios passed on isolated + reruns after failing in the shared batch; the wrapper records two batch-sensitive cases. +- Browser demo verified active profile, hidden Bots navigation and module compatibility labels. Screenshots show + mock IPC; native checks above exercised the actual backend and server. + +The gameplay harness required fixture-only synchronization of already applied migration history by verified SQL +hashes and its normal Windows module config path. Owned server files and databases were not modified. diff --git a/docs/screenshots/wildcard-demo/modules.jpg b/docs/screenshots/wildcard-demo/modules.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebe4fb00431ee81d22079754f45ccaa4a0e19371 GIT binary patch literal 88574 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 B8_)m% literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 648c60b..b34297d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coa-server-manager", - "version": "0.1.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coa-server-manager", - "version": "0.1.0", + "version": "0.5.0", "dependencies": { "@tauri-apps/api": "^2.12.0", "@tauri-apps/plugin-dialog": "^2.8.0", diff --git a/package.json b/package.json index 62b888a..e4e6958 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coa-server-manager", "private": true, - "version": "0.4.0", + "version": "0.5.0", "license": "AGPL-3.0-only", "type": "module", "scripts": { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ac98b30..9e0d47e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -189,6 +189,7 @@ async fn run_verb(state: &AppState, id: String, verb: Verb) -> std::result::Resu #[tauri::command] async fn start_server(state: State<'_, AppState>, id: String) -> std::result::Result { + let _guard = BusyGuard::acquire(&state, &id)?; // Servers installed before module configs were created automatically get them now (missing files only). Imported // servers only get the module files they lack (see below); nothing they have is changed. if let Ok(root) = path_of(&state, &id) { @@ -209,12 +210,14 @@ async fn start_server(state: State<'_, AppState>, id: String) -> std::result::Re }) .await; } - let out = run_verb(&state, id.clone(), Verb::StartAll).await?; + let root = path_of(&state, &id)?; + let out = blocking(move || driver::run(&root, Verb::StartAll)).await?; if out.ok { if let Ok(root) = path_of(&state, &id) { // Companions requested while the server was stopped are created now (once; a failure is only logged). let r = root.clone(); let _ = tauri::async_runtime::spawn_blocking(move || -> Result<()> { + if coa_core::realms::guard_module(&r, "companions").is_err() { return Ok(()); } let pending = meta_dir(&r)?.join("companions.pending.json"); if let Ok(v) = coa_core::fsx::read_json::(&pending) { let _ = std::fs::remove_file(&pending); @@ -278,6 +281,7 @@ async fn save_settings( changes: BTreeMap, ) -> std::result::Result { let root = path_of(&state, &id)?; + let _guard = BusyGuard::acquire(&state, &id)?; blocking(move || { let meta = meta_dir(&root)?; config::save(&root, &meta, scope, &changes) @@ -512,9 +516,35 @@ async fn modules_list(state: State<'_, AppState>, id: String) -> std::result::Re blocking(move || Ok(coa_core::modules::list(&root))).await } +#[tauri::command] +async fn realm_profiles(state: State<'_, AppState>, id: String) -> std::result::Result { + let root = path_of(&state, &id)?; + blocking(move || coa_core::realms::view(&root)).await +} + +#[tauri::command] +async fn realm_select(state: State<'_, AppState>, id: String, mode: coa_core::realms::Mode, restart: bool) -> std::result::Result { + let root = path_of(&state, &id)?; + let _guard = BusyGuard::acquire(&state, &id)?; + let result = blocking(move || { + if restart { + let out = driver::run(&root, Verb::StopAll)?; + if !out.ok { return Err(Error::Invalid(out.output)); } + } + let view = coa_core::realms::select(&root, mode)?; + if restart { + let out = driver::run(&root, Verb::StartAll)?; + if !out.ok { return Err(Error::Invalid(format!("Realm selected, but startup failed: {}", out.output))); } + } + Ok(view) + }).await; + result +} + #[tauri::command] async fn module_set_enabled(state: State<'_, AppState>, id: String, module: String, enabled: bool) -> std::result::Result<(), UiError> { let root = path_of(&state, &id)?; + let _guard = BusyGuard::acquire(&state, &id)?; blocking(move || { let (dir, _) = install_meta(&root)?; coa_core::modules::set_enabled(&root, &dir, &module, enabled) @@ -531,6 +561,7 @@ async fn module_settings(state: State<'_, AppState>, id: String, module: String) #[tauri::command] async fn module_save_settings(state: State<'_, AppState>, id: String, module: String, changes: BTreeMap) -> std::result::Result, UiError> { let root = path_of(&state, &id)?; + let _guard = BusyGuard::acquire(&state, &id)?; blocking(move || { let (dir, _) = install_meta(&root)?; coa_core::modules::save_settings(&root, &dir, &module, &changes) @@ -547,6 +578,7 @@ async fn all_settings(state: State<'_, AppState>, id: String) -> std::result::Re #[tauri::command] async fn all_settings_save(state: State<'_, AppState>, id: String, changes: BTreeMap) -> std::result::Result, UiError> { let root = path_of(&state, &id)?; + let _guard = BusyGuard::acquire(&state, &id)?; blocking(move || { let (dir, _) = install_meta(&root)?; coa_core::allsettings::save(&root, &dir, &changes) @@ -1395,6 +1427,8 @@ pub fn run() { realmlist_delete, realmlist_activate, modules_list, + realm_profiles, + realm_select, module_set_enabled, module_settings, module_save_settings, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6fd761b..219b2a5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CoA Server Manager", - "version": "0.4.0", + "version": "0.5.0", "identifier": "com.coa.servermanager", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/dev/mock.ts b/src/dev/mock.ts index 71ce41f..ee96a8b 100644 --- a/src/dev/mock.ts +++ b/src/dev/mock.ts @@ -6,9 +6,10 @@ import serverPresets from "../../schemas/presets/server.json"; export {}; (() => { - let added = false, running = false; + let added = new URLSearchParams(location.search).has("demo"), running = false; + let realm: "coa" | "wildcard" = "coa", wildcardCreated = false; const modState: Record = { "companions": true, "content-scaling": true, "client-compat": true, "tbc-content": true, "wotlk-content": true, "npc-enchanter": true }; - const report = {path:"C:\games\CoA-Repack",classification:"healthy",items:[ + const report = {path:"C:\\games\\CoA-Repack",classification:"healthy",items:[ {key:"worldserver",label:"World server",status:"found",detail:null}, {key:"authserver",label:"Auth server",status:"found",detail:null}, {key:"database_content",label:"Auth / Characters / World databases",status:"found",detail:"acore_auth, acore_characters, acore_world"}, @@ -21,13 +22,15 @@ export {}; (window as any).__TAURI_INTERNALS__ = { transformCallback: (cb: unknown)=>cb, unregisterCallback(){}, convertFileSrc:(x: unknown)=>x, invoke: async (cmd: string, args?: any)=>{ + if(cmd==="realm_profiles") return {active:realm,wildcard_created:wildcardCreated,supported:true,recovery_pending:false}; + if(cmd==="realm_select"){await new Promise(r=>setTimeout(r,600));realm=args.mode;wildcardCreated ||= realm==="wildcard";modState.companions=realm==="coa";return {active:realm,wildcard_created:wildcardCreated,supported:true,recovery_pending:false};} if(cmd==="list_servers") return added?[{id:"1",name:"CoA-Repack",path:report.path}]:[]; if(cmd==="scan_server") return report; if(cmd==="add_server"){added=true;return {id:"1",name:"CoA-Repack",path:report.path};} if(cmd==="server_status"){const s=running?"running":"stopped";return {observed:{mysql:svc("mysql",3307,s),auth:svc("auth",3724,s),world:svc("world",8085,s)},busy:false,path_exists:true};} if(cmd==="start_server"){await new Promise((r: any)=>setTimeout(r,1500));running=true;return {ok:true,exit_code:0,code:null,human:null,output:""};} if(cmd==="stop_server"){await new Promise((r: any)=>setTimeout(r,1500));running=false;return {ok:true,exit_code:0,code:null,human:null,output:""};} - if(cmd.startsWith("plugin:dialog")) return "C:\games\CoA-Repack"; + if(cmd.startsWith("plugin:dialog")) return "C:\\games\\CoA-Repack"; if(cmd==="get_settings"){const sc=args?.scope==="server"?serverSchema:botsSchema;return {scope:args?.scope,categories:sc.categories,settings:(sc.settings as any[]).map(x=>({...x,options:x.options??[],value:x.default,is_default:true,present:true,problem:null,drift:false})),unknown_keys:3,drift_keys:[],files:[]};} if(cmd==="list_presets"){const pr=args?.scope==="server"?serverPresets:botsPresets;return (pr.presets as any[]).map(p=>({id:p.id,title:p.title,description:p.description}));} if(cmd==="preview_preset"){const a=args;const sc=a.scope==="server"?serverSchema:botsSchema;const pr=(a.scope==="server"?serverPresets:botsPresets).presets as any[];const p=pr.find(x=>x.id===a.preset)??{id:a.preset??"defaults",title:"Recommended defaults",description:"Every setting returns to its default value."};const st=(sc.settings as any[]).filter(x=>x.type==="int"||x.type==="float"||x.type==="enum").slice(0,3);return {id:p.id,title:p.title,description:p.description,changes:st.map(x=>({key:x.key,title:x.title,from:x.default,to:x.type==="enum"?(x.options?.[1]?.value??x.default):(Number(x.default)||1)*2,dangerous:false}))};} @@ -56,7 +59,7 @@ export {}; if(cmd==="realmlist_save"){const w=window as any;const rl=w.__rl;if(args.profileId){const p=rl.profiles.find((x: any)=>x.id===args.profileId);p.name=args.name;p.data=args.data;return p;}if(rl.profiles.some((x: any)=>x.name.toLowerCase()===String(args.name).toLowerCase()))throw {human:{code:"unknown",title:"",message:"",actions:[]},technical:"A realmlist with that name already exists."};const p={id:String(args.name).toLowerCase().replace(/[^a-z0-9]+/g,"-"),name:args.name,data:(/\s/.test(args.data.trim())?args.data.trim():"set realmlist "+args.data.trim())+"\r\n"};rl.profiles.push(p);return p;} if(cmd==="realmlist_delete"){const rl=(window as any).__rl;rl.profiles=rl.profiles.filter((x: any)=>x.id!==args.profileId);return null;} if(cmd==="account_delete") return null; - if(cmd==="modules_list") return [{id:"companions",name:"CoA Companions",description:{en:"Companion bots for groups, dungeons and your guild.",ru:"Боты-компаньоны для групп, подземелий и гильдии."},repo:"https://github.com/Corfirean/mod-coa-playerbots",installed:true,enabled:modState["companions"],switchable:true,status:"beta",icon:"bot",page:"bots"},{id:"content-scaling",name:"Content Scaling",description:{en:"Scales dungeons and raids to your group, with solo modes.",ru:"Подстраивает подземелья и рейды под вашу группу, есть соло-режимы."},repo:"https://github.com/Corfirean/mod-coa-content-scaling",installed:true,enabled:modState["content-scaling"],switchable:true,status:"early",icon:"scaling",page:null},{id:"tbc-content",name:"TBC Content",description:{en:"Outland dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Запределья, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-tbc-content",installed:true,enabled:modState["tbc-content"],switchable:true,status:"early",icon:"tbc",page:null},{id:"wotlk-content",name:"WotLK Content",description:{en:"Northrend dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Нордскола, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-wotlk-content",installed:true,enabled:modState["wotlk-content"],switchable:true,status:"early",icon:"wotlk",page:null},{id:"ah-bot",name:"Auction House Bot",description:{en:"Fills the auction house with realistic, adjustable listings.",ru:"Наполняет аукцион реалистичными настраиваемыми лотами."},repo:"https://github.com/NathanHandley/mod-ah-bot-plus",installed:false,enabled:false,switchable:false,status:"soon",icon:"gavel",page:null,hidden:false},{id:"npc-enchanter",name:"NPC Enchanter",description:{en:"An NPC that enchants your gear for you.",ru:"НПС, который зачаровывает ваше снаряжение."},repo:"https://github.com/azerothcore/mod-npc-enchanter",installed:true,enabled:modState["npc-enchanter"],switchable:true,status:"early",icon:"sparkles",page:null,hidden:false},{id:"playerbots",name:"SQUID's Playerbots",description:{en:"An alternative bot system with random bots and full AI.",ru:"Альтернативная система ботов со случайными ботами и полным ИИ."},repo:"https://github.com/Zyth45/mod-playerbots",installed:false,enabled:false,switchable:false,status:"soon",icon:"swords",page:null,hidden:false},{id:"client-compat",name:"Client compatibility",description:{en:"Client-server link and appearance unlocks.",ru:"Связь клиента с сервером и разблокировка внешних видов."},repo:"https://github.com/Corfirean/azerothcore-wotlk-coa",installed:true,enabled:modState["client-compat"],switchable:false,status:"release",icon:"plug",page:null,hidden:true}]; + if(cmd==="modules_list") return [{id:"companions",name:"CoA Companions",description:{en:"Companion bots for groups, dungeons and your guild.",ru:"Боты-компаньоны для групп, подземелий и гильдии."},repo:"https://github.com/Corfirean/mod-coa-playerbots",installed:true,enabled:modState["companions"],switchable:true,status:"beta",icon:"bot",page:"bots"},{id:"content-scaling",name:"Content Scaling",description:{en:"Scales dungeons and raids to your group, with solo modes.",ru:"Подстраивает подземелья и рейды под вашу группу, есть соло-режимы."},repo:"https://github.com/Corfirean/mod-coa-content-scaling",installed:true,enabled:modState["content-scaling"],switchable:true,status:"early",icon:"scaling",page:null},{id:"tbc-content",name:"TBC Content",description:{en:"Outland dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Запределья, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-tbc-content",installed:true,enabled:modState["tbc-content"],switchable:true,status:"early",icon:"tbc",page:null},{id:"wotlk-content",name:"WotLK Content",description:{en:"Northrend dungeons and raids, scaled to your level range.",ru:"Подземелья и рейды Нордскола, подогнанные под ваши уровни."},repo:"https://github.com/Corfirean/mod-coa-content-scaling/tree/main/content-packs/mod-coa-wotlk-content",installed:true,enabled:modState["wotlk-content"],switchable:true,status:"early",icon:"wotlk",page:null},{id:"ah-bot",name:"Auction House Bot",description:{en:"Fills the auction house with realistic, adjustable listings.",ru:"Наполняет аукцион реалистичными настраиваемыми лотами."},repo:"https://github.com/NathanHandley/mod-ah-bot-plus",installed:false,enabled:false,switchable:false,status:"soon",icon:"gavel",page:null,hidden:false},{id:"npc-enchanter",name:"NPC Enchanter",description:{en:"An NPC that enchants your gear for you.",ru:"НПС, который зачаровывает ваше снаряжение."},repo:"https://github.com/azerothcore/mod-npc-enchanter",installed:true,enabled:modState["npc-enchanter"],switchable:true,status:"early",icon:"sparkles",page:null,hidden:false},{id:"playerbots",name:"SQUID's Playerbots",description:{en:"An alternative bot system with random bots and full AI.",ru:"Альтернативная система ботов со случайными ботами и полным ИИ."},repo:"https://github.com/Zyth45/mod-playerbots",installed:false,enabled:false,switchable:false,status:"soon",icon:"swords",page:null,hidden:false},{id:"client-compat",name:"Client compatibility",description:{en:"Client-server link and appearance unlocks.",ru:"Связь клиента с сервером и разблокировка внешних видов."},repo:"https://github.com/Corfirean/azerothcore-wotlk-coa",installed:true,enabled:modState["client-compat"],switchable:false,status:"release",icon:"plug",page:null,hidden:true}].map(m => ({...m, compatibility: realm === "wildcard" ? (["companions", "playerbots"].includes(m.id) ? "unsupported" : m.id === "client-compat" ? "compatible" : "experimental") : "compatible", switchable: realm === "wildcard" && ["companions", "playerbots"].includes(m.id) ? false : m.switchable})); if(cmd==="module_set_enabled"){ modState[args.module]=args.enabled; return null; } if(cmd==="all_settings") return [{key:"Visibility.Distance.Continents",value:"100",default:"90",doc:"Description: Visibility distance on continents. Range: 45-250 Default: 90",changed:true},{key:"Death.SicknessLevel",value:"11",default:"11",doc:"Description: Character level up to which characters get resurrection sickness. Default: 11",changed:false},{key:"Rate.Drop.Item.Epic",value:"3",default:"1",doc:"Description: Drop rates for money and items based on quality. Default: 1",changed:true},{key:"Instance.ResetTimeHour",value:"4",default:"4",doc:"Description: Hour of the day when the global instance reset occurs. Range: 0-23 Default: 4 - (04:00 AM)",changed:false},{key:"PlayerLimit",value:"1000",default:"1000",doc:"Description: Maximum number of players in the world.",changed:false}]; if(cmd==="all_settings_save") return Object.keys(args&&args.changes||{}); diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 5463747..3470f3b 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -697,6 +697,18 @@ const de: Partial> = { "rep.copied": "In die Zwischenablage kopiert.", // Realmlist switcher and modules page + "realm.profileLabel": "Realm", + "realm.switching": "Realm wird gewechselt…", + "realm.create": "Wildcard-Realm erstellen", + "realm.restart": "Wechseln und neu starten", + "realm.select": "Realm auswählen", + "realm.active": "Aktiv: {name}", + "realm.separate": "Es läuft jeweils eine Welt. Jeder Realm behält eigene Charaktere, Fortschritte und Moduleinstellungen.", + "realm.modules": "CoA-Begleiter sind hier nicht verfügbar. Content Scaling und andere Module sind auf Wildcard experimentell.", + "realm.update": "Aktualisiere den Server auf eine Version mit Wildcard-Unterstützung, um diesen Realm zu erstellen.", + "realm.recovery": "Ein Realm-Wechsel wurde unterbrochen. Wähle einen Realm, um die gespeicherte Konfiguration wiederherzustellen.", + "realm.unsupported": "Auf Wildcard nicht verfügbar: Die Klassenmechanik wurde noch nicht geprüft.", + "realm.experimental": "Experimentell auf Wildcard · Spielkompatibilität noch nicht geprüft.", "realm.label": "Realm", "realm.other": "Anderer", "realm.add": "Realmlist hinzufügen …", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 04fd349..52525dc 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1,5 +1,17 @@ /** English is the source of truth: every key must exist here; other locales fall back to it. */ const en = { + "realm.profileLabel": "Realm", + "realm.switching": "Switching realm…", + "realm.create": "Create Wildcard realm", + "realm.restart": "Switch and restart", + "realm.select": "Select realm", + "realm.active": "Active: {name}", + "realm.separate": "One world runs at a time. Each realm keeps its own characters, progress and module settings.", + "realm.modules": "CoA companions are unavailable here. Content Scaling and other modules remain experimental on Wildcard.", + "realm.update": "Update the server to a build with Wildcard support to create this realm.", + "realm.recovery": "A realm switch was interrupted. Select a realm to recover the saved configuration.", + "realm.unsupported": "Unavailable on Wildcard: class mechanics have not been validated.", + "realm.experimental": "Experimental on Wildcard · gameplay compatibility is not yet verified.", "app.name": "CoA Server Manager", "nav.overview": "Overview", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 7cb5269..8cd44d3 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -697,6 +697,18 @@ const es: Partial> = { "rep.copied": "Copiado al portapapeles.", // Realmlist switcher and modules page + "realm.profileLabel": "Reino", + "realm.switching": "Cambiando de reino…", + "realm.create": "Crear reino Wildcard", + "realm.restart": "Cambiar y reiniciar", + "realm.select": "Seleccionar reino", + "realm.active": "Activo: {name}", + "realm.separate": "Solo funciona un mundo a la vez. Cada reino conserva sus personajes, progreso y ajustes de módulos.", + "realm.modules": "Los compañeros de CoA no están disponibles aquí. Content Scaling y los demás módulos siguen siendo experimentales en Wildcard.", + "realm.update": "Actualiza el servidor a una versión compatible con Wildcard para crear este reino.", + "realm.recovery": "Se interrumpió un cambio de reino. Selecciona un reino para recuperar la configuración guardada.", + "realm.unsupported": "No disponible en Wildcard: las mecánicas de clase aún no se han validado.", + "realm.experimental": "Experimental en Wildcard · compatibilidad en juego sin verificar.", "realm.label": "Reino", "realm.other": "Otro", "realm.add": "Añadir una realmlist…", diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index f79e5db..8ebab1b 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -697,6 +697,18 @@ const fr: Partial> = { "rep.copied": "Copié dans le presse-papiers.", // Realmlist switcher and modules page + "realm.profileLabel": "Royaume", + "realm.switching": "Changement de royaume…", + "realm.create": "Créer le royaume Wildcard", + "realm.restart": "Changer et redémarrer", + "realm.select": "Choisir le royaume", + "realm.active": "Actif : {name}", + "realm.separate": "Un seul monde fonctionne à la fois. Chaque royaume conserve ses personnages, sa progression et les paramètres de ses modules.", + "realm.modules": "Les compagnons CoA sont indisponibles ici. Content Scaling et les autres modules restent expérimentaux sur Wildcard.", + "realm.update": "Mettez le serveur à jour vers une version compatible avec Wildcard pour créer ce royaume.", + "realm.recovery": "Un changement de royaume a été interrompu. Choisissez un royaume pour restaurer la configuration enregistrée.", + "realm.unsupported": "Indisponible sur Wildcard : les mécaniques de classe ne sont pas encore validées.", + "realm.experimental": "Expérimental sur Wildcard · compatibilité en jeu non vérifiée.", "realm.label": "Royaume", "realm.other": "Autre", "realm.add": "Ajouter une realmlist…", diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index d61937c..f93dda0 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -2,6 +2,18 @@ import type en from "./en"; /** Русский. Черновой перевод; требует проверки носителем перед релизом. */ const ru: Partial> = { + "realm.profileLabel": "Реалм", + "realm.switching": "Переключение реалма…", + "realm.create": "Создать реалм Wildcard", + "realm.restart": "Переключить и перезапустить", + "realm.select": "Выбрать реалм", + "realm.active": "Активен: {name}", + "realm.separate": "Работает один мир. У каждого реалма свои персонажи, прогресс и настройки модулей.", + "realm.modules": "CoA-боты здесь недоступны. Скалирование и другие модули пока экспериментальны для Wildcard.", + "realm.update": "Для создания реалма обновите сервер до сборки с поддержкой Wildcard.", + "realm.recovery": "Переключение прервалось. Выберите реалм, чтобы восстановить сохранённые настройки.", + "realm.unsupported": "Недоступен в Wildcard: классовые механики ещё не проверены.", + "realm.experimental": "Экспериментально в Wildcard · игровая совместимость пока не проверена.", "nav.overview": "Обзор", "nav.bots": "Боты", "nav.server": "Сервер", diff --git a/src/lib/api.ts b/src/lib/api.ts index d66a96f..0b5c95e 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -50,6 +50,9 @@ export interface StatusView { path_exists: boolean; } +export type RealmMode = "coa" | "wildcard"; +export interface RealmProfiles { active: RealmMode; wildcard_created: boolean; supported: boolean; recovery_pending: boolean } + export interface Human { code: string; title: string; @@ -149,6 +152,7 @@ export interface SaveReport { export type BackupKind = "quick" | "full" | "config" | "database"; export interface RecoveryPoint { + realm?: RealmMode; schema: number; id: string; kind: BackupKind; @@ -193,6 +197,7 @@ export interface ModuleView { page: string | null; /** not shown on the Modules page */ hidden: boolean; + compatibility?: "compatible" | "experimental" | "unsupported"; } export interface ModuleSetting { @@ -361,6 +366,8 @@ export interface ConsoleLine { } export const api = { + realmProfiles: (id: string) => invoke("realm_profiles", { id }), + realmSelect: (id: string, mode: RealmMode, restart: boolean) => invoke("realm_select", { id, mode, restart }), defaultInstallDir: () => invoke("default_install_dir"), scan: (path: string) => invoke("scan_server", { path }), add: (path: string) => invoke("add_server", { path }), diff --git a/src/screens/BackupsPage.tsx b/src/screens/BackupsPage.tsx index 288ac9e..84144f5 100644 --- a/src/screens/BackupsPage.tsx +++ b/src/screens/BackupsPage.tsx @@ -33,7 +33,11 @@ export function BackupsPage({ serverId }: { serverId: string }) { const human = useHuman(); const w = (iso: string) => when(iso, t, locale); const trigger = (x: string) => (TRIGGER_KEYS.includes(x) ? t(`bk.trigger.${x}` as Key) : x); - const dbLabel = (x: string) => (DB_KEYS.includes(x) ? t(`bk.db.${x}` as Key) : x); + const dbLabel = (x: string) => { + const [realm, kind] = x.split("-"); + if (kind && DB_KEYS.includes(kind)) return `${realm === "coa" ? "CoA" : "Wildcard"} · ${t(`bk.db.${kind}` as Key)}`; + return DB_KEYS.includes(x) ? t(`bk.db.${x}` as Key) : x; + }; const [points, setPoints] = useState(null); const [busy, setBusy] = useState(null); const [step, setStep] = useState(null); @@ -174,7 +178,7 @@ export function BackupsPage({ serverId }: { serverId: string }) {

    {p.label && p.trigger === "manual" && p.label !== "e2e" ? p.label : trigger(p.trigger)}

    - {w(p.created_at)} · {t(`bk.kind.${p.kind}` as Key)} · {size(p.components.reduce((a, c) => a + c.bytes, 0))} + {w(p.created_at)} · {p.realm === "wildcard" ? "Wildcard" : "CoA"} · {t(`bk.kind.${p.kind}` as Key)} · {size(p.components.reduce((a, c) => a + c.bytes, 0))}

    diff --git a/src/screens/ModulesPage.tsx b/src/screens/ModulesPage.tsx index e3907c9..7309123 100644 --- a/src/screens/ModulesPage.tsx +++ b/src/screens/ModulesPage.tsx @@ -134,6 +134,8 @@ export function ModulesPage({ serverId, onChanged }: { serverId: string; onChang

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

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

    {t("realm.unsupported")}

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

    {t("realm.experimental")}

    } {m.installed && m.switchable && ( )} - {m.installed && !m.switchable && m.status !== "soon" && {t("mod.alwaysOn")}} + {m.installed && !m.switchable && m.status !== "soon" && m.compatibility !== "unsupported" && {t("mod.alwaysOn")}} {!m.installed && m.status !== "soon" && {t("mod.notHere")}}
    @@ -158,7 +160,7 @@ export function ModulesPage({ serverId, onChanged }: { serverId: string; onChang > - {m.installed && m.status !== "soon" && ( + {m.installed && m.status !== "soon" && m.compatibility !== "unsupported" && ( diff --git a/src/screens/Overview.tsx b/src/screens/Overview.tsx index cab9a83..008e563 100644 --- a/src/screens/Overview.tsx +++ b/src/screens/Overview.tsx @@ -8,6 +8,7 @@ import { applyServerUpdate, useServerUpdate } from "@/lib/serverUpdate"; import { checkClient, useClientStatus } from "@/lib/clientUpdate"; import { ClientDialog, type ClientDialogMode } from "@/screens/ClientDialog"; import { RealmlistMenu } from "@/screens/RealmlistMenu"; +import { RealmPicker } from "@/screens/RealmPicker"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; @@ -63,11 +64,12 @@ function Row({ label, s }: { label: string; s: ServiceStatus }) { ); } -export function Overview({ server, companions = true, onForget, onOpenUpdates }: { server: ServerSummary; /** the companions module is on */ companions?: boolean; onForget: () => Promise; onOpenUpdates: () => void }) { +export function Overview({ server, companions = true, onForget, onOpenUpdates, onRealmChanged }: { server: ServerSummary; /** the companions module is on */ companions?: boolean; onForget: () => Promise; onOpenUpdates: () => void; onRealmChanged: () => void }) { const t = useT(); const human = useHuman(); const [status, setStatus] = useState(null); const [action, setAction] = useState(null); + const [realmBusy, setRealmBusy] = useState(false); const [failure, setFailure] = useState(null); const [showDetails, setShowDetails] = useState(false); const [pop, setPop] = useState(null); @@ -197,10 +199,10 @@ export function Overview({ server, companions = true, onForget, onOpenUpdates }: const all = [mysql, auth, world]; const running = all.every((s) => s.state === "running"); const anyUp = all.some((s) => s.state !== "stopped"); - const transitioning = action !== null || status.busy; + const transitioning = action !== null || status.busy || realmBusy; const conflict = all.find((s) => s.conflict)?.conflict ?? null; - const headline = transitioning + const headline = realmBusy ? t("realm.switching") : transitioning ? action === "stopping" ? t("status.stopping") : t("status.starting") @@ -215,6 +217,7 @@ export function Overview({ server, companions = true, onForget, onOpenUpdates }:

    {server.name}

    {server.path}

    + { onRealmChanged(); void poll(); }} />
    {t("overview.server")}
    diff --git a/src/screens/RealmPicker.tsx b/src/screens/RealmPicker.tsx new file mode 100644 index 0000000..6751db8 --- /dev/null +++ b/src/screens/RealmPicker.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react"; +import { Layers, Loader2 } from "lucide-react"; +import { api, asUiError, type RealmMode, type RealmProfiles } from "@/lib/api"; +import { useT } from "@/i18n"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; + +export function RealmPicker({ serverId, running, disabled, onChanged, onBusy }: { + serverId: string; running: boolean; disabled: boolean; onChanged: () => void; onBusy: (busy: boolean) => void; +}) { + const t = useT(); + const [view, setView] = useState(null); + const [selected, setSelected] = useState("coa"); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + useEffect(() => { + let alive = true; + void api.realmProfiles(serverId).then(v => { if (alive) { setView(v); setSelected(v.active); } }).catch(e => { if (alive) setError(asUiError(e).technical); }); + return () => { alive = false; }; + }, [serverId]); + + async function select() { + setBusy(true); onBusy(true); setError(null); + try { const next = await api.realmSelect(serverId, selected, running); setView(next); onChanged(); } + catch (e) { + setError(asUiError(e).technical); + // Selection may have committed even if the subsequent server restart failed. + try { setView(await api.realmProfiles(serverId)); onChanged(); } catch { /* keep the operation error */ } + } + finally { setBusy(false); onBusy(false); } + } + + return +
    + + + + {view && (selected !== view.active || view.recovery_pending) && } + {view && {t("realm.active", { name: view.active === "coa" ? "CoA" : "Wildcard" })}} +
    +

    {t("realm.separate")}

    + {selected === "wildcard" &&

    {t("realm.modules")}

    } + {view && !view.supported &&

    {t("realm.update")}

    } + {view?.recovery_pending &&

    {t("realm.recovery")}

    } + {error &&

    {error}

    } +
    ; +} diff --git a/src/screens/Shell.tsx b/src/screens/Shell.tsx index b9cd991..8dfe98c 100644 --- a/src/screens/Shell.tsx +++ b/src/screens/Shell.tsx @@ -51,7 +51,7 @@ export function Shell(props: { const reloadModules = useCallback(() => { void api .modulesList(props.activeId) - .then((list) => setHiddenPages(list.filter((m) => m.page && (!m.installed || (m.switchable && !m.enabled))).map((m) => m.page as string))) + .then((list) => setHiddenPages(list.filter((m) => m.page && (!m.installed || m.compatibility === "unsupported" || (m.switchable && !m.enabled))).map((m) => m.page as string))) .catch(() => setHiddenPages([])); }, [props.activeId]); useEffect(() => { @@ -122,7 +122,7 @@ export function Shell(props: {
    {page === "overview" ? ( - props.onForget(server.id)} onOpenUpdates={() => setPage("settings")} /> + props.onForget(server.id)} onOpenUpdates={() => setPage("settings")} onRealmChanged={reloadModules} /> ) : page === "bots" || page === "server" ? ( ) : page === "console" ? ( diff --git a/tools/make-fixture.ps1 b/tools/make-fixture.ps1 index 1b40366..1f5f64c 100644 --- a/tools/make-fixture.ps1 +++ b/tools/make-fixture.ps1 @@ -3,7 +3,8 @@ param( [string]$Source = 'C:\games\CoA-Repack', [string]$Target = 'C:\games\coa-fixture', - [int]$PortBase = 13000 + [int]$PortBase = 13000, + [string]$DatabaseArchive = '' ) $ErrorActionPreference = 'Stop' if ((Resolve-Path $Source).Path -eq $Target) { throw 'source and target are the same' } @@ -27,7 +28,8 @@ foreach ($d in 'bin', 'lib', 'share') { Copy-Tree "mysql\$d" } Copy-Item "$Source\RELEASE.json", "$Source\MANIFEST.json" $Target # Pristine packaged database (never the live data directory). -& 'C:\Program Files\7-Zip\7z.exe' x "$Source\mysql\data.7z" "-o$Target\mysql" -y | Out-Null +if (-not $DatabaseArchive) { $DatabaseArchive = "$Source\mysql\data.7z" } +& 'C:\Program Files\7-Zip\7z.exe' x $DatabaseArchive "-o$Target\mysql" -y | Out-Null if (-not (Test-Path "$Target\mysql\data\acore_characters")) { throw 'database extraction failed' } New-Item -ItemType Junction -Path "$Target\Data" -Target "$Source\Data" | Out-Null diff --git a/vite.config.ts b/vite.config.ts index 2767b47..906b72b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,6 @@ export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, clearScreen: false, - server: { port: 1420, strictPort: true }, + server: { port: 1420, strictPort: true, watch: { ignored: ["**/target/**", "**/src-tauri/**"] } }, build: { target: "es2022", outDir: "dist" }, });