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 <noreply@anthropic.com>
This commit is contained in:
ДмитрийandClaude Sonnet 5.5 committed 2026-10-02 05:31:28 +05:00
1 parent 9444434734
commit 05397c4505
9 files changed
+157 -5

No files matched your search

+70
View File
@@ -101,6 +101,11 @@ pub struct ScanReport {
pub database_schemas: Vec<String>,
pub client: Option<ClientInfo>,
pub notes: Vec<String>,
/// 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<String>,
/// "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<ScanReport> {
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<ScanReport> {
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<PathBuf> {
if let Some(parent) = root.parent() {
if is_repack_root(parent) {
return Some(parent.to_path_buf());
}
}
let subdirs = |dir: &Path| -> Vec<PathBuf> {
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());
+5 -1
View File
@@ -113,7 +113,7 @@ const de: Partial<Record<keyof typeof en, string>> = {
"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<Record<keyof typeof en, string>> = {
"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;
+5 -1
View File
@@ -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;
+5 -1
View File
@@ -113,7 +113,7 @@ const es: Partial<Record<keyof typeof en, string>> = {
"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<Record<keyof typeof en, string>> = {
"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;
+5 -1
View File
@@ -113,7 +113,7 @@ const fr: Partial<Record<keyof typeof en, string>> = {
"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<Record<keyof typeof en, string>> = {
"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;
+5 -1
View File
@@ -113,7 +113,7 @@ const ru: Partial<Record<keyof typeof en, string>> = {
"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<Record<keyof typeof en, string>> = {
"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;
+3
View File
@@ -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;
}
+25
View File
@@ -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: {
))}
</ul>
{report.suggested_path && (
<div className="mt-4 rounded-md border border-gold/40 bg-gold/5 p-3" role="status">
<p className="font-medium">{t("import.suggest.title")}</p>
<p className="mt-1 text-sm text-muted">{t("import.suggest.text")}</p>
<p className="selectable mt-1 break-all text-sm">{report.suggested_path}</p>
<Button className="mt-2" size="sm" variant="primary" disabled={busy} onClick={() => void scanPath(report.suggested_path!)}>
{t("import.suggest.use")}
</Button>
</div>
)}
{report.hint === "not-repack" && <p className="mt-3 text-sm text-warn">{t("import.notRepack")}</p>}
{report.notes.map((n) => (
<p key={n} className="mt-3 text-sm text-warn">
{n}
+34
View File
@@ -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)