From 03991571f03683a16816f2d6aa80fd53562ccff0 Mon Sep 17 00:00:00 2001 From: Adrian <78108584+AdrianCassar@users.noreply.github.com> Date: Sat, 25 Jan 2025 21:46:06 +0000 Subject: [PATCH] Support for Properties --- .../AddSessionPropertyCommandHandler.ts | 2 +- .../commands/AddSessionPropertyCommand.ts | 6 +- .../SessionSearchQueryHandler.ts | 2 +- src/domain/aggregates/Session.ts | 40 +++++++-- src/domain/value-objects/Property.ts | 83 +++++++++++++++++++ .../mappers/SessionDomainMapper.ts | 7 +- .../mappers/SessionPersistanceMapper.ts | 6 +- .../persistance/models/SessionSchema.ts | 2 +- .../controllers/session.controller.ts | 27 ++++-- .../requests/GetSessionPropertyRequest.ts | 2 +- .../responses/SessionPropertyResponse.ts | 2 +- 11 files changed, 156 insertions(+), 23 deletions(-) create mode 100644 src/domain/value-objects/Property.ts diff --git a/src/application/commandHandlers/AddSessionPropertyCommandHandler.ts b/src/application/commandHandlers/AddSessionPropertyCommandHandler.ts index 8670618..32a6ccb 100644 --- a/src/application/commandHandlers/AddSessionPropertyCommandHandler.ts +++ b/src/application/commandHandlers/AddSessionPropertyCommandHandler.ts @@ -24,7 +24,7 @@ export class AddSessionPropertyCommandHandler return undefined; } - session.addProperty({ property: command.properties }); + session.addProperties({ properties: command.properties }); await this.repository.save(session); return session; diff --git a/src/application/commands/AddSessionPropertyCommand.ts b/src/application/commands/AddSessionPropertyCommand.ts index db66c7f..38e5214 100644 --- a/src/application/commands/AddSessionPropertyCommand.ts +++ b/src/application/commands/AddSessionPropertyCommand.ts @@ -1,3 +1,4 @@ +import Property from 'src/domain/value-objects/Property'; import SessionId from 'src/domain/value-objects/SessionId'; import TitleId from 'src/domain/value-objects/TitleId'; @@ -5,9 +6,6 @@ export class AddSessionPropertyCommand { constructor( public readonly titleId: TitleId, public readonly sessionId: SessionId, - public readonly properties: Map< - number, - { propertyId: number; value: number } - >, + public readonly properties: Array, ) {} } diff --git a/src/application/queryHandlers/SessionSearchQueryHandler.ts b/src/application/queryHandlers/SessionSearchQueryHandler.ts index a556af1..ff163e2 100644 --- a/src/application/queryHandlers/SessionSearchQueryHandler.ts +++ b/src/application/queryHandlers/SessionSearchQueryHandler.ts @@ -18,7 +18,7 @@ export class SessionSearchQueryHandler } async execute(query: SessionSearchQuery) { - this.logger.debug('Session Search ' + query.searchIndex); + this.logger.verbose(`Matchmaking Query ID: ${query.searchIndex}`); return this.repository.findAdvertisedSessions( query.title, diff --git a/src/domain/aggregates/Session.ts b/src/domain/aggregates/Session.ts index 0183d62..3967511 100644 --- a/src/domain/aggregates/Session.ts +++ b/src/domain/aggregates/Session.ts @@ -5,6 +5,7 @@ import SessionFlags from '../value-objects/SessionFlags'; import SessionId from '../value-objects/SessionId'; import TitleId from '../value-objects/TitleId'; import Xuid from '../value-objects/Xuid'; +import Property from '../value-objects/Property'; interface SessionProps { id: SessionId; @@ -22,7 +23,7 @@ interface SessionProps { players: Map; deleted: boolean; context: Map; - properties: Map; + properties: Array; migration?: SessionId; } @@ -59,7 +60,7 @@ interface ContextProps { context: Map; } interface PropertyProps { - property: Map; + properties: Array; } interface JoinProps { members: Map; @@ -69,6 +70,9 @@ interface LeaveProps { xuids: Xuid[]; } +const X_PROPERTY_GAMER_HOSTNAME = 0x40008109; +const X_PROPERTY_GAMER_PUID = 0x20008107; + export default class Session { private readonly props: SessionProps; @@ -104,7 +108,7 @@ export default class Session { players: new Map(), deleted: false, context: new Map(), - properties: new Map(), + properties: new Array(), }); } @@ -153,9 +157,9 @@ export default class Session { }); } - public addProperty(props: PropertyProps) { - props.property.forEach((entry) => { - this.props.properties.set(entry.propertyId.toString(16), entry.value); + public addProperties(props: PropertyProps) { + props.properties.forEach((entry) => { + this.props.properties.push(entry); }); } @@ -352,4 +356,28 @@ export default class Session { get properties() { return this.props.properties; } + + get propertiesStringArray() { + const properties: Array = this.props.properties.map((prop) => { + return prop.toString(); + }); + + return properties; + } + + get propertyHostGamerName() { + const GAMER_HOSTNAME = this.props.properties.find((prop) => { + return prop.id == X_PROPERTY_GAMER_HOSTNAME; + }); + + return GAMER_HOSTNAME; + } + + get propertyPUID() { + const PUID = this.props.properties.find((prop) => { + return prop.id == X_PROPERTY_GAMER_PUID; + }); + + return PUID; + } } diff --git a/src/domain/value-objects/Property.ts b/src/domain/value-objects/Property.ts new file mode 100644 index 0000000..eaca188 --- /dev/null +++ b/src/domain/value-objects/Property.ts @@ -0,0 +1,83 @@ +import { TinyTypeOf } from 'tiny-types'; + +export default class Property extends TinyTypeOf() { + buffer: Buffer; + data: Buffer; + + id_hex: string = ''; + id: number = 0; + size: number = 0; + type: number = 0; + + public constructor(base64: string) { + super(base64); + + this.buffer = Buffer.from(base64, 'base64'); + + // Check if base64 is valid + if (this.buffer.toString('base64') !== base64) { + throw new Error('Invalid base64'); + } + + this.id = this.buffer.readInt32LE(0); + this.type = (this.buffer.readInt32BE(0) & 0xff) >> 4; + this.size = this.buffer.readInt32LE(4); + + const offset: number = 8; + this.data = this.buffer.subarray(offset, offset + this.size); + this.id_hex = this.id.toString(16); + } + + getUTF16() { + if (this.type != 4) { + return ''; + } + + const decoder = new TextDecoder('utf-16be'); + + let size = 0; + while (size < this.data.length) { + const value = this.data.readUInt16BE(size); + + // TextDecoder doesn't support null terminator so we need to find it + if (value === 0) { + break; + } + + size += 2; + } + + const unicode_buffer = this.data.subarray(0, size); + const decoded_unicode: string = decoder.decode(unicode_buffer); + + return decoded_unicode; + } + + getData() { + return this.data; + } + + getType() { + return this.type; + } + + getID() { + return this.id; + } + + getIDString() { + return this.id_hex; + } + + getSize() { + return this.size; + } + + toString(): string { + return this.value; + } + + toStringPretty() { + return `ID: ${this.getIDString()} Type: ${this.getType()} Size: ${this.getSize()}`; + } +} diff --git a/src/infrastructure/persistance/mappers/SessionDomainMapper.ts b/src/infrastructure/persistance/mappers/SessionDomainMapper.ts index 35b238d..bd970eb 100644 --- a/src/infrastructure/persistance/mappers/SessionDomainMapper.ts +++ b/src/infrastructure/persistance/mappers/SessionDomainMapper.ts @@ -7,12 +7,17 @@ import SessionFlags from 'src/domain/value-objects/SessionFlags'; import MacAddress from 'src/domain/value-objects/MacAddress'; import SessionId from 'src/domain/value-objects/SessionId'; import Xuid from 'src/domain/value-objects/Xuid'; +import Property from 'src/domain/value-objects/Property'; @Injectable() export default class SessionDomainMapper { constructor(private readonly logger: ConsoleLogger) {} public mapToDomainModel(session: SessionModel): Session { + const properties: Array = session.properties.map((prop) => { + return new Property(prop); + }); + return new Session({ id: new SessionId(session.id), titleId: new TitleId(session.titleId), @@ -29,7 +34,7 @@ export default class SessionDomainMapper { players: session.players, deleted: session.deleted, context: session.context, - properties: session.properties, + properties: properties, migration: session.migration ? new SessionId(session.migration) : undefined, diff --git a/src/infrastructure/persistance/mappers/SessionPersistanceMapper.ts b/src/infrastructure/persistance/mappers/SessionPersistanceMapper.ts index 8942d9b..d885e46 100644 --- a/src/infrastructure/persistance/mappers/SessionPersistanceMapper.ts +++ b/src/infrastructure/persistance/mappers/SessionPersistanceMapper.ts @@ -5,6 +5,10 @@ import { Session as SessionModel } from '../models/SessionSchema'; @Injectable() export default class SessionPersistanceMapper { public mapToDataModel(session: Session, updatedAt: Date): SessionModel { + const properties: Array = session.properties.map((prop) => { + return prop.toString(); + }); + return { id: session.id.value, titleId: session.titleId.toString(), @@ -22,7 +26,7 @@ export default class SessionPersistanceMapper { players: session.players, deleted: session.deleted, context: session.context, - properties: session.properties, + properties: properties, migration: session.migration ? session.migration.value : undefined, updatedAt, }; diff --git a/src/infrastructure/persistance/models/SessionSchema.ts b/src/infrastructure/persistance/models/SessionSchema.ts index eba6181..b5f16fe 100644 --- a/src/infrastructure/persistance/models/SessionSchema.ts +++ b/src/infrastructure/persistance/models/SessionSchema.ts @@ -38,7 +38,7 @@ export class Session { @Prop({ required: true }) context: Map; @Prop({ required: true }) - properties: Map; + properties: Array; @Prop({ required: false }) migration: string; diff --git a/src/infrastructure/presentation/controllers/session.controller.ts b/src/infrastructure/presentation/controllers/session.controller.ts index b619fcd..be1f162 100644 --- a/src/infrastructure/presentation/controllers/session.controller.ts +++ b/src/infrastructure/presentation/controllers/session.controller.ts @@ -57,6 +57,7 @@ import { RealIP } from 'nestjs-real-ip'; import { ProcessClientAddressCommand } from 'src/application/commands/ProcessClientAddressCommand'; import Session from 'src/domain/aggregates/Session'; import { UpdatePlayerCommand } from 'src/application/commands/UpdatePlayerCommand'; +import Property from 'src/domain/value-objects/Property'; @ApiTags('Sessions') @Controller('/title/:titleId/sessions') @@ -575,20 +576,34 @@ export class SessionController { @Post('/:sessionId/properties') @ApiParam({ name: 'titleId', example: '4D5307E6' }) - @ApiParam({ name: 'sessionId', example: 'B36B3FE8467CFAC7' }) + @ApiParam({ name: 'sessionId', example: 'AE00000000000000' }) async sessionPropertySet( @Param('titleId') titleId: string, @Param('sessionId') sessionId: string, @Body() request: GetSessionPropertyRequest, ) { - const session = await this.commandBus.execute( + const properties: Array = request.properties.map( + (base64: string) => { + return new Property(base64); + }, + ); + + const session: Session = await this.commandBus.execute( new AddSessionPropertyCommand( new TitleId(titleId), new SessionId(sessionId), - request.properties, + properties, ), ); + this.logger.verbose( + `Host Gamer Name: ${session.propertyHostGamerName.getUTF16()}`, + ); + + this.logger.verbose( + `Host PUID: ${session.propertyPUID.getData().readBigInt64BE().toString(16)}`, + ); + if (!session) { throw new NotFoundException(`Session ${sessionId} was not found.`); } @@ -596,12 +611,12 @@ export class SessionController { @Get('/:sessionId/properties') @ApiParam({ name: 'titleId', example: '4D5307E6' }) - @ApiParam({ name: 'sessionId', example: 'B36B3FE8467CFAC7' }) + @ApiParam({ name: 'sessionId', example: 'AE00000000000000' }) async sessionPropertyGet( @Param('titleId') titleId: string, @Param('sessionId') sessionId: string, ): Promise { - const session = await this.queryBus.execute( + const session: Session = await this.queryBus.execute( new GetSessionQuery(new TitleId(titleId), new SessionId(sessionId)), ); @@ -610,7 +625,7 @@ export class SessionController { } return { - properties: session.properties, + properties: session.propertiesStringArray, }; } diff --git a/src/infrastructure/presentation/requests/GetSessionPropertyRequest.ts b/src/infrastructure/presentation/requests/GetSessionPropertyRequest.ts index 1b104f3..7693e7d 100644 --- a/src/infrastructure/presentation/requests/GetSessionPropertyRequest.ts +++ b/src/infrastructure/presentation/requests/GetSessionPropertyRequest.ts @@ -2,5 +2,5 @@ import { ApiProperty } from '@nestjs/swagger'; export class GetSessionPropertyRequest { @ApiProperty() - properties: Map; + properties: Array; } diff --git a/src/infrastructure/presentation/responses/SessionPropertyResponse.ts b/src/infrastructure/presentation/responses/SessionPropertyResponse.ts index 498c99a..b6dcccb 100644 --- a/src/infrastructure/presentation/responses/SessionPropertyResponse.ts +++ b/src/infrastructure/presentation/responses/SessionPropertyResponse.ts @@ -1,3 +1,3 @@ export interface SessionPropertyResponse { - properties: Map; + properties: Array; }