From dd4907cde9fc2f6e0918d77a75095d16517049ee Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 13 Nov 2023 14:23:20 +0000 Subject: [PATCH] Add the ability to upload a replay+WIP GBX parser --- backend/trackmania-replays/gbx-header.js | 2 +- backend/trackmania-replays/gbx-replay.js | 139 ++++++++++++++++++ server.js | 9 +- .../season-details.component.html | 1 + .../season-details.component.less | 3 + .../season-details.component.ts | 17 +++ .../upload-replay-dialog.component.html | 13 +- .../upload-replay-dialog.component.less | 3 + .../upload-replay-dialog.component.ts | 16 ++ 9 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 backend/trackmania-replays/gbx-replay.js diff --git a/backend/trackmania-replays/gbx-header.js b/backend/trackmania-replays/gbx-header.js index 84a9c4b..bccbf4e 100644 --- a/backend/trackmania-replays/gbx-header.js +++ b/backend/trackmania-replays/gbx-header.js @@ -61,7 +61,7 @@ class gbxHeader { } this.id = buff.readUInt32(); - console.log(id) + console.log(this.id) if (this.version >=6) { diff --git a/backend/trackmania-replays/gbx-replay.js b/backend/trackmania-replays/gbx-replay.js new file mode 100644 index 0000000..bc1bc20 --- /dev/null +++ b/backend/trackmania-replays/gbx-replay.js @@ -0,0 +1,139 @@ +class gbxReplay { + + CHUNKS_OFFSET = 17; + HEADER_OFFSET = 21; + UNASSIGNED = 0xFFFFFFFF; + + calcChunkOffset(num) { + return (((num) * 8) + this.HEADER_OFFSET) + } + + isNumber(id) { return (((id) & 0xC0000000) == 0)} + isString(id) { return (((id) & 0xC0000000) != 0)} + isUnassigned(id) {return ((id) == this.UNASSIGNED)} + getIndex(id) {return ((id) & 0x3FFFFFFF)} + + readIdentifier(buff, idList, outString, stringLen) + { + if ( idList.version < 3 ) { + idList.version = buff.readUInt32(); + } + + let id = buff.readUInt32(); + if(this.isUnassigned(id)) { + return; + } + + if(this.isNumber(id)){ + console.log("number") + } + + if(idList.version == 2) { + outString = buff.readString(stringLen) + console.log(outString) + } + + if(this.isString(id) && this.getIndex(id) == 0) { + outString = buff.readString(stringLen) + + console.log(outString) + } + } + + parseReplayChunk(buff, chunkInfo) { + buff.seek(0+chunkInfo.dwOffset); + let version = buff.readUInt32(); + let isVSK = version >= 9999 ? true : false; + + let idList = { version: 0, index: 0, list:[[]]} + + if( chunkInfo.dwSize <= 4) { + return; + } + + if((!isVSK && version >= 3) || (isVSK && version >= 10000)) { + let outString = ""; + this.readIdentifier(buff, idList, outString, 512); + } + } + + parseAuthorChunk(buff, chunkInfo) { + + } + + parseCommunityChunk(buff, chunkInfo) { + + } + + parse(buff) + { + buff.seek(0+this.CHUNKS_OFFSET); + let numChunks = buff.readUInt32(); + + if(numChunks == 0) { + return true; + } + else if (numChunks > 0xff) { + return false; + } + + let chunkId, chunkSize = 0; + let chunkOffset = this.calcChunkOffset(numChunks) + let chunkVersion = {} + let chunkCommunity = {} + let chunkAuthor = {} + + for (let counter = 1; counter <= numChunks; counter++) { + chunkId = buff.readUInt32(); + chunkSize = buff.readUInt32(); + + chunkSize &= 0x7FFFFFFF; + + switch (chunkId){ + case 0x03093000: // (TM) + case 0x2403F000: // (VSK, TM) + chunkVersion.dwId = chunkId; + chunkVersion.dwSize = chunkSize; + chunkVersion.dwOffset = chunkOffset; + chunkOffset += chunkSize; + break; + case 0x03093001: // (TM) + case 0x2403F001: // (VSK, TM) + chunkCommunity.dwId = chunkId; + chunkCommunity.dwSize = chunkSize; + chunkCommunity.dwOffset = chunkOffset; + chunkOffset += chunkSize; + //OutputTextFmt(hwndCtl, szOutput, _countof(szOutput), g_szChunk, dwCouter, dwChunkId, dwChunkSize); + break; + + case 0x03093002: // (MP) + chunkAuthor.dwId = chunkId; + chunkAuthor.dwSize = chunkSize; + chunkAuthor.dwOffset = chunkOffset; + chunkOffset += chunkSize; + //OutputTextFmt(hwndCtl, szOutput, _countof(szOutput), g_szChunk, dwCouter, dwChunkId, dwChunkSize); + break; + + default: + chunkOffset += chunkSize; + //OutputTextFmt(hwndCtl, szOutput, _countof(szOutput), g_szChunk, dwCouter, dwChunkId, dwChunkSize); + } + } + + if (chunkVersion.dwSize > 0) { + this.parseReplayChunk(buff, chunkVersion); + } + + if (chunkCommunity.dwSize > 0) { + this.parseCommunityChunk(buff, chunkCommunity); + } + + if (chunkAuthor.dwSize > 0) { + this.parseAuthorChunk(buff, chunkAuthor); + } + } + + +} + +module.exports = gbxReplay; \ No newline at end of file diff --git a/server.js b/server.js index 3327ca0..7d8b9e4 100644 --- a/server.js +++ b/server.js @@ -61,6 +61,7 @@ const upload = require('./backend/routes/upload-replay'); const AdvancableBuffer = require('./backend/utilities/AdvancableBuffer.js'); const gbxHeader = require('./backend/trackmania-replays/gbx-header.js'); +const gbxReplay = require('./backend/trackmania-replays/gbx-replay'); // handling CORS app.use(cors()); @@ -205,8 +206,14 @@ app.post('/api/upload-replay', upload.single('file'), (req, res) => { fs.readFile(file.path, function(err, buffer) { buff = new AdvancableBuffer(buffer); - header = new gbxHeader().parse(buffer); + header = new gbxHeader() + header.parse(buff); + console.log(header); + replay = new gbxReplay(); + replay.parse(buff); }) + + res.status(200); }); app.listen(3000, () => { diff --git a/src/app/components/season-details/season-details.component.html b/src/app/components/season-details/season-details.component.html index 12b71f9..1573084 100644 --- a/src/app/components/season-details/season-details.component.html +++ b/src/app/components/season-details/season-details.component.html @@ -4,6 +4,7 @@
+
diff --git a/src/app/components/season-details/season-details.component.less b/src/app/components/season-details/season-details.component.less index e69de29..7315d3f 100644 --- a/src/app/components/season-details/season-details.component.less +++ b/src/app/components/season-details/season-details.component.less @@ -0,0 +1,3 @@ +.full-width-button { + width: 100%; + } \ No newline at end of file diff --git a/src/app/components/season-details/season-details.component.ts b/src/app/components/season-details/season-details.component.ts index 126910d..f00aad4 100644 --- a/src/app/components/season-details/season-details.component.ts +++ b/src/app/components/season-details/season-details.component.ts @@ -5,6 +5,7 @@ import { ApiService } from '../../services/api.service'; import { Season, SeasonWeek, SeasonStandingsEntry } from '../../models/season'; import { AuthService } from 'src/app/services/auth-service'; import { NewRaceDialogComponent } from '../new-race-dialog/new-race-dialog.component'; +import { UploadReplayDialogComponent } from '../upload-replay-dialog/upload-replay-dialog.component'; @Component({ selector: 'app-season-details', @@ -69,5 +70,21 @@ export class SeasonDetailsComponent { }); } + openUploadReplayDialog(): void { + const dialogRef = this.dialog.open(UploadReplayDialogComponent, { + width: '400px', + data: { ...this.newRaceDetails } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + console.log('Follow details saved:', result); + // You can update your data or perform other actions here + } else { + console.log('Dialog canceled'); + } + }); + } + } diff --git a/src/app/components/upload-replay-dialog/upload-replay-dialog.component.html b/src/app/components/upload-replay-dialog/upload-replay-dialog.component.html index c73f0a8..e080777 100644 --- a/src/app/components/upload-replay-dialog/upload-replay-dialog.component.html +++ b/src/app/components/upload-replay-dialog/upload-replay-dialog.component.html @@ -1 +1,12 @@ -

upload-replay-dialog works!

+ + +
+ + {{fileName || "No file uploaded yet."}} + + +
diff --git a/src/app/components/upload-replay-dialog/upload-replay-dialog.component.less b/src/app/components/upload-replay-dialog/upload-replay-dialog.component.less index e69de29..06787c2 100644 --- a/src/app/components/upload-replay-dialog/upload-replay-dialog.component.less +++ b/src/app/components/upload-replay-dialog/upload-replay-dialog.component.less @@ -0,0 +1,3 @@ +.file-input { + display: none; +} \ No newline at end of file diff --git a/src/app/components/upload-replay-dialog/upload-replay-dialog.component.ts b/src/app/components/upload-replay-dialog/upload-replay-dialog.component.ts index be990fe..dfd94b0 100644 --- a/src/app/components/upload-replay-dialog/upload-replay-dialog.component.ts +++ b/src/app/components/upload-replay-dialog/upload-replay-dialog.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { ApiService } from '../../services/api.service'; @Component({ selector: 'app-upload-replay-dialog', @@ -6,5 +7,20 @@ import { Component } from '@angular/core'; styleUrls: ['./upload-replay-dialog.component.less'] }) export class UploadReplayDialogComponent { + fileName = ''; + constructor(private apiService: ApiService) {} + + onFileSelected(event: any) + { + const file:File = event.target.files[0]; + if (file) + { + this.fileName = file.name; + let formData = new FormData(); + formData.append("file", file); + const upload$ = this.apiService.postReplayUpload(formData); + upload$.subscribe(); + } + } }