diff --git a/package-lock.json b/package-lock.json index 2372a4f..5f46e43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,17 +14,21 @@ "@tauri-apps/plugin-shell": ">=2.0.0", "@types/d3": "^7.4.3", "d3": "^7.9.0", + "file-saver": "^2.0.5", "fs": "^0.0.1-security", "marked": "^14.1.4", "moons-ta-client": "^1.1.14", + "pako": "^2.1.0", "prismjs": "^1.30.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "@tauri-apps/cli": ">=2.0.0", + "@types/file-saver": "^2.0.7", "@types/node": "^22.7.6", "autoprefixer": "^10.4.20", "postcss": "^8.4.49", @@ -1578,6 +1582,12 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/geojson": { "version": "7946.0.14", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", @@ -1618,6 +1628,14 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -1847,6 +1865,18 @@ } ] }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1885,6 +1915,14 @@ "periscopic": "^3.1.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1934,6 +1972,17 @@ "node": ">= 0.6" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2635,6 +2684,11 @@ "reusify": "^1.0.4" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2664,6 +2718,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -6207,6 +6269,11 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -6724,6 +6791,17 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -7339,6 +7417,22 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -7458,6 +7552,26 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yaml": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", diff --git a/package.json b/package.json index 30db63e..8f5bca0 100644 --- a/package.json +++ b/package.json @@ -18,17 +18,21 @@ "@tauri-apps/plugin-shell": ">=2.0.0", "@types/d3": "^7.4.3", "d3": "^7.9.0", + "file-saver": "^2.0.5", "fs": "^0.0.1-security", "marked": "^14.1.4", "moons-ta-client": "^1.1.14", + "pako": "^2.1.0", "prismjs": "^1.30.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "@tauri-apps/cli": ">=2.0.0", + "@types/file-saver": "^2.0.7", "@types/node": "^22.7.6", "autoprefixer": "^10.4.20", "postcss": "^8.4.49", diff --git a/src/lib/components/modules/PreviouslyPlayedSongs.svelte b/src/lib/components/modules/PreviouslyPlayedSongs.svelte new file mode 100644 index 0000000..546ff78 --- /dev/null +++ b/src/lib/components/modules/PreviouslyPlayedSongs.svelte @@ -0,0 +1,600 @@ + + +
+
+

Previously Played ({maps.length})

+
+ + {#if maps.length === 0} +
+ history +

No maps played yet

+

Completed maps will appear here

+
+ {:else} +
+ {#each maps as map (map.taData.guid)} +
+
+ Map Cover +
+
+

{map.beatsaverData.name || 'Unknown Song'}

+

{map.beatsaverData.metadata?.songAuthorName || 'Unknown Artist'}

+
+ + {getDifficultyName(map.taData.gameplayParameters?.beatmap?.difficulty || 0)} + + + {getCompletionTypeIcon(map.completionType)} + {map.completionType} + + {#if getModifierArray(map).length > 0} +
+ {#each getModifierArray(map) as modifier} + {modifier} + {/each} +
+ {/if} +
+
+ {#if map.scores.length > 0} + {@const topScore = map.scores.reduce((max, score) => score.score > max.score ? score : max)} + + emoji_events + {topScore.player?.name || topScore.player?.discordInfo?.username}: {Number(topScore.tournamentId).toLocaleString()} + + {/if} +
+
+
+ + + + +
+
+ {/each} +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/lib/components/modules/SongQueue.svelte b/src/lib/components/modules/SongQueue.svelte new file mode 100644 index 0000000..84c1871 --- /dev/null +++ b/src/lib/components/modules/SongQueue.svelte @@ -0,0 +1,680 @@ + + +
+
+

Song Queue ({maps.length})

+ +
+ + {#if maps.length === 0} +
+ queue_music +

No maps in queue

+

Add maps to get started

+
+ {:else} +
+ {#each maps as map, index (map.taData.guid)} +
handleMapClick(map)} on:keydown={(e) => e.key === 'Enter' && handleMapClick(map)} tabindex="0" role="button"> +
+ {index + 1} +
+
+ Map Cover +
+
+

{map.beatsaverData.name || 'Unknown Song'}

+

{map.beatsaverData.metadata?.songAuthorName || 'Unknown Artist'}

+
+ + {getDifficultyName(map.taData.gameplayParameters?.beatmap?.difficulty || 0)} + + {#if getModifierArray(map).length > 0} +
+ {#each getModifierArray(map) as modifier} + {modifier} + {/each} +
+ {/if} +
+
+ +
+ + + +
+
+ {/each} +
+ {/if} +
+ + +{#if showConfirmDialog} + +
e.key === 'Escape' && cancelLoad()}> +
+
+

Load Map

+ +
+
+

Are you sure you want to load this map for all players in the match?

+ {#if mapToLoad} +
+ Map Cover +
+

{mapToLoad.beatsaverData.name || 'Unknown Song'}

+

{mapToLoad.beatsaverData.metadata?.songAuthorName || 'Unknown Artist'}

+ + {getDifficultyName(mapToLoad.taData.gameplayParameters?.beatmap?.difficulty || 0)} + +
+
+ {/if} +
+
+ + +
+
+
+{/if} + + \ No newline at end of file diff --git a/src/lib/components/popups/EditMap.svelte b/src/lib/components/popups/EditMap.svelte index bd65ba6..582a085 100644 --- a/src/lib/components/popups/EditMap.svelte +++ b/src/lib/components/popups/EditMap.svelte @@ -2,13 +2,12 @@ import { createEventDispatcher } from "svelte"; //@ts-ignore import { Response_ResponseType, Map, Characteristic, GameplayModifiers_GameOptions } from 'moons-ta-client'; - import { TAServerPort, TAServerUrl, authTokenStore, client } from "$lib/stores"; + import { TAServerPort, TAServerUrl, authTokenStore } from "$lib/stores"; import { v4 as uuidv4 } from "uuid"; import { onMount } from "svelte"; import type { BeatSaverMap } from "$lib/services/beatsaver"; export let tournamentGuid: string; - export let poolGuid: string; export let mode: 'qualifier' | 'playing' = 'playing'; export let map: { taData: Map, @@ -17,12 +16,6 @@ const dispatch = createEventDispatcher(); - // Remove 'Bearer ' from the token - if ($authTokenStore) { - const cleanToken = $authTokenStore.replace('Bearer ', ''); - client.setAuthToken(cleanToken); - } - // Form state let isLoading = false; let error: string | null = null; @@ -407,7 +400,7 @@ selectedCharacteristic = availableCharacteristics[0] || ""; selectedDifficulty = availableDifficulties[selectedCharacteristic]?.[0] || ""; - mapBeatmapId = `custom_level_${latestVersion.hash.toLocaleUpperCase()}`; + mapBeatmapId = `custom_level_${latestVersion.hash.toUpperCase()}`; return data; } catch (err) { @@ -446,21 +439,6 @@ error = null; try { - if(!client.isConnected) { - const connectResult = await client.connect($TAServerUrl, $TAServerPort); - - if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) { - throw new Error(connectResult.details.connect.message); - } - } - - if(!client.stateManager.getTournament(tournamentGuid)!.users.some(user => user.guid == client.stateManager.getSelfGuid())) { - const joinResult = await client.joinTournament(tournamentGuid); - if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) { - throw new Error('Could not join tournament'); - } - } - const modifiersBitmap = getActiveModifiersAsBitmap(); const mapData: Map = { guid: map?.taData.guid || uuidv4(), diff --git a/src/lib/components/popups/QualifierLeaderboard.svelte b/src/lib/components/popups/QualifierLeaderboard.svelte new file mode 100644 index 0000000..1ceaefd --- /dev/null +++ b/src/lib/components/popups/QualifierLeaderboard.svelte @@ -0,0 +1,835 @@ + + + + + + + + \ No newline at end of file diff --git a/src/lib/services/beatsaver.ts b/src/lib/services/beatsaver.ts index 626ec64..ba03a91 100644 --- a/src/lib/services/beatsaver.ts +++ b/src/lib/services/beatsaver.ts @@ -55,6 +55,7 @@ export interface BeatSaverMap { resets: number; }; stars: number; + maxScore: number; }>; downloadURL: string; previewURL: string; diff --git a/src/lib/taDocs/0-TODO b/src/lib/taDocs/0-TODO new file mode 100644 index 0000000..388a8eb --- /dev/null +++ b/src/lib/taDocs/0-TODO @@ -0,0 +1,12 @@ +still left: +- the last 3 stateManager docs, +- all the base client methods, +- all the modals +- all the enums +- further docs on streamsync +- how to correctly load a map +- how to handle scores (sec. 6) +- whatever else comes to my mind + +Section 6: events you can listen to (realtimescore, matchupdate, etc) +Section 7: Best Practices? or something like that, sort of an 'I did this and so should you' \ No newline at end of file diff --git a/src/lib/taDocs/1-fam-states-intro.md b/src/lib/taDocs/1-fam-states-intro.md new file mode 100644 index 0000000..df41ecf --- /dev/null +++ b/src/lib/taDocs/1-fam-states-intro.md @@ -0,0 +1,29 @@ +# Familiarising yourself with the TA Client +It is important that you don't just copy and paste code from here if you want to actually understand the client. The best way to use the client is to test out the features for yourself and play around with what you can do. + +# What does "State-Based" mean? +The TournamentAssistant Client being state based means that not every action will result in a database query or ven a request in the first place. A great example of this can be seen on ShyyTAUI when you switch in-between the tabs on the navbar. Once the tournaments are loaded, they are saved in the state of the user. This means that it will take significantly less time to load for clients. If you request the tournaments once, but another user just requested it after a long period of inactivity, the tournaments are loaded in the state and a database query on the backend is not necessary to serve your request, hence the server will respond more promptly. + +This also leads us to event listeners, or just listeners for short. Listeners serve the purpose of making sure that when the state changes on the server, your client also receives the update. A quick example of listeners is: +```ts +client.stateManager.on('matchCreated', handleMatchCreated); +client.stateManager.on('matchDeleted', handleMatchDeleted); +client.stateManager.on('matchUpdated', handleMatchUpdated); +client.stateManager.on('userConnected', handleUserConnected); +client.stateManager.on('userDisconnected', handleUserDisconnected); +``` +
+ info +
+ Example: This is an example from the page of ShyyTAUI where the matches and available players are shown. +
+
+ +In this case, we use `client.stateManger`, which indicates that we want to listen to the changes in the state. It is often very confusing what the difference between `client.on()` and `client.stateManager.on()` is. The best way to explain it is to say that the `client` is YOU and the `stateManager` is THE SERVER. So when you listen using the `client`(`client.on()`), then you are listening to events you actioned. (`client.on('createdMatch', callback)` for example listens to the confirmation of when you created the match). On the other hand, listening with `client.stateManager.on()` acts as a global state listener(`client.stateManager.on('matchCreated', callback)` listens to when a match is created in general. Such as by another coordinator). + +
+ warning +
+ Note: This will be explained further in the 'State Manager' section, along with further behaviour descriptors. (most info will even be repeated) +
+
\ No newline at end of file diff --git a/src/lib/taDocs/intro.md b/src/lib/taDocs/1-installation.md similarity index 80% rename from src/lib/taDocs/intro.md rename to src/lib/taDocs/1-installation.md index 3815452..8f69c5f 100644 --- a/src/lib/taDocs/intro.md +++ b/src/lib/taDocs/1-installation.md @@ -1,11 +1,10 @@ -# Introduction -TournamentAssistant is a piece of woftware which has a backend(TournamentAssistantServer), frontend(TournamentAssistantUI), and in-game plugin(TournamentAssistant). It is important to note before proceeding that TA uses a websocket for connections and DOES NOT have an API. The websocket uses protobuf, so the packets themseves are not readable by humans naturally. That is why the TA client is important. It includes the proto models and deserialises them. ## Installing the npm package The client can be installed through the npm repository and has a crucial role in transforming packets and seding them to the websocket. ```code npm i moons-ta-client ``` Once installed we can run some basic scripts in any JS/TS file. I strongly recommend the use of TS, as it is what I will be using in the examples which I provide. + ## Let's run a basic script We will construct a script which will connect to the default TournamentAssistant server and get the information of a tournament. ```ts @@ -25,7 +24,12 @@ taClient.disconnect(); ``` You might notice that we have not actually connected to a tournament or fetched any details. This is intentional, as the connectResult already inclides the current state of the given TA server. -As of now, it is not possible to host your own TA server. This is not the intention of the new TA, rather it is meant to centralise everything as it is difficult for everyone to swap servers constantly. +
+ info +
+ Note: As of now, it is not possible to host your own TA server. This is not the intention of the new TA, rather it is meant to centralise everything as it is difficult for everyone to swap servers constantly. +
+
The connectResult has the following schema: ```ts diff --git a/src/lib/taDocs/1-intro.md b/src/lib/taDocs/1-intro.md new file mode 100644 index 0000000..91cfba7 --- /dev/null +++ b/src/lib/taDocs/1-intro.md @@ -0,0 +1,13 @@ +# Introduction +TournamentAssistant is a piece of woftware which has a backend(TournamentAssistantServer), frontend(TournamentAssistantUI), and in-game plugin(TournamentAssistant). It is important to note before proceeding that TA uses a websocket for connections and DOES NOT have a RESTful API. The websocket uses protobuf, so the packets themseves are not readable by humans naturally. That is why the TA client is important. It includes the proto models and deserialises them. + +## Prerequisites +You should probably get familiar with: +- Serialisation +- Typescript +- Websocket connections + +Since the TA Client is a websocket connection on the coordinator end, it is important to understand when and how connections can break and how to check for this. Typescript is the preferred language to follow this guide in, so having ablank typescript project or any .ts file that you cna transpile and work with is almost required, although raw javascript also works, but you will have less control. + +## The Guide +This guide will show every function within the TournamentAssistant Client and I will also be providing some implementation examples from [ShyyTAUI](/). Keep in mind that you will be seeing code that works in Svelte 4, and I will not always remove irrelevant code, as I want to show direct implementation which includes all of the side-processes being done. I will do my best to keep this documentation as up to date as possible, however TA constantly changes and is still somewhat a work in progress tool. If you beleive something is incorrect in this guide, or would like to make corrections, please contact me on [Discord](https://discord.com/users/469171963236057120). \ No newline at end of file diff --git a/src/lib/taDocs/taInfo.md b/src/lib/taDocs/1-taInfo.md similarity index 95% rename from src/lib/taDocs/taInfo.md rename to src/lib/taDocs/1-taInfo.md index 55c1fcb..12bcc16 100644 --- a/src/lib/taDocs/taInfo.md +++ b/src/lib/taDocs/1-taInfo.md @@ -16,8 +16,8 @@ When you initiate the websocket connection, the initial state of the core server You might also notice that the stateManager is the only way to fetch tournament info etc. This is again done to ease development and redice server load. -
- info +
+ question_mark
Will the stateManager be out-of-date? The stateManager automatically updates its state when a packet is received or sent, so no, you do not have to worry about out-of-date information being stored.
diff --git a/src/lib/taDocs/2-stateManager-allFunctions.md b/src/lib/taDocs/2-stateManager-allFunctions.md new file mode 100644 index 0000000..38ab6cc --- /dev/null +++ b/src/lib/taDocs/2-stateManager-allFunctions.md @@ -0,0 +1,38 @@ +# All of the methods of the StateManager + +Below you can find a table that shows all of the methods of the stateManager. +| taClient.stateManager. (method) | Purpose and functionality | Is Async | +|:----------------------------------:|:--------------------------|:--------:| +|emit()|Emit a custom event. Only use this if you know what you are doing.|True| +|getKnownServers()|Get the known servers which have been sent in the connect state. **This is a cached list.**|False| +|getMatch()|Get the details of a match.|False| +|getMatches()|Get an array of all of the matches in a tournament.|False| +|getQualifier()|Get the details of a qualifier event.|False| +|getQualifiers()|Get an array of all of the qualifiers in a tournament.|False| +|getSelfGuid()|Get the `.guid` property of the currently logged in user or bot token.|False| +|getTournament()|Get the information about a tournament.|False| +|getTournaments()|Get an array of all of the tournaments of a Core Server|False| +|getUser()|Get all of the details of a user in a tournament.|False| +|getUsers()|Get an array of all of the users in a tournament.|False| +|handlePacket()|Handle the response to a packet sent to you by the TA Core Server. Only use if you know what you are doing.|False| +|on()|Standard listener for the taClient. This is how real time score can be subscribed to.|False| +|once()|This is essentially the same as `taClient.stateManager.on()` except it unsubscribes after the first event.|False| +|removeListener()|Unsibscribe from the events that you have started to listen to.|False| + +## How to listen, what is the general schema? +Generally, the format is just: +```ts +taClient.stateManager.on('eventType', () => { + // callback function +}) +``` +or +```ts +async function handleEvent(params) { + // code here +} + +taClient.stateManager.on('eventType', handleEvent); +``` + +All of the event types and how to handle them are within this State Manager section. \ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-emit.md b/src/lib/taDocs/2-stateManager-emit.md new file mode 100644 index 0000000..ae6be89 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-emit.md @@ -0,0 +1,15 @@ +# The stateManager.emit() method + +
+ info +
+ Async: This is a method you would want to use await with +
+
+ +## General usage: +```ts +await taClient.stateManager.emit('eventType', params); +``` + +I will not proide further documentation for this method, as it should NOT be used. Every way you could use it exists with another method. \ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getKnownServers.md b/src/lib/taDocs/2-stateManager-getKnownServers.md new file mode 100644 index 0000000..f186197 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getKnownServers.md @@ -0,0 +1,31 @@ +# The stateManager.getKnownServers() method +This documents the `stateManager.getKnownServers() method.` + +## General usage: +```ts +const servers: CoreServer[] = taClient.stateManager.getKnownServers(); +``` + +This method will return an array of the known `CoreServer`s. A `CoreServer` can be described by the following TypeScript Interface: +```ts +interface CoreServer { + /** + * @generated from protobuf field: string name = 1; + */ + name: string; + /** + * @generated from protobuf field: string address = 2; + */ + address: string; + /** + * @generated from protobuf field: int32 port = 3; + */ + port: number; + /** + * @generated from protobuf field: int32 websocket_port = 4; + */ + websocketPort: number; +} +``` + +This means that a `CoreServer` has a `name`(such as 'Default Server' in the case of the default server), an `address`('server.tournamentassistant.net' in the case of the default server), a `port` which the players connect to(8675 by default), and a `websocketPort` to which coordinators, overlays and the client connects to(by default 8676) \ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getMatch.md b/src/lib/taDocs/2-stateManager-getMatch.md new file mode 100644 index 0000000..59d9103 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getMatch.md @@ -0,0 +1,84 @@ +# The stateManager.getMatch() method +This documents the `stateManager.getMatch()` method. + +This method has input parameters. The function could easily be defined as: +```ts +function getMatch(tournamentGuid: string, matchGuid: string): Match | undefined { + // backend logic +} +``` +## General usage: +```ts +const matchResponse: Match | undefined = taClient.stateManager.getMatch(tournamentGuid, matchGuid); +``` + +This method will return either the `Match` object or undefined. A `Match` can be described by the following TypeScript Interface: +```ts +interface Match { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: repeated string associated_users = 2; + */ + associatedUsers: string[]; + /** + * @generated from protobuf field: string leader = 3; + */ + leader: string; + /** + * @generated from protobuf field: proto.models.Map selected_map = 4; + */ + selectedMap?: Map; +} +``` + +For the definition of a Map please look at the [Modals -> Map page](/documentation#Modals/4-Map) + +A direct implementation: +```ts +try { + // Check if the client is connected, connect to the server if not + if(!client.isConnected) { + // Connec to the server + const connectResult = await client.connect($TAServerUrl, $TAServerPort); + // Check if the connection is successful + if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) { + throw new Error(connectResult.details.connect.message); + } + } + + // Get tournament and match data + tournament = client.stateManager.getTournament(tournamentGuid); + + // This is the implementation, we just set the match object to the one we neem + match = client.stateManager.getMatch(tournamentGuid, matchGuid); + + // Check if the logged in coordinator or user is added to the match properly + if(!match?.associatedUsers.includes(client.stateManager.getSelfGuid())) { + // If not, add the user to the match + await addSelfToMatch(); + } + + // Check to see if we are actually viewing a valid match + if (!match) { + throw new Error('Match not found'); + } + + // This function just does some magic with the beatsaver data of the currently selected map + await refreshMatchDetails(match); +} catch (err) { + console.error('Error fetching match data:', err); + error = err instanceof Error ? err.message : 'An unknown error occurred'; +} finally { + // Stop loading the page and allow everything to show data without errors + isLoading = false; +} +``` +
+ info +
+ Example: This is an example from the page of ShyyTAUI within the match coorination page. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getMatches.md b/src/lib/taDocs/2-stateManager-getMatches.md new file mode 100644 index 0000000..c67c8f6 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getMatches.md @@ -0,0 +1,37 @@ +# The stateManager.getMatches() method +This documents the `stateManager.getMatches()` method. + +This method has input parameters. The function could easily be defined as: +```ts +function getMatches(tournamentGuid: string,): Match[] | undefined { + // backend logic +} +``` +## General usage: +```ts +const matchesResponse: Match[] | undefined = taClient.stateManager.getMatches(tournamentGuid); +``` + +This method will return either an array of the `Match` object or undefined. A `Match` can be described by the following TypeScript Interface: +```ts +interface Match { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: repeated string associated_users = 2; + */ + associatedUsers: string[]; + /** + * @generated from protobuf field: string leader = 3; + */ + leader: string; + /** + * @generated from protobuf field: proto.models.Map selected_map = 4; + */ + selectedMap?: Map; +} +``` + +For the definition of a Map please look at the [Modals -> Map page](/documentation#Modals/4-Map) \ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getQualifier.md b/src/lib/taDocs/2-stateManager-getQualifier.md new file mode 100644 index 0000000..ec8e6e8 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getQualifier.md @@ -0,0 +1,65 @@ +# The stateManager.getQualifier() method +This documents the `stateManager.getQualifier()` method. + +This method has input parameters. The function could easily be defined as: +```ts +function getQualifier(tournamentGuid: string, qualifierGuid: string): QualifierEvent | undefined { + // backend logic +} +``` +## General usage: +```ts +const qualifier: QualifierEvent | undefined = taClient.stateManager.getQualifier(tournamentGuid, qualifierGuid); +``` + +This method will return either the `QualifierEvent` object or undefined. A `QualifierEvent` can be described by the following TypeScript Interface: +```ts +interface QualifierEvent { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: string name = 2; + */ + name: string; + /** + * @generated from protobuf field: bytes image = 3; + */ + image: Uint8Array; + /** + * @generated from protobuf field: proto.discord.Channel info_channel = 4; + */ + infoChannel?: { + /** + * @generated from protobuf field: string id = 1; + */ + id: string; + /** + * @generated from protobuf field: string name = 2; + */ + name: string; + }; + /** + * @generated from protobuf field: repeated proto.models.Map qualifier_maps = 5; + */ + qualifierMaps: Map[]; + /** + * @generated from protobuf field: proto.models.QualifierEvent.EventSettings flags = 6; + */ + flags: QualifierEvent_EventSettings; + /** + * @generated from protobuf field: proto.models.QualifierEvent.LeaderboardSort sort = 7; + */ + sort: QualifierEvent_LeaderboardSort; +} +``` + +For the definition of the `QualifierEvent_EventSettings` look at the [Enums -> QualifierEvent_EventSettings](/documentation#Enums/5-QualifierEvent_EventSettings) and for the `QualifierEvent_LeaderboardSort` look at the [Enums -> QualifierEvent_EventSettings](/documentation#Enums/5-QualifierEvent_LeaderboardSort) page. + +
+ info +
+ Example: A direct implementation example is not present, since this method is not used in ShyyTAUI. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getQualifiers.md b/src/lib/taDocs/2-stateManager-getQualifiers.md new file mode 100644 index 0000000..e495043 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getQualifiers.md @@ -0,0 +1,71 @@ +# The stateManager.getQualifiers() method +This documents the `stateManager.getQualifiers()` method. + +This method has input parameters. The function could easily be defined as: +```ts +function getQualifiers(tournamentGuid: string): QualifierEvent[] | undefined { + // backend logic +} +``` +## General usage: +```ts +const qualifiers: QualifierEvent[] | undefined = taClient.stateManager.getQualifiers(tournamentGuid); +``` + +This method will return either an array of the `QualifierEvent` object or undefined. A `QualifierEvent` can be described by the following TypeScript Interface: +```ts +interface QualifierEvent { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: string name = 2; + */ + name: string; + /** + * @generated from protobuf field: bytes image = 3; + */ + image: Uint8Array; + /** + * @generated from protobuf field: proto.discord.Channel info_channel = 4; + */ + infoChannel?: { + /** + * @generated from protobuf field: string id = 1; + */ + id: string; + /** + * @generated from protobuf field: string name = 2; + */ + name: string; + }; + /** + * @generated from protobuf field: repeated proto.models.Map qualifier_maps = 5; + */ + qualifierMaps: Map[]; + /** + * @generated from protobuf field: proto.models.QualifierEvent.EventSettings flags = 6; + */ + flags: QualifierEvent_EventSettings; + /** + * @generated from protobuf field: proto.models.QualifierEvent.LeaderboardSort sort = 7; + */ + sort: QualifierEvent_LeaderboardSort; +} +``` +
+ info +
+ Good to know: For the sake of simplification, the type of infoChannel has been substituted. Find the documentation on the original type here: Modals -> Channel +
+
+ +For the definition of the `QualifierEvent_EventSettings` look at the [Enums -> QualifierEvent_EventSettings](/documentation#Enums/5-QualifierEvent_EventSettings) and for the `QualifierEvent_LeaderboardSort` look at the [Enums -> QualifierEvent_EventSettings](/documentation#Enums/5-QualifierEvent_LeaderboardSort) page. + +
+ info +
+ Example: A direct implementation example is not present, since this method is not used in ShyyTAUI. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getSelfGuid.md b/src/lib/taDocs/2-stateManager-getSelfGuid.md new file mode 100644 index 0000000..24379ee --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getSelfGuid.md @@ -0,0 +1,28 @@ +# The stateManager.getSelfGuid() method +This documents the `stateManager.getSelfGuid()` method. + +This is perhaps the simplest method of the `stateManager`, as its response is a simple string which is the guid of the currently connected client. +## General usage: +```ts +const myGuid: string = taClient.stateManager.getSelfGuid(); +``` + +There is not much more to explain here. This method will simply return the currently connected client's GUID. + +It can be useful in scenarios such as: +```ts +async function addSelfToMatch() { + if(!match?.associatedUsers.includes(client.stateManager.getSelfGuid())) { + const response = await client.addUserToMatch(tournamentGuid, matchGuid, client.stateManager.getSelfGuid()); + console.log("Added Self to match!", response); + } +} +``` +
+ info +
+ Example: This is an example from the page of ShyyTAUI within the match coorination page. +
+
+ +The example above demonstrates the `addSelfToMatch` method used in ShyyTAUI in order to ensure that the coordinator is correctly connected and associated to the match. \ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getTournament.md b/src/lib/taDocs/2-stateManager-getTournament.md new file mode 100644 index 0000000..26861a3 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getTournament.md @@ -0,0 +1,69 @@ +# The stateManager.getTournament() method +This documents the `stateManager.getTournament()` method. + +This method has input parameters. The function could easily be defined as: +```ts +function getTournament(tournamentGuid: string): Tournament | undefined { + // backend logic +} +``` +## General usage: +```ts +const tournament: Tournament | undefined = taClient.stateManager.getTournament(tournamentGuid); +``` + +This method will return either the `Tournament` object or undefined. A `Tournament` can be described by the following TypeScript Interface: +```ts +interface Tournament { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: proto.models.Tournament.TournamentSettings settings = 2; + */ + settings?: Tournament_TournamentSettings; + /** + * @generated from protobuf field: repeated proto.models.User users = 3; + */ + users: User[]; + /** + * @generated from protobuf field: repeated proto.models.Match matches = 4; + */ + matches: Match[]; + /** + * @generated from protobuf field: repeated proto.models.QualifierEvent qualifiers = 5; + */ + qualifiers: QualifierEvent[]; + /** + * @generated from protobuf field: proto.models.CoreServer server = 6; + */ + server?: CoreServer; +} +``` + +- For the definition of a Tournament_TournamentSettings please look at the [Modals -> Tournament_TournamentSettings page](/documentation#Modals/4-Tournament_TournamentSettings) +- For the definition of a User please look at the [Modals -> User page](/documentation#Modals/4-User) +- For the definition of a Match please look at the [Modals -> Match page](/documentation#Modals/4-Match) +- For the definition of a QualifierEvent please look at the [Modals -> QualifierEvent page](/documentation#Modals/4-QualifierEvent) +- For the definition of a CoreServer please look at the [Modals -> CoreServer page](/documentation#Modals/4-CoreServer) + +A direct implementation: +```ts +// Get the tournament data, and check if the current user is within the users that are currently in the touranament +if(!client.stateManager.getTournament(tournamentGuid)!.users.some(user => user.guid == client.stateManager.getSelfGuid())) { + // If the user is not in the tournament, join it + const joinResult = await client.joinTournament(tournamentGuid); + // Check if the join was successful + if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) { + // Throw an error if not + throw new Error('Could not join tournament'); + } +} +``` +
+ info +
+ Example: This is an example from the page of ShyyTAUI within the qualifiers page. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getTournaments.md b/src/lib/taDocs/2-stateManager-getTournaments.md new file mode 100644 index 0000000..06d3ad7 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getTournaments.md @@ -0,0 +1,60 @@ +# The stateManager.getTournamenst() method +This documents the `stateManager.getTournaments()` method. + +```ts +function getTournaments(): Tournament[] | undefined { + // backend logic +} +``` +## General usage: +```ts +const tournaments: Tournament[] | undefined = taClient.stateManager.getTournaments(); +``` + +This method will return either an array of the `Tournament` object or undefined. A `Tournament` can be described by the following TypeScript Interface: +```ts +interface Tournament { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: proto.models.Tournament.TournamentSettings settings = 2; + */ + settings?: Tournament_TournamentSettings; + /** + * @generated from protobuf field: repeated proto.models.User users = 3; + */ + users: User[]; + /** + * @generated from protobuf field: repeated proto.models.Match matches = 4; + */ + matches: Match[]; + /** + * @generated from protobuf field: repeated proto.models.QualifierEvent qualifiers = 5; + */ + qualifiers: QualifierEvent[]; + /** + * @generated from protobuf field: proto.models.CoreServer server = 6; + */ + server?: CoreServer; +} +``` + +- For the definition of a Tournament_TournamentSettings please look at the [Modals -> Tournament_TournamentSettings page](/documentation#Modals/4-Tournament_TournamentSettings) +- For the definition of a User please look at the [Modals -> User page](/documentation#Modals/4-User) +- For the definition of a Match please look at the [Modals -> Match page](/documentation#Modals/4-Match) +- For the definition of a QualifierEvent please look at the [Modals -> QualifierEvent page](/documentation#Modals/4-QualifierEvent) +- For the definition of a CoreServer please look at the [Modals -> CoreServer page](/documentation#Modals/4-CoreServer) + +A direct implementation: +```ts +// Get tournaments from the client +const tournamentsList = client.stateManager.getTournaments(); +``` +
+ info +
+ Example: This is an example from the page of ShyyTAUI within the tournament list / select page. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getUser.md b/src/lib/taDocs/2-stateManager-getUser.md new file mode 100644 index 0000000..d1a69ff --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getUser.md @@ -0,0 +1,116 @@ +# The stateManager.getUser() method +This documents the `stateManager.getUser()` method. + +This method has input parameters. The function could easily be defined as: +```ts +function getUser(tournamentGuid: string, userGuid: string): User | undefined { + // backend logic +} +``` +## General usage: +```ts +const user: User | undefined = taClient.stateManager.getUser(tournamentGuid, userGuid); +``` + +This method will return either the `User` object or undefined. A `User` can be described by the following TypeScript Interface: +```ts +interface User { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: string name = 2; + */ + name: string; + /** + * @generated from protobuf field: string platform_id = 3; + */ + platformId: string; + /** + * @generated from protobuf field: proto.models.User.ClientTypes client_type = 4; + */ + clientType: User_ClientTypes; + /** + * @generated from protobuf field: string team_id = 5; + */ + teamId: string; + /** + * @generated from protobuf field: proto.models.User.PlayStates play_state = 6; + */ + playState: User_PlayStates; + /** + * @generated from protobuf field: proto.models.User.DownloadStates download_state = 7; + */ + downloadState: User_DownloadStates; + /** + * @generated from protobuf field: repeated string mod_list = 8; + */ + modList: string[]; + /** + * @generated from protobuf field: proto.models.User.Point stream_screen_coordinates = 9; + */ + streamScreenCoordinates?: User_Point; + /** + * @generated from protobuf field: int64 stream_delay_ms = 10; + */ + streamDelayMs: bigint; + /** + * @generated from protobuf field: int64 stream_sync_start_ms = 11; + */ + streamSyncStartMs: bigint; + /** + * @generated from protobuf field: proto.models.User.DiscordInfo discord_info = 12; + */ + discordInfo?: User_DiscordInfo; + /** + * @generated from protobuf field: bytes user_image = 13; + */ + userImage: Uint8Array; + /** + * @generated from protobuf field: proto.models.Permissions permissions = 14; + */ + permissions: Permissions; +} +``` + +- For the definition of a User_ClientTypes please look at the [Enums -> User_ClientTypes page](/documentation#Enums/5-User_ClientTypes) +- For the definition of a User_PlayStates please look at the [Enums -> User_PlayStates page](/documentation#Enums/5-User_PlayStates) +- For the definition of a User_DownloadStates please look at the [Enums -> User_DownloadStates page](/documentation#Enums/5-User_DownloadStates) +- For the definition of a User_Point please look at the [Modals -> User_Point page](/documentation#Modals/4-User_Point) +- For the definition of a User_DiscordInfo please look at the [Modals -> User_DiscordInfo page](/documentation#Modals/4-User_DiscordInfo) +- For the definition of a Permissions please look at the [Enums -> Permissions page](/documentation#Enums/5-Permissions) + +A direct implementation: +```ts +async function refreshMatchDetails(match: Match) { + // Get match players + // Fetch each of the players in the match my using map on the associated users array which contains the connected user's guid + matchPlayers = match.associatedUsers + .map(userGuid => client.stateManager.getUser(tournamentGuid, userGuid)) + // Make sure that the client is a player(0) and not a websocket or REST connection (1 or 2) + .filter(user => user && user.clientType === 0) as User[]; + + // Remaining logic in the function + // Get current song if available + currentSong = match.selectedMap?.gameplayParameters || null; + if(currentSong) { + try { + // Fetch the map beatsaver data + const tempMapData = await fetchMapByLevelId(currentSong.beatmap.levelId); + + if(tempMapData) { + currentSongData = tempMapData; + } + } catch (error) { + console.log('Current song beatsaver data fetch failed'); + } + } +} +``` +
+ info +
+ Example: This is an example from the page of ShyyTAUI within the match cordination page. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-getUsers.md b/src/lib/taDocs/2-stateManager-getUsers.md new file mode 100644 index 0000000..d4e4085 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-getUsers.md @@ -0,0 +1,89 @@ +# The stateManager.getUsers() method +This documents the `stateManager.getUsers()` method. + +This method has input parameters. The function could easily be defined as: +```ts +function getUsers(tournamentGuid: string): User[] | undefined { + // backend logic +} +``` +## General usage: +```ts +const users: User[] | undefined = taClient.stateManager.getUsers(tournamentGuid); +``` + +This method will return either an array of the `User` object or undefined. A `User` can be described by the following TypeScript Interface: +```ts +interface User { + /** + * @generated from protobuf field: string guid = 1; + */ + guid: string; + /** + * @generated from protobuf field: string name = 2; + */ + name: string; + /** + * @generated from protobuf field: string platform_id = 3; + */ + platformId: string; + /** + * @generated from protobuf field: proto.models.User.ClientTypes client_type = 4; + */ + clientType: User_ClientTypes; + /** + * @generated from protobuf field: string team_id = 5; + */ + teamId: string; + /** + * @generated from protobuf field: proto.models.User.PlayStates play_state = 6; + */ + playState: User_PlayStates; + /** + * @generated from protobuf field: proto.models.User.DownloadStates download_state = 7; + */ + downloadState: User_DownloadStates; + /** + * @generated from protobuf field: repeated string mod_list = 8; + */ + modList: string[]; + /** + * @generated from protobuf field: proto.models.User.Point stream_screen_coordinates = 9; + */ + streamScreenCoordinates?: User_Point; + /** + * @generated from protobuf field: int64 stream_delay_ms = 10; + */ + streamDelayMs: bigint; + /** + * @generated from protobuf field: int64 stream_sync_start_ms = 11; + */ + streamSyncStartMs: bigint; + /** + * @generated from protobuf field: proto.models.User.DiscordInfo discord_info = 12; + */ + discordInfo?: User_DiscordInfo; + /** + * @generated from protobuf field: bytes user_image = 13; + */ + userImage: Uint8Array; + /** + * @generated from protobuf field: proto.models.Permissions permissions = 14; + */ + permissions: Permissions; +} +``` + +- For the definition of a User_ClientTypes please look at the [Enums -> User_ClientTypes page](/documentation#Enums/5-User_ClientTypes) +- For the definition of a User_PlayStates please look at the [Enums -> User_PlayStates page](/documentation#Enums/5-User_PlayStates) +- For the definition of a User_DownloadStates please look at the [Enums -> User_DownloadStates page](/documentation#Enums/5-User_DownloadStates) +- For the definition of a User_Point please look at the [Modals -> User_Point page](/documentation#Modals/4-User_Point) +- For the definition of a User_DiscordInfo please look at the [Modals -> User_DiscordInfo page](/documentation#Modals/4-User_DiscordInfo) +- For the definition of a Permissions please look at the [Enums -> Permissions page](/documentation#Enums/5-Permissions) + +
+ info +
+ Example: A direct example is not available, as this function is not used in ShyyTAUI. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-handlePacket.md b/src/lib/taDocs/2-stateManager-handlePacket.md new file mode 100644 index 0000000..529cc92 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-handlePacket.md @@ -0,0 +1,11 @@ +# The stateManager.handlePacket() method +This method should NOT be used, as it is already handled, just exists. + +## General usage: +```ts +await taClient.stateManager.handlePacket(packet: Packet); +``` + +For the definition of a `Packet` please refer to [Modals -> Packet](/documentation#Modals/Packet) + +I will not proide further documentation for this method, as it should NOT be used. Every way you could use it exists with another method. \ No newline at end of file diff --git a/src/lib/taDocs/2-stateManager-intro.md b/src/lib/taDocs/2-stateManager-intro.md new file mode 100644 index 0000000..9b2f510 --- /dev/null +++ b/src/lib/taDocs/2-stateManager-intro.md @@ -0,0 +1,48 @@ +# An Introduction to the StateManager +The `client.stateManager`(`stateManager` for short), is the simple way of communicating and syncing with the state on the `CoreServer`. If you alrady understand what 'state-based' means, then you may skip this page. + +The `stateManager` is the way you would fetch tournaments, get matches, or do anything related to getting specific data that does not require a database query. It is best if you are familiar with what methods it has and how you can use them. + +For the ease of this explanation let me provide an example: +```ts +onMount(async() => { + // Check if the user is logged in + if ($authTokenStore) { + // Fetch the match data that we need to display (current map etc. This function will appear later in docs too) + await fetchMatchData(); + // Using stateManger listen to global changes to our match + client.stateManager.on('matchDeleted', handleMatchDeleted); + client.stateManager.on('matchUpdated', handleMatchUpdated); + client.stateManager.on('userDisconnected', handleUserDisconnected); + client.stateManager.on('userUpdated', handleUserUpdated); + // Using the client listen to events that will not be stored in the state, such as scores and song finish events + client.on('realtimeScore', handleRealtimeScoreUpdate); + client.on('songFinished', handleSongFinished); + + // This is a helper function you can ignore, it just prevents the user from reloading the page while in the match + window.addEventListener('beforeunload', beforeUnloadHandler); + } else { + // If the user is not authenticated, they will be thrown to the discord authentication page + window.location.href = "/discordAuth" + } +}); + +onDestroy(() => { + // Remove the listeners that were added on mount + client.stateManager.removeListener('matchUpdated', handleMatchUpdated); + client.stateManager.removeListener('matchDeleted', handleMatchDeleted); + client.stateManager.removeListener('userDisconnected', handleUserDisconnected); + client.stateManager.removeListener('userUpdated', handleUserUpdated); + client.removeListener('realtimeScore', handleRealtimeScoreUpdate); + client.removeListener('songFinished', handleSongFinished); + + // Remove the no-refresh behaviour + window.removeEventListener('beforeunload', beforeUnloadHandler); +}); +``` +
+ info +
+ Example: This is an example from the page of ShyyTAUI within the match coorination page. +
+
\ No newline at end of file diff --git a/src/lib/taDocs/client.md b/src/lib/taDocs/3-client.md similarity index 82% rename from src/lib/taDocs/client.md rename to src/lib/taDocs/3-client.md index 45a4c88..ad01ea3 100644 --- a/src/lib/taDocs/client.md +++ b/src/lib/taDocs/3-client.md @@ -76,20 +76,3 @@ The table below shows all of the functions of the taClient. From this point onwa These are the main functions of the taClient. It is important to note that these are mostly asyncronous and must be awaited for if you wish to use the response they provide. There is however another important part of the taClient, which is the `taClient.stateManger`. This `stateManger` also has more functions which I have also provided in a table below. The `stateManger` loads its data once it connects to the TA CoreServer. -| taClient.stateManager. (function) | Purpose and functionality | Is Async | -|:----------------------------------:|:--------------------------|:--------:| -|emit()|Emit a custom event. Only use this if you know what you are doing.|True| -|getKnownServers()|Get the known servers which have been sent in the connect state. **This is a cached list.**|False| -|getMatch()|Get the details of a match.|False| -|getMatches()|Get an array of all of the matches in a tournament.|False| -|getQualifier()|Get the details of a qualifier event.|False| -|getQualifiers()|Get an array of all of the qualifiers in a tournament.|False| -|getSelfGuid()|Get the `.guid` property of the currently logged in user or bot token.|False| -|getTournament()|Get the information about a tournament.|False| -|getTournaments()|Get an array of all of the tournaments of a Core Server|False| -|getUser()|Get all of the details of a user in a tournament.|False| -|getUsers()|Get an array of all of the users in a tournament.|False| -|handlePacket()|Handle the response to a packet sent to you by the TA Core Server. Only use if you know what you are doing.|False| -|**on()**|Standard listener for the taClient. This is how real time score can be subscribed to.|False| -|once()|**SAME AS ON? WHAT IS THE DIFFERENCE BETWEEN TACLIENT.NO AND STATEMANAGER.ON????**|False| -|removeListener()|**why can I subscribe to events in the statemanager? I am quite unclear about this hehe**|False| \ No newline at end of file diff --git a/src/lib/taDocs/4-models-Map.md b/src/lib/taDocs/4-models-Map.md new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/taDocs/qualifierLeaderboardSort.md b/src/lib/taDocs/qualifierLeaderboardSort.md new file mode 100644 index 0000000..08e94f8 --- /dev/null +++ b/src/lib/taDocs/qualifierLeaderboardSort.md @@ -0,0 +1,267 @@ +# QualifierEvent_LeaderboardSort Enum + +This enum defines all supported leaderboard sorting strategies for qualifier events in the TournamentAssistant Client. + +Each sort type allows leaderboard data to be ranked according to specific gameplay metrics, with different sorting modes available. + +## Sorting Modes + +- **Default (Descending)** + Sorts scores from highest to lowest. This is the default for most metrics where higher numbers are better (e.g., ModifiedScore, MaxCombo). + +- **Ascending** + Sorts scores from lowest to highest. Ideal for metrics where fewer is better (e.g., NotesMissed, BadCuts). + +- **Target** + Sorts based on how close a score is to a predefined target value. The absolute difference is used for ranking (i.e., `|target - playerValue|`), with the smallest difference ranked highest. + +--- + +## Enum Values + +### ModifiedScore +```protobuf +ModifiedScore = 0; +```` + +Sorts by Modified Score, descending (highest first). + +### ModifiedScoreAscending + +```protobuf +ModifiedScoreAscending = 1; +``` + +Sorts by Modified Score, ascending (lowest first). + +### ModifiedScoreTarget + +```protobuf +ModifiedScoreTarget = 2; +``` + +Sorts by proximity to a target Modified Score. +Example with target = 107000: + +``` +Player A: 107114 → 114 +Player B: 107310 → 310 +Player C: 103851 → 3149 +``` + +--- + +### NotesMissed + +```protobuf +NotesMissed = 3; +``` + +Sorts by number of missed notes, descending. + +### NotesMissedAscending + +```protobuf +NotesMissedAscending = 4; +``` + +Sorts by number of missed notes, ascending. + +### NotesMissedTarget + +```protobuf +NotesMissedTarget = 5; +``` + +Sorts by proximity to a target missed notes value. + +--- + +### BadCuts + +```protobuf +BadCuts = 6; +``` + +Sorts by number of bad cuts, descending. + +### BadCutsAscending + +```protobuf +BadCutsAscending = 7; +``` + +Sorts by number of bad cuts, ascending. +Example: + +``` +28 +39 +43 +``` + +### BadCutsTarget + +```protobuf +BadCutsTarget = 8; +``` + +Sorts by proximity to a target bad cuts value. +Example with target = 40: + +``` +Player A: 39 → 1 +Player B: 43 → 3 +Player C: 28 → 12 +``` + +--- + +### MaxCombo + +```protobuf +MaxCombo = 9; +``` + +Sorts by max combo achieved, descending. + +### MaxComboAscending + +```protobuf +MaxComboAscending = 10; +``` + +Sorts by max combo, ascending. + +### MaxComboTarget + +```protobuf +MaxComboTarget = 11; +``` + +Sorts by proximity to a target max combo value. + +--- + +### GoodCuts + +```protobuf +GoodCuts = 12; +``` + +Sorts by number of good cuts, descending. + +### GoodCutsAscending + +```protobuf +GoodCutsAscending = 13; +``` + +Sorts by number of good cuts, ascending. + +### GoodCutsTarget + +```protobuf +GoodCutsTarget = 14; +``` + +Sorts by proximity to a target good cuts value. + +--- + +## Summary Table + +| Enum Value | Bit Value | Sort Type | Description | +| ---------------------- | --------- | ---------- | ----------------------------------- | +| ModifiedScore | 0 | Descending | Standard leaderboard score | +| ModifiedScoreAscending | 1 | Ascending | Inverse score order | +| ModifiedScoreTarget | 2 | Target | Closeness to target score | +| NotesMissed | 3 | Descending | More missed notes = worse | +| NotesMissedAscending | 4 | Ascending | Fewer missed notes = better | +| NotesMissedTarget | 5 | Target | Closeness to target missed notes | +| BadCuts | 6 | Descending | More bad cuts = worse | +| BadCutsAscending | 7 | Ascending | Fewer bad cuts = better | +| BadCutsTarget | 8 | Target | Closeness to target bad cuts | +| MaxCombo | 9 | Descending | Highest combo achieved | +| MaxComboAscending | 10 | Ascending | Lowest combo achieved | +| MaxComboTarget | 11 | Target | Closeness to target combo count | +| GoodCuts | 12 | Descending | Most good cuts = better | +| GoodCutsAscending | 13 | Ascending | Fewest good cuts = better | +| GoodCutsTarget | 14 | Target | Closeness to target good cuts count | + + +# New DB fileserver idea: + +4 columns: +- uploader (discordid) +- file_name (guid identifier) +- file (Unit8Array) +- created_at (time) + +PostgreSQL: +```sql +CREATE TABLE files ( + id SERIAL PRIMARY KEY, + uploader TEXT NOT NULL, + file_name TEXT NOT NULL UNIQUE, + file BYTEA NOT NULL, -- BYTEA stores binary data + created_at TIMESTAMP DEFAULT NOW() +); +``` + +MySQL: +```sql +CREATE TABLE files ( + id INT AUTO_INCREMENT PRIMARY KEY, + uploader VARCHAR(64) NOT NULL, + file_name VARCHAR(255) NOT NULL UNIQUE, + file LONGBLOB NOT NULL, -- LONGBLOB supports up to 4GB of binary data + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +## Implementation example (using my beloved Drizzle ORM and a postgres database): + +```ts +export const files = pgTable("files", { + id: serial("id").primaryKey(), + uploader: text("uploader").notNull(), + fileName: text("file_name").notNull().unique(), + file: blob("file").notNull(), // Drizzle handles BYTEA for pg as blob + createdAt: timestamp("created_at").defaultNow(), +}); +``` + +```ts +import { db } from "./db"; // drizzle db setup +import { files } from "./schema"; // import the table + +const uploadFile = async (uploader: string, fileName: string, binaryData: Uint8Array) => { + await db.insert(files).values({ + uploader, + fileName, + file: Buffer.from(binaryData), // Convert Uint8Array to Buffer for insertion + }); +}; +``` + +```ts +async function getFile(fileName: string) { + const result = await db.select().from(files).where(eq(files.fileName, fileName)); + if (!result.length) return null; + + const fileRecord = result[0]; + const uint8Array = new Uint8Array(fileRecord.file); // convert Buffer to Uint8Array + return uint8Array; +}; + +app.get("/files/:fileName", async (req, res) => { + const fileName = req.params.fileName; + const file = await getFile(fileName); + if (!file) return res.status(404).send("File not found"); + + res.setHeader("Content-Type", "application/octet-stream"); + res.send(Buffer.from(file)); +}); + +``` \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b1b5eb5..4d6fbcd 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -121,7 +121,7 @@ code GitHub - + sports_esports Tournament Assistant @@ -168,6 +168,14 @@ --border-color: #333333; --navbar-height: 4rem; --footer-bg: #191919; + --success-color: #22c55e; + --warning-color: #f59e0b; + --error-color: #ef4444; + + --comfortable-red: #d9534f; + --comfortable-red-hover: #c9302c; + --pick-green: #28a745; + --pick-green-hover: #218838; } /* Global styles */ diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ddf1a93..a6d8488 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -68,8 +68,8 @@ onMount(() => {
devices
-

Fully Responsive

-

Hopefully works seamlessly across all devices, from desktop to mobile. (Although I am very much working...)

+

Reliable

+

ShyyTAUI is built to be safe and reliable. This means you don't have to worry about accidentally ending a match, or refreshing when you shouldn't.

@@ -77,7 +77,7 @@ onMount(() => { code

Build More

-

Use moons-ta-client to build what you need! Overlays, casting panels, think of anything!

+

Use moons-ta-client to build what you need! This site has the official TournamentAssistant Client documentation.

diff --git a/src/routes/documentation/+page.svelte b/src/routes/documentation/+page.svelte index 1316237..6cd293f 100644 --- a/src/routes/documentation/+page.svelte +++ b/src/routes/documentation/+page.svelte @@ -17,26 +17,58 @@ category: "Getting Started", icon: "rocket_launch", items: [ - { title: "Introduction", file: "intro", order: 1 }, - { title: "Installation", file: "sample", order: 2 }, - { title: "Client", file: "client", order: 3 }, - { title: "How TA Works, Info", file: "taInfo", order: 4 } + { title: "Introduction", file: "1-intro", order: 1 }, + { title: "Installation", file: "1-installation", order: 2 }, + { title: "Familiarisation, States", file: "1-fam-states-intro", order: 3 }, + { title: "How TA Works, Info", file: "1-taInfo", order: 4 }, ] }, { - category: "Core Concepts", - icon: "psychology", + category: "State Manager", + icon: "account_tree", items: [ - { title: "Tournaments", file: "", order: 1 }, - { title: "Matches", file: "", order: 2 }, - { title: "Players", file: "", order: 3 } + { title: "Introduction to the StateManager", file: "2-stateManager-intro", order: 1 }, + { title: "All Functions", file: "2-stateManager-allFunctions", order: 2 }, + { title: "Emit", file: "2-stateManager-emit", order: 3 }, + { title: "GetKnownServers", file: "2-stateManager-getKnownServers", order: 4 }, + { title: "GetMatch", file: "2-stateManager-getMatch", order: 6 }, + { title: "GetMatches", file: "2-stateManager-getMatches", order: 7 }, + { title: "GetQualifier", file: "2-stateManager-getQualifier", order: 7 }, + { title: "GetQualifiers", file: "2-stateManager-getQualifiers", order: 8 }, + { title: "GetSelfGuid", file: "2-stateManager-getSelfGuid", order: 9 }, + { title: "GetTournament", file: "2-stateManager-getTournament", order: 10 }, + { title: "GetTournaments", file: "2-stateManager-getTournaments", order: 11 }, + { title: "GetUser", file: "2-stateManager-getUser", order: 12 }, + { title: "GetUsers", file: "2-stateManager-getUsers", order: 13 }, + { title: "HandlePacket", file: "2-stateManager-handlePacket", order: 14 }, + { title: "On", file: "2-stateManager-on", order: 15 }, + { title: "Once", file: "2-stateManager-once", order: 16 }, + { title: "RemoveListener", file: "2-stateManager-removeListener", order: 17 }, ] }, { - category: "API Reference", + category: "Client", icon: "api", items: [ - { title: "Authentication", file: "", order: 1 }, + { title: "3-", file: "", order: 1 }, + { title: "Endpoints", file: "", order: 2 }, + { title: "Response Types", file: "", order: 3 } + ] + }, + { + category: "Models", + icon: "view_in_ar", + items: [ + { title: "4-", file: "", order: 1 }, + { title: "Endpoints", file: "", order: 2 }, + { title: "Response Types", file: "", order: 3 } + ] + }, + { + category: "Enums", + icon: "view_in_ar", + items: [ + { title: "5-", file: "", order: 1 }, { title: "Endpoints", file: "", order: 2 }, { title: "Response Types", file: "", order: 3 } ] diff --git a/src/routes/tournaments/[tournamentguid]/+page.svelte b/src/routes/tournaments/[tournamentguid]/+page.svelte index cf55526..2c0c29b 100644 --- a/src/routes/tournaments/[tournamentguid]/+page.svelte +++ b/src/routes/tournaments/[tournamentguid]/+page.svelte @@ -3,7 +3,7 @@ import { onMount, onDestroy } from "svelte"; import { bkAPIUrl } from '$lib/config.json'; //@ts-ignore - import { Match, Tournament, TAClient, Response_ResponseType, User, User_PlayStates } from 'moons-ta-client'; + import { Match, Tournament, TAClient, Response_ResponseType, User, User_PlayStates, User_ClientTypes } from 'moons-ta-client'; import { writable } from "svelte/store"; import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte"; import { goto } from "$app/navigation"; @@ -153,8 +153,11 @@ } async function handleUserConnected(params: [User, Tournament]) { - if(params[1].guid == tournament?.guid) { - availablePlayers.push(params[0]); + if(params[1].guid == tournament?.guid && params[0].clientType == User_ClientTypes.Player) { + availablePlayers = [ + ...availablePlayers, + params[0] + ]; } } diff --git a/src/routes/tournaments/[tournamentguid]/mappools/[poolGuid]/+page.svelte b/src/routes/tournaments/[tournamentguid]/mappools/[poolGuid]/+page.svelte index bbdd2ae..ffca3fd 100644 --- a/src/routes/tournaments/[tournamentguid]/mappools/[poolGuid]/+page.svelte +++ b/src/routes/tournaments/[tournamentguid]/mappools/[poolGuid]/+page.svelte @@ -447,7 +447,7 @@ map.taData.guid == currentEditMap?.guid)} + map={maps.find(map => map.taData.guid == currentEditMap?.guid) || null} on:close={closeEditMapModal} on:mapUpdated={handleMapUpdated} on:mapAdded={handleMapAdded} diff --git a/src/routes/tournaments/[tournamentguid]/matches/[matchGuid]/+page.svelte b/src/routes/tournaments/[tournamentguid]/matches/[matchGuid]/+page.svelte index 3b54938..4a8d78f 100644 --- a/src/routes/tournaments/[tournamentguid]/matches/[matchGuid]/+page.svelte +++ b/src/routes/tournaments/[tournamentguid]/matches/[matchGuid]/+page.svelte @@ -4,13 +4,41 @@ import { bkAPIUrl } from '$lib/config.json'; import Notification from '$lib/components/notifications/Popup.svelte'; //@ts-ignore - import { Match, Tournament, TAClient, Response_ResponseType, User } from 'moons-ta-client'; + import { Match, Tournament, TAClient, Response_ResponseType, User, RealtimeScore, Push_SongFinished, Map, User_DownloadStates, User_PlayStates } from 'moons-ta-client'; import { writable } from "svelte/store"; import { goto } from "$app/navigation"; - import { fetchMapByLevelId } from "$lib/services/beatsaver.js"; + import { fetchMapByLevelId, type BeatSaverMap } from "$lib/services/beatsaver.js"; + import SongQueue from "$lib/components/modules/SongQueue.svelte"; + import EditMap from "$lib/components/popups/EditMap.svelte"; + import Popup from "$lib/components/notifications/Popup.svelte"; + import PreviouslyPlayedSongs from "$lib/components/modules/PreviouslyPlayedSongs.svelte"; + import { xml } from "d3"; export let data; + interface CustomMap { + taData: Map, + beatsaverData: BeatSaverMap + } + + interface RealTimeScoreForPlayers { + player: User, + recentScore: RealtimeScore + } + + interface ScoreWithAccuracy extends Push_SongFinished { + accuracy: number; + } + + interface PreviousResults { + taData: Map; + beatsaverData: BeatSaverMap; + scores: ScoreWithAccuracy[]; + completionType: 'Completed' | 'Still Awaiting Scores' | 'Exited To Menu'; + } + + let upcomingQueueMaps: CustomMap[] = []; + const tournamentGuid: string = data.tournamentGuid; const matchGuid: string = data.matchGuid; @@ -20,29 +48,54 @@ let error: string | null = null; let matchPlayers: User[] = []; let currentSong: any = null; - let currentSongData: any = null; + let currentSongData: BeatSaverMap; let isMatchActive = false; let streamSyncEnabled = false; + + let realTimeScores: RealTimeScoreForPlayers[] = []; + let previousMatchResults: PreviousResults[] = []; + + const colorPresets = { + primary: 'var(--accent-glow)', + secondary: 'var(--text-secondary)', + success: '#4CAF50', + warning: '#FFC107', + error: '#F44336', + info: '#2196F3', + default: 'var(--text-primary)' + }; + type ColorPreset = keyof typeof colorPresets; + type ButtonType = 'primary' | 'secondary' | 'text' | 'outlined'; + interface ButtonConfig { + text: string; + type: ButtonType; + href?: string; + color?: ColorPreset; + icon?: string; + action?: () => void; + } + // Modal states let showKickConfirmModal = false; let showBackToMenuModal = false; let showExitMatchModal = false; let playerToKick: User | null = null; let showCannotExitPage: boolean = false; - - // Player status enum - enum PlayerStatus { - Downloading = "downloading", - SelectingSong = "selecting", - Idle = "idle" - } + let modalMessage: string = ""; + let modalIcon: string = "danger"; + let modalIconColor: ColorPreset = 'error'; + let modalButtons: ButtonConfig[] = []; + let modalCustomImage: string = ""; + let modalAutoClose: number = 0; // no auto close - enum PlayerPlayState { - In_Menu = 0, - Waiting_For_Coordinator = 1, - In_Game = 2 - } + let showMapModal: boolean = false; + let currentlyEditingMap: CustomMap | null = null; + + let loadingSongForPlayers: boolean = false; + let failedToLoadSongForPlayers: boolean = false; + + const activeSongPlayers = new Set<`${string}-${string}`>(); // Format: "userGuid-levelId" // Remove 'Bearer ' from the token if($authTokenStore) { @@ -50,10 +103,7 @@ client.setAuthToken(cleanToken); } - // Check if any player is currently playing - function isAnyPlayerPlaying(): boolean { - return matchPlayers.some(player => (player.playState as any) === PlayerPlayState.In_Game); - } + $: isAnyPlayerPlaying = matchPlayers.some(player => (player.playState as any) === User_PlayStates.InGame); // Show notification function (you mentioned you have this) function showNotification(message: string, type: 'warning' | 'error' | 'success' = 'warning') { @@ -62,19 +112,56 @@ } // Handle back to matches with confirmation if needed - async function handleBackToMatches() { - if (isAnyPlayerPlaying()) { - showExitMatchModal = true; - } else { - // await client.deleteMatch(tournamentGuid, matchGuid); + async function handleBackToMatches(endMatch: boolean) { + if(match!.leader !== client.stateManager.getSelfGuid()) { + await removeSelfFromMatch(); goto(`/tournaments/${tournamentGuid}`); } - } - // Handle back to matches with confirmation if needed - async function handleEndMatch() { - await client.deleteMatch(tournamentGuid, matchGuid); - goto(`/tournaments/${tournamentGuid}`); + if(endMatch) { + if (isAnyPlayerPlaying) { + showExitMatchModal = true; + modalMessage = `Players are currently in-game. Exiting will close the match for all players. Continue?`; + modalIcon = 'warning'; + modalButtons = [ + { + text: `Close (NO)`, + type: 'primary', + action: () => showBackToMenuModal = false, + icon: 'close' + }, + { + text: `Yes (YES)`, + type: 'secondary', + action: async() => await confirmExitMatch(), + icon: 'backspace' + } + ]; + } else { + await removeSelfFromMatch(); + await client.deleteMatch(tournamentGuid, matchGuid); + goto(`/tournaments/${tournamentGuid}`); + } + } + + showExitMatchModal = true; + modalMessage = `This should not cause issues as it should not end the match. But TA can be weird sometimes. THIS SHOULD BE SAFE. Do you want to continue?`; + modalIcon = 'info'; + modalIconColor = 'info'; + modalButtons = [ + { + text: `Close (NO)`, + type: 'primary', + action: () => showBackToMenuModal = false, + icon: 'close' + }, + { + text: `Yes (YES)`, + type: 'secondary', + action: async() => await confirmExitMatch(), + icon: 'backspace' + } + ]; } // Confirm exit match @@ -87,12 +174,31 @@ // Handle kick player function handleKickPlayer(player: User) { playerToKick = player; + modalMessage = `Are you sure you want to kick ${player.name} from the match?`; + modalIcon = 'warning'; + modalButtons = [ + { + text: `Close (NO)`, + type: 'primary', + action: () => showKickConfirmModal = false, + icon: 'close' + }, + { + text: `Kick ${player.name} (YES)`, + type: 'secondary', + action: async() => await confirmKickPlayer(), + icon: 'sports_martial_arts' + } + ]; showKickConfirmModal = true; } // Confirm kick player async function confirmKickPlayer() { - if (!playerToKick) return; + if (!playerToKick) { + showKickConfirmModal = false; + return; + } try { // Implementation for kicking player from match @@ -117,7 +223,6 @@ } await startSongForMatch(); - showNotification('Song started for all players', 'success'); isMatchActive = true; } catch (err) { console.error('Error starting song:', err); @@ -142,6 +247,22 @@ // Back to menu function handleBackToMenu() { showBackToMenuModal = true; + modalMessage = `This will send all players back to the menu and stop the current song. Continue?`; + modalIcon = 'warning'; + modalButtons = [ + { + text: `Close (NO)`, + type: 'primary', + action: () => showBackToMenuModal = false, + icon: 'close' + }, + { + text: `Yes (YES)`, + type: 'secondary', + action: async() => await confirmBackToMenu(), + icon: 'backspace' + } + ]; } // Confirm back to menu @@ -160,29 +281,27 @@ } // Return status color based on player status - function getStatusColor(playState: any): string { + function getStatusColor(playState: User_PlayStates, downloadState: User_DownloadStates): string { + if(downloadState == User_DownloadStates.Downloading) return "var(--danger-color)"; + if(downloadState == User_DownloadStates.DownloadError) return "var(--danger-color)"; + if(playState == User_PlayStates.InGame) return "var(--danger-color)"; + switch (playState) { - case PlayerPlayState.In_Game: - return "var(--danger-color)"; - case PlayerPlayState.Waiting_For_Coordinator: + case User_PlayStates.WaitingForCoordinator: return "#FCD34D"; // Yellow - case PlayerPlayState.In_Menu: + case User_PlayStates.InMenu: return "#10B981"; // Green default: return "var(--text-secondary)"; } } - // Get status text - function getStatusText(playState: any): string { - return PlayerPlayState[playState].replace('_', ' '); - } - // Handle match events - function handleMatchUpdated(params: any) { + async function handleMatchUpdated(params: [Match, Tournament]) { console.log("Match updated:", params); - if (params.details.updateMatch.match.guid === matchGuid) { - fetchMatchData(); + if (params[0].guid === matchGuid) { + match = params[0]; + await refreshMatchDetails(params[0]); } } @@ -212,6 +331,27 @@ } } + async function refreshMatchDetails(match: Match) { + // Get match players + matchPlayers = match.associatedUsers + .map(userGuid => client.stateManager.getUser(tournamentGuid, userGuid)) + .filter(user => user && user.clientType === 0) as User[]; + + // Get current song if available + currentSong = match.selectedMap?.gameplayParameters || null; + if(currentSong) { + try { + const tempMapData = await fetchMapByLevelId(currentSong.beatmap.levelId); + + if(tempMapData) { + currentSongData = tempMapData; + } + } catch (error) { + console.log('Current song beatsaver data fetch failed'); + } + } + } + async function fetchMatchData() { if (!$authTokenStore) { window.location.href = '/discordAuth'; @@ -234,22 +374,15 @@ tournament = client.stateManager.getTournament(tournamentGuid); match = client.stateManager.getMatch(tournamentGuid, matchGuid); - console.log("match", match) - + if(!match?.associatedUsers.includes(client.stateManager.getSelfGuid())) { + await addSelfToMatch(); + } + if (!match) { throw new Error('Match not found'); } - // Get match players - matchPlayers = match.associatedUsers - .map(userGuid => client.stateManager.getUser(tournamentGuid, userGuid)) - .filter(user => user && user.clientType === 0) as User[]; - - // Get current song if available - currentSong = match.selectedMap?.gameplayParameters?.beatmap || null; - if(currentSong) { - currentSongData = await fetchMapByLevelId(currentSong.levelId); - } + await refreshMatchDetails(match); } catch (err) { console.error('Error fetching match data:', err); error = err instanceof Error ? err.message : 'An unknown error occurred'; @@ -265,8 +398,23 @@ } async function startSongForMatch() { - // const response = await client.playSong() - console.log('Starting song for match'); + if (!currentSong) return; + + const levelId = currentSong.beatmap.levelId; + const playingPlayerIds = matchPlayers.map(player => player.guid); + + // Clear any existing entries for this levelId + Array.from(activeSongPlayers) + .filter(key => key.endsWith(`-${levelId}`)) + .forEach(key => activeSongPlayers.delete(key)); + + // Add all current players for this song + playingPlayerIds.forEach(playerGuid => { + activeSongPlayers.add(`${playerGuid}-${levelId}`); + }); + + const response = await client.playSong(currentSong, playingPlayerIds); + console.log('startMap', response); } async function startSongWithStreamSyncForMatch() { @@ -275,8 +423,8 @@ } async function sendPlayersBackToMenu() { - // Implement back to menu logic - console.log('Sending players back to menu'); + const backToMenuResponse = await client.returnToMenu(matchPlayers.filter(x => x.playState == User_PlayStates.InGame).map(x => x.guid)); + console.log("Sent players back to menu!", backToMenuResponse) } async function handleUserDisconnected(params: [User, Tournament]) { @@ -286,6 +434,226 @@ } } + function getTimeStringFromSeconds(seconds: number) { + let newString: string = ""; + const minutes = Math.floor(seconds / 60); + + if(minutes > 0) { + newString += `${minutes} minutes `; + } + + const timeSeconds = (seconds - Math.floor(seconds / 60) * 60).toFixed(1); + + if(!timeSeconds.startsWith("0")) { + newString += `${timeSeconds} seconds `; + } + return newString; + } + + async function handleRealtimeScoreUpdate(rts: RealtimeScore) { + console.log(`Player ${matchPlayers.find(x => x.guid == rts.userGuid)?.name}: ${rts.accuracy}`, rts); + + const player = matchPlayers.find(x => x.guid === rts.userGuid)!; + + const index = realTimeScores.findIndex(x => x.player.guid === rts.userGuid); + + if (index !== -1) { + // Update the existing entry + realTimeScores[index] = { player, recentScore: rts }; + } else { + // Append only if it's a new player + realTimeScores = [ + ...realTimeScores, + { player, recentScore: rts } + ]; + } + } + + function getPlayerRtsScore(userGuid: string) { + let playerScore = realTimeScores.find(x => x.player.guid == userGuid); + if(!playerScore) { + playerScore = { + player: matchPlayers.find(x => x.guid == userGuid)!, + recentScore: { + userGuid: userGuid, + score: 0, + scoreWithModifiers: 0, + maxScore: 0, + maxScoreWithModifiers: 0, + combo: 0, + playerHealth: 0, + accuracy: 0, + songPosition: 0, + notesMissed: 0, + badCuts: 0, + bombHits: 0, + wallHits: 0, + maxCombo: 0 + } + } + } + + return playerScore; + } + + async function handleSongFinished(rts: Push_SongFinished) { + if (rts.matchId !== matchGuid) return; + + const levelId = rts.beatmap?.levelId; + if (!levelId) return; + + // Remove player from active song tracking + activeSongPlayers.delete(`${rts.player!.guid}-${levelId}`); + + const map = upcomingQueueMaps.find(x => + x.taData.gameplayParameters!.beatmap!.levelId.toUpperCase() === levelId.toUpperCase() + ); + + if (!map) { + console.log('Received a score for unknown map', rts); + return; + } + + const newRts: ScoreWithAccuracy = {...rts, accuracy: 0}; + + // Check if any players are still playing this song + const playersStillPlaying = Array.from(activeSongPlayers) + .some(key => key.endsWith(`-${levelId}`)); + + const completionType = playersStillPlaying + ? 'Still Awaiting Scores' + : 'Completed'; + + // Find or create the result record + const existingRecordIndex = previousMatchResults.findIndex(x => + x.completionType === 'Still Awaiting Scores' && + x.taData.gameplayParameters?.beatmap?.levelId.toUpperCase() === levelId.toUpperCase() + ); + + if (existingRecordIndex === -1) { + previousMatchResults = [ + { + taData: map.taData, + beatsaverData: map.beatsaverData, + completionType, + scores: [newRts] + }, + ...previousMatchResults + ]; + } else { + const existingRecord = previousMatchResults[existingRecordIndex]; + previousMatchResults = [ + { + taData: map.taData, + beatsaverData: map.beatsaverData, + completionType, + scores: [...existingRecord.scores, newRts] + }, + ...previousMatchResults.filter((_, i) => i !== existingRecordIndex) + ]; + } + } + + async function handleMapUpdated(event: CustomEvent) { + const map = upcomingQueueMaps.find(x => x.taData.guid == event.detail.map.guid); + if(!map) return; + upcomingQueueMaps = [ + ...upcomingQueueMaps.filter(x => x.taData.guid !== event.detail.map.guid), + { + taData: (event.detail.map as Map), + beatsaverData: map.beatsaverData + } + ]; + } + + async function handleMapAdded(event: CustomEvent) { + const beatsaverData: BeatSaverMap | null = await fetchMapByLevelId(event.detail.map.gameplayParameters.beatmap.levelId); + if(!beatsaverData) { + console.log('Failed to fetch beatsaver map data for new map, aborting map addition!'); + return; + } + const newMap: CustomMap = { + taData: event.detail.map, + beatsaverData: beatsaverData + }; + + upcomingQueueMaps = [ + ...upcomingQueueMaps, + newMap + ]; + } + + function closeEditMapModal() { + showMapModal = false; + } + + async function handleAddMap() { + currentlyEditingMap = null; + showMapModal = true; + } + + async function handleEditMap(event: CustomEvent) { + currentlyEditingMap = event.detail.map; + showMapModal = true; + } + + async function handleRemoveMapFromQueue(event: CustomEvent) { + upcomingQueueMaps = [ + ...upcomingQueueMaps.filter(x => x.taData.guid !== event.detail.map.taData.guid) + ]; + } + + async function handleLoadMap(event: CustomEvent) { + const map: CustomMap = event.detail.map; + loadingSongForPlayers = false; + failedToLoadSongForPlayers = false; + + let playingPlayerIds = matchPlayers + .filter(player => player.playState !== User_PlayStates.InGame) + .map(player => player.guid); + + if(!playingPlayerIds || playingPlayerIds.length == 0) { + playingPlayerIds = []; + } + + loadingSongForPlayers = true; + const matchResponse = await client.setMatchMap(tournamentGuid, matchGuid, map.taData); + if(matchResponse.type == Response_ResponseType.Fail) return; + const loadResponse = await client.loadSong(map.taData.gameplayParameters!.beatmap!.levelId, playingPlayerIds, 3 * 60 * 1000); + failedToLoadSongForPlayers = false; + + const failedLoads = loadResponse.filter(x => x.response.type == Response_ResponseType.Fail); + + if(failedLoads.length > 0) { + failedToLoadSongForPlayers = true; + showNotification('Failed to load song as one or more players failed to load! Please try again', 'error'); + } else { + loadingSongForPlayers = false; + showNotification('Successfully loaded song!', 'success'); + } + + console.log("songLoaded", loadResponse); + } + + async function handleUserUpdated(params: [User, Tournament]) { + if(params[1].guid !== tournamentGuid) return; + + const userIndex = matchPlayers.findIndex(x => x.guid === params[0].guid); + if(userIndex !== -1) { + matchPlayers = [ + ...matchPlayers.slice(0, userIndex), + params[0], + ...matchPlayers.slice(userIndex + 1) + ]; + } else { + matchPlayers = [...matchPlayers, params[0]]; + } + } + + const beforeUnloadHandler = (event: BeforeUnloadEvent) => { + event.preventDefault(); // Not necessary in some browsers but safe + }; + onMount(async() => { if ($authTokenStore) { // Fetch the match data that we need to display @@ -293,7 +661,11 @@ // Using stateManger listen to global changes to our match client.stateManager.on('matchDeleted', handleMatchDeleted); client.stateManager.on('matchUpdated', handleMatchUpdated); - client.stateManager.on('userDisconnected', handleUserDisconnected) + client.stateManager.on('userDisconnected', handleUserDisconnected); + client.stateManager.on('userUpdated', handleUserUpdated); + client.on('realtimeScore', handleRealtimeScoreUpdate); + client.on('songFinished', handleSongFinished); + window.addEventListener('beforeunload', beforeUnloadHandler); } else { window.location.href = "/discordAuth" } @@ -304,6 +676,11 @@ client.stateManager.removeListener('matchUpdated', handleMatchUpdated); client.stateManager.removeListener('matchDeleted', handleMatchDeleted); client.stateManager.removeListener('userDisconnected', handleUserDisconnected); + client.stateManager.removeListener('userUpdated', handleUserUpdated); + client.removeListener('realtimeScore', handleRealtimeScoreUpdate); + client.removeListener('songFinished', handleSongFinished); + + window.removeEventListener('beforeunload', beforeUnloadHandler); }); @@ -313,17 +690,17 @@
- @@ -361,7 +738,7 @@ Song Cover
-

{currentSong.name || 'Unknown Song'}

+

{currentSong.beatmap.name || 'Unknown Song'}

{currentSongData.metadata.songAuthorName || 'Unknown Artist'}

@@ -371,6 +748,33 @@

No song selected

{/if} + + {#if failedToLoadSongForPlayers} +
+ warning +
+ FAILED TO LOAD SONG FOR ALL PLAYERS! You can still start the map, but it will not start for those whos state is "Downloading"! +
+
+ {/if} + + {#if loadingSongForPlayers} +
+ warning +
+ LOADING SONG FOR PLAYERS! You can still start the map, but it will not start for those whos state is "Downloading"! +
+
+ {/if} + + {#if isAnyPlayerPlaying} +
+ warning +
+ PLAYERS ARE IN GAME! Some players are currently in game. Please refrain from loading new songs, starting maps, or exiting this match. You may of course add new maps to the queue etc. +
+
+ {/if}
- {#if isMatchActive} + {#if isAnyPlayerPlaying}
+ + {#if matchPlayers.some(player => (player.playState) === User_PlayStates.InGame)} +
+
+ {#each realTimeScores as rtsPlayer} +
+
+ +
+

{rtsPlayer.player.name} ({rtsPlayer.player.discordInfo?.username || 'Discord Unknown'})

+

{(rtsPlayer.recentScore.accuracy * 100).toFixed(2)}%

+

{rtsPlayer.recentScore.notesMissed} Misses - {rtsPlayer.recentScore.badCuts} Bad Cuts - {rtsPlayer.recentScore.bombHits} Bomb Hits - {rtsPlayer.recentScore.wallHits} Wall Hits

+
+
+

Song Progress: {((rtsPlayer.recentScore.songPosition / currentSongData.metadata.duration) * 100).toFixed(2)}%

+

{getTimeStringFromSeconds(rtsPlayer.recentScore.songPosition)} of {getTimeStringFromSeconds(currentSongData.metadata.duration)}

+
+ +
+ {/each} +
+
+ {/if} +

Players ({matchPlayers.length})

@@ -406,7 +838,7 @@
{#each matchPlayers as player}
-
+

{player.name} ({player.discordInfo?.username || 'Unknown'})

-

{getStatusText(player.playState)}

+

{User_DownloadStates[player.downloadState]}

+

{User_PlayStates[player.playState]}

- +
-

Song Queue

-
- queue_music -

Queue feature coming soon

-
+ +
+ + +
+
{/if} @@ -439,74 +881,28 @@
- -{#if showKickConfirmModal} - - - +{#if showMapModal} + {/if} - - -{#if showBackToMenuModal} - - - -{/if} - - - -{#if showExitMatchModal} - - -{/if} +