mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 11:51:03 +00:00
feat(manager): multi-realm character database routing, RA dispatch & extended bot manager UI
This commit is contained in:
1 parent
3d0de60d84
commit
825aceffec
5 files changed
+1415
-92
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+38
-62
@@ -1771,11 +1771,12 @@ async fn get_population(
|
||||
async fn companions_login_all(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
realm: Option<coa_core::realms::Mode>,
|
||||
) -> std::result::Result<String, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
let _guard = BusyGuard::acquire(&state, &id)?;
|
||||
blocking(move || {
|
||||
let mut ra = Ra::connect(&root)?;
|
||||
let mut ra = Ra::connect_target(&root, realm)?;
|
||||
ra.login_all()
|
||||
})
|
||||
.await
|
||||
@@ -1787,11 +1788,12 @@ async fn companions_spawn_fleet(
|
||||
id: String,
|
||||
race: u8,
|
||||
level: u8,
|
||||
realm: Option<coa_core::realms::Mode>,
|
||||
) -> std::result::Result<String, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
let _guard = BusyGuard::acquire(&state, &id)?;
|
||||
blocking(move || {
|
||||
let mut ra = Ra::connect(&root)?;
|
||||
let mut ra = Ra::connect_target(&root, realm)?;
|
||||
ra.spawn_fleet(race, level)
|
||||
})
|
||||
.await
|
||||
@@ -1807,11 +1809,12 @@ struct CompanionAction {
|
||||
async fn companions_stop_spawning(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
realm: Option<coa_core::realms::Mode>,
|
||||
) -> std::result::Result<CompanionAction, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
blocking(move || {
|
||||
Ok(CompanionAction {
|
||||
count: Ra::connect(&root)?.cancel_spawning()?,
|
||||
count: Ra::connect_target(&root, realm)?.cancel_spawning()?,
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -1821,12 +1824,12 @@ async fn companions_stop_spawning(
|
||||
async fn companions_take_offline(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
realm: Option<coa_core::realms::Mode>,
|
||||
) -> std::result::Result<CompanionAction, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
let _guard = BusyGuard::acquire(&state, &id)?;
|
||||
blocking(move || {
|
||||
let mut ra = Ra::connect(&root)?;
|
||||
// Anything still waiting to be created must not come back right after.
|
||||
let mut ra = Ra::connect_target(&root, realm)?;
|
||||
let _ = ra.cancel_spawning();
|
||||
Ok(CompanionAction {
|
||||
count: ra.despawn_all()?,
|
||||
@@ -1835,13 +1838,12 @@ async fn companions_take_offline(
|
||||
.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,
|
||||
realm: Option<coa_core::realms::Mode>,
|
||||
) -> std::result::Result<CompanionAction, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
let _guard = BusyGuard::acquire(&state, &id)?;
|
||||
@@ -1849,13 +1851,18 @@ async fn companions_despawn_some(
|
||||
if !(1..=5000).contains(&count) {
|
||||
return Err(Error::Invalid("Choose a number between 1 and 5000.".into()));
|
||||
}
|
||||
let mode = realm.unwrap_or_else(|| {
|
||||
coa_core::realms::view(&root).map(|v| v.active).unwrap_or_default()
|
||||
});
|
||||
let target = coa_core::multiworld::resolve_bot_operation_target(&root, mode)?;
|
||||
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};"
|
||||
"SELECT c.guid FROM {}.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};",
|
||||
target.chars_db
|
||||
))?;
|
||||
let guids: Vec<u64> = rows.lines().filter_map(|l| l.trim().parse().ok()).collect();
|
||||
let mut ra = Ra::connect(&root)?;
|
||||
let mut ra = Ra::connect_target(&root, Some(mode))?;
|
||||
let mut done = 0;
|
||||
for g in guids {
|
||||
if ra.despawn_bot(g).unwrap_or(false) {
|
||||
@@ -1867,31 +1874,35 @@ async fn companions_despawn_some(
|
||||
.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,
|
||||
realm: Option<coa_core::realms::Mode>,
|
||||
) -> std::result::Result<CompanionAction, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
let _guard = BusyGuard::acquire(&state, &id)?;
|
||||
blocking(move || {
|
||||
let mode = realm.unwrap_or_else(|| {
|
||||
coa_core::realms::view(&root).map(|v| v.active).unwrap_or_default()
|
||||
});
|
||||
let target = coa_core::multiworld::resolve_bot_operation_target(&root, mode)?;
|
||||
let meta = meta_dir(&root)?;
|
||||
backup::create(
|
||||
&root,
|
||||
&meta,
|
||||
Kind::Quick,
|
||||
Trigger::BeforeDangerousChange,
|
||||
Some("before deleting all companions".into()),
|
||||
Some(format!("before deleting all {} companions", target.realm.label())),
|
||||
&|step| {
|
||||
let _ = app.emit("backup-progress", step);
|
||||
},
|
||||
)?;
|
||||
let before = coa_core::population::query(&root)
|
||||
let before = coa_core::population::query_for_realm(&root, mode)
|
||||
.map(|p| p.bots_total)
|
||||
.unwrap_or(0);
|
||||
let mut ra = Ra::connect(&root)?;
|
||||
let mut ra = Ra::connect_target(&root, Some(mode))?;
|
||||
let _ = ra.cancel_spawning();
|
||||
ra.purge_all()?;
|
||||
Ok(CompanionAction { count: before })
|
||||
@@ -1948,6 +1959,10 @@ async fn add_companions(
|
||||
id: String,
|
||||
count: u32,
|
||||
realm: Option<coa_core::realms::Mode>,
|
||||
level: Option<u8>,
|
||||
class_id: Option<u8>,
|
||||
spec_id: Option<u32>,
|
||||
archetype: Option<String>,
|
||||
) -> std::result::Result<CompanionsResult, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
let _guard = BusyGuard::acquire(&state, &id)?;
|
||||
@@ -1960,15 +1975,10 @@ async fn add_companions(
|
||||
let mode = realm.unwrap_or_else(|| {
|
||||
coa_core::realms::view(&root).map(|v| v.active).unwrap_or_default()
|
||||
});
|
||||
let chars_db = match mode {
|
||||
coa_core::realms::Mode::Coa => "acore_characters",
|
||||
coa_core::realms::Mode::Wildcard => "acore_characters_wildcard",
|
||||
coa_core::realms::Mode::Reborn => "acore_characters_reborn",
|
||||
};
|
||||
let target = coa_core::multiworld::resolve_bot_operation_target(&root, mode)?;
|
||||
let meta = meta_dir(&root)?;
|
||||
let mut changes = BTreeMap::new();
|
||||
changes.insert("CoaBots.AutoLoginOnStartup".to_string(), Value::Bool(true));
|
||||
// Every companion, old and new, should come back at the next start, so the start-up limit only ever grows here.
|
||||
let existing_max = config::load(&root, Scope::Bots)
|
||||
.ok()
|
||||
.and_then(|v| {
|
||||
@@ -1986,56 +1996,22 @@ async fn add_companions(
|
||||
Value::from(existing_max.max(already + count).min(5000)),
|
||||
);
|
||||
config::save(&root, &meta, Scope::Bots, &changes)?;
|
||||
let o = coa_core::process::observe(&root, &layout::read_ports(&root));
|
||||
// Large batches while the server is stopped: create them in the database right now, fully equipped (CoA schema only).
|
||||
if o.world.state == coa_core::process::ServiceState::Stopped
|
||||
&& o.auth.state == coa_core::process::ServiceState::Stopped
|
||||
&& count >= coa_core::companions::OFFLINE_MIN
|
||||
&& mode == coa_core::realms::Mode::Coa
|
||||
&& coa_core::companions::offline_factory(&root).is_some()
|
||||
{
|
||||
let started_db = o.mysql.state != coa_core::process::ServiceState::Running;
|
||||
if started_db {
|
||||
let out = driver::run(&root, Verb::StartMysql)?;
|
||||
if !out.ok {
|
||||
return Err(Error::Invalid("The database could not be started.".into()));
|
||||
}
|
||||
}
|
||||
let made = (|| -> Result<()> {
|
||||
coa_core::companions::ensure_templates_for_schema(
|
||||
&coa_core::db::Db::from_repack(&root, coa_core::db::Account::Admin)?,
|
||||
chars_db,
|
||||
)?;
|
||||
coa_core::companions::offline_create(
|
||||
&root,
|
||||
&meta.join("logs").join("companions-offline.log"),
|
||||
count,
|
||||
)?;
|
||||
Ok(())
|
||||
})();
|
||||
if started_db {
|
||||
let _ = driver::run(&root, Verb::StopAll);
|
||||
}
|
||||
made?;
|
||||
tracing::info!(count, "companions created offline");
|
||||
return Ok(CompanionsResult {
|
||||
spawned: None,
|
||||
created: Some(count),
|
||||
baseline: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mut baseline = None;
|
||||
let spawned = if o.world.state == coa_core::process::ServiceState::Running {
|
||||
// Older servers have no template characters to copy bots from: add them first.
|
||||
let spawned = if target.running {
|
||||
if let Ok(admin_db) = coa_core::db::Db::from_repack(&root, coa_core::db::Account::Admin) {
|
||||
let _ = coa_core::companions::ensure_templates_for_schema(&admin_db, chars_db);
|
||||
let _ = coa_core::companions::ensure_templates_for_schema(&admin_db, target.chars_db);
|
||||
}
|
||||
baseline = coa_core::population::query_for_realm(&root, mode)
|
||||
.ok()
|
||||
.map(|p| p.bots_total);
|
||||
Some(Ra::connect(&root)?.spawn_bots(count)?)
|
||||
let mut ra = Ra::connect_target(&root, Some(mode))?;
|
||||
if let Some(lvl) = level {
|
||||
Some(ra.spawn_custom(count, lvl, class_id, spec_id, archetype.as_deref())?)
|
||||
} else {
|
||||
Some(ra.spawn_bots(count)?)
|
||||
}
|
||||
} else {
|
||||
// The server is stopped: creating bots needs it running, so remember the request and do it after the next start.
|
||||
coa_core::fsx::atomic_write_json(
|
||||
&meta.join("companions.pending.json"),
|
||||
&serde_json::json!({ "count": count }),
|
||||
|
||||
+24
-8
@@ -813,17 +813,33 @@ export const api = {
|
||||
rollbackUpdate: (id: string, txn: string) => invoke<UpdateTxn>("rollback_update", { id, txn }),
|
||||
retryUpdateValidation: (id: string, txn: string) => invoke<UpdateTxn>("retry_update_validation", { id, txn }),
|
||||
population: (id: string, realm?: RealmMode) => invoke<Population | null>("get_population", { id, realm: realm ?? null }),
|
||||
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 }),
|
||||
loginAllCompanions: (id: string) => invoke<string>("companions_login_all", { id }),
|
||||
spawnBotFleet: (id: string, race: number, level: number) => invoke<string>("companions_spawn_fleet", { id, race, level }),
|
||||
stopSpawning: (id: string, realm?: RealmMode) => invoke<{ count: number }>("companions_stop_spawning", { id, realm: realm ?? null }),
|
||||
takeOffline: (id: string, realm?: RealmMode) => invoke<{ count: number }>("companions_take_offline", { id, realm: realm ?? null }),
|
||||
despawnSome: (id: string, count: number, realm?: RealmMode) => invoke<{ count: number }>("companions_despawn_some", { id, count, realm: realm ?? null }),
|
||||
deleteAllCompanions: (id: string, realm?: RealmMode) => invoke<{ count: number }>("companions_delete_all", { id, realm: realm ?? null }),
|
||||
loginAllCompanions: (id: string, realm?: RealmMode) => invoke<string>("companions_login_all", { id, realm: realm ?? null }),
|
||||
spawnBotFleet: (id: string, race: number, level: number, realm?: RealmMode) => invoke<string>("companions_spawn_fleet", { id, race, level, realm: realm ?? null }),
|
||||
openLink: (url: string) => invoke<void>("open_link", { url }),
|
||||
performance: (id: string) => invoke<Performance | null>("get_performance", { id }),
|
||||
companionSizes: () => invoke<CompanionSizes>("companion_sizes"),
|
||||
addCompanions: (id: string, count: number, realm?: RealmMode) =>
|
||||
invoke<{ spawned: string | null; created: number | null; baseline: number | null }>("add_companions", { id, count, realm: realm ?? null }),
|
||||
addCompanions: (
|
||||
id: string,
|
||||
count: number,
|
||||
realm?: RealmMode,
|
||||
level?: number,
|
||||
classId?: number,
|
||||
specId?: number,
|
||||
archetype?: string
|
||||
) =>
|
||||
invoke<{ spawned: string | null; created: number | null; baseline: number | null }>("add_companions", {
|
||||
id,
|
||||
count,
|
||||
realm: realm ?? null,
|
||||
level: level ?? null,
|
||||
class_id: classId ?? null,
|
||||
spec_id: specId ?? null,
|
||||
archetype: archetype ?? null,
|
||||
}),
|
||||
clientGraphics: (id: string) => invoke<{ dxvk_disabled: boolean; enhancements_disabled: boolean }>("client_graphics", { id }),
|
||||
clientSetGraphics: (id: string, name: string, disabled: boolean) => invoke<{ dxvk_disabled: boolean; enhancements_disabled: boolean }>("client_set_graphics", { id, name, disabled }),
|
||||
clientInfo: (id: string) => invoke<ClientInfo | null>("client_info", { id }),
|
||||
|
||||
@@ -55,8 +55,9 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
|
||||
const activeServerRealm = profiles?.active ?? "coa";
|
||||
const isViewingActiveRealm = selectedRealm === activeServerRealm;
|
||||
const isRealmOperational = isViewingActiveRealm || running;
|
||||
|
||||
const maxLevelForRealm = selectedRealm === "reborn" ? 60 : 80;
|
||||
const maxLevelForRealm = 60;
|
||||
|
||||
// Load realm profiles
|
||||
const loadProfiles = async () => {
|
||||
@@ -158,12 +159,27 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
setNote(null);
|
||||
try {
|
||||
if (spawnMode === "fleet") {
|
||||
// Fleet spawn: race 1 (Human) or 2 (Orc)
|
||||
const race = 1;
|
||||
const out = await api.spawnBotFleet(serverId, race, targetLevel);
|
||||
const out = await api.spawnBotFleet(serverId, race, targetLevel, selectedRealm);
|
||||
setNote(out || `Spawned level ${targetLevel} fleet`);
|
||||
} else {
|
||||
const r = await api.addCompanions(serverId, count, selectedRealm);
|
||||
const classNum = selectedClass !== "all" ? parseInt(selectedClass, 10) : undefined;
|
||||
let specNum: number | undefined = undefined;
|
||||
if (selectedRole === "tank") {
|
||||
if (selectedRealm === "reborn") {
|
||||
specNum = (classNum === 7 || classNum === 9 || classNum === 11) ? 4 : (classNum === 2 ? 2 : classNum === 1 ? 3 : 1);
|
||||
} else if (selectedRealm === "wildcard") {
|
||||
specNum = 1;
|
||||
}
|
||||
} else if (selectedRole === "healer") {
|
||||
if (selectedRealm === "reborn") {
|
||||
specNum = (classNum === 2 || classNum === 5) ? 1 : (classNum === 7 || classNum === 11 ? 3 : 1);
|
||||
} else if (selectedRealm === "wildcard") {
|
||||
specNum = 2;
|
||||
}
|
||||
}
|
||||
const archStr = selectedArchetype !== "all" ? selectedArchetype : undefined;
|
||||
const r = await api.addCompanions(serverId, count, selectedRealm, targetLevel, classNum, specNum, archStr);
|
||||
if (r.spawned && r.baseline !== null) {
|
||||
setJob({ baseline: r.baseline, target: count, since: Date.now() });
|
||||
}
|
||||
@@ -186,7 +202,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
// Bring offline bots online
|
||||
const handleLoginAll = () =>
|
||||
act(async () => {
|
||||
const out = await api.loginAllCompanions(serverId);
|
||||
const out = await api.loginAllCompanions(serverId, selectedRealm);
|
||||
await fetchPopulation();
|
||||
return out || "All available offline bots queued for login.";
|
||||
});
|
||||
@@ -194,7 +210,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
// Stop queued creations
|
||||
const stopCreating = () =>
|
||||
act(async () => {
|
||||
const r = await api.stopSpawning(serverId);
|
||||
const r = await api.stopSpawning(serverId, selectedRealm);
|
||||
setJob(null);
|
||||
await fetchPopulation();
|
||||
return t("comp.stopped", { n: r.count });
|
||||
@@ -204,7 +220,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
const takeOffline = () => {
|
||||
if (!window.confirm(t("comp.offlineConfirm"))) return;
|
||||
void act(async () => {
|
||||
const r = await api.takeOffline(serverId);
|
||||
const r = await api.takeOffline(serverId, selectedRealm);
|
||||
setJob(null);
|
||||
await fetchPopulation();
|
||||
return t("comp.offlineDone2", { n: r.count });
|
||||
@@ -216,7 +232,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
const dnValid = Number.isInteger(dn) && dn >= 1 && dn <= 5000;
|
||||
const despawnSome = () =>
|
||||
act(async () => {
|
||||
const r = await api.despawnSome(serverId, dn);
|
||||
const r = await api.despawnSome(serverId, dn, selectedRealm);
|
||||
await fetchPopulation();
|
||||
return t("comp.despawnSomeDone", { n: r.count });
|
||||
});
|
||||
@@ -225,7 +241,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
const deleteAll = () => {
|
||||
if (!window.confirm(t("comp.deleteConfirm", { n: pop?.bots_total ?? 0 }))) return;
|
||||
void act(async () => {
|
||||
await api.deleteAllCompanions(serverId);
|
||||
await api.deleteAllCompanions(serverId, selectedRealm);
|
||||
setJob(null);
|
||||
await fetchPopulation();
|
||||
return t("comp.deleteDone");
|
||||
@@ -514,17 +530,27 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
<option value="all">All Realm Classes</option>
|
||||
{selectedRealm === "coa" && (
|
||||
<>
|
||||
<option value="12">Guardian</option>
|
||||
<option value="13">Starcaller</option>
|
||||
<option value="14">Demon Hunter</option>
|
||||
<option value="15">Necromancer</option>
|
||||
<option value="16">Monk</option>
|
||||
<option value="17">Pyromancer</option>
|
||||
<option value="18">Chronomancer</option>
|
||||
<option value="19">Cultist</option>
|
||||
<option value="20">Runemaster</option>
|
||||
<option value="21">Primalist</option>
|
||||
<option value="22">Tinker</option>
|
||||
<option value="12">Barbarian</option>
|
||||
<option value="13">Witch Doctor</option>
|
||||
<option value="14">Felsworn</option>
|
||||
<option value="15">Witch Hunter</option>
|
||||
<option value="16">Stormbringer</option>
|
||||
<option value="17">Knight of Xoroth</option>
|
||||
<option value="18">Guardian</option>
|
||||
<option value="19">Templar</option>
|
||||
<option value="20">Bloodmage</option>
|
||||
<option value="21">Ranger</option>
|
||||
<option value="22">Chronomancer</option>
|
||||
<option value="23">Necromancer</option>
|
||||
<option value="24">Pyromancer</option>
|
||||
<option value="25">Cultist</option>
|
||||
<option value="26">Starcaller</option>
|
||||
<option value="27">Tinker</option>
|
||||
<option value="28">Venomancer</option>
|
||||
<option value="29">Reaper</option>
|
||||
<option value="30">Sun Cleric</option>
|
||||
<option value="31">Runemaster</option>
|
||||
<option value="32">Primalist</option>
|
||||
</>
|
||||
)}
|
||||
{selectedRealm === "wildcard" && (
|
||||
@@ -605,7 +631,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
<div className="flex flex-wrap items-center gap-3 pt-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={busy || !valid || job !== null || !isViewingActiveRealm}
|
||||
disabled={busy || !valid || job !== null || !isRealmOperational}
|
||||
onClick={() => void handleSpawn()}
|
||||
>
|
||||
{busy && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
|
||||
@@ -617,7 +643,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
{running && pop && pop.bots_total > pop.bots_online && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={busy || job !== null || !isViewingActiveRealm}
|
||||
disabled={busy || job !== null || !isRealmOperational}
|
||||
onClick={() => void handleLoginAll()}
|
||||
title="Bring all created bots online"
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user