Browse Source

Improvements to the replay upload and display

new_auth
Dan 2 years ago
parent
commit
5ed3b14ff6
  1. 2
      packages/bridge-server/src/app.module.ts
  2. 40
      packages/bridge-server/src/race-results/race-results.service.ts
  3. 29
      packages/bridge-server/src/racers/racers.service.ts
  4. 12
      packages/bridge-server/src/races/race.model.ts
  5. 43
      packages/bridge-server/src/races/races.controller.ts
  6. 3
      packages/bridge-server/src/races/races.module.ts
  7. 73
      packages/bridge-server/src/races/races.service.ts
  8. 34
      packages/bridge-server/src/season-standings/season-standings.service.ts
  9. 2
      packages/bridge-server/src/seasons/seasons.service.ts
  10. 8
      packages/bridge-server/src/trackmania-replays/gbx-replay.ts
  11. 9
      packages/bridge-server/src/upload/upload.controller.ts
  12. 21
      packages/bridge-server/src/upload/upload.module.ts
  13. 18
      packages/bridge-server/src/upload/upload.service.spec.ts
  14. 83
      packages/bridge-server/src/upload/upload.service.ts
  15. 4
      packages/bridge-server/src/utilities/advanceable-buffer.ts
  16. 9
      packages/bridge-ui/src/app/components/new-race-dialog/new-race-dialog.component.ts
  17. 2
      packages/bridge-ui/src/app/components/race-details/race-details.component.html
  18. 3
      packages/bridge-ui/src/app/components/race-details/race-details.component.scss
  19. 36
      packages/bridge-ui/src/app/components/race-details/race-details.component.ts
  20. 5
      packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts
  21. 4
      packages/bridge-ui/src/app/models/season.model.ts
  22. 2
      packages/bridge-ui/src/app/pages/season-details/season-details.component.html
  23. 14
      packages/bridge-ui/src/app/pages/season-details/season-details.component.ts
  24. 22
      packages/bridge-ui/src/app/services/races.service.ts
  25. 18
      packages/bridge-ui/src/app/services/seasons.service.ts

2
packages/bridge-server/src/app.module.ts

@ -21,6 +21,7 @@ import { RaceResultsModule } from './race-results/race-results.module';
import { RacesModule } from './races/races.module';
import { RacersModule } from './racers/racers.module';
import { SeasonStandingsModule } from './season-standings/season-standings.module';
import { UploadModule } from './upload/upload.module';
@Module({
imports: [
@ -40,6 +41,7 @@ import { SeasonStandingsModule } from './season-standings/season-standings.modul
RacesModule,
RacersModule,
SeasonStandingsModule,
UploadModule,
],
controllers: [AppController, SeasonsController, UploadController],
providers: [AppService, UsersService, RacersService, RacesService, SeasonStandingsService, RaceResultsService],

40
packages/bridge-server/src/race-results/race-results.service.ts

@ -1,4 +1,42 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { RaceResult } from './race-result.model';
import { Sequelize } from 'sequelize-typescript';
import { Racer } from 'src/racers/racer.model';
@Injectable()
export class RaceResultsService {}
export class RaceResultsService {
constructor(
@InjectModel(RaceResult) private raceResultModel: typeof RaceResult,
private sequelize: Sequelize
)
{}
async getFastestTimesForRace(raceId: number) {
return this.raceResultModel.findAll({
where:{ raceId: raceId },
order: [['time', "ASC"]],
group:['racerId'],
include:[Racer]
});
}
async create(raceId: number, racerId: number, time: number, replayPath: string) {
try {
await this.sequelize.transaction( async t => {
const transactionHost = { transaction: t };
await this.raceResultModel.create({
raceId: raceId,
racerId: racerId,
replayPath: replayPath,
time: time
},
transactionHost
);
});
} catch (error) {
}
}
}

29
packages/bridge-server/src/racers/racers.service.ts

@ -1,4 +1,31 @@
import { Injectable } from '@nestjs/common';
import { Racer } from './racer.model';
import { InjectModel } from '@nestjs/sequelize';
import { Sequelize } from 'sequelize-typescript';
@Injectable()
export class RacersService {}
export class RacersService {
constructor(
@InjectModel(Racer) private racerModel: typeof Racer,
private sequelize: Sequelize
)
{}
async findOrCreate(gameHandle: string): Promise<Racer> {
try {
return this.sequelize.transaction( async t => {
return this.racerModel.findOrCreate({
where: {
gameHandle: gameHandle
},
transaction: t
}).then((value) => {
return value[0];
})
});
} catch (error) {
console.log(error);
}
}
}

12
packages/bridge-server/src/races/race.model.ts

@ -22,6 +22,18 @@ export class Race extends Model {
@Column
endDate: Date;
@Column
authorTime: number;
@Column
goldTime: number;
@Column
silverTime: number;
@Column
bronzeTime: number;
@HasMany(() => RaceResult)
results: RaceResult[];

43
packages/bridge-server/src/races/races.controller.ts

@ -1,27 +1,42 @@
import { Controller, Get, Param } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { from } from 'rxjs';
import { RacesService } from './races.service';
import { RaceResultsService } from 'src/race-results/race-results.service';
import * as TMIO from 'trackmania.io';
import { AuthGuard } from 'src/auth/auth.guard';
@Controller('races')
export class RacesController {
TMIO: any;
constructor() {
constructor(
private racesService: RacesService,
private raceResultsService: RaceResultsService
) {
this.TMIO = new TMIO.Client();
}
@Get('/map/:id')
@Get(':id')
findOne(@Param() params: any) {
let promise = new Promise((resolve, reject) => {
try{
this.TMIO.maps.get(params.id).then(async mapInfo => {
console.log(mapInfo);
resolve(mapInfo._data);
});
} catch (error) {
reject(error);
}
});
return from(promise);
return this.racesService.findOne(params.id);
}
@Get('/map/:id')
getMapInfo(@Param() params: any) {
return from(this.racesService.getMapDetails(params.id));
}
@Get(':id/results')
getResults(@Param() params: any) {
return this.raceResultsService.getFastestTimesForRace(params.id);
}
@UseGuards(AuthGuard)
@Post()
create(@Body() body: any) {
console.log("Races/Create")
return this.racesService.create(body.mapUID, body.startDate, body.endDate, body.seasonId);
}
}

3
packages/bridge-server/src/races/races.module.ts

@ -3,9 +3,10 @@ import { SequelizeModule } from '@nestjs/sequelize';
import { Race } from './race.model';
import { RacesService } from './races.service';
import { RacesController } from './races.controller';
import { RaceResultsModule } from 'src/race-results/race-results.module';
@Module({
imports: [SequelizeModule.forFeature([Race])],
imports: [SequelizeModule.forFeature([Race]), RaceResultsModule],
providers: [RacesService],
exports: [SequelizeModule, RacesService],
controllers: [RacesController]

73
packages/bridge-server/src/races/races.service.ts

@ -1,4 +1,75 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Sequelize } from 'sequelize-typescript';
import { Race } from './race.model';
import { RaceResult } from 'src/race-results/race-result.model';
import { Season } from 'src/seasons/season.model';
import * as TMIO from 'trackmania.io';
import { from } from 'rxjs';
import { Racer } from 'src/racers/racer.model';
@Injectable()
export class RacesService {}
export class RacesService {
TMIO: any;
constructor(
@InjectModel(Race) private raceModel: typeof Race,
private sequelize: Sequelize
)
{
this.TMIO = new TMIO.Client();
}
async findOne(id: number) {
return this.raceModel.findOne({
where: {id: id},
include:[{model: RaceResult, include:[Racer]}, Season]
});
}
async findOneBySeasonAndMapUID(seasonId: number, mapUID: string) {
return this.raceModel.findOne({ where: {seasonId: seasonId, mapUID: mapUID} })
}
async getMapDetails(mapUId: string) {
let promise = new Promise((resolve, reject) => {
try{
this.TMIO.maps.get(mapUId).then(async mapInfo => {
console.log(mapInfo);
resolve(mapInfo._data);
});
} catch (error) {
reject(error);
}
});
return promise;
}
async create(mapUID: string, startDate: Date, endDate: Date, seasonId: number) {
try {
let mapDetails = await this.getMapDetails(mapUID) as any;
await this.sequelize.transaction( async t => {
const transactionHost = { transaction: t };
await this.raceModel.create({
mapName: mapDetails.name,
mapURL: "",
mapUID: mapUID,
mapImgUrl: mapDetails.thumbnailUrl,
authorTime: mapDetails.authorScore,
goldTime: mapDetails.goldScore,
silverTime: mapDetails.silverScore,
bronzeTime: mapDetails.bronzeScore,
startDate: startDate,
endDate: endDate,
seasonId: seasonId
},
transactionHost
);
});
} catch (error) {
}
}
}

34
packages/bridge-server/src/season-standings/season-standings.service.ts

@ -1,4 +1,36 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { SeasonStanding } from './season-standing.model';
import { Sequelize } from 'sequelize-typescript';
@Injectable()
export class SeasonStandingsService {}
export class SeasonStandingsService {
constructor(
@InjectModel(SeasonStanding) private seasonStandingModel: typeof SeasonStanding,
private sequelize: Sequelize
)
{}
async findOrCreate(racerId: number, seasonId: number): Promise<SeasonStanding> {
try {
return this.sequelize.transaction( async t => {
return this.seasonStandingModel.findOrCreate({
where: {
racerId: racerId,
seasonId: seasonId,
},
transaction: t
}).then((value) => {
return value[0];
})
});
} catch (error) {
console.log(error);
}
}
updateStandings(seasonId: number) {
}
}

2
packages/bridge-server/src/seasons/seasons.service.ts

@ -18,7 +18,7 @@ export class SeasonsService {
}
async findOne(id: number) {
return this.seasonModel.findOne({ where: {id: id} , include:[Race, SeasonStanding] })
return this.seasonModel.findOne({ where: {id: id} , include:{ all: true } })
}
async create(title: string, subTitle: string, startingDate: Date) {

8
packages/bridge-server/src/trackmania-replays/gbx-replay.ts

@ -1,4 +1,4 @@
interface ChunkInfo {
class ChunkInfo {
dwId: number;
dwSize: number;
dwOffset: number;
@ -117,9 +117,9 @@ class gbxReplay {
let chunkId, chunkSize = 0;
let chunkOffset = this.calcChunkOffset(numChunks)
let chunkVersion: ChunkInfo;
let chunkCommunity: ChunkInfo;
let chunkAuthor: ChunkInfo;
let chunkVersion: ChunkInfo = new ChunkInfo;
let chunkCommunity: ChunkInfo = new ChunkInfo;
let chunkAuthor: ChunkInfo = new ChunkInfo;
for (let counter = 1; counter <= numChunks; counter++) {
chunkId = buff.readUInt32();

9
packages/bridge-server/src/upload/upload.controller.ts

@ -2,13 +2,18 @@ import { Controller, Post, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Body, UploadedFile } from '@nestjs/common/decorators';
import { Express } from 'express'
import { UploadService } from './upload.service';
@Controller('upload')
export class UploadController {
constructor(
private uploadService: UploadService
){}
@Post('replay')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body: any) {
console.log(body);
console.log(file);
this.uploadService.replayUploaded(file, body);
}
}

21
packages/bridge-server/src/upload/upload.module.ts

@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
import { RacersModule } from 'src/racers/racers.module';
import { RacesModule } from 'src/races/races.module';
import { SeasonStandingsModule } from 'src/season-standings/season-standings.module';
import { RaceResultsModule } from 'src/race-results/race-results.module';
@Module({
imports:[
RacersModule,
RacesModule,
RaceResultsModule,
SeasonStandingsModule,
],
providers: [UploadService],
controllers: [UploadController],
exports: [UploadService]
})
export class UploadModule {}

18
packages/bridge-server/src/upload/upload.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UploadService } from './upload.service';
describe('UploadService', () => {
let service: UploadService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UploadService],
}).compile();
service = module.get<UploadService>(UploadService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

83
packages/bridge-server/src/upload/upload.service.ts

@ -0,0 +1,83 @@
import { Injectable } from '@nestjs/common';
import { RacersService } from 'src/racers/racers.service';
import { RaceResultsService } from 'src/race-results/race-results.service';
import { RacesService } from 'src/races/races.service';
import { SeasonStandingsService } from 'src/season-standings/season-standings.service';
const AdvancableBuffer = require('../utilities/advanceable-buffer');
const gbxHeader = require('../trackmania-replays/gbx-header');
const gbxReplay = require('../trackmania-replays/gbx-replay');
const fs = require('fs');
@Injectable()
export class UploadService {
constructor(
private racersService: RacersService,
private racesService: RacesService,
private raceResultsService: RaceResultsService,
private seasonStandingsService: SeasonStandingsService
)
{}
async replayUploaded(file: Express.Multer.File, body: any) {
console.log(file.path)
let buffer = fs.readFileSync(file.path)
console.log(buffer)
let buff = new AdvancableBuffer(buffer);
let header = new gbxHeader();
header.parse(buff);
if (header.is_vaild == false)
{
return;
}
console.log(header);
let replay = new gbxReplay();
replay.parse(buff);
let currentRacer = await this.racersService.findOrCreate(replay.gamerHandle);
console.log(currentRacer);
let race = await this.racesService.findOneBySeasonAndMapUID(body.seasonId, replay.mapUID)
console.log(race);
let result = await this.raceResultsService.create(race.id, currentRacer.id, replay.bestTime, file.path);
console.log(result);
this.seasonStandingsService.updateStandings(body.seasonId);
/*
fs.readFile(file.path, async function(err, buffer)
{
console.log(err)
console.log(buffer)
let buff = new AdvancableBuffer(buffer);
let header = new gbxHeader();
header.parse(buff);
if (header.is_vaild == false)
{
return;
}
console.log(header);
let replay = new gbxReplay();
replay.parse(buff);
let currentRacer = await this.racersService.findOrCreate(replay.gamerHandle);
console.log(currentRacer);
let race = await this.racesService.findOne({season: body.seasonId, mapUID: replay.mapUID})
console.log(race);
let result = await this.raceResultsService.create({race: race, racer: currentRacer, timeInMilliseconds: replay.bestTime, replayPath: file.destination});
console.log(result);
this.seasonStandingsService.updateStandings(body.seasonId);
//res.status(200);
});
*/
}
}

4
packages/bridge-server/src/utilities/advanceable-buffer.ts

@ -3,7 +3,7 @@ class NadeoStringResult {
str: string;
}
class Advancablebuffer {
class AdvancableBuffer {
readPos: number = 0;
internalBuffer: Buffer;
littleEndian: boolean = true
@ -72,4 +72,4 @@ class Advancablebuffer {
}
}
module.exports = {Advancablebuffer, NadeoStringResult};
module.exports = AdvancableBuffer;

9
packages/bridge-ui/src/app/components/new-race-dialog/new-race-dialog.component.ts

@ -91,7 +91,16 @@ export class NewRaceDialogComponent {
}
submit() {
let mapUID = this.mapUID.value != undefined ? this.mapUID.value : "";
let startDate = this.startDate.value != undefined ? this.startDate.value : "";
let endDate = this.endDate.value != undefined ? this.endDate.value : "";
let _startDate = new Date(startDate);
let _endDate = new Date(endDate);
this.racesService.create(mapUID, _startDate, _endDate, parseInt(this.seasonId)).subscribe(data =>{
console.log(data);
});
}
}

2
packages/bridge-ui/src/app/components/race-details/race-details.component.html

@ -18,7 +18,7 @@
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Name </th>
<td mat-cell *matCellDef="let element"> {{getRacerName(element.racer_id)}} </td>
<td mat-cell *matCellDef="let element"> {{getRacerName(element.racer)}} </td>
</ng-container>
<!-- Weight Column -->

3
packages/bridge-ui/src/app/components/race-details/race-details.component.scss

@ -0,0 +1,3 @@
.img-thumbnail{
width: 400px;
}

36
packages/bridge-ui/src/app/components/race-details/race-details.component.ts

@ -6,6 +6,7 @@ import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
import { RacesService } from '../../services/races.service';
import { RacersService } from '../../services/racers.service';
import { RaceResultService } from '../../services/race-result.service';
import { Race } from '../../models/race.model';
@ -29,7 +30,8 @@ import { RaceEntry } from '../../models/raceEntry.model';
export class RaceDetailsComponent {
@Input() race?: Race;
racers: Map<string, Racer> = new Map<string, Racer>();
raceResults: Map<string, RaceEntry[]> = new Map<string, RaceEntry[]>;
//raceResults: Map<string, RaceEntry[]> = new Map<string, RaceEntry[]>;
raceResults: any;
displayedColumns: string[] = ['position', 'name', 'runTime', 'ghost'];
sortedResults: RaceEntry[] = [];
@ -37,10 +39,34 @@ export class RaceDetailsComponent {
constructor(
private racersService: RacersService,
private raceResultService: RaceResultService,
private racesService: RacesService,
) {}
ngOnInit() {
if( this.race == undefined) {
console.log("No race in race-details")
return;
}
this.racesService.getRaceResults(this.race.id).subscribe(data => {
console.log(data)
this.raceResults = data;
for(let result of this.raceResults){
this.sortedResults.push(result)
}
this.sortedResults.sort((a, b) => {
if( a.time == b.time ) { return 0 };
if( a.time < b.time ) { return -1};
return 1;
})
this.sortedResults = [...this.sortedResults];
})
/*
if( this.race.racers == undefined) {
console.log("No racers in race-details")
return;
}
for( let racer_id of this.race?.racers ) {
@ -66,6 +92,7 @@ export class RaceDetailsComponent {
}
});
}
*/
}
getBestTimeForRacer(racer_id: string): RaceEntry | undefined {
@ -95,9 +122,12 @@ export class RaceDetailsComponent {
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${String(remainingMilliseconds).padStart(3, '0')}`;
}
getRacerName(racerId: string): string {
let racer = this.racers.get(racerId);
getRacerName(racer: any): string {
if( racer != undefined) {
if( racer.name == undefined || racer.name == "" ) {
return racer.gameHandle;
}
return racer.name + " (" + racer.gameHandle + ")"
}

5
packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts

@ -56,6 +56,8 @@ export class SeasonStandingsComponent {
return;
}
return;
/*
for (let race_id of this.season.races) {
this.racesService.getRace(race_id).subscribe( race => {
if( race != undefined ){
@ -86,7 +88,8 @@ export class SeasonStandingsComponent {
}
}
});
}
}*/
}
getRacerName(racerId: string): string {

4
packages/bridge-ui/src/app/models/season.model.ts

@ -1,9 +1,11 @@
import { Race } from "./race.model";
export class Season {
id: string = "0";
title: string = "";
subTitle: string = "";
startingDate: Date = new Date(Date.now());
races: string[] = [];
races: Race[] = [];
racers: string[] = [];
standings: string[] = [];
}

2
packages/bridge-ui/src/app/pages/season-details/season-details.component.html

@ -18,7 +18,7 @@
<app-season-standings [season]="season"></app-season-standings>
</mat-tab>
@for (race of races; track race) {
<mat-tab label="{{'Week #' +race.id}}">
<mat-tab label="{{'Race #' +race.id}}">
<app-race-details [race]="race"></app-race-details>
</mat-tab>
}

14
packages/bridge-ui/src/app/pages/season-details/season-details.component.ts

@ -49,17 +49,9 @@ export class SeasonDetailsComponent {
const id = String(this.route.snapshot.paramMap.get('id'));
this.seasonsService.getSeason(id).subscribe( data => {
this.season = data;
if(this.season == undefined) {
return;
}
for (let race_id of this.season.races) {
this.racesService.getRace(race_id).subscribe( data => {
if( data != undefined ){
this.races.push(data);
}
});
console.log(data);
if (this.season) {
this.races = this.season?.races;
}
});
}

22
packages/bridge-ui/src/app/services/races.service.ts

@ -16,21 +16,19 @@ export class RacesService {
return this.httpClient.get<Array<Race>>("assets/races.json");
}
getRace(id: string): Observable<Race | undefined> {
let promise = new Promise<Race|undefined>((resolve, reject) => {
this.httpClient.get<Array<Race>>("assets/race.json").subscribe(data => {
for(let race of data) {
if(race.id == id) {
resolve(race);
}
}
reject(undefined);
});
});
return from(promise);
getRace(id: string) {
return this.httpClient.get("http://localhost:3000/races/"+id);
}
getRaceResults(id: string) {
return this.httpClient.get("http://localhost:3000/races/"+id+"/results");
}
getMapInfo(mapUID: string) {
return this.httpClient.get("http://localhost:3000/races/map/"+mapUID);
}
create(mapUID: string, startDate: Date, endDate: Date, seasonId: number) {
return this.httpClient.post("http://localhost:3000/races", { mapUID: mapUID, startDate: startDate, endDate: endDate, seasonId: seasonId});
}
}

18
packages/bridge-ui/src/app/services/seasons.service.ts

@ -10,7 +10,7 @@ import { Season } from '../models/season.model';
export class SeasonsService {
seasons: Season[] = [];
useTestData: boolean = true;
useTestData: boolean = false;
constructor(private httpClient: HttpClient) {
}
@ -22,17 +22,9 @@ export class SeasonsService {
_season.subTitle = data.subTitle;
_season.startingDate = new Date(data.startingDate);
_season.racers = [];
if ("races" in Object.keys(data)) {
_season.races = data.races;
} else {
_season.races = [];
}
if ("standings" in Object.keys(data)) {
_season.standings = data.standings;
} else {
_season.standings = [];
}
_season.races = data.races;
_season.standings = data.standings;
console.log(_season);
return _season;
}
@ -75,6 +67,8 @@ export class SeasonsService {
let promise = new Promise<Season|undefined>((resolve, reject) =>{
this.httpClient.get<Array<Season>>("http://localhost:3000/seasons/"+id).subscribe(data => {
let _season = this.convertToClientSeason(data);
console.log(data);
console.log(_season);
resolve(_season);
});
});

Loading…
Cancel
Save