Skip to content

Commit

Permalink
Merge pull request #779 from gettakaro/gameserver-dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
niekcandaele authored Dec 23, 2023
2 parents 171111c + 2af54e6 commit 14d3a8c
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsNumber, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator';
import { IsBoolean, IsNumber, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator';
import { ITakaroQuery } from '@takaro/db';
import { APIOutput, apiResponse } from '@takaro/http';
import { AuthenticatedRequest, AuthService } from '../service/AuthService.js';
Expand Down Expand Up @@ -42,6 +42,10 @@ class PlayerOnGameServerSearchInputAllowedFilters {
@IsUUID(4, { each: true })
@IsOptional()
playerId!: string;

@IsOptional()
@IsBoolean({ each: true })
online!: boolean;
}

class PlayerOnGameServerSearchInputDTO extends ITakaroQuery<PlayerOnGameServerSearchInputAllowedFilters> {
Expand Down
15 changes: 15 additions & 0 deletions packages/app-api/src/db/playerOnGameserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../service/PlayerOnGameserverService.js';
import { PlayerRoleAssignmentOutputDTO } from '../service/RoleService.js';
import { ItemRepo } from './items.js';
import { IGamePlayer } from '@takaro/modules';

export const PLAYER_ON_GAMESERVER_TABLE_NAME = 'playerOnGameServer';
const PLAYER_INVENTORY_TABLE_NAME = 'playerInventory';
Expand All @@ -34,6 +35,8 @@ export class PlayerOnGameServerModel extends TakaroModel {

currency: number;

online: boolean;

static get relationMappings() {
return {
gameServer: {
Expand Down Expand Up @@ -354,4 +357,16 @@ export class PlayerOnGameServerRepo extends ITakaroRepo<
throw error;
}
}

async setOnlinePlayers(gameServerId: string, players: IGamePlayer[]) {
const { query: query1 } = await this.getModel();
const { query: query2 } = await this.getModel();
const gameIds = players.map((player) => player.gameId);

await Promise.all([
query1.whereNotIn('gameId', gameIds).andWhere({ gameServerId }).update({ online: false }),

query2.whereIn('gameId', gameIds).andWhere({ gameServerId }).update({ online: true }),
]);
}
}
9 changes: 8 additions & 1 deletion packages/app-api/src/service/PlayerOnGameserverService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TakaroService } from './Base.js';

import { IsIP, IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator';
import { IsBoolean, IsIP, IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator';
import { TakaroDTO, TakaroModelDTO, ctx, errors, traceableClass } from '@takaro/util';
import { ITakaroQuery } from '@takaro/db';
import { PaginatedOutput } from '../db/base.js';
Expand All @@ -9,6 +9,7 @@ import { IItemDTO, IPlayerReferenceDTO } from '@takaro/gameserver';
import { Type } from 'class-transformer';
import { PlayerRoleAssignmentOutputDTO, RoleService } from './RoleService.js';
import { EVENT_TYPES, EventCreateDTO, EventService } from './EventService.js';
import { IGamePlayer } from '@takaro/modules';

export class PlayerOnGameserverOutputDTO extends TakaroModelDTO<PlayerOnGameserverOutputDTO> {
@IsString()
Expand Down Expand Up @@ -43,6 +44,9 @@ export class PlayerOnGameserverOutputDTO extends TakaroModelDTO<PlayerOnGameserv
@IsNumber()
currency: number;

@IsBoolean()
online: boolean;

@ValidateNested({ each: true })
@Type(() => IItemDTO)
inventory: IItemDTO[];
Expand Down Expand Up @@ -252,4 +256,7 @@ export class PlayerOnGameServerService extends TakaroService<
})
);
}
async setOnlinePlayers(gameServerId: string, players: IGamePlayer[]) {
await this.repo.setOnlinePlayers(gameServerId, players);
}
}
42 changes: 24 additions & 18 deletions packages/app-api/src/workers/playerSyncWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function processJob(job: Job<IGameServerQueueData>) {
const gameserverService = new GameServerService(domain.id);
const gameServers = await gameserverService.find({});
promises.push(
gameServers.results.map(async (gs) => {
...gameServers.results.map(async (gs) => {
const reachable = await gameserverService.testReachability(gs.id);
if (reachable.connectable) {
await queueService.queues.playerSync.queue.add(
Expand All @@ -67,32 +67,38 @@ export async function processJob(job: Job<IGameServerQueueData>) {

if (job.data.gameServerId) {
const { domainId, gameServerId } = job.data;
log.info(`Processing playerSync job for domain: ${domainId} and game server: ${gameServerId}`);
const gameServerService = new GameServerService(domainId);
const playerService = new PlayerService(domainId);
const playerOnGameServerService = new PlayerOnGameServerService(domainId);

const onlinePlayers = await gameServerService.getPlayers(gameServerId);

const promises = onlinePlayers.map(async (player) => {
log.debug(`Syncing player ${player.gameId} on game server ${gameServerId}`);
await playerService.sync(player, gameServerId);
const resolvedPlayer = await playerService.resolveRef(player, gameServerId);
await gameServerService.getPlayerLocation(gameServerId, resolvedPlayer.id);

await playerOnGameServerService.addInfo(
player,
gameServerId,
await new PlayerOnGameServerUpdateDTO().construct({
ip: player.ip,
ping: player.ping,
})
);
});
const promises = [];

promises.push(
...onlinePlayers.map(async (player) => {
log.debug(`Syncing player ${player.gameId} on game server ${gameServerId}`);
await playerService.sync(player, gameServerId);
const resolvedPlayer = await playerService.resolveRef(player, gameServerId);
await gameServerService.getPlayerLocation(gameServerId, resolvedPlayer.id);

await playerOnGameServerService.addInfo(
player,
gameServerId,
await new PlayerOnGameServerUpdateDTO().construct({
ip: player.ip,
ping: player.ping,
})
);
})
);

promises.push(playerOnGameServerService.setOnlinePlayers(gameServerId, onlinePlayers));

await Promise.allSettled(promises);
await Promise.all(promises);

// Processing for a specific game server
log.info(`Processing playerSync job for domain: ${domainId} and game server: ${gameServerId}`);
await gameServerService.syncInventories(gameServerId);

return;
Expand Down
24 changes: 24 additions & 0 deletions packages/lib-apiclient/src/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4558,6 +4558,12 @@ export interface PlayerOnGameServerSearchInputAllowedFilters {
* @memberof PlayerOnGameServerSearchInputAllowedFilters
*/
playerId?: Array<string>;
/**
*
* @type {Array<boolean>}
* @memberof PlayerOnGameServerSearchInputAllowedFilters
*/
online?: Array<boolean>;
}
/**
*
Expand Down Expand Up @@ -4684,6 +4690,12 @@ export interface PlayerOnGameServerUpdateDTO {
* @memberof PlayerOnGameServerUpdateDTO
*/
currency?: number;
/**
*
* @type {boolean}
* @memberof PlayerOnGameServerUpdateDTO
*/
online?: boolean;
}
/**
*
Expand Down Expand Up @@ -4764,6 +4776,12 @@ export interface PlayerOnGameserverOutputDTO {
* @memberof PlayerOnGameserverOutputDTO
*/
currency: number;
/**
*
* @type {boolean}
* @memberof PlayerOnGameserverOutputDTO
*/
online: boolean;
/**
*
* @type {Array<IItemDTO>}
Expand Down Expand Up @@ -4892,6 +4910,12 @@ export interface PlayerOnGameserverOutputWithRolesDTO {
* @memberof PlayerOnGameserverOutputWithRolesDTO
*/
currency: number;
/**
*
* @type {boolean}
* @memberof PlayerOnGameserverOutputWithRolesDTO
*/
online: boolean;
/**
*
* @type {Array<IItemDTO>}
Expand Down
13 changes: 13 additions & 0 deletions packages/lib-db/src/migrations/sql/20231223132631-pog-online.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('playerOnGameServer', (table) => {
table.boolean('online').defaultTo(false);
});
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('playerOnGameServer', (table) => {
table.dropColumn('online');
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe('rust event detection', () => {
expect(location).to.deep.equal({
x: -770.0,
y: 1.0,
z: -1090.7,
z: -1090,
});
});

Expand All @@ -142,7 +142,7 @@ describe('rust event detection', () => {
expect(location).to.deep.equal({
x: -780.0,
y: 2.0,
z: -1100.7,
z: -1100,
});
});
});
Expand Down
29 changes: 29 additions & 0 deletions packages/web-main/src/pages/gameserver/GameServerDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,32 @@ import { useSocket } from 'hooks/useSocket';
import { useGameServer } from 'queries/gameservers';
import { useSelectedGameServer } from 'hooks/useSelectedGameServerContext';
import { useGameServerDocumentTitle } from 'hooks/useDocumentTitle';
import { ChatMessagesCard } from './cards/ChatMessages';
import { OnlinePlayersCard } from './cards/OnlinePlayers';

const GridContainer = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 1rem;
min-height: 25vh;
max-height: 25vh;
`;

const DashboardCard = styled.div`
background-color: ${({ theme }) => theme.colors.backgroundAccent};
padding: 1rem;
border-radius: 1rem;
`;

const ConsoleContainer = styled.div`
height: 80vh;
`;

const OnlinePlayerContainer = styled(DashboardCard)``;

const ChatContainer = styled(DashboardCard)``;

const GameServerDashboard: FC = () => {
const { selectedGameServerId } = useSelectedGameServer();
useGameServerDocumentTitle('dashboard');
Expand Down Expand Up @@ -88,6 +109,14 @@ const GameServerDashboard: FC = () => {

return (
<Fragment>
<GridContainer>
<OnlinePlayerContainer>
<OnlinePlayersCard />
</OnlinePlayerContainer>
<ChatContainer>
<ChatMessagesCard />
</ChatContainer>
</GridContainer>
<ConsoleContainer>
<Console
messages={messages}
Expand Down
87 changes: 87 additions & 0 deletions packages/web-main/src/pages/gameserver/cards/ChatMessages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
EventOutputDTO,
EventSearchInputAllowedFiltersEventNameEnum,
EventSearchInputDTOSortDirectionEnum,
} from '@takaro/apiclient';
import { Loading, styled } from '@takaro/lib-components';
import { useSelectedGameServer } from 'hooks/useSelectedGameServerContext';
import { useSocket } from 'hooks/useSocket';
import { useEvents } from 'queries/events';
import { FC, useEffect } from 'react';

const SteamAvatar = styled.img`
width: 2rem;
height: 100%;
border-radius: 50%;
margin: auto;
`;

const ChatContainer = styled.div`
border-radius: 8px;
max-height: 20vh;
overflow-y: scroll;
`;

const Message = styled.span`
display: block;
padding: 5px;
border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAlt};
height: 2rem;
& > * {
margin-right: 1rem;
}
`;

const PlayerName = styled.span`
font-weight: bold;
margin-right: 10px;
`;

const ChatMessage: FC<{ chatMessage: EventOutputDTO }> = ({ chatMessage }) => {
if (!chatMessage.meta || !('message' in chatMessage.meta)) return null;
let avatarUrl = '/favicon.ico';

if (chatMessage.player?.steamAvatar) avatarUrl = chatMessage.player?.steamAvatar;

const friendlyTimeStamp = new Date(chatMessage.createdAt).toLocaleTimeString();

return (
<Message>
<span>{friendlyTimeStamp}</span>
<SteamAvatar src={avatarUrl} />
<PlayerName>{chatMessage.player?.name}</PlayerName>
<span>{chatMessage.meta.message as string}</span>
</Message>
);
};

export const ChatMessagesCard: FC = () => {
const { selectedGameServerId } = useSelectedGameServer();
const { socket } = useSocket();

const { data, isLoading, refetch } = useEvents({
filters: {
gameserverId: [selectedGameServerId],
eventName: [EventSearchInputAllowedFiltersEventNameEnum.ChatMessage],
},
sortBy: 'createdAt',
sortDirection: EventSearchInputDTOSortDirectionEnum.Desc,
extend: ['player'],
});

useEffect(() => {
socket.on('event', (event: EventOutputDTO) => {
if (event.eventName === 'chat-message') refetch();
});

return () => {
socket.off('event');
};
}, []);

if (isLoading) return <Loading />;

const components = data?.pages[0].data?.map((event) => <ChatMessage key={event.id} chatMessage={event} />);

return <ChatContainer>{components}</ChatContainer>;
};
Loading

0 comments on commit 14d3a8c

Please sign in to comment.