From 4b5f66d497d00c4c016b4ca50ea99c55bdb91d77 Mon Sep 17 00:00:00 2001 From: seavor Date: Thu, 16 Apr 2026 01:28:42 -0500 Subject: [PATCH] add missing requests/responses --- webclient/src/api/index.ts | 3 + webclient/src/api/request/GameRequestImpl.ts | 142 ++++++++++++++++++ .../src/api/request/SessionRequestImpl.ts | 8 + webclient/src/api/request/index.ts | 8 +- .../src/api/response/SessionResponseImpl.ts | 8 + webclient/src/api/response/index.ts | 6 +- .../store/server/__mocks__/server-fixtures.ts | 2 + .../src/store/server/server.actions.spec.ts | 15 ++ .../src/store/server/server.dispatch.spec.ts | 11 ++ webclient/src/store/server/server.dispatch.ts | 6 + .../src/store/server/server.interfaces.ts | 2 + .../src/store/server/server.reducer.spec.ts | 19 +++ webclient/src/store/server/server.reducer.ts | 10 ++ .../src/store/server/server.selectors.spec.ts | 12 ++ .../src/store/server/server.selectors.ts | 2 + webclient/src/store/server/server.types.ts | 2 + .../__mocks__/sessionCommandMocks.ts | 2 + .../commands/game/gameCommands.spec.ts | 8 + .../src/websocket/commands/game/index.ts | 1 + .../src/websocket/commands/game/rollDie.ts | 8 + .../commands/session/deckDownload.ts | 17 +++ .../src/websocket/commands/session/index.ts | 2 + .../commands/session/replayDownload.ts | 17 +++ .../session/sessionCommands-simple.spec.ts | 38 +++++ .../websocket/interfaces/WebClientRequest.ts | 40 +++++ .../websocket/interfaces/WebClientResponse.ts | 2 + webclient/src/websocket/interfaces/index.ts | 1 + 27 files changed, 382 insertions(+), 10 deletions(-) create mode 100644 webclient/src/api/request/GameRequestImpl.ts create mode 100644 webclient/src/websocket/commands/game/rollDie.ts create mode 100644 webclient/src/websocket/commands/session/deckDownload.ts create mode 100644 webclient/src/websocket/commands/session/replayDownload.ts diff --git a/webclient/src/api/index.ts b/webclient/src/api/index.ts index 651bc3162..0acaf2d93 100644 --- a/webclient/src/api/index.ts +++ b/webclient/src/api/index.ts @@ -29,4 +29,7 @@ export const request: IWebClientRequest = { get moderator() { return WebClient.instance.request.moderator; }, + get game() { + return WebClient.instance.request.game; + }, }; diff --git a/webclient/src/api/request/GameRequestImpl.ts b/webclient/src/api/request/GameRequestImpl.ts new file mode 100644 index 000000000..e594118cc --- /dev/null +++ b/webclient/src/api/request/GameRequestImpl.ts @@ -0,0 +1,142 @@ +import type { IGameRequest } from '@app/websocket'; +import { GameCommands } from '@app/websocket'; + +import { Data } from '@app/types'; + +export class GameRequestImpl implements IGameRequest { + leaveGame(gameId: number): void { + GameCommands.leaveGame(gameId); + } + + kickFromGame(gameId: number, params: Data.KickFromGameParams): void { + GameCommands.kickFromGame(gameId, params); + } + + gameSay(gameId: number, params: Data.GameSayParams): void { + GameCommands.gameSay(gameId, params); + } + + readyStart(gameId: number, params: Data.ReadyStartParams): void { + GameCommands.readyStart(gameId, params); + } + + concede(gameId: number): void { + GameCommands.concede(gameId); + } + + unconcede(gameId: number): void { + GameCommands.unconcede(gameId); + } + + judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void { + GameCommands.judge(gameId, targetId, innerGameCommand); + } + + nextTurn(gameId: number): void { + GameCommands.nextTurn(gameId); + } + + setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void { + GameCommands.setActivePhase(gameId, params); + } + + reverseTurn(gameId: number): void { + GameCommands.reverseTurn(gameId); + } + + moveCard(gameId: number, params: Data.MoveCardParams): void { + GameCommands.moveCard(gameId, params); + } + + flipCard(gameId: number, params: Data.FlipCardParams): void { + GameCommands.flipCard(gameId, params); + } + + attachCard(gameId: number, params: Data.AttachCardParams): void { + GameCommands.attachCard(gameId, params); + } + + createToken(gameId: number, params: Data.CreateTokenParams): void { + GameCommands.createToken(gameId, params); + } + + setCardAttr(gameId: number, params: Data.SetCardAttrParams): void { + GameCommands.setCardAttr(gameId, params); + } + + setCardCounter(gameId: number, params: Data.SetCardCounterParams): void { + GameCommands.setCardCounter(gameId, params); + } + + incCardCounter(gameId: number, params: Data.IncCardCounterParams): void { + GameCommands.incCardCounter(gameId, params); + } + + drawCards(gameId: number, params: Data.DrawCardsParams): void { + GameCommands.drawCards(gameId, params); + } + + undoDraw(gameId: number): void { + GameCommands.undoDraw(gameId); + } + + createArrow(gameId: number, params: Data.CreateArrowParams): void { + GameCommands.createArrow(gameId, params); + } + + deleteArrow(gameId: number, params: Data.DeleteArrowParams): void { + GameCommands.deleteArrow(gameId, params); + } + + createCounter(gameId: number, params: Data.CreateCounterParams): void { + GameCommands.createCounter(gameId, params); + } + + setCounter(gameId: number, params: Data.SetCounterParams): void { + GameCommands.setCounter(gameId, params); + } + + incCounter(gameId: number, params: Data.IncCounterParams): void { + GameCommands.incCounter(gameId, params); + } + + delCounter(gameId: number, params: Data.DelCounterParams): void { + GameCommands.delCounter(gameId, params); + } + + shuffle(gameId: number, params: Data.ShuffleParams): void { + GameCommands.shuffle(gameId, params); + } + + dumpZone(gameId: number, params: Data.DumpZoneParams): void { + GameCommands.dumpZone(gameId, params); + } + + revealCards(gameId: number, params: Data.RevealCardsParams): void { + GameCommands.revealCards(gameId, params); + } + + changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void { + GameCommands.changeZoneProperties(gameId, params); + } + + deckSelect(gameId: number, params: Data.DeckSelectParams): void { + GameCommands.deckSelect(gameId, params); + } + + setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void { + GameCommands.setSideboardPlan(gameId, params); + } + + setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void { + GameCommands.setSideboardLock(gameId, params); + } + + mulligan(gameId: number, params: Data.MulliganParams): void { + GameCommands.mulligan(gameId, params); + } + + rollDie(gameId: number, params: Data.RollDieParams): void { + GameCommands.rollDie(gameId, params); + } +} diff --git a/webclient/src/api/request/SessionRequestImpl.ts b/webclient/src/api/request/SessionRequestImpl.ts index 0075b9418..c7b0e267a 100644 --- a/webclient/src/api/request/SessionRequestImpl.ts +++ b/webclient/src/api/request/SessionRequestImpl.ts @@ -41,4 +41,12 @@ export class SessionRequestImpl implements ISessionRequest { getUserGames(userName: string): void { SessionCommands.getGamesOfUser(userName); } + + deckDownload(deckId: number): void { + SessionCommands.deckDownload(deckId); + } + + replayDownload(replayId: number): void { + SessionCommands.replayDownload(replayId); + } } diff --git a/webclient/src/api/request/index.ts b/webclient/src/api/request/index.ts index 85e55a739..b5934d72a 100644 --- a/webclient/src/api/request/index.ts +++ b/webclient/src/api/request/index.ts @@ -3,20 +3,18 @@ import type { IWebClientRequest } from '@app/websocket'; import { AuthenticationRequestImpl } from './AuthenticationRequestImpl'; import { SessionRequestImpl } from './SessionRequestImpl'; import { RoomsRequestImpl } from './RoomsRequestImpl'; +import { GameRequestImpl } from './GameRequestImpl'; import { AdminRequestImpl } from './AdminRequestImpl'; import { ModeratorRequestImpl } from './ModeratorRequestImpl'; -export { AuthenticationRequestImpl } from './AuthenticationRequestImpl'; -export { SessionRequestImpl } from './SessionRequestImpl'; -export { RoomsRequestImpl } from './RoomsRequestImpl'; -export { AdminRequestImpl } from './AdminRequestImpl'; -export { ModeratorRequestImpl } from './ModeratorRequestImpl'; +export { AuthenticationRequestImpl, SessionRequestImpl, RoomsRequestImpl, GameRequestImpl, AdminRequestImpl, ModeratorRequestImpl }; export function createWebClientRequest(): IWebClientRequest { return { authentication: new AuthenticationRequestImpl(), session: new SessionRequestImpl(), rooms: new RoomsRequestImpl(), + game: new GameRequestImpl(), admin: new AdminRequestImpl(), moderator: new ModeratorRequestImpl(), }; diff --git a/webclient/src/api/response/SessionResponseImpl.ts b/webclient/src/api/response/SessionResponseImpl.ts index 98e1fbd70..7d6c16b47 100644 --- a/webclient/src/api/response/SessionResponseImpl.ts +++ b/webclient/src/api/response/SessionResponseImpl.ts @@ -229,4 +229,12 @@ export class SessionResponseImpl implements ISessionResponse { replayDeleteMatch(gameId: number): void { ServerDispatch.replayDeleteMatch(gameId); } + + downloadServerDeck(deckId: number, response: Data.Response_DeckDownload): void { + ServerDispatch.deckDownloaded(deckId, response.deck); + } + + replayDownloaded(replayId: number, response: Data.Response_ReplayDownload): void { + ServerDispatch.replayDownloaded(replayId, response.replayData); + } } diff --git a/webclient/src/api/response/index.ts b/webclient/src/api/response/index.ts index 21941a1b0..b37b343ab 100644 --- a/webclient/src/api/response/index.ts +++ b/webclient/src/api/response/index.ts @@ -6,11 +6,7 @@ import { GameResponseImpl } from './GameResponseImpl'; import { AdminResponseImpl } from './AdminResponseImpl'; import { ModeratorResponseImpl } from './ModeratorResponseImpl'; -export { SessionResponseImpl } from './SessionResponseImpl'; -export { RoomResponseImpl } from './RoomResponseImpl'; -export { GameResponseImpl } from './GameResponseImpl'; -export { AdminResponseImpl } from './AdminResponseImpl'; -export { ModeratorResponseImpl } from './ModeratorResponseImpl'; +export { SessionResponseImpl, RoomResponseImpl, GameResponseImpl, AdminResponseImpl, ModeratorResponseImpl }; export function createWebClientResponse(): IWebClientResponse { return { diff --git a/webclient/src/store/server/__mocks__/server-fixtures.ts b/webclient/src/store/server/__mocks__/server-fixtures.ts index efc442db7..be47e93e2 100644 --- a/webclient/src/store/server/__mocks__/server-fixtures.ts +++ b/webclient/src/store/server/__mocks__/server-fixtures.ts @@ -174,6 +174,8 @@ export function makeServerState(overrides: Partial = {}): ServerSta adminNotes: {}, replays: {}, backendDecks: null, + downloadedDeck: null, + downloadedReplay: null, gamesOfUser: {}, registrationError: null, ...overrides, diff --git a/webclient/src/store/server/server.actions.spec.ts b/webclient/src/store/server/server.actions.spec.ts index 7823d06c6..3638201b7 100644 --- a/webclient/src/store/server/server.actions.spec.ts +++ b/webclient/src/store/server/server.actions.spec.ts @@ -351,6 +351,21 @@ describe('Actions', () => { expect(Actions.deckDelete({ deckId: 42 })).toEqual({ type: Types.DECK_DELETE, payload: { deckId: 42 } }); }); + it('deckDownloaded', () => { + expect(Actions.deckDownloaded({ deckId: 42, deck: '' })).toEqual({ + type: Types.DECK_DOWNLOADED, + payload: { deckId: 42, deck: '' }, + }); + }); + + it('replayDownloaded', () => { + const replayData = new Uint8Array([1, 2, 3]); + expect(Actions.replayDownloaded({ replayId: 99, replayData })).toEqual({ + type: Types.REPLAY_DOWNLOADED, + payload: { replayId: 99, replayData }, + }); + }); + it('gamesOfUser', () => { const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] }); const action = Actions.gamesOfUser({ userName: 'alice', response }); diff --git a/webclient/src/store/server/server.dispatch.spec.ts b/webclient/src/store/server/server.dispatch.spec.ts index 21b535c6c..3d89c6330 100644 --- a/webclient/src/store/server/server.dispatch.spec.ts +++ b/webclient/src/store/server/server.dispatch.spec.ts @@ -388,6 +388,17 @@ describe('Dispatch', () => { expect(mockDispatch).toHaveBeenCalledWith(Actions.deckDelete({ deckId: 42 })); }); + it('deckDownloaded dispatches correctly', () => { + Dispatch.deckDownloaded(42, ''); + expect(mockDispatch).toHaveBeenCalledWith(Actions.deckDownloaded({ deckId: 42, deck: '' })); + }); + + it('replayDownloaded dispatches correctly', () => { + const data = new Uint8Array([1, 2, 3]); + Dispatch.replayDownloaded(99, data); + expect(mockDispatch).toHaveBeenCalledWith(Actions.replayDownloaded({ replayId: 99, replayData: data })); + }); + it('gamesOfUser dispatches correctly', () => { const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] }); Dispatch.gamesOfUser('alice', response); diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index 1e2ecd94e..77a2845df 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -207,6 +207,12 @@ export const Dispatch = { deckDelete: (deckId: number) => { store.dispatch(Actions.deckDelete({ deckId })); }, + deckDownloaded: (deckId: number, deck: string) => { + store.dispatch(Actions.deckDownloaded({ deckId, deck })); + }, + replayDownloaded: (replayId: number, replayData: Uint8Array) => { + store.dispatch(Actions.replayDownloaded({ replayId, replayData })); + }, gamesOfUser: (userName: string, response: Data.Response_GetGamesOfUser) => { store.dispatch(Actions.gamesOfUser({ userName, response })); }, diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts index 2e2aadf3a..4f3e05ca9 100644 --- a/webclient/src/store/server/server.interfaces.ts +++ b/webclient/src/store/server/server.interfaces.ts @@ -34,6 +34,8 @@ export interface ServerState { /** Replays keyed by gameId for O(1) lookup/update. */ replays: { [gameId: number]: Data.ServerInfo_ReplayMatch }; backendDecks: Data.Response_DeckList | null; + downloadedDeck: { deckId: number; deck: string } | null; + downloadedReplay: { replayId: number; replayData: Uint8Array } | null; gamesOfUser: { [userName: string]: Enriched.Game[] }; registrationError: string | null; } diff --git a/webclient/src/store/server/server.reducer.spec.ts b/webclient/src/store/server/server.reducer.spec.ts index 5c852f36f..33bfc42ff 100644 --- a/webclient/src/store/server/server.reducer.spec.ts +++ b/webclient/src/store/server/server.reducer.spec.ts @@ -624,6 +624,25 @@ describe('Deck Storage', () => { const result = serverReducer(state, Actions.deckDelDir({ path: 'parent/child' })); expect(result.backendDecks!.root!.items[0].folder!.items).toHaveLength(0); }); + + it('DECK_DOWNLOADED → sets downloadedDeck', () => { + const state = makeServerState(); + const result = serverReducer(state, Actions.deckDownloaded({ deckId: 42, deck: '' })); + expect(result.downloadedDeck).toEqual({ deckId: 42, deck: '' }); + }); + + it('DECK_DOWNLOADED → overwrites previous download', () => { + const state = makeServerState({ downloadedDeck: { deckId: 1, deck: 'old' } }); + const result = serverReducer(state, Actions.deckDownloaded({ deckId: 2, deck: 'new' })); + expect(result.downloadedDeck).toEqual({ deckId: 2, deck: 'new' }); + }); + + it('REPLAY_DOWNLOADED → sets downloadedReplay', () => { + const state = makeServerState(); + const data = new Uint8Array([1, 2, 3]); + const result = serverReducer(state, Actions.replayDownloaded({ replayId: 99, replayData: data })); + expect(result.downloadedReplay).toEqual({ replayId: 99, replayData: data }); + }); }); // ── GAMES_OF_USER ───────────────────────────────────────────────────────────── diff --git a/webclient/src/store/server/server.reducer.ts b/webclient/src/store/server/server.reducer.ts index af28b59b4..3fa13ce71 100644 --- a/webclient/src/store/server/server.reducer.ts +++ b/webclient/src/store/server/server.reducer.ts @@ -102,6 +102,8 @@ const initialState: ServerState = { adminNotes: {}, replays: {}, backendDecks: null, + downloadedDeck: null, + downloadedReplay: null, gamesOfUser: {}, registrationError: null, }; @@ -341,6 +343,14 @@ export const serverSlice = createSlice({ }); }, + deckDownloaded: (state, action: PayloadAction<{ deckId: number; deck: string }>) => { + state.downloadedDeck = action.payload; + }, + + replayDownloaded: (state, action: PayloadAction<{ replayId: number; replayData: Uint8Array }>) => { + state.downloadedReplay = action.payload; + }, + gamesOfUser: (state, action: PayloadAction<{ userName: string; response: Data.Response_GetGamesOfUser }>) => { const { userName, response } = action.payload; const gametypeMap = normalizeGametypeMap( diff --git a/webclient/src/store/server/server.selectors.spec.ts b/webclient/src/store/server/server.selectors.spec.ts index 251637d84..61917e580 100644 --- a/webclient/src/store/server/server.selectors.spec.ts +++ b/webclient/src/store/server/server.selectors.spec.ts @@ -133,6 +133,18 @@ describe('Selectors', () => { expect(Selectors.getBackendDecks(rootState(state))).toBeNull(); }); + it('getDownloadedDeck → returns downloadedDeck', () => { + const downloadedDeck = { deckId: 42, deck: '' }; + const state = makeServerState({ downloadedDeck }); + expect(Selectors.getDownloadedDeck(rootState(state))).toEqual(downloadedDeck); + }); + + it('getDownloadedReplay → returns downloadedReplay', () => { + const downloadedReplay = { replayId: 99, replayData: new Uint8Array([1, 2, 3]) }; + const state = makeServerState({ downloadedReplay }); + expect(Selectors.getDownloadedReplay(rootState(state))).toEqual(downloadedReplay); + }); + it('getRegistrationError → returns registrationError', () => { const state = makeServerState({ registrationError: 'bad input' }); expect(Selectors.getRegistrationError(rootState(state))).toBe('bad input'); diff --git a/webclient/src/store/server/server.selectors.ts b/webclient/src/store/server/server.selectors.ts index 0cd515d35..535f3c43e 100644 --- a/webclient/src/store/server/server.selectors.ts +++ b/webclient/src/store/server/server.selectors.ts @@ -39,6 +39,8 @@ export const Selectors = { ), getLogs: ({ server }: State) => server.logs, getBackendDecks: ({ server }: State) => server.backendDecks, + getDownloadedDeck: ({ server }: State) => server.downloadedDeck, + getDownloadedReplay: ({ server }: State) => server.downloadedReplay, getRegistrationError: ({ server }: State) => server.registrationError, getSortUsersBy: ({ server }: State) => server.sortUsersBy, diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts index 9c23bbd93..d3def9004 100644 --- a/webclient/src/store/server/server.types.ts +++ b/webclient/src/store/server/server.types.ts @@ -73,6 +73,8 @@ export const Types = { DECK_DEL_DIR: a.deckDelDir.type, DECK_UPLOAD: a.deckUpload.type, DECK_DELETE: a.deckDelete.type, + DECK_DOWNLOADED: a.deckDownloaded.type, + REPLAY_DOWNLOADED: a.replayDownloaded.type, // User games GAMES_OF_USER: a.gamesOfUser.type, } as const; diff --git a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts index 1d9352cac..e12d89ccf 100644 --- a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts +++ b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts @@ -73,6 +73,8 @@ export function makeSessionPersistenceMock() { replayAdded: vi.fn(), replayModifyMatch: vi.fn(), replayDeleteMatch: vi.fn(), + downloadServerDeck: vi.fn(), + replayDownloaded: vi.fn(), resetPasswordChallenge: vi.fn(), resetPassword: vi.fn(), resetPasswordFailed: vi.fn(), diff --git a/webclient/src/websocket/commands/game/gameCommands.spec.ts b/webclient/src/websocket/commands/game/gameCommands.spec.ts index 62b63ce7a..dee19f57c 100644 --- a/webclient/src/websocket/commands/game/gameCommands.spec.ts +++ b/webclient/src/websocket/commands/game/gameCommands.spec.ts @@ -34,6 +34,7 @@ import { nextTurn } from './nextTurn'; import { readyStart } from './readyStart'; import { revealCards } from './revealCards'; import { reverseTurn } from './reverseTurn'; +import { rollDie } from './rollDie'; import { setActivePhase } from './setActivePhase'; import { setCardAttr } from './setCardAttr'; import { setCardCounter } from './setCardCounter'; @@ -262,6 +263,13 @@ describe('Game commands — delegate to WebClient.instance.protobuf.sendGameComm expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_Unconcede_ext, expect.any(Object)); }); + it('rollDie sends Command_RollDie', () => { + rollDie(gameId, { sides: 6, count: 2 }); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( + gameId, Data.Command_RollDie_ext, expect.objectContaining({ sides: 6, count: 2 }) + ); + }); + it('judge sends Command_Judge with targetId and wrapped gameCommand array', () => { const targetId = 3; const innerCmd = create(Data.GameCommandSchema); diff --git a/webclient/src/websocket/commands/game/index.ts b/webclient/src/websocket/commands/game/index.ts index e91555f30..bed2a9776 100644 --- a/webclient/src/websocket/commands/game/index.ts +++ b/webclient/src/websocket/commands/game/index.ts @@ -31,3 +31,4 @@ export { deckSelect } from './deckSelect'; export { setSideboardPlan } from './setSideboardPlan'; export { setSideboardLock } from './setSideboardLock'; export { mulligan } from './mulligan'; +export { rollDie } from './rollDie'; diff --git a/webclient/src/websocket/commands/game/rollDie.ts b/webclient/src/websocket/commands/game/rollDie.ts new file mode 100644 index 000000000..c568e2de4 --- /dev/null +++ b/webclient/src/websocket/commands/game/rollDie.ts @@ -0,0 +1,8 @@ +import { create } from '@bufbuild/protobuf'; +import { WebClient } from '../../WebClient'; + +import { Data } from '@app/types'; + +export function rollDie(gameId: number, params: Data.RollDieParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_RollDie_ext, create(Data.Command_RollDieSchema, params)); +} diff --git a/webclient/src/websocket/commands/session/deckDownload.ts b/webclient/src/websocket/commands/session/deckDownload.ts new file mode 100644 index 000000000..baffbf262 --- /dev/null +++ b/webclient/src/websocket/commands/session/deckDownload.ts @@ -0,0 +1,17 @@ +import { create } from '@bufbuild/protobuf'; +import { WebClient } from '../../WebClient'; + +import { Data } from '@app/types'; + +export function deckDownload(deckId: number): void { + WebClient.instance.protobuf.sendSessionCommand( + Data.Command_DeckDownload_ext, + create(Data.Command_DeckDownloadSchema, { deckId }), + { + responseExt: Data.Response_DeckDownload_ext, + onSuccess: (response) => { + WebClient.instance.response.session.downloadServerDeck(deckId, response); + }, + } + ); +} diff --git a/webclient/src/websocket/commands/session/index.ts b/webclient/src/websocket/commands/session/index.ts index a6d3d884b..db2efc4c9 100644 --- a/webclient/src/websocket/commands/session/index.ts +++ b/webclient/src/websocket/commands/session/index.ts @@ -6,6 +6,7 @@ export * from './addToList'; export * from './connect'; export * from './deckDel'; export * from './deckDelDir'; +export * from './deckDownload'; export * from './deckList'; export * from './deckNewDir'; export * from './deckUpload'; @@ -24,6 +25,7 @@ export * from './ping'; export * from './register'; export * from './removeFromList'; export * from './replayDeleteMatch'; +export * from './replayDownload'; export * from './replayGetCode'; export * from './replayList'; export * from './replayModifyMatch'; diff --git a/webclient/src/websocket/commands/session/replayDownload.ts b/webclient/src/websocket/commands/session/replayDownload.ts new file mode 100644 index 000000000..950bf5891 --- /dev/null +++ b/webclient/src/websocket/commands/session/replayDownload.ts @@ -0,0 +1,17 @@ +import { create } from '@bufbuild/protobuf'; +import { WebClient } from '../../WebClient'; + +import { Data } from '@app/types'; + +export function replayDownload(replayId: number): void { + WebClient.instance.protobuf.sendSessionCommand( + Data.Command_ReplayDownload_ext, + create(Data.Command_ReplayDownloadSchema, { replayId }), + { + responseExt: Data.Response_ReplayDownload_ext, + onSuccess: (response) => { + WebClient.instance.response.session.replayDownloaded(replayId, response); + }, + } + ); +} diff --git a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts index 3864acb42..97b2acdab 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts @@ -27,6 +27,7 @@ import { accountImage } from './accountImage'; import { accountPassword } from './accountPassword'; import { deckDel } from './deckDel'; import { deckDelDir } from './deckDelDir'; +import { deckDownload } from './deckDownload'; import { deckList } from './deckList'; import { deckNewDir } from './deckNewDir'; import { deckUpload } from './deckUpload'; @@ -39,6 +40,7 @@ import { listUsers } from './listUsers'; import { message } from './message'; import { ping } from './ping'; import { replayDeleteMatch } from './replayDeleteMatch'; +import { replayDownload } from './replayDownload'; import { replayList } from './replayList'; import { replayModifyMatch } from './replayModifyMatch'; import { addToList, addToBuddyList, addToIgnoreList } from './addToList'; @@ -450,3 +452,39 @@ describe('replaySubmitCode', () => { expect(onError).toHaveBeenCalledWith(404); }); }); + +describe('deckDownload', () => { + it('sends Command_DeckDownload', () => { + deckDownload(42); + expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( + Data.Command_DeckDownload_ext, + expect.objectContaining({ deckId: 42 }), + expect.objectContaining({ responseExt: Data.Response_DeckDownload_ext }) + ); + }); + + it('calls downloadServerDeck on success', () => { + deckDownload(42); + const resp = { deck: 'deck-content' }; + invokeOnSuccess(resp, { responseCode: 0 }); + expect(WebClient.instance.response.session.downloadServerDeck).toHaveBeenCalledWith(42, resp); + }); +}); + +describe('replayDownload', () => { + it('sends Command_ReplayDownload', () => { + replayDownload(99); + expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( + Data.Command_ReplayDownload_ext, + expect.objectContaining({ replayId: 99 }), + expect.objectContaining({ responseExt: Data.Response_ReplayDownload_ext }) + ); + }); + + it('calls replayDownloaded on success', () => { + replayDownload(99); + const resp = { replayData: new Uint8Array([1, 2, 3]) }; + invokeOnSuccess(resp, { responseCode: 0 }); + expect(WebClient.instance.response.session.replayDownloaded).toHaveBeenCalledWith(99, resp); + }); +}); diff --git a/webclient/src/websocket/interfaces/WebClientRequest.ts b/webclient/src/websocket/interfaces/WebClientRequest.ts index e70191663..ba44e9841 100644 --- a/webclient/src/websocket/interfaces/WebClientRequest.ts +++ b/webclient/src/websocket/interfaces/WebClientRequest.ts @@ -22,6 +22,8 @@ export interface ISessionRequest { sendDirectMessage(userName: string, message: string): void; getUserInfo(userName: string): void; getUserGames(userName: string): void; + deckDownload(deckId: number): void; + replayDownload(replayId: number): void; } export interface IRoomsRequest { @@ -54,10 +56,48 @@ export interface IModeratorRequest { warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void; } +export interface IGameRequest { + leaveGame(gameId: number): void; + kickFromGame(gameId: number, params: Data.KickFromGameParams): void; + gameSay(gameId: number, params: Data.GameSayParams): void; + readyStart(gameId: number, params: Data.ReadyStartParams): void; + concede(gameId: number): void; + unconcede(gameId: number): void; + judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void; + nextTurn(gameId: number): void; + setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void; + reverseTurn(gameId: number): void; + moveCard(gameId: number, params: Data.MoveCardParams): void; + flipCard(gameId: number, params: Data.FlipCardParams): void; + attachCard(gameId: number, params: Data.AttachCardParams): void; + createToken(gameId: number, params: Data.CreateTokenParams): void; + setCardAttr(gameId: number, params: Data.SetCardAttrParams): void; + setCardCounter(gameId: number, params: Data.SetCardCounterParams): void; + incCardCounter(gameId: number, params: Data.IncCardCounterParams): void; + drawCards(gameId: number, params: Data.DrawCardsParams): void; + undoDraw(gameId: number): void; + createArrow(gameId: number, params: Data.CreateArrowParams): void; + deleteArrow(gameId: number, params: Data.DeleteArrowParams): void; + createCounter(gameId: number, params: Data.CreateCounterParams): void; + setCounter(gameId: number, params: Data.SetCounterParams): void; + incCounter(gameId: number, params: Data.IncCounterParams): void; + delCounter(gameId: number, params: Data.DelCounterParams): void; + shuffle(gameId: number, params: Data.ShuffleParams): void; + dumpZone(gameId: number, params: Data.DumpZoneParams): void; + revealCards(gameId: number, params: Data.RevealCardsParams): void; + changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void; + deckSelect(gameId: number, params: Data.DeckSelectParams): void; + setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void; + setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void; + mulligan(gameId: number, params: Data.MulliganParams): void; + rollDie(gameId: number, params: Data.RollDieParams): void; +} + export interface IWebClientRequest { authentication: IAuthenticationRequest; session: ISessionRequest; rooms: IRoomsRequest; + game: IGameRequest; admin: IAdminRequest; moderator: IModeratorRequest; } diff --git a/webclient/src/websocket/interfaces/WebClientResponse.ts b/webclient/src/websocket/interfaces/WebClientResponse.ts index 48bc77581..8a60fc4e9 100644 --- a/webclient/src/websocket/interfaces/WebClientResponse.ts +++ b/webclient/src/websocket/interfaces/WebClientResponse.ts @@ -50,12 +50,14 @@ export interface ISessionResponse { deleteServerDeck(deckId: number): void; updateServerDecks(deckList: Data.Response_DeckList): void; uploadServerDeck(path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem): void; + downloadServerDeck(deckId: number, response: Data.Response_DeckDownload): void; createServerDeckDir(path: string, dirName: string): void; deleteServerDeckDir(path: string): void; replayList(matchList: Data.ServerInfo_ReplayMatch[]): void; replayAdded(matchInfo: Data.ServerInfo_ReplayMatch): void; replayModifyMatch(gameId: number, doNotHide: boolean): void; replayDeleteMatch(gameId: number): void; + replayDownloaded(replayId: number, response: Data.Response_ReplayDownload): void; } export interface IRoomResponse { diff --git a/webclient/src/websocket/interfaces/index.ts b/webclient/src/websocket/interfaces/index.ts index 3218e5c7b..abe347cd5 100644 --- a/webclient/src/websocket/interfaces/index.ts +++ b/webclient/src/websocket/interfaces/index.ts @@ -11,6 +11,7 @@ export type { IAuthenticationRequest, ISessionRequest, IRoomsRequest, + IGameRequest, IAdminRequest, IModeratorRequest, IWebClientRequest,