feat(client): install custom race patches in player mode

This commit is contained in:
Дмитрий committed 2026-10-11 15:37:44 +05:00
1 parent cfea54e203
commit 7c866b116d
9 files changed
+27 -5

No files matched your search

@@ -0,0 +1,7 @@
---
area: manager
type: added
audience: players
title: Players can install custom race patches when connecting to a friend
---
Install the custom race client patch directly from the Game client section without hosting a server.
+5 -5
View File
@@ -668,9 +668,9 @@ async fn module_set_enabled(state: State<'_, AppState>, id: String, module: Stri
#[tauri::command]
async fn custom_races_status(state: State<'_, AppState>, id: String) -> std::result::Result<Option<coa_core::custom_races::ClientPatchStatus>, UiError> {
let root = path_of(&state, &id)?;
let ctx = client_context(&state, &id)?;
blocking(move || {
let client = linked_client(&root)?;
let client = ctx.linked();
Ok(client.as_deref().map(coa_core::custom_races::client_patch_status))
})
.await
@@ -678,12 +678,12 @@ async fn custom_races_status(state: State<'_, AppState>, id: String) -> std::res
#[tauri::command]
async fn custom_races_install(app: AppHandle, state: State<'_, AppState>, id: String) -> std::result::Result<(), UiError> {
let root = path_of(&state, &id)?;
let ctx = client_context(&state, &id)?;
let _guard = BusyGuard::acquire(&state, &id)?;
let cancel = begin_client_job(&state)?;
let sid = id.clone();
let result = blocking(move || {
let client = linked_client(&root)?.ok_or_else(|| Error::Invalid("No game client is linked to this server.".into()))?;
let client = ctx.require()?;
coa_core::custom_races::install_client_patches(&client, &cancel, &|step, percent| {
let _ = app.emit("custom-races-progress", serde_json::json!({
"id": sid,
@@ -691,7 +691,7 @@ async fn custom_races_install(app: AppHandle, state: State<'_, AppState>, id: St
"percent": percent,
}));
})?;
let enabled = coa_core::modules::list(&root).iter().any(|module| module.id == "custom-races" && module.enabled);
let enabled = ctx.root.as_ref().map_or(true, |root| coa_core::modules::list(root).iter().any(|module| module.id == "custom-races" && module.enabled));
coa_core::custom_races::set_client_patch_enabled(&client, enabled)
})
.await;
+1
View File
@@ -195,6 +195,7 @@ const de: Partial<Record<keyof typeof en, string>> = {
"comp.saved": "Gespeichert. {n} Begleiter werden beim Serverstart erstellt.",
// Client
"client.races.install": "Patch für benutzerdefinierte Völker installieren",
"client.title": "Spielclient",
"client.looking": "Suche …",
"client.intro": "Sag dem Manager, wo dein Spiel installiert ist, um einen SPIELEN-Button zu erhalten. Deine Spieldateien werden nie geändert, außer der Serveradresse und dem Begleiter-Addon.",
+1
View File
@@ -238,6 +238,7 @@ const en = {
"comp.saved": "Saved. {n} companions will be created when the server starts.",
// Client
"client.races.install": "Install custom race patch",
"client.title": "Game client",
"client.looking": "Looking…",
"client.intro": "Tell Manager where your game is installed to get a PLAY button. Your game files are never changed except the server address and the companion addon.",
+1
View File
@@ -195,6 +195,7 @@ const es: Partial<Record<keyof typeof en, string>> = {
"comp.saved": "Guardado. Se crearán {n} compañeros cuando se inicie el servidor.",
// Client
"client.races.install": "Instalar parche de razas personalizadas",
"client.title": "Cliente del juego",
"client.looking": "Buscando…",
"client.intro": "Indica al Manager dónde está instalado el juego para obtener un botón JUGAR. Tus archivos del juego nunca se modifican, salvo la dirección del servidor y el addon de compañeros.",
+1
View File
@@ -195,6 +195,7 @@ const fr: Partial<Record<keyof typeof en, string>> = {
"comp.saved": "Enregistré. {n} compagnons seront créés au démarrage du serveur.",
// Client
"client.races.install": "Installer le patch des races personnalisées",
"client.title": "Client du jeu",
"client.looking": "Recherche…",
"client.intro": "Indiquez au Manager où le jeu est installé pour obtenir un bouton JOUER. Vos fichiers de jeu ne sont jamais modifiés, sauf l’adresse du serveur et l’addon des compagnons.",
+1
View File
@@ -228,6 +228,7 @@ const ru: Partial<Record<keyof typeof en, string>> = {
"comp.saved": "Сохранено. Компаньоны ({n}) будут созданы при запуске сервера.",
// Client
"client.races.install": "Установить патч кастомных рас",
"client.title": "Клиент игры",
"client.looking": "Ищу…",
"client.intro": "Укажите, где установлена игра, чтобы появилась кнопка «Играть». Файлы игры не изменяются, кроме адреса сервера и аддона компаньонов.",
+1
View File
@@ -207,6 +207,7 @@ const zh = {
"comp.saved": "已保存。{n} 名随从将在服务器启动时创建。",
// Client
"client.races.install": "安装自定义种族补丁",
"client.title": "游戏客户端",
"client.looking": "正在寻找…",
"client.intro": "指定游戏客户端安装路径即可启用“开始游戏”按钮。除服务器地址和随从插件外,你的游戏文件绝不会被修改。",
+9
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import { open } from "@tauri-apps/plugin-dialog";
import { api, asUiError, type ClientInfo, type ClientStatus, type UiError } from "@/lib/api";
import { checkClient } from "@/lib/clientUpdate";
@@ -16,6 +17,7 @@ export function ClientCard({ serverId, remote = false }: { serverId: string; rem
const [note, setNote] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [status, setStatus] = useState<ClientStatus | null>(null);
const [patchProgress, setPatchProgress] = useState<string | null>(null);
const [dialog, setDialog] = useState<ClientDialogMode | null>(null);
const [playAfter, setPlayAfter] = useStartAfterUpdate("client", serverId);
@@ -25,6 +27,10 @@ export function ClientCard({ serverId, remote = false }: { serverId: string; rem
};
useEffect(() => {
void refresh();
const unlisten = listen<{ id: string; step: string; percent: number }>("custom-races-progress", e => {
if (e.payload.id === serverId) setPatchProgress(`${e.payload.step} (${Math.round(e.payload.percent * 100)}%)`);
});
return () => { void unlisten.then(f => f()); };
}, [serverId]); // eslint-disable-line react-hooks/exhaustive-deps
async function run(fn: () => Promise<string | void>) {
@@ -40,6 +46,7 @@ export function ClientCard({ serverId, remote = false }: { serverId: string; rem
setError(asUiError(e));
} finally {
setBusy(false);
setPatchProgress(null);
}
}
@@ -111,11 +118,13 @@ export function ClientCard({ serverId, remote = false }: { serverId: string; rem
{status && !status.managed && (
<Button size="sm" disabled={busy} onClick={() => setDialog("track")}>{t("client.track.button")}</Button>
)}
{remote && <Button size="sm" disabled={busy} onClick={() => void run(() => api.customRacesInstall(serverId))}>{t("client.races.install")}</Button>}
<Button size="sm" variant="ghost" disabled={busy} onClick={() => void choose()}>{t("client.changeFolder")}</Button>
<Button size="sm" variant="ghost" disabled={busy} onClick={() => setDialog("setup")}>{t("client.downloadInstead")}</Button>
</div>
</>
)}
{patchProgress && <p className="mt-3 text-sm text-muted" role="status">{patchProgress}</p>}
{note && <p className="mt-3 text-sm text-ok" role="status">{note}</p>}
{error && <p className="mt-3 text-sm text-bad" role="alert">{error.human.code === "unknown" ? error.technical : human(error.human).message}</p>}
{dialog && (