Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,29 @@ describe('OnlinePlayersService.addPlayerOnline() test suite', () => {
.setStatus(OnlinePlayerStatus.BATTLE_WAIT)
.build();
const expectedKey = `${CacheKeys.ONLINE_PLAYERS}:${player1._id}`;
const expectedPayload = JSON.stringify(
onlinePlayerBuilder
.setId(playerToAdd.player_id)
.setName(player1.name)
.setStatus(playerToAdd.status)
.setAdditional({ queueNumber: 0 })
.build(),
);
const builtPlayer = onlinePlayerBuilder
.setId(playerToAdd.player_id)
.setName(player1.name)
.setStatus(playerToAdd.status)
.setAdditional({ queueNumber: 0 })
.build();

(builtPlayer as any).client_version = '0.0.0';

const expectedPayload = JSON.stringify(builtPlayer);

const redisSet = jest.spyOn(redisService, 'set');

await service.addPlayerOnline({
player_id: player1._id,
status: OnlinePlayerStatus.BATTLE_WAIT,
client_version: '0.0.0',
});

expect(redisSet).toHaveBeenCalledWith(expectedKey, expectedPayload, 90);
const actualPayloadSent = redisSet.mock.calls[2][1];

expect(JSON.parse(actualPayloadSent)).toEqual(JSON.parse(expectedPayload));

expect(redisSet).toHaveBeenCalledWith(expectedKey, expect.any(String), 90);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('OnlinePlayersService.getOnlinePlayerById() test suite', () => {
_id: player1._id,
name: player1.name,
status: OnlinePlayerStatus.UI,
client_version: '0.0.0',
};
jest
.spyOn(redisService, 'get')
Expand Down
5 changes: 5 additions & 0 deletions src/common/service/basicService/SEReason.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export enum SEReason {
*/
VALIDATION = 'VALIDATION',

/**
* Provided game versions are incompatible with each other
*/
VERSION_MISMATCH = 'VERSION_MISMATCH',

/**
* The error is unexpected
*/
Expand Down
9 changes: 6 additions & 3 deletions src/onlinePlayers/battleQueue/battleQueue.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Query } from '@nestjs/common';
import { BattleQueueService } from './battleQueue.service';
import OnlinePlayerDto from '../dto/onlinePlayer.dto';
import { UniformResponse } from '../../common/decorator/response/UniformResponse';
Expand Down Expand Up @@ -29,9 +29,12 @@ export class BattleQueueController {
})
@Get()
@UniformResponse(null, OnlinePlayerDto)
async getBattleQueue() {
async getBattleQueue(@Query('version') clientVersion: string) {
const queuePlayers = await this.onlinePlayersService.getOnlinePlayers({
filter: { status: [OnlinePlayerStatus.BATTLE_WAIT] },
filter: {
status: [OnlinePlayerStatus.BATTLE_WAIT],
client_version: clientVersion,
},
});
return this.service.sortPlayersByQueueNumber(queuePlayers);
}
Expand Down
18 changes: 18 additions & 0 deletions src/onlinePlayers/battleQueue/battleQueue.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ export class BattleQueueService {

if (filterErrors) return [null, filterErrors];

const firstVersion = validPlayers[0].client_version;
const hasVersionMismatch = validPlayers.some(
(p) => p.client_version !== firstVersion,
);

if (hasVersionMismatch) {
return [
null,
[
new ServiceError({
reason: SEReason.VERSION_MISMATCH,
message:
'Version mismatch: Players in queue have different client versions',
}),
],
];
}

const queueNumbers = validPlayers.map((p) => p.additional.queueNumber);

const queueMax = this.queueNumberMax + 1;
Expand Down
11 changes: 10 additions & 1 deletion src/onlinePlayers/dto/InformPlayerIsOnline.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OnlinePlayerStatus } from '../enum/OnlinePlayerStatus';
import { IsEnum, IsOptional } from 'class-validator';
import { IsEnum, IsOptional, IsString, IsNotEmpty } from 'class-validator';

export default class InformPlayerIsOnlineDto {
/**
Expand All @@ -11,4 +11,13 @@ export default class InformPlayerIsOnlineDto {
@IsOptional()
@IsEnum(OnlinePlayerStatus)
status?: OnlinePlayerStatus;

/**
* The version of the game client.
* Required to ensure version-compatible matchmaking.
* @example "0.6.2"
*/
@IsString()
@IsNotEmpty()
client_version: string;
}
1 change: 1 addition & 0 deletions src/onlinePlayers/onlinePlayers.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class OnlinePlayersController {
return this.onlinePlayersService.addPlayerOnline({
player_id: user.player_id,
status: body.status,
client_version: body.client_version,
});
}

Expand Down
16 changes: 13 additions & 3 deletions src/onlinePlayers/onlinePlayers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class OnlinePlayersService {
async addPlayerOnline(
playerInfo: AddOnlinePlayer,
): Promise<IServiceReturn<void>> {
const { player_id, status } = playerInfo;
const { player_id, status, client_version } = playerInfo;

const [player, errors] = await this.playerService.getPlayerById(player_id);
if (errors) return [null, errors];
Expand All @@ -44,6 +44,7 @@ export class OnlinePlayersService {
_id: player_id,
name: player.name,
status: status ?? OnlinePlayerStatus.UI,
client_version: client_version,
};

if (status === OnlinePlayerStatus.BATTLE_WAIT) {
Expand All @@ -69,7 +70,10 @@ export class OnlinePlayersService {
* @returns Array of OnlinePlayers or empty array if nothing found
*/
async getOnlinePlayers(options?: {
filter?: { status?: OnlinePlayerStatus[] };
filter?: {
status?: OnlinePlayerStatus[];
client_version?: string;
};
}): Promise<OnlinePlayer[]> {
const players = await this.redisService.getValuesByKeyPattern(
`${this.ONLINE_PLAYERS_KEY}:*`,
Expand All @@ -82,10 +86,16 @@ export class OnlinePlayersService {
) as OnlinePlayer[];

//TODO: Remove it after there are no versions anymore that uses old implementation of saving online players
const filteredPlayers = onlinePlayers.filter(
let filteredPlayers = onlinePlayers.filter(
(player) => typeof player !== 'string' && typeof player !== 'number',
);

if (options?.filter?.client_version) {
filteredPlayers = filteredPlayers.filter(
(p) => p.client_version === options.filter.client_version,
);
}

if (options?.filter?.status) {
return filteredPlayers.filter((p) =>
options.filter.status.includes(p.status),
Expand Down
14 changes: 13 additions & 1 deletion src/onlinePlayers/payload/AddOnlinePlayer.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { IsEnum, IsOptional, IsString } from 'class-validator';
import { OnlinePlayerStatus } from '../enum/OnlinePlayerStatus';

export default class AddOnlinePlayer {
/**
* Player _id to be added
*/
@IsString()
player_id: string;

/**
* Player status to set
* Player status to setAddOnlinePlayer
*
* @default "UI"
*/
@IsEnum(OnlinePlayerStatus)
@IsOptional()
status?: OnlinePlayerStatus;

/**
* The version of the game client.
* Used to isolate matchmaking pools and prevent desyncs between incompatible builds.
* @example "1.0.4-beta"
*/
@IsString()
client_version: string;
}
12 changes: 12 additions & 0 deletions src/onlinePlayers/payload/OnlinePlayer.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { OnlinePlayerStatus } from '../enum/OnlinePlayerStatus';
import { Expose } from 'class-transformer';

export default class OnlinePlayer<Additional = undefined> {
/**
* Player _id
*/
@Expose()
_id: string;

/**
* Player's name
*/
@Expose()
name: string;

/**
* Player status
*/
status: OnlinePlayerStatus;

/**
* The version of the game client.
* Used to isolate matchmaking pools and prevent desyncs between incompatible builds.
* @example "1.0.4-beta"
*/
@Expose()
client_version: string;

/**
* Any additional information online player has
*/
@Expose()
additional?: Additional;
}