From 05397c4505f153ce7bf39af8b08dff8fad22de4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 2 Oct 2026 05:31:28 +0500 Subject: [PATCH] Import: when the chosen folder is the Core folder or an outer folder, offer the server's main folder; say clearly when it is a standalone AzerothCore server Co-Authored-By: Claude Sonnet 5.5 --- crates/coa-core/src/layout.rs | 70 +++++++++++++++++++++++++++++++++++ src/i18n/locales/de.ts | 6 ++- src/i18n/locales/en.ts | 6 ++- src/i18n/locales/es.ts | 6 ++- src/i18n/locales/fr.ts | 6 ++- src/i18n/locales/ru.ts | 6 ++- src/lib/api.ts | 3 ++ src/screens/ImportServer.tsx | 25 +++++++++++++ tools/add_import_keys.py | 34 +++++++++++++++++ 9 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 tools/add_import_keys.py diff --git a/crates/coa-core/src/layout.rs b/crates/coa-core/src/layout.rs index a30f095..1db1d33 100644 --- a/crates/coa-core/src/layout.rs +++ b/crates/coa-core/src/layout.rs @@ -101,6 +101,11 @@ pub struct ScanReport { pub database_schemas: Vec, pub client: Option, pub notes: Vec, + /// When the chosen folder is not a server but a repack's main folder is right next to it (its parent, or a folder + /// inside it): the folder the person most likely meant. + pub suggested_path: Option, + /// "not-repack": a server was found but it is not laid out like the CoA Repack the Manager works with. + pub hint: Option<&'static str>, /// Always false: a scan never modifies anything. pub modifies_files: bool, } @@ -294,6 +299,16 @@ pub fn scan(root: &Path) -> Result { notes.push("This looks like a game client folder, not a server folder.".into()); } + let (suggested_path, hint) = if classification == Classification::Incompatible && !looks_like_client { + match find_repack_nearby(&root) { + Some(p) => (Some(p.to_string_lossy().into_owned()), None), + None if root.join("worldserver.exe").is_file() || has_exes => (None, Some("not-repack")), + None => (None, None), + } + } else { + (None, None) + }; + Ok(ScanReport { path: root.to_string_lossy().into_owned(), classification, @@ -308,10 +323,34 @@ pub fn scan(root: &Path) -> Result { database_schemas, client, notes, + suggested_path, + hint, modifies_files: false, }) } +fn is_repack_root(p: &Path) -> bool { + p.join("Core/worldserver.exe").is_file() && p.join("Scripts/manage.py").is_file() +} + +/// People often pick the `Core` folder (it holds worldserver.exe) or the folder an archive was unpacked into, with the real +/// server one level inside. Look at the parent, then at the folders inside (two levels), for the repack's main folder. +fn find_repack_nearby(root: &Path) -> Option { + if let Some(parent) = root.parent() { + if is_repack_root(parent) { + return Some(parent.to_path_buf()); + } + } + let subdirs = |dir: &Path| -> Vec { + fs::read_dir(dir).map(|rd| rd.flatten().map(|e| e.path()).filter(|p| p.is_dir()).take(200).collect()).unwrap_or_default() + }; + let first = subdirs(root); + if let Some(p) = first.iter().find(|p| is_repack_root(p)) { + return Some(p.clone()); + } + first.iter().flat_map(|d| subdirs(d)).find(|p| is_repack_root(p)) +} + /// Paths of the executables used by an installation, if present. pub fn executables(root: &Path) -> (PathBuf, PathBuf, PathBuf) { (root.join("Core/worldserver.exe"), root.join("Core/authserver.exe"), root.join("mysql/bin/mysqld.exe")) @@ -405,6 +444,37 @@ mod tests { assert_eq!(scan(&root).unwrap().classification, Classification::UnknownCustom); } + #[test] + fn the_core_folder_or_an_outer_folder_points_at_the_real_server_folder() { + let dir = tempfile::tempdir().unwrap(); + let repack = dir.path().join("CoA-Repack"); + testkit::fake_repack(&repack); + let core = scan(&repack.join("Core")).unwrap(); + assert_eq!(core.classification, Classification::Incompatible); + assert!(Path::new(core.suggested_path.as_deref().unwrap()).ends_with("CoA-Repack")); + + let outer = tempfile::tempdir().unwrap(); + testkit::fake_repack(&outer.path().join("unpacked").join("CoA-Repack")); + let r = scan(outer.path()).unwrap(); + assert_eq!(r.classification, Classification::Incompatible); + assert!(Path::new(r.suggested_path.as_deref().unwrap()).ends_with("CoA-Repack"), "found two levels down"); + + let healthy = scan(&repack).unwrap(); + assert!(healthy.suggested_path.is_none() && healthy.hint.is_none()); + } + + #[test] + fn a_plain_azerothcore_folder_is_told_apart_from_a_wrong_folder() { + let dir = tempfile::tempdir().unwrap(); + let plain = dir.path().join("acore"); + fs::create_dir_all(&plain).unwrap(); + fs::write(plain.join("worldserver.exe"), b"w").unwrap(); + let r = scan(&plain).unwrap(); + assert_eq!(r.classification, Classification::Incompatible); + assert!(r.suggested_path.is_none()); + assert_eq!(r.hint, Some("not-repack")); + } + #[test] fn missing_folder_is_an_error_not_a_verdict() { assert!(scan(Path::new("C:/definitely/not/here")).is_err()); diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index c85a3c3..6609a49 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -113,7 +113,7 @@ const de: Partial> = { "import.custom.title": "Angepasster Server erkannt", "import.custom.text": "Dieser Build wird nicht erkannt. Einige Update- und Installationsfunktionen sind deaktiviert.", "import.incompatible.title": "Das ist kein Serverordner", - "import.incompatible.text": "Wähle den Ordner, der Core, Data und mysql (oder deine worldserver.exe) enthält.", + "import.incompatible.text": "Wähle den Hauptordner des Servers: den, der die Ordner Core, Data, mysql, Scripts und Settings enthält (nicht den Ordner Core selbst).", "import.techDetails": "Technische Details anzeigen", "import.detected": "Erkannt", "import.notInstalled": "Nicht installiert", @@ -735,6 +735,10 @@ const de: Partial> = { "all.useDefault": "Standardwert verwenden ({v})", "all.save": "{n} Änderung(en) speichern", "all.saved": "Gespeichert. Starte den Server neu, um die Änderungen zu übernehmen.", + "import.suggest.title": "Meintest du diesen Ordner?", + "import.suggest.text": "Der gewählte Ordner gehört zu einem Server. Der Hauptordner des Servers ist:", + "import.suggest.use": "Diesen Ordner verwenden", + "import.notRepack": "Das sieht nach einem eigenständigen AzerothCore-Server aus. Der Manager arbeitet mit dem Aufbau des CoA Repack (ein Ordner mit Core, mysql, Scripts und Settings). Nutze stattdessen „Neuen Server installieren“ oder binde ein CoA Repack ein.", }; export default de; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 29cbe8b..b2bbcaa 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -113,7 +113,7 @@ const en = { "import.custom.title": "Custom server detected", "import.custom.text": "This build is not recognised. Some update and install features will be disabled.", "import.incompatible.title": "This is not a server folder", - "import.incompatible.text": "Choose the folder that contains Core, Data and mysql (or your worldserver.exe).", + "import.incompatible.text": "Choose the main server folder: the one that contains the folders Core, Data, mysql, Scripts and Settings (not the Core folder itself).", "import.techDetails": "Show technical details", "import.detected": "Detected", "import.notInstalled": "Not installed", @@ -735,6 +735,10 @@ const en = { "all.useDefault": "Use the default ({v})", "all.save": "Save {n} change(s)", "all.saved": "Saved. Restart the server to apply the changes.", + "import.suggest.title": "Did you mean this folder?", + "import.suggest.text": "The folder you chose belongs to a server. The server's main folder is:", + "import.suggest.use": "Use this folder", + "import.notRepack": "This looks like a standalone AzerothCore server. The Manager works with the CoA Repack layout (a folder that contains Core, mysql, Scripts and Settings). Use \"Install a new server\" instead, or import a CoA Repack.", } as const; export default en; diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index ed940b5..14d6b58 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -113,7 +113,7 @@ const es: Partial> = { "import.custom.title": "Servidor personalizado detectado", "import.custom.text": "Esta versión no se reconoce. Algunas funciones de actualización e instalación estarán desactivadas.", "import.incompatible.title": "Esta no es una carpeta de servidor", - "import.incompatible.text": "Elige la carpeta que contiene Core, Data y mysql (o tu worldserver.exe).", + "import.incompatible.text": "Elige la carpeta principal del servidor: la que contiene las carpetas Core, Data, mysql, Scripts y Settings (no la carpeta Core en sí).", "import.techDetails": "Mostrar detalles técnicos", "import.detected": "Detectado", "import.notInstalled": "No instalado", @@ -735,6 +735,10 @@ const es: Partial> = { "all.useDefault": "Usar el valor predeterminado ({v})", "all.save": "Guardar {n} cambio(s)", "all.saved": "Guardado. Reinicia el servidor para aplicar los cambios.", + "import.suggest.title": "¿Quisiste decir esta carpeta?", + "import.suggest.text": "La carpeta elegida pertenece a un servidor. La carpeta principal del servidor es:", + "import.suggest.use": "Usar esta carpeta", + "import.notRepack": "Parece un servidor AzerothCore independiente. El Manager funciona con la estructura del CoA Repack (una carpeta que contiene Core, mysql, Scripts y Settings). Usa «Instalar un servidor nuevo» o importa un CoA Repack.", }; export default es; diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 91d1812..7cb6642 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -113,7 +113,7 @@ const fr: Partial> = { "import.custom.title": "Serveur personnalisé détecté", "import.custom.text": "Cette version n’est pas reconnue. Certaines fonctions de mise à jour et d’installation seront désactivées.", "import.incompatible.title": "Ce n’est pas un dossier de serveur", - "import.incompatible.text": "Choisissez le dossier qui contient Core, Data et mysql (ou votre worldserver.exe).", + "import.incompatible.text": "Choisissez le dossier principal du serveur : celui qui contient les dossiers Core, Data, mysql, Scripts et Settings (et non le dossier Core lui-même).", "import.techDetails": "Afficher les détails techniques", "import.detected": "Détecté", "import.notInstalled": "Non installé", @@ -735,6 +735,10 @@ const fr: Partial> = { "all.useDefault": "Utiliser la valeur par défaut ({v})", "all.save": "Enregistrer {n} modification(s)", "all.saved": "Enregistré. Redémarrez le serveur pour appliquer les changements.", + "import.suggest.title": "Vouliez-vous ce dossier ?", + "import.suggest.text": "Le dossier choisi fait partie d'un serveur. Le dossier principal du serveur est :", + "import.suggest.use": "Utiliser ce dossier", + "import.notRepack": "Cela ressemble à un serveur AzerothCore autonome. Le Manager fonctionne avec la structure du CoA Repack (un dossier contenant Core, mysql, Scripts et Settings). Utilisez plutôt « Installer un nouveau serveur » ou importez un CoA Repack.", }; export default fr; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 81e457c..3280192 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -113,7 +113,7 @@ const ru: Partial> = { "import.custom.title": "Найден нестандартный сервер", "import.custom.text": "Эта сборка не распознана. Часть функций обновления и установки будет отключена.", "import.incompatible.title": "Это не папка сервера", - "import.incompatible.text": "Выберите папку, в которой лежат Core, Data и mysql (или ваш worldserver.exe).", + "import.incompatible.text": "Выберите главную папку сервера: ту, где лежат папки Core, Data, mysql, Scripts и Settings (а не саму папку Core).", "import.techDetails": "Показать технические подробности", "import.detected": "Найдено", "import.notInstalled": "Не установлено", @@ -735,6 +735,10 @@ const ru: Partial> = { "all.useDefault": "Вернуть значение по умолчанию ({v})", "all.save": "Сохранить изменения: {n}", "all.saved": "Сохранено. Перезапустите сервер, чтобы изменения вступили в силу.", + "import.suggest.title": "Возможно, вы имели в виду эту папку?", + "import.suggest.text": "Выбранная папка является частью сервера. Главная папка сервера:", + "import.suggest.use": "Использовать эту папку", + "import.notRepack": "Похоже на отдельный сервер AzerothCore. Менеджер работает со структурой CoA Repack (папка, где лежат Core, mysql, Scripts и Settings). Выберите «Установить новый сервер» или подключите CoA Repack.", }; export default ru; diff --git a/src/lib/api.ts b/src/lib/api.ts index 92ef6cb..65f2888 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -26,6 +26,9 @@ export interface ScanReport { bot_config_keys: number; client: { path: string; executable: string } | null; notes: string[]; + /** the repack's main folder when the chosen folder is part of it */ + suggested_path?: string | null; + hint?: "not-repack" | null; modifies_files: boolean; } diff --git a/src/screens/ImportServer.tsx b/src/screens/ImportServer.tsx index 61a4ff6..39524c1 100644 --- a/src/screens/ImportServer.tsx +++ b/src/screens/ImportServer.tsx @@ -39,6 +39,19 @@ export function ImportServer(props: { } } + async function scanPath(path: string) { + setError(null); + setBusy(true); + setReport(null); + try { + setReport(await api.scan(path)); + } catch (e) { + setError(asUiError(e)); + } finally { + setBusy(false); + } + } + async function add() { if (!report) return; setBusy(true); @@ -118,6 +131,18 @@ export function ImportServer(props: { ))} + {report.suggested_path && ( +
+

{t("import.suggest.title")}

+

{t("import.suggest.text")}

+

{report.suggested_path}

+ +
+ )} + {report.hint === "not-repack" &&

{t("import.notRepack")}

} + {report.notes.map((n) => (

{n} diff --git a/tools/add_import_keys.py b/tools/add_import_keys.py new file mode 100644 index 0000000..1987007 --- /dev/null +++ b/tools/add_import_keys.py @@ -0,0 +1,34 @@ +"""One-off: import-screen guidance for a wrongly chosen folder (all locales).""" +import os, re +ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") +NEW = { + "en": {"import.incompatible.text": "Choose the main server folder: the one that contains the folders Core, Data, mysql, Scripts and Settings (not the Core folder itself).", + "import.suggest.title": "Did you mean this folder?", "import.suggest.text": "The folder you chose belongs to a server. The server's main folder is:", "import.suggest.use": "Use this folder", + "import.notRepack": "This looks like a standalone AzerothCore server. The Manager works with the CoA Repack layout (a folder that contains Core, mysql, Scripts and Settings). Use \"Install a new server\" instead, or import a CoA Repack."}, + "ru": {"import.incompatible.text": "Выберите главную папку сервера: ту, где лежат папки Core, Data, mysql, Scripts и Settings (а не саму папку Core).", + "import.suggest.title": "Возможно, вы имели в виду эту папку?", "import.suggest.text": "Выбранная папка является частью сервера. Главная папка сервера:", "import.suggest.use": "Использовать эту папку", + "import.notRepack": "Похоже на отдельный сервер AzerothCore. Менеджер работает со структурой CoA Repack (папка, где лежат Core, mysql, Scripts и Settings). Выберите «Установить новый сервер» или подключите CoA Repack."}, + "de": {"import.incompatible.text": "Wähle den Hauptordner des Servers: den, der die Ordner Core, Data, mysql, Scripts und Settings enthält (nicht den Ordner Core selbst).", + "import.suggest.title": "Meintest du diesen Ordner?", "import.suggest.text": "Der gewählte Ordner gehört zu einem Server. Der Hauptordner des Servers ist:", "import.suggest.use": "Diesen Ordner verwenden", + "import.notRepack": "Das sieht nach einem eigenständigen AzerothCore-Server aus. Der Manager arbeitet mit dem Aufbau des CoA Repack (ein Ordner mit Core, mysql, Scripts und Settings). Nutze stattdessen „Neuen Server installieren“ oder binde ein CoA Repack ein."}, + "fr": {"import.incompatible.text": "Choisissez le dossier principal du serveur : celui qui contient les dossiers Core, Data, mysql, Scripts et Settings (et non le dossier Core lui-même).", + "import.suggest.title": "Vouliez-vous ce dossier ?", "import.suggest.text": "Le dossier choisi fait partie d'un serveur. Le dossier principal du serveur est :", "import.suggest.use": "Utiliser ce dossier", + "import.notRepack": "Cela ressemble à un serveur AzerothCore autonome. Le Manager fonctionne avec la structure du CoA Repack (un dossier contenant Core, mysql, Scripts et Settings). Utilisez plutôt « Installer un nouveau serveur » ou importez un CoA Repack."}, + "es": {"import.incompatible.text": "Elige la carpeta principal del servidor: la que contiene las carpetas Core, Data, mysql, Scripts y Settings (no la carpeta Core en sí).", + "import.suggest.title": "¿Quisiste decir esta carpeta?", "import.suggest.text": "La carpeta elegida pertenece a un servidor. La carpeta principal del servidor es:", "import.suggest.use": "Usar esta carpeta", + "import.notRepack": "Parece un servidor AzerothCore independiente. El Manager funciona con la estructura del CoA Repack (una carpeta que contiene Core, mysql, Scripts y Settings). Usa «Instalar un servidor nuevo» o importa un CoA Repack."}, +} +def q(v): return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"' +for lang, keys in NEW.items(): + p = os.path.join(ROOT, "src", "i18n", "locales", lang + ".ts") + s = open(p, encoding="utf-8").read() + for k, v in keys.items(): + pat = re.compile(r'^(\s*)"%s": .*,$' % re.escape(k), re.M) + if pat.search(s): + s = pat.sub(lambda m: '%s"%s": %s,' % (m.group(1), k, q(v)), s, count=1) + else: + lines = s.split("\n") + idx = max(i for i, l in enumerate(lines) if l.startswith("}")) + lines.insert(idx, ' "%s": %s,' % (k, q(v))) + s = "\n".join(lines) + open(p, "w", encoding="utf-8", newline="\n").write(s)