mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
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:
1 parent
9444434734
commit
05397c4505
9 files changed
+157
-5
No files matched your search
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user