diff --git a/crates/coa-core/src/ra.rs b/crates/coa-core/src/ra.rs index 464abee..d54db4a 100644 --- a/crates/coa-core/src/ra.rs +++ b/crates/coa-core/src/ra.rs @@ -115,6 +115,11 @@ impl Ra { Ok(first_number(&self.bot_command("botcmd despawnall")?)) } + /// Log one bot out (the character stays saved). False when that bot was not online. + pub fn despawn_bot(&mut self, guid: u64) -> Result { + Ok(self.bot_command(&format!("botcmd despawn {guid}"))?.to_lowercase().contains("despawned")) + } + /// Log every bot out and delete every bot character for good (the caller saves a recovery point first). pub fn purge_all(&mut self) -> Result { let out = self.bot_command("botcmd purgeall")?; diff --git a/index.html b/index.html index 53c9d0e..9846f57 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ CoA Server Manager +
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 84e4f77..2309ef4 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index 36c8699..4523fe9 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index dd73ed9..02b86f0 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png index f45c7cb..4c35803 100644 Binary files a/src-tauri/icons/64x64.png and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 211147b..c34c151 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index 70c001b..00ad164 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 2fff6d3..68cde01 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index 41e6ef5..2668c92 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index adce861..5f896fa 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index bd242aa..18c26d1 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index a1f2a47..4c8eee8 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index e329ffb..dbe1f0b 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index 006a7cd..d1dcfff 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index ee7e017..e27db6d 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index bda5606..99bc449 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 6cb408e..a5ed306 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index be951b5..64f2eac 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/source.png b/src-tauri/icons/source.png index 312790f..08f27d7 100644 Binary files a/src-tauri/icons/source.png and b/src-tauri/icons/source.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6733d30..55066e3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -539,6 +539,34 @@ async fn companions_take_offline(state: State<'_, AppState>, id: String) -> std: .await } +/// Log `count` randomly chosen online companions out to lower the load. Uses the per-bot command that every server +/// build has, so it works on older builds too. +#[tauri::command] +async fn companions_despawn_some(state: State<'_, AppState>, id: String, count: u32) -> std::result::Result { + let root = path_of(&state, &id)?; + let _guard = BusyGuard::acquire(&state, &id)?; + blocking(move || { + if !(1..=5000).contains(&count) { + return Err(Error::Invalid("Choose a number between 1 and 5000.".into())); + } + let prefix = coa_core::population::bot_account_prefix(&root).to_uppercase(); + let db = coa_core::db::Db::from_repack(&root, coa_core::db::Account::App)?; + let rows = db.query(&format!( + "SELECT c.guid FROM acore_characters.characters c JOIN acore_auth.account a ON a.id=c.account WHERE c.online=1 AND UPPER(a.username) LIKE '{prefix}%' ORDER BY RAND() LIMIT {count};" + ))?; + let guids: Vec = rows.lines().filter_map(|l| l.trim().parse().ok()).collect(); + let mut ra = Ra::connect(&root)?; + let mut done = 0; + for g in guids { + if ra.despawn_bot(g).unwrap_or(false) { + done += 1; + } + } + Ok(CompanionAction { count: done }) + }) + .await +} + /// Delete every companion for good. A recovery point of the characters and accounts is saved first. #[tauri::command] async fn companions_delete_all(app: AppHandle, state: State<'_, AppState>, id: String) -> std::result::Result { @@ -976,6 +1004,7 @@ pub fn run() { companions_stop_spawning, companions_take_offline, companions_delete_all, + companions_despawn_some, open_link, companion_sizes, add_companions, diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..3401816 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 77db165..5a3cdc6 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -562,6 +562,12 @@ const de: Partial> = { "comp.deleteDone": "Alle Begleiter wurden gelöscht. Zuvor wurde ein Wiederherstellungspunkt gespeichert.", "err.companion_command_unsupported.title": "Diese Serverversion kann das noch nicht", "err.companion_command_unsupported.message": "Das Bot-Modul dieses Server-Builds kennt diesen Befehl nicht. Aktualisiere den Server oder starte ihn neu, um Laufendes zu stoppen.", + + // Despawn some + "comp.despawnSome": "{n} Bots entfernen", + "comp.despawnCount": "Wie viele abmelden", + "comp.despawnHint": "Entlastet sofort: Jeder Bot online kostet Serverzeit. Sie bleiben gespeichert und kommen beim nächsten Start zurück, wenn die automatische Anmeldung an ist.", + "comp.despawnSomeDone": "{n} Begleiter abgemeldet. Sie bleiben gespeichert.", }; export default de; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index e552878..1619c83 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -562,6 +562,12 @@ const en = { "comp.deleteDone": "All companions were deleted. A recovery point was saved first.", "err.companion_command_unsupported.title": "This server version can not do that yet", "err.companion_command_unsupported.message": "The bot module of this server build has no such command. Update the server, or restart it to stop what is running.", + + // Despawn some + "comp.despawnSome": "Despawn {n} bots", + "comp.despawnCount": "How many to log out", + "comp.despawnHint": "Eases the load right away: every online bot costs server time. They stay saved and return at the next start if automatic login is on.", + "comp.despawnSomeDone": "{n} companions logged out. They stay saved.", } as const; export default en; diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index b80b19f..e9fb022 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -562,6 +562,12 @@ const es: Partial> = { "comp.deleteDone": "Se eliminaron todos los compañeros. Antes se guardó un punto de recuperación.", "err.companion_command_unsupported.title": "Esta versión del servidor aún no puede hacerlo", "err.companion_command_unsupported.message": "El módulo de bots de esta versión del servidor no tiene ese comando. Actualiza el servidor o reinícialo para detener lo que esté en marcha.", + + // Despawn some + "comp.despawnSome": "Retirar {n} bots", + "comp.despawnCount": "Cuántos desconectar", + "comp.despawnHint": "Alivia la carga al instante: cada bot conectado cuesta tiempo de servidor. Quedan guardados y vuelven en el próximo inicio si el inicio de sesión automático está activado.", + "comp.despawnSomeDone": "{n} compañeros desconectados. Quedan guardados.", }; export default es; diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 144320f..aafd1eb 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -562,6 +562,12 @@ const fr: Partial> = { "comp.deleteDone": "Tous les compagnons ont été supprimés. Un point de restauration a été enregistré d’abord.", "err.companion_command_unsupported.title": "Cette version du serveur ne sait pas encore le faire", "err.companion_command_unsupported.message": "Le module de bots de cette version du serveur n’a pas cette commande. Mettez le serveur à jour ou redémarrez-le pour arrêter ce qui est en cours.", + + // Despawn some + "comp.despawnSome": "Retirer {n} bots", + "comp.despawnCount": "Combien déconnecter", + "comp.despawnHint": "Allège aussitôt la charge : chaque bot en ligne coûte du temps serveur. Ils restent enregistrés et reviennent au prochain démarrage si la connexion automatique est activée.", + "comp.despawnSomeDone": "{n} compagnons déconnectés. Ils restent enregistrés.", }; export default fr; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index c1b5a6f..9335063 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -562,6 +562,12 @@ const ru: Partial> = { "comp.deleteDone": "Все компаньоны удалены. Сначала была сохранена точка восстановления.", "err.companion_command_unsupported.title": "Эта версия сервера пока этого не умеет", "err.companion_command_unsupported.message": "В модуле ботов этой сборки сервера нет такой команды. Обновите сервер или перезапустите его, чтобы остановить текущую работу.", + + // Despawn some + "comp.despawnSome": "Убрать ботов: {n}", + "comp.despawnCount": "Сколько убрать", + "comp.despawnHint": "Сразу снижает нагрузку: каждый бот в сети отнимает время сервера. Они остаются сохранёнными и вернутся при следующем запуске, если включён автоматический вход.", + "comp.despawnSomeDone": "Убрано из игры компаньонов: {n}. Они остаются сохранёнными.", }; export default ru; diff --git a/src/lib/api.ts b/src/lib/api.ts index 64ff23b..6bf4132 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -291,6 +291,7 @@ export const api = { population: (id: string) => invoke("get_population", { id }), stopSpawning: (id: string) => invoke<{ count: number }>("companions_stop_spawning", { id }), takeOffline: (id: string) => invoke<{ count: number }>("companions_take_offline", { id }), + despawnSome: (id: string, count: number) => invoke<{ count: number }>("companions_despawn_some", { id, count }), deleteAllCompanions: (id: string) => invoke<{ count: number }>("companions_delete_all", { id }), openLink: (url: string) => invoke("open_link", { url }), performance: (id: string) => invoke("get_performance", { id }), diff --git a/src/screens/CompanionsCard.tsx b/src/screens/CompanionsCard.tsx index 57d3c03..09870c9 100644 --- a/src/screens/CompanionsCard.tsx +++ b/src/screens/CompanionsCard.tsx @@ -23,6 +23,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) { const [job, setJob] = useState<{ baseline: number; target: number; since: number } | null>(null); const [startNote, setStartNote] = useState(null); const [startBusy, setStartBusy] = useState(false); + const [despawnN, setDespawnN] = useState("10"); useEffect(() => { void api @@ -67,6 +68,14 @@ export function CompanionsCard({ serverId }: { serverId: string }) { }); }; + const dn = Number(despawnN); + const dnValid = Number.isInteger(dn) && dn >= 1 && dn <= 5000; + const despawnSome = () => + act(async () => { + const r = await api.despawnSome(serverId, dn); + return t("comp.despawnSomeDone", { n: r.count }); + }); + const deleteAll = () => { if (!window.confirm(t("comp.deleteConfirm", { n: pop?.bots_total ?? 0 }))) return; void act(async () => { @@ -213,7 +222,22 @@ export function CompanionsCard({ serverId }: { serverId: string }) { {running && pop && pop.bots_total > 0 && (

{t("comp.manageTitle")}

-
+
+ + setDespawnN(e.target.value)} + inputMode="numeric" + aria-invalid={!dnValid} + className="w-20 rounded-md border border-line bg-bg px-3 py-1.5 text-right outline-none focus:border-gold" + /> + +
+

{t("comp.despawnHint")}

+
diff --git a/src/screens/Shell.tsx b/src/screens/Shell.tsx index 6abb0fb..602d244 100644 --- a/src/screens/Shell.tsx +++ b/src/screens/Shell.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Archive, Bot, Gauge, Globe, Server, Settings, Terminal, Users, type LucideIcon } from "lucide-react"; import { type ServerSummary } from "@/lib/api"; import { useT, type Key } from "@/i18n"; +import logo from "@/assets/logo.png"; import { cn } from "@/lib/utils"; import { Overview } from "@/screens/Overview"; import { Placeholder } from "@/screens/Placeholder"; @@ -41,7 +42,10 @@ export function Shell(props: { return (