From 7639e948e6c5f250c6dcbdb3c1099e308733b0f4 Mon Sep 17 00:00:00 2001 From: Mike Rosack Date: Fri, 8 Dec 2023 08:41:37 -0600 Subject: [PATCH] finish substitutions work --- api/controllers/gameController/_models.ts | 8 ++ api/controllers/gameController/listOpen.ts | 38 ++++--- .../gameController/replacePlayer.ts | 102 ++++++++++++++---- 3 files changed, 112 insertions(+), 36 deletions(-) diff --git a/api/controllers/gameController/_models.ts b/api/controllers/gameController/_models.ts index b03eea4..9afac8a 100644 --- a/api/controllers/gameController/_models.ts +++ b/api/controllers/gameController/_models.ts @@ -37,6 +37,9 @@ export interface ReplacePlayerRequestBody { oldSteamId: string; newSteamId: string; } +export interface ReplaceRequestedSubstitutionPlayerRequestBody extends ReplacePlayerRequestBody { + password?: string; +} export interface OpenGamesResponse { notStarted: Game[]; @@ -54,3 +57,8 @@ export interface StartTurnSubmitResponse { export interface GameTurnListItem extends GameTurn { hasSave: boolean; } + +export interface OpenSlotsGame extends Game { + joinAfterStart: boolean; + substitutionRequested: boolean; +} diff --git a/api/controllers/gameController/listOpen.ts b/api/controllers/gameController/listOpen.ts index 482df06..010f683 100644 --- a/api/controllers/gameController/listOpen.ts +++ b/api/controllers/gameController/listOpen.ts @@ -3,7 +3,7 @@ import { Get, Route, Tags } from 'tsoa'; import { GAME_REPOSITORY_SYMBOL, IGameRepository } from '../../../lib/dynamoose/gameRepository'; import { inject, provideSingleton } from '../../../lib/ioc'; import { Game } from '../../../lib/models'; -import { OpenGamesResponse } from './_models'; +import { OpenGamesResponse, OpenSlotsGame } from './_models'; @Route('game') @Tags('game') @@ -44,20 +44,28 @@ export class GameController_ListOpen { } @Get('openSlots') - public async openSlots(): Promise { + public async openSlots(): Promise { const games = await this.gameRepository.incompleteGames(); - return orderBy(games, ['createdAt'], ['desc']).filter(game => { - const numHumans = game.players.filter(player => { - return !!player.steamId; - }).length; - - return ( - game.inProgress && - game.allowJoinAfterStart && - !game.completed && - ((numHumans < game.players.length && numHumans < game.humans) || - game.players.some(x => x.substitutionRequested)) - ); - }); + return orderBy(games, ['createdAt'], ['desc']) + .filter(game => game.inProgress && !game.completed) + .map(game => { + const numHumans = game.players.filter(player => { + return !!player.steamId; + }).length; + + const joinAfterStart = + game.allowJoinAfterStart && + !game.hashedPassword && + numHumans < game.players.length && + numHumans < game.humans; + const substitutionRequested = game.players.some(x => x.substitutionRequested); + + return { + ...game, + joinAfterStart, + substitutionRequested + }; + }) + .filter(x => x.joinAfterStart || x.substitutionRequested); } } diff --git a/api/controllers/gameController/replacePlayer.ts b/api/controllers/gameController/replacePlayer.ts index 5207a92..30dfaea 100644 --- a/api/controllers/gameController/replacePlayer.ts +++ b/api/controllers/gameController/replacePlayer.ts @@ -1,13 +1,19 @@ +import * as bcrypt from 'bcryptjs'; import { Body, Post, Request, Response, Route, Security, Tags } from 'tsoa'; import { GAME_REPOSITORY_SYMBOL, IGameRepository } from '../../../lib/dynamoose/gameRepository'; import { IUserRepository, USER_REPOSITORY_SYMBOL } from '../../../lib/dynamoose/userRepository'; import { inject, provideSingleton } from '../../../lib/ioc'; -import { Game } from '../../../lib/models'; +import { Game, GamePlayer, GameTurn, User } from '../../../lib/models'; import { GAME_TURN_SERVICE_SYMBOL, IGameTurnService } from '../../../lib/services/gameTurnService'; import { ISnsProvider, SNS_PROVIDER_SYMBOL } from '../../../lib/snsProvider'; import { UserUtil } from '../../../lib/util/userUtil'; import { ErrorResponse, HttpRequest, HttpResponseError } from '../../framework'; -import { ReplacePlayerRequestBody } from './_models'; +import { ReplacePlayerRequestBody, ReplaceRequestedSubstitutionPlayerRequestBody } from './_models'; +import { + GAME_TURN_REPOSITORY_SYMBOL, + IGameTurnRepository +} from '../../../lib/dynamoose/gameTurnRepository'; +import { GameUtil } from '../../../lib/util/gameUtil'; @Route('game') @Tags('game') @@ -16,10 +22,36 @@ export class GameController_ReplacePlayer { constructor( @inject(USER_REPOSITORY_SYMBOL) private userRepository: IUserRepository, @inject(GAME_REPOSITORY_SYMBOL) private gameRepository: IGameRepository, + @inject(GAME_TURN_REPOSITORY_SYMBOL) private gameTurnRepository: IGameTurnRepository, @inject(GAME_TURN_SERVICE_SYMBOL) private gameTurnService: IGameTurnService, @inject(SNS_PROVIDER_SYMBOL) private sns: ISnsProvider ) {} + @Security('api_key') + @Response(401, 'Unauthorized') + @Post('{gameId}/turn/replaceRequestedSubstitutionPlayer') + public async replaceRequestedSubstitutionPlayer( + @Request() request: HttpRequest, + gameId: string, + @Body() body: ReplaceRequestedSubstitutionPlayerRequestBody + ) { + return this.coreReplace(gameId, body, async ({ game, oldPlayer }) => { + if (body.newSteamId !== request.user) { + throw new HttpResponseError(400, 'You can only ask to put yourself in this game!'); + } + + if (!oldPlayer.substitutionRequested) { + throw new HttpResponseError(400, `This player hasn't asked to be substituted!`); + } + + if (game.hashedPassword) { + if (!(await bcrypt.compare(body.password || '', game.hashedPassword))) { + throw new HttpResponseError(400, 'Supplied password does not match game password!'); + } + } + }); + } + @Security('api_key') @Response(401, 'Unauthorized') @Post('{gameId}/turn/replacePlayer') @@ -28,23 +60,43 @@ export class GameController_ReplacePlayer { gameId: string, @Body() body: ReplacePlayerRequestBody ): Promise { + return this.coreReplace(gameId, body, ({ game, newUser }) => { + if ( + request.user !== '76561197973299801' && + game.createdBySteamId !== request.user && + body.oldSteamId !== request.user + ) { + throw new HttpResponseError( + 400, + "You don't have permission to replace a player in this game!" + ); + } + + if ( + request.user !== '76561197973299801' && + (!newUser.willSubstituteForGameTypes || + newUser.willSubstituteForGameTypes.indexOf(game.gameType) < 0) + ) { + throw new HttpResponseError(400, 'User to substitute has not given permission!'); + } + }); + } + + private async coreReplace( + gameId: string, + body: ReplacePlayerRequestBody, + extraValidations: (state: { + game: Game; + newUser: User; + oldPlayer: GamePlayer; + }) => Promise | void + ) { const game = await this.gameRepository.getOrThrow404(gameId); if (!game.inProgress) { throw new HttpResponseError(400, 'Game must be in progress to replace!'); } - if ( - request.user !== '76561197973299801' && - game.createdBySteamId !== request.user && - body.oldSteamId !== request.user - ) { - throw new HttpResponseError( - 400, - "You don't have permission to replace a player in this game!" - ); - } - const oldPlayer = game.players.find(x => x.steamId === body.oldSteamId); if (!oldPlayer) { @@ -68,13 +120,7 @@ export class GameController_ReplacePlayer { throw new HttpResponseError(400, 'New user not found!'); } - if ( - request.user !== '76561197973299801' && - (!newUser.willSubstituteForGameTypes || - newUser.willSubstituteForGameTypes.indexOf(game.gameType) < 0) - ) { - throw new HttpResponseError(400, 'User to substitute has not given permission!'); - } + await extraValidations({ game, newUser, oldPlayer }); users.push(newUser); @@ -83,14 +129,28 @@ export class GameController_ReplacePlayer { oldPlayer.steamId = body.newSteamId; + let turn: GameTurn; + if (game.currentPlayerSteamId === body.oldSteamId) { game.currentPlayerSteamId = body.newSteamId; + + turn = await this.gameTurnRepository.get({ gameId, turn: game.gameTurnRangeKey }); + + // Reset turn stats + turn.startDate = new Date(); + turn.playerSteamId = newUser.steamId; } + GameUtil.possiblyUpdateAdmin(game); + + // Reset substitution requested flag in all flows (just in case an admin is doing this) + oldPlayer.substitutionRequested = false; + await Promise.all([ this.userRepository.saveVersioned(oldUser), this.userRepository.saveVersioned(newUser), - this.gameRepository.saveVersioned(game) + this.gameRepository.saveVersioned(game), + ...(turn ? [this.gameTurnRepository.saveVersioned(turn)] : []) ]); await this.gameTurnService.getAndUpdateSaveFileForGameState(game, users);