todo: QUAL LEADERBAORDS AND SORTING

This commit is contained in:
Luna 2025-06-15 03:30:58 +02:00
parent e9d0305566
commit 02c03e3751
12 changed files with 2581 additions and 108 deletions

View file

@ -5,6 +5,7 @@
import { TAServerPort, TAServerUrl, authTokenStore, client } from "$lib/stores"; import { TAServerPort, TAServerUrl, authTokenStore, client } from "$lib/stores";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { BeatSaverMap } from "$lib/services/beatsaver";
export let tournamentGuid: string; export let tournamentGuid: string;
export let poolGuid: string; export let poolGuid: string;
@ -53,33 +54,6 @@
// Playing-specific settings // Playing-specific settings
let disableCustomNotesOnStream = false; let disableCustomNotesOnStream = false;
type BeatSaverMap = {
id: string;
metadata: {
songName: string;
songAuthorName: string;
levelAuthorName: string;
songSubName: string;
duration: number;
bpm: number;
};
versions: Array<{
hash: string;
state: string;
createdAt: string;
downloadURL: string;
diffs: Array<{
characteristic: string;
difficulty: string;
maxScore: number;
nps: number;
}>;
coverURL: string;
previewURL: string;
}>;
uploaded: string;
};
// Beat Saber Modifiers with incompatibility groups // Beat Saber Modifiers with incompatibility groups
interface Modifier { interface Modifier {
@ -433,7 +407,7 @@
selectedCharacteristic = availableCharacteristics[0] || ""; selectedCharacteristic = availableCharacteristics[0] || "";
selectedDifficulty = availableDifficulties[selectedCharacteristic]?.[0] || ""; selectedDifficulty = availableDifficulties[selectedCharacteristic]?.[0] || "";
mapBeatmapId = `custom_level_${latestVersion.hash}`; mapBeatmapId = `custom_level_${latestVersion.hash.toLocaleUpperCase()}`;
return data; return data;
} catch (err) { } catch (err) {
@ -510,20 +484,25 @@
disableScoresaberSubmission: mode === 'playing' ? disableScoresaberSubmission : false, disableScoresaberSubmission: mode === 'playing' ? disableScoresaberSubmission : false,
showScoreboard: mode === 'playing' ? showScoreboard : false, showScoreboard: mode === 'playing' ? showScoreboard : false,
useSync: false, useSync: false,
target: 0 target: 0,
playerSettings: {
options: 0,
playerHeight: 0,
sfxVolume: 0,
saberTrailIntensity: 0,
noteJumpStartBeatOffset: 0,
noteJumpFixedDuration: 0,
noteJumpDurationTypeSettings: 0,
arcVisibilityType: 0,
}
} }
}; };
if (map) { if (map) {
// I will need to fix this, for now this is a TA server issue
dispatch('mapUpdated', { map: mapData }); dispatch('mapUpdated', { map: mapData });
} else { } else {
await client.addTournamentPoolMaps(tournamentGuid, poolGuid, [mapData]);
dispatch('mapAdded', { map: mapData }); dispatch('mapAdded', { map: mapData });
} }
// if (response.type !== Response_ResponseType.Success) {
// throw new Error("Failed to save map");
// }
isLoading = false; isLoading = false;
return dispatch('close'); return dispatch('close');
} catch (err) { } catch (err) {
@ -747,19 +726,7 @@
<h4>Tournament Assistant Settings</h4> <h4>Tournament Assistant Settings</h4>
<p class="section-desc">Configure game behavior options</p> <p class="section-desc">Configure game behavior options</p>
<div class="qualifier-options"> <div class="qualifier-options">
<div class="qualifier-toggle">
<label class="switch-label">
<input
type="checkbox"
bind:checked={disableFail}
on:change={handleDisableFailChange}
/>
<span class="switch"></span>
<span class="label-text">Disable Fail</span>
</label>
</div>
<div class="qualifier-toggle"> <div class="qualifier-toggle">
<label class="switch-label"> <label class="switch-label">
<input <input
@ -772,6 +739,18 @@
</div> </div>
{#if mode === 'playing'} {#if mode === 'playing'}
<div class="qualifier-toggle">
<label class="switch-label">
<input
type="checkbox"
bind:checked={disableFail}
on:change={handleDisableFailChange}
/>
<span class="switch"></span>
<span class="label-text">Disable Fail</span>
</label>
</div>
<div class="qualifier-toggle"> <div class="qualifier-toggle">
<label class="switch-label"> <label class="switch-label">
<input <input
@ -816,7 +795,6 @@
<span class="switch"></span> <span class="switch"></span>
<span class="label-text">Limit Attempts</span> <span class="label-text">Limit Attempts</span>
</label> </label>
<p class="setting-description">Restrict the number of attempts allowed for this qualifier</p>
</div> </div>
{#if limitAttemptsEnabled} {#if limitAttemptsEnabled}

View file

@ -1,4 +1,4 @@
interface BeatSaverMap { export interface BeatSaverMap {
id: string; id: string;
name: string; name: string;
description: string; description: string;
@ -58,6 +58,7 @@ interface BeatSaverMap {
}>; }>;
downloadURL: string; downloadURL: string;
previewURL: string; previewURL: string;
coverURL: string;
}>; }>;
} }
@ -117,7 +118,22 @@ export async function fetchMapsByHashesOrKeys(identifiers: string[]): Promise<Be
} }
// Process hashes in batches // Process hashes in batches
if (hashes.length > 0) { if (hashes.length == 1) {
try {
const response = await fetch(`${BEATSAVER_API_BASE}/maps/hash/${hashes[0]}`);
if (!response.ok) {
throw new Error(`BeatSaver API error for hashes: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const maps = [data];
allMaps.push(...maps);
} catch (error) {
console.error('Error fetching hash batch:', error);
throw error;
}
} else if (hashes.length > 0) {
const hashBatches: string[][] = []; const hashBatches: string[][] = [];
for (let i = 0; i < hashes.length; i += BATCH_SIZE) { for (let i = 0; i < hashes.length; i += BATCH_SIZE) {
hashBatches.push(hashes.slice(i, i + BATCH_SIZE)); hashBatches.push(hashes.slice(i, i + BATCH_SIZE));
@ -126,7 +142,7 @@ export async function fetchMapsByHashesOrKeys(identifiers: string[]): Promise<Be
for (const batch of hashBatches) { for (const batch of hashBatches) {
try { try {
const hashesParam = batch.join(','); const hashesParam = batch.join(',');
const response = await fetch(`${BEATSAVER_API_BASE}/maps/hashes/${hashesParam}`); const response = await fetch(`${BEATSAVER_API_BASE}/maps/hash/${hashesParam}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`BeatSaver API error for hashes: ${response.status} ${response.statusText}`); throw new Error(`BeatSaver API error for hashes: ${response.status} ${response.statusText}`);

View file

@ -26,5 +26,7 @@ export const TAServerUrl = createPersistedStore('TAServerUrl', "server.tournamen
export const TAServerPort = createPersistedStore('TAServerPort', "8676"); export const TAServerPort = createPersistedStore('TAServerPort', "8676");
export const TAServerPlayerPort = createPersistedStore('TAServerPlayerPort', "8675");
// In the future, use a store for TAClient, since svelte is neat :))) // In the future, use a store for TAClient, since svelte is neat :)))
export const client = new TAClient(); export const client = new TAClient();

View file

@ -134,11 +134,11 @@
<div class="footer-section contact"> <div class="footer-section contact">
<h4>Contact</h4> <h4>Contact</h4>
<a href="mailto:support@tournamentassistant.net" class="contact-link"> <a href="mailto:support@beatkhana.com" class="contact-link">
<span class="material-icons">email</span> <span class="material-icons">email</span>
support@tournamentassistant.net support@beatkhana.com
</a> </a>
<a href="https://discord.gg/tournament" class="contact-link"> <a href="https://discord.gg/AnkmKk6AD8" class="contact-link">
<span class="material-icons">forum</span> <span class="material-icons">forum</span>
Join our Discord Join our Discord
</a> </a>
@ -146,7 +146,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<div class="copyright"> <div class="copyright">
&copy; {new Date().getFullYear()} Tournament Assistant. All rights reserved. &copy; {new Date().getFullYear()} Luna & Moon. All rights reserved.
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -3,7 +3,7 @@
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import Notification from '$lib/components/notifications/Popup.svelte'; import Notification from '$lib/components/notifications/Popup.svelte';
import { discordAuthUrl } from '$lib/config.json'; import { discordAuthUrl } from '$lib/config.json';
import { discordDataStore, discordTokenStore, authTokenStore, TAServerPort, TAServerUrl, client } from '$lib/stores'; import { discordDataStore, discordTokenStore, authTokenStore, TAServerPort, TAServerUrl, client, TAServerPlayerPort } from '$lib/stores';
import { TAClient, Response_ResponseType, Tournament } from 'moons-ta-client'; import { TAClient, Response_ResponseType, Tournament } from 'moons-ta-client';
import { bufferToImageUrl } from '$lib/services/taImages'; import { bufferToImageUrl } from '$lib/services/taImages';
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@ -125,13 +125,6 @@
} }
}); });
onDestroy(() => {
// if (client.isConnected == true) {
// // Properly disconnect and clean up any listeners
// client.disconnect();
// }
});
async function fetchTournamentAuthorisedUsers(tournamentGuid: string) { async function fetchTournamentAuthorisedUsers(tournamentGuid: string) {
// Get the authorised users for the tournament // Get the authorised users for the tournament
let authorisedUsers = []; let authorisedUsers = [];
@ -213,14 +206,7 @@
try { try {
creating = true; creating = true;
// Connect to the TA server if(!client.isConnected) return;
const connectResult = await client.connect('server.tournamentassistant.net', '8676');
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
authError = connectResult.details.connect.message;
creating = false;
return;
}
// Create the tournament object // Create the tournament object
let tournament: Tournament = { let tournament: Tournament = {
@ -241,6 +227,12 @@
pools: [], pools: [],
allowUnauthorizedView: true allowUnauthorizedView: true
}, },
server: {
name: `${$TAServerUrl}:${$TAServerPort}`,
address: $TAServerUrl,
port: $TAServerPlayerPort,
websocketPort: $TAServerPort
}
}; };
// Convert image to Uint8Array if an image was provided // Convert image to Uint8Array if an image was provided

View file

@ -3,7 +3,7 @@
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { bkAPIUrl } from '$lib/config.json'; import { bkAPIUrl } from '$lib/config.json';
//@ts-ignore //@ts-ignore
import { Match, Tournament, TAClient, Response_ResponseType } from 'moons-ta-client'; import { Match, Tournament, TAClient, Response_ResponseType, User, User_PlayStates } from 'moons-ta-client';
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte"; import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
@ -15,25 +15,10 @@
let isLoading = false; let isLoading = false;
let error: string | null = null; let error: string | null = null;
let matches: Match[] = []; let matches: Match[] = [];
let availablePlayers: any[] = []; let availablePlayers: User[] = [];
let selectedPlayers: any[] = [];
let isCreatingMatch: boolean = false;
// Store for selected player guids // Store for selected player guids
const selectedPlayerGuids = writable<string[]>([]); const selectedPlayerGuids = writable<string[]>([]);
// Player status enum
enum PlayerStatus {
Downloading = "downloading",
SelectingSong = "selecting",
Idle = "idle"
}
enum PlayerPlayState {
In_Menu = 0,
Waiting_For_Coordinator = 1,
In_Game = 2
}
// Remove 'Bearer ' from the token // Remove 'Bearer ' from the token
if($authTokenStore) { if($authTokenStore) {
@ -49,13 +34,13 @@
} }
// Return status color based on player status // Return status color based on player status
function getStatusColor(status: PlayerStatus): string { function getStatusColor(status: User_PlayStates): string {
switch (status) { switch (status) {
case PlayerStatus.Downloading: case User_PlayStates.InGame:
return "var(--danger-color)"; return "var(--danger-color)";
case PlayerStatus.SelectingSong: case User_PlayStates.InMenu:
return "#FCD34D"; // Yellow return "#FCD34D"; // Yellow
case PlayerStatus.Idle: case User_PlayStates.WaitingForCoordinator:
return "#10B981"; // Green return "#10B981"; // Green
default: default:
return "var(--text-secondary)"; return "var(--text-secondary)";
@ -110,6 +95,7 @@
tournament = client.stateManager.getTournament(tournamentGuid); tournament = client.stateManager.getTournament(tournamentGuid);
if(!tournament?.users.some(user => user.guid == client.stateManager.getSelfGuid())) { if(!tournament?.users.some(user => user.guid == client.stateManager.getSelfGuid())) {
const joinResult = await client.joinTournament(tournamentGuid); const joinResult = await client.joinTournament(tournamentGuid);
console.log('jointournament', joinResult);
if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) { if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) {
throw new Error('Could not join tournament'); throw new Error('Could not join tournament');
} }
@ -159,19 +145,46 @@
associatedUsers: $selectedPlayerGuids associatedUsers: $selectedPlayerGuids
}); });
console.log(response) console.log("Match created! Response: ", response);
const newMatch = (response as any).details.createMatch.match; const newMatch = (response as any).details.createMatch.match;
goto(`/tournaments/${tournamentGuid}/matches/${newMatch.guid}`); goto(`/tournaments/${tournamentGuid}/matches/${newMatch.guid}`);
} }
async function handleUserConnected(params: [User, Tournament]) {
if(params[1].guid == tournament?.guid) {
availablePlayers.push(params[0]);
}
}
async function handleUserDisconnected(params: [User, Tournament]) {
if(params[1].guid == tournament?.guid) {
if(availablePlayers.some(x => x.guid == params[0].guid)) {
availablePlayers = availablePlayers.filter(x => x.guid !== params[0].guid);
} else {
let newMatchObject = matches.find(x => x.associatedUsers.includes(params[0].guid))!;
newMatchObject.associatedUsers = newMatchObject.associatedUsers.filter(x => x !== params[0].guid);
matches = [
...matches.filter(x => x.guid !== newMatchObject.guid),
newMatchObject
]
}
}
}
function getDiscordNameBecauseShit(user: User) {
return user.discordInfo?.username || "";
}
onMount(async() => { onMount(async() => {
if ($authTokenStore) { if ($authTokenStore) {
await fetchTournamentData(); await fetchTournamentData();
client.stateManager.on('matchCreated', handleMatchCreated); client.stateManager.on('matchCreated', handleMatchCreated);
client.stateManager.on('matchDeleted', handleMatchDeleted); client.stateManager.on('matchDeleted', handleMatchDeleted);
client.stateManager.on('matchUpdated', handleMatchUpdated); client.stateManager.on('matchUpdated', handleMatchUpdated);
client.stateManager.on('userConnected', handleUserConnected);
client.stateManager.on('userDisconnected', handleUserDisconnected);
} else { } else {
window.location.href = "/discordAuth" window.location.href = "/discordAuth"
} }
@ -181,6 +194,8 @@
client.stateManager.removeListener('matchUpdated', handleMatchUpdated); client.stateManager.removeListener('matchUpdated', handleMatchUpdated);
client.stateManager.removeListener('matchDeleted', handleMatchDeleted); client.stateManager.removeListener('matchDeleted', handleMatchDeleted);
client.stateManager.removeListener('matchCreated', handleMatchDeleted); client.stateManager.removeListener('matchCreated', handleMatchDeleted);
client.stateManager.removeListener('userConnected', handleUserConnected);
client.stateManager.removeListener('userDisconnected', handleUserDisconnected);
client.removeListener('createdMatch', handleMatchUpdated) client.removeListener('createdMatch', handleMatchUpdated)
// client.disconnect(); // client.disconnect();
}); });
@ -287,8 +302,8 @@
alt="Player" alt="Player"
class="player-avatar"> class="player-avatar">
<div class="player-info"> <div class="player-info">
<h4>{player.name} ({player.discordInfo.username})</h4> <h4>{player.name} ({getDiscordNameBecauseShit(player)})</h4>
<p>{PlayerPlayState[player.playState]}</p> <p>{User_PlayStates[player.playState]}</p>
</div> </div>
</div> </div>
{/each} {/each}

View file

@ -149,16 +149,22 @@
} }
async function handleMapUpdated(event: CustomEvent) { async function handleMapUpdated(event: CustomEvent) {
const res = await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map); await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
console.log(res)
fetchMapPoolData(); fetchMapPoolData();
showSuccessNotification = true; showSuccessNotification = true;
setTimeout(() => {
showSuccessNotification = false;
}, 2500);
successMessage = "Map successfully updated!"; successMessage = "Map successfully updated!";
} }
function handleMapAdded(event: CustomEvent) { async function handleMapAdded(event: CustomEvent) {
await client.addTournamentPoolMaps(tournamentGuid, poolGuid, [event.detail.map]);
fetchMapPoolData(); fetchMapPoolData();
showSuccessNotification = true; showSuccessNotification = true;
setTimeout(() => {
showSuccessNotification = false;
}, 2500);
successMessage = "Map successfully added!"; successMessage = "Map successfully added!";
} }
@ -260,25 +266,25 @@
} }
const response: BeatSaverMap = await data.json(); const response: BeatSaverMap = await data.json();
return response; return response;
} }
function getActiveModifiersCompact(gameplayModifiers: number): string[] { function getActiveModifiersCompact(gameplayModifiers: number): string[] {
const activeModifiers: string[] = []; const activeModifiers: string[] = [];
// Get all numeric enum values and sort them // Get all numeric enum values (powers of 2) and sort them
const values = Object.values(GameplayModifiers_GameOptions) const values = Object.values(GameplayModifiers_GameOptions)
.filter((value): value is number => .filter((value): value is number =>
typeof value === 'number' && typeof value === 'number' &&
value !== GameplayModifiers_GameOptions.None value !== GameplayModifiers_GameOptions.None &&
value > 0
) )
.sort((a, b) => a - b); .sort((a, b) => a - b);
console.log(values)
for (const value of values) { for (const value of values) {
if (gameplayModifiers & value) { // Use bitwise AND to check if this specific flag is set
if ((gameplayModifiers & value) === value) {
const name = modifierNameMap[value]; const name = modifierNameMap[value];
if (name && name.trim() !== "") { if (name && name.trim() !== "") {
activeModifiers.push(name); activeModifiers.push(name);
@ -290,11 +296,11 @@
} }
function getModifierArray(map: any): string[] { function getModifierArray(map: any): string[] {
if (map.taData.gameplayParameters?.gameplayModifiers.options == 0) { if (map.taData.gameplayParameters?.gameplayModifiers?.options == 0) {
return []; return [];
} }
return getActiveModifiersCompact(map.taData.gameplayParameters.gameplayModifiers); return getActiveModifiersCompact(map.taData.gameplayParameters?.gameplayModifiers?.options || 0);
} }
onMount(async() => { onMount(async() => {
@ -414,6 +420,18 @@
{:else} {:else}
<span class="no-modifiers">No modifiers active</span> <span class="no-modifiers">No modifiers active</span>
{/each} {/each}
{#if map.taData.gameplayParameters?.disableFail}
<span class="modifier-tag">Disable Fail</span>
{/if}
{#if map.taData.gameplayParameters?.disableCustomNotesOnStream}
<span class="modifier-tag">Disable Custom Notes On Stream</span>
{/if}
{#if map.taData.gameplayParameters?.disablePause}
<span class="modifier-tag">Disable Pause</span>
{/if}
{#if map.taData.gameplayParameters?.disableScoresaberSubmission}
<span class="modifier-tag">Disable ScoreSaber Submission</span>
{/if}
</div> </div>
</div> </div>
</div> </div>

View file

@ -258,7 +258,6 @@
} }
} }
// API functions - implement these based on your TA client
async function kickPlayerFromMatch(playerGuid: string) { async function kickPlayerFromMatch(playerGuid: string) {
const response = await client.removeUserFromMatch(tournamentGuid, matchGuid, playerGuid); const response = await client.removeUserFromMatch(tournamentGuid, matchGuid, playerGuid);
console.log('Kicking player:', playerGuid); console.log('Kicking player:', playerGuid);
@ -280,6 +279,13 @@
console.log('Sending players back to menu'); console.log('Sending players back to menu');
} }
async function handleUserDisconnected(params: [User, Tournament]) {
if(matchPlayers.some(user => user.guid == params[0].guid)) {
console.log("User disconnected from match. User: ", params[0])
matchPlayers = matchPlayers.filter(x => x.guid !== params[0].guid);
}
}
onMount(async() => { onMount(async() => {
if ($authTokenStore) { if ($authTokenStore) {
// Fetch the match data that we need to display // Fetch the match data that we need to display
@ -287,6 +293,7 @@
// Using stateManger listen to global changes to our match // Using stateManger listen to global changes to our match
client.stateManager.on('matchDeleted', handleMatchDeleted); client.stateManager.on('matchDeleted', handleMatchDeleted);
client.stateManager.on('matchUpdated', handleMatchUpdated); client.stateManager.on('matchUpdated', handleMatchUpdated);
client.stateManager.on('userDisconnected', handleUserDisconnected)
} else { } else {
window.location.href = "/discordAuth" window.location.href = "/discordAuth"
} }
@ -296,6 +303,7 @@
// Remove the listeners that were added on mount // Remove the listeners that were added on mount
client.stateManager.removeListener('matchUpdated', handleMatchUpdated); client.stateManager.removeListener('matchUpdated', handleMatchUpdated);
client.stateManager.removeListener('matchDeleted', handleMatchDeleted); client.stateManager.removeListener('matchDeleted', handleMatchDeleted);
client.stateManager.removeListener('userDisconnected', handleUserDisconnected);
}); });
</script> </script>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
export function load({ params }: any){
return {
tournamentGuid: params.tournamentguid,
params: params
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
export function load({ params }: any){
return {
tournamentGuid: params.tournamentguid,
qualifierGuid: params.qualifierGuid,
params: params
}
}