Skip to content

Commit

Permalink
Support for friends presence
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrianCassar committed Sep 20, 2024
1 parent 424c6f4 commit bfd9eb8
Show file tree
Hide file tree
Showing 19 changed files with 370 additions and 20 deletions.
4 changes: 4 additions & 0 deletions src/application/application.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ import { GetSessionsQueryHandler } from './queryHandlers/GetSessionQueryHandler'
import { SessionSearchQueryHandler } from './queryHandlers/SessionSearchQueryHandler';
import { AggregateSessionCommandHandler } from './commandHandlers/AggregateSessionCommandHandler';
import { ProcessClientAddressCommandHandler } from './commandHandlers/ProcessClientAddressCommandHandler';
import { GetPlayersQueryHandler } from './queryHandlers/GetPlayersQueryHandler';
import { UpdatePlayerCommandHandler } from './commandHandlers/UpdatePlayerCommandHandler';

export const queryHandlers = [
GetSessionsQueryHandler,
SessionSearchQueryHandler,
FindPlayerQueryHandler,
FindPlayerSessionQueryHandler,
GetPlayerQueryHandler,
GetPlayersQueryHandler,
UpdatePlayerCommandHandler,
FindLeaderboardsQueryHandler,
];

Expand Down
27 changes: 27 additions & 0 deletions src/application/commandHandlers/UpdatePlayerCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Inject } from '@nestjs/common';
import { ICommandHandler, CommandHandler } from '@nestjs/cqrs';
import IPlayerRepository, {
IPlayerRepositorySymbol,
} from 'src/domain/repositories/IPlayerRepository';
import { UpdatePlayerCommand } from '../commands/UpdatePlayerCommand';

@CommandHandler(UpdatePlayerCommand)
export class UpdatePlayerCommandHandler
implements ICommandHandler<UpdatePlayerCommand>
{
constructor(
@Inject(IPlayerRepositorySymbol)
private repository: IPlayerRepository,
) {}

async execute(command: UpdatePlayerCommand) {
const player = await this.repository.findByXuid(command.xuid);

if (!player) {
return undefined;
}

player.updatePlayer(command.player);
await this.repository.save(player);
}
}
9 changes: 9 additions & 0 deletions src/application/commands/UpdatePlayerCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Player from 'src/domain/aggregates/Player';
import Xuid from 'src/domain/value-objects/Xuid';

export class UpdatePlayerCommand {
constructor(
public readonly xuid: Xuid,
public readonly player: Player,
) {}
}
5 changes: 5 additions & 0 deletions src/application/queries/GetPlayersQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Xuid from 'src/domain/value-objects/Xuid';

export class GetPlayersQuery {
constructor(public readonly xuids: Array<Xuid>) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export class FindLeaderboardsQueryHandler

leaderboardResponse.players.push({
xuid: player.value,
gamertag: user?.gamertag?.value ? user.gamertag.value : '',
gamertag: user?.gamertag?.value
? user.gamertag.value
: 'Xenia User',
stats,
});
}),
Expand Down
18 changes: 18 additions & 0 deletions src/application/queryHandlers/GetPlayersQueryHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Inject } from '@nestjs/common';
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import IPlayerRepository, {
IPlayerRepositorySymbol,
} from 'src/domain/repositories/IPlayerRepository';
import { GetPlayersQuery } from '../queries/GetPlayersQuery';

@QueryHandler(GetPlayersQuery)
export class GetPlayersQueryHandler implements IQueryHandler<GetPlayersQuery> {
constructor(
@Inject(IPlayerRepositorySymbol)
private repository: IPlayerRepository,
) {}

async execute(query: GetPlayersQuery) {
return this.repository.findByXuids(query.xuids);
}
}
38 changes: 38 additions & 0 deletions src/domain/aggregates/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import MacAddress from '../value-objects/MacAddress';
import SessionId from '../value-objects/SessionId';
import Xuid from '../value-objects/Xuid';
import Gamertag from '../value-objects/Gamertag';
import TitleId from '../value-objects/TitleId';
import StateFlag, { StateFlags } from '../value-objects/StateFlag';

interface PlayerProps {
xuid: Xuid;
Expand All @@ -12,6 +14,8 @@ interface PlayerProps {
machineId: Xuid;
port: number;
sessionId?: SessionId;
titleId?: TitleId;
state?: StateFlag;
}

interface CreateProps {
Expand All @@ -33,13 +37,39 @@ export default class Player {
return new Player({
...props,
port: 36000, // Port hard-coded?
state: new StateFlag(
StateFlags.ONLINE | StateFlags.JOINABLE | StateFlags.PLAYING,
),
sessionId: new SessionId('0'.repeat(16)),
titleId: new TitleId('0'),
// gamertag: new Gamertag(''),
});
}

public updatePlayer(player: Player) {
this.props.xuid = player.xuid;
this.props.gamertag = player.gamertag;
this.props.hostAddress = player.hostAddress;
this.props.macAddress = player.macAddress;
this.props.machineId = player.machineId;
this.props.port = player.port;
this.props.sessionId = player.sessionId;
this.props.titleId = player.titleId;
this.props.state = player.state;
}

public setSession(sessionId: SessionId) {
this.props.sessionId = sessionId;
}

public setTitleId(titleId: TitleId) {
this.props.titleId = titleId;
}

public setGamertag(gamertag: Gamertag) {
this.props.gamertag = gamertag;
}

get xuid() {
return this.props.xuid;
}
Expand Down Expand Up @@ -67,4 +97,12 @@ export default class Player {
get sessionId() {
return this.props.sessionId;
}

get titleId() {
return this.props.titleId;
}

get state() {
return this.props.state;
}
}
1 change: 1 addition & 0 deletions src/domain/repositories/IPlayerRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Xuid from '../value-objects/Xuid';

export default interface IPlayerRepository {
findByXuid: (xuid: Xuid) => Promise<Player | undefined>;
findByXuids: (xuid: Xuid[]) => Promise<Player[] | undefined>;
findByAddress: (hostAddress: IpAddress) => Promise<Player | undefined>;
save: (player: Player) => Promise<void>;
}
Expand Down
2 changes: 2 additions & 0 deletions src/domain/value-objects/SessionId.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { TinyTypeOf } from 'tiny-types';

// Change base type to number/same as TitleId?

export default class SessionId extends TinyTypeOf<string>() {
public constructor(value: string) {
if (!/^[0-9A-Fa-f]+$/.test(value) || value.length != 16) {
Expand Down
48 changes: 48 additions & 0 deletions src/domain/value-objects/StateFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TinyTypeOf } from 'tiny-types';

export enum StateFlags {
NONE = 0x0,
ONLINE = 0x1,
PLAYING = 0x2,
VOICE = 0x8,
JOINABLE = 1 << 4,
FRIENDS_ONLY = 1 << 8,
}

export default class StateFlag extends TinyTypeOf<number>() {
public constructor(value: number) {
super(Number(value));
}

private isFlagSet(flag: number) {
return (this.value & flag) == flag;
}

public isOnline(): boolean {
return this.isFlagSet(StateFlags.ONLINE);
}

public isJoinable(): boolean {
return this.isFlagSet(StateFlags.JOINABLE);
}

public isPlaying(): boolean {
return this.isFlagSet(StateFlags.PLAYING);
}

public setOnline() {
this.value = this.value | StateFlags.ONLINE;
}

public setJoinable() {
this.value = this.value | StateFlags.JOINABLE;
}

public setPlaying() {
this.value = this.value | StateFlags.PLAYING;
}

public toString(): string {
return this.value.toString(16).toUpperCase();
}
}
72 changes: 65 additions & 7 deletions src/infrastructure/persistance/mappers/PlayerDomainMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,78 @@ import Xuid from 'src/domain/value-objects/Xuid';
import MacAddress from 'src/domain/value-objects/MacAddress';
import SessionId from 'src/domain/value-objects/SessionId';
import Gamertag from 'src/domain/value-objects/Gamertag';
import TitleId from 'src/domain/value-objects/TitleId';
import StateFlag, { StateFlags } from 'src/domain/value-objects/StateFlag';

@Injectable()
export default class PlayerDomainMapper {
constructor(private readonly logger: ConsoleLogger) {}

public mapToDomainModel(player: PlayerModel): Player {
// Define default values for a player

let xuid: Xuid = new Xuid('0'.repeat(16));
let hostAddress: IpAddress = new IpAddress('0.0.0.0');
let macAddress: MacAddress = new MacAddress('002212345678');
let machineId: Xuid = new Xuid('FA00002212345678');
let port: number = 0;
let gamertag: Gamertag = new Gamertag('Xenia User');
let sessionId: SessionId = new SessionId('0'.repeat(16));
let titleId: TitleId = new TitleId('0');
let state: StateFlag = new StateFlag(
StateFlags.ONLINE | StateFlags.JOINABLE | StateFlags.PLAYING,
);

try {
if (player?.xuid) {
xuid = new Xuid(player.xuid);
}

if (player?.gamertag) {
gamertag = new Gamertag(player.gamertag);
}

if (player?.hostAddress) {
hostAddress = new IpAddress(player.hostAddress);
}

if (player?.macAddress) {
macAddress = new MacAddress(player.macAddress);
}

if (player?.machineId) {
machineId = new Xuid(player.machineId);
}

if (player?.port) {
port = player.port;
}

if (player?.sessionId) {
sessionId = new SessionId(player.sessionId.toString());
}

if (player?.titleId) {
titleId = new TitleId(player.titleId.toString());
}

if (player?.state) {
state = new StateFlag(player.state);
}
} catch (error) {
this.logger.fatal(error);
}

return new Player({
xuid: new Xuid(player.xuid),
gamertag: player.gamertag ? new Gamertag(player.gamertag) : undefined,
hostAddress: new IpAddress(player.hostAddress),
macAddress: new MacAddress(player.macAddress),
machineId: new Xuid(player.machineId),
port: player.port,
sessionId: player.sessionId ? new SessionId(player.sessionId) : undefined,
xuid: xuid,
gamertag: gamertag,
hostAddress: hostAddress,
macAddress: macAddress,
machineId: machineId,
port: port,
sessionId: sessionId,
titleId: titleId,
state: state,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export default class PlayerPersistanceMapper {
port: player.port,
sessionId: player.sessionId?.value,
updatedAt: new Date(),
titleId: player.titleId?.toString(),
state: player.state?.value,
};
}
}
4 changes: 4 additions & 0 deletions src/infrastructure/persistance/models/PlayerSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export class Player {
sessionId?: string;
@Prop({ type: Date, expires: '1d', default: Date.now(), required: true })
updatedAt: Date;
@Prop()
titleId?: string;
@Prop()
state?: number;
}

export const PlayerSchema = SchemaFactory.createForClass(Player);
20 changes: 18 additions & 2 deletions src/infrastructure/persistance/repositories/PlayerRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,23 @@ export default class PlayerRepository implements IPlayerRepository {
);
}

public async findByXuid(xuid: Xuid) {
public async findByXuids(xuids: Xuid[]): Promise<Player[]> {
const player_xuids: string[] = xuids.map((_xuid: Xuid) => {
return _xuid.value;
});

const players_docs = await this.PlayerModel.find({
xuid: player_xuids,
});

const players: Player[] = players_docs.map((document) => {
return this.playerDomainMapper.mapToDomainModel(document);
});

return players;
}

public async findByXuid(xuid: Xuid): Promise<Player> {
const player = await this.PlayerModel.findOne({
xuid: xuid.value,
});
Expand All @@ -46,7 +62,7 @@ export default class PlayerRepository implements IPlayerRepository {
return this.playerDomainMapper.mapToDomainModel(player);
}

public async findByAddress(ip: IpAddress) {
public async findByAddress(ip: IpAddress): Promise<Player> {
const player = await this.PlayerModel.findOne({
hostAddress: ip.value,
});
Expand Down
Loading

0 comments on commit bfd9eb8

Please sign in to comment.