From 5c0852f97557579ba5a47db0bd0ddfc3a9aaca40 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sat, 17 Oct 2026 16:00:16 +0700 Subject: [PATCH] up --- proto/main.proto | 18 ++- .../crabs/evening_detective_server/index.ts | 31 +++++ src/components/GamePage.vue | 115 ++++++++++++++---- 3 files changed, 142 insertions(+), 22 deletions(-) diff --git a/proto/main.proto b/proto/main.proto index dee482e..e6a0530 100644 --- a/proto/main.proto +++ b/proto/main.proto @@ -675,6 +675,7 @@ service EveningDetectiveServer { }; } + // Each request creates a fresh setup package, replacing any unfinished preparation. rpc StartGameExport(StartGameExportReq) returns (GameExportRsp) { option (google.api.http) = { post: "/api/games/{ID}/export" @@ -682,6 +683,14 @@ service EveningDetectiveServer { }; } + // Release the transfer without discarding ready packages or their future results. + rpc ResetGameLocalMode(ResetGameLocalModeReq) returns (ResetGameLocalModeRsp) { + option (google.api.http) = { + post: "/api/games/{ID}/local-mode/reset" + body: "*" + }; + } + rpc GetGameExport(GetGameExportReq) returns (GameExportRsp) { option (google.api.http) = {get: "/api/games/{ID}/export"}; } @@ -1719,7 +1728,7 @@ message Game { // GET /api/games/{ID}/export/download (not an HttpBody RPC). message GameExport { string ID = 1; - string state = 2; // preparing, ready or failed; preparing/ready lock game writes. + string state = 2; // preparing, ready or failed; ready indicates a completed package. string packageID = 3; int64 size = 4; string sha256 = 5; @@ -1736,6 +1745,13 @@ message StartGameExportReq { int32 ID = 1; } message GetGameExportReq { int32 ID = 1; } +message ResetGameLocalModeReq { + int32 ID = 1; + string exportID = 2; // Expected latest export, protecting against stale confirmations. +} + +message ResetGameLocalModeRsp {} + message GameExportRsp { GameExport export = 1; } enum ScreenMode { diff --git a/src/api/generated/crabs/evening_detective_server/index.ts b/src/api/generated/crabs/evening_detective_server/index.ts index 0c6c49f..e91897c 100644 --- a/src/api/generated/crabs/evening_detective_server/index.ts +++ b/src/api/generated/crabs/evening_detective_server/index.ts @@ -588,6 +588,14 @@ export type GetGameExportReq = { ID: number | undefined; }; +export type ResetGameLocalModeReq = { + ID: number | undefined; + exportID: string | undefined; +}; + +export type ResetGameLocalModeRsp = { +}; + export type GameExportRsp = { export: GameExport | undefined; }; @@ -1419,7 +1427,10 @@ export interface EveningDetectiveServer { // Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit. DownloadGameResultsPackage(request: GameResultsPackageReq): Promise; ApplyGameResultsPackage(request: ApplyGameResultsPackageReq): Promise; + // Each request creates a fresh setup package, replacing any unfinished preparation. StartGameExport(request: StartGameExportReq): Promise; + // Release the transfer without discarding ready packages or their future results. + ResetGameLocalMode(request: ResetGameLocalModeReq): Promise; GetGameExport(request: GetGameExportReq): Promise; IssueGameTickets(request: IssueGameTicketsReq): Promise; GetTicket(request: GetTicketReq): Promise; @@ -2682,6 +2693,26 @@ export function createEveningDetectiveServerClient( method: "StartGameExport", }) as Promise; }, + ResetGameLocalMode(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/local-mode/reset`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "ResetGameLocalMode", + }) as Promise; + }, GetGameExport(request) { // eslint-disable-line @typescript-eslint/no-unused-vars if (!request.ID) { throw new Error("missing required field request.ID"); diff --git a/src/components/GamePage.vue b/src/components/GamePage.vue index 27272ec..630da45 100644 --- a/src/components/GamePage.vue +++ b/src/components/GamePage.vue @@ -202,6 +202,14 @@ const canExport = computed( ) const isStartingExport = ref(false) const isDownloadingPackage = ref(false) +const canResetLocalMode = computed( + () => + canExport.value && + (game.value.export?.state === 'preparing' || game.value.export?.state === 'ready'), +) +const isResettingLocalMode = ref(false) +const isResetLocalModeModalOpen = ref(false) +const resetLocalModeExportID = ref('') const canDownloadResults = computed( () => uiStore.environment === 'venue' && @@ -649,14 +657,7 @@ async function toTeamStory(id: string, password: string, newTab: boolean = false } async function preparePackage() { - if ( - !canExport.value || - isStartingExport.value || - game.value.export?.state === 'preparing' || - game.value.export?.state === 'ready' || - game.value.status !== 'draft' - ) - return + if (!canExport.value || isStartingExport.value || isResettingLocalMode.value) return isStartingExport.value = true try { const res = await client.StartGameExport({ ID: game.value.ID }) @@ -668,6 +669,30 @@ async function preparePackage() { } } +function openResetLocalMode() { + if (!canResetLocalMode.value || isStartingExport.value || isResettingLocalMode.value) return + resetLocalModeExportID.value = game.value.export?.ID || '' + isResetLocalModeModalOpen.value = true +} + +async function resetLocalMode() { + if (!canExport.value || !resetLocalModeExportID.value || isResettingLocalMode.value) return + isResettingLocalMode.value = true + try { + await client.ResetGameLocalMode({ + ID: game.value.ID, + exportID: resetLocalModeExportID.value, + }) + isResetLocalModeModalOpen.value = false + await getGame(Number(game.value.ID)) + message.success('Локальный режим сброшен') + } catch (err) { + message.error(err instanceof Error ? err.message : 'Не удалось сбросить локальный режим') + } finally { + isResettingLocalMode.value = false + } +} + async function downloadPackage() { if (!canExport.value || isDownloadingPackage.value || game.value.export?.state !== 'ready') return isDownloadingPackage.value = true @@ -1264,7 +1289,9 @@ useSimplePolling(() => {

Пакет содержит снимок на момент подготовки. Последующие онлайн-изменения в него не - попадут. Готовность пакета не подтверждает импорт на площадку. + попадут. На площадке игра начнётся с нуля: ходы, ответы и баллы в пакет не входят. + Статус и прогресс игры на сервере сохраняются. Готовность пакета не подтверждает + импорт на площадку.

Подготовка пакета… { >Скачать пакет {{ - game.export?.state === 'failed' ? 'Повторить подготовку' : 'Подготовить пакет' - }} -

- Подготовка доступна до старта игры. + Подготовить новый пакет + +

+ Новый пакет заменяет текущую подготовку. Результаты ранее скачанных пакетов можно + загрузить позже.

+ + Сбросить локальный режим +
@@ -1559,6 +1590,43 @@ useSimplePolling(() => { + + + +

+ Игра вернётся в обычный режим на сервере. Данные игры сохранятся. Локальная площадка продолжит + работать отдельно; её результаты можно будет загрузить позже. +

+ +
+
+ {