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 { v4 as uuidv4 } from "uuid";
import { onMount } from "svelte";
import type { BeatSaverMap } from "$lib/services/beatsaver";
export let tournamentGuid: string;
export let poolGuid: string;
@ -54,33 +55,6 @@
// Playing-specific settings
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
interface Modifier {
id: GameplayModifiers_GameOptions;
@ -433,7 +407,7 @@
selectedCharacteristic = availableCharacteristics[0] || "";
selectedDifficulty = availableDifficulties[selectedCharacteristic]?.[0] || "";
mapBeatmapId = `custom_level_${latestVersion.hash}`;
mapBeatmapId = `custom_level_${latestVersion.hash.toLocaleUpperCase()}`;
return data;
} catch (err) {
@ -510,20 +484,25 @@
disableScoresaberSubmission: mode === 'playing' ? disableScoresaberSubmission : false,
showScoreboard: mode === 'playing' ? showScoreboard : 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) {
// I will need to fix this, for now this is a TA server issue
dispatch('mapUpdated', { map: mapData });
} else {
await client.addTournamentPoolMaps(tournamentGuid, poolGuid, [mapData]);
dispatch('mapAdded', { map: mapData });
}
// if (response.type !== Response_ResponseType.Success) {
// throw new Error("Failed to save map");
// }
isLoading = false;
return dispatch('close');
} catch (err) {
@ -748,18 +727,6 @@
<p class="section-desc">Configure game behavior options</p>
<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">
<label class="switch-label">
<input
@ -772,6 +739,18 @@
</div>
{#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">
<label class="switch-label">
<input
@ -816,7 +795,6 @@
<span class="switch"></span>
<span class="label-text">Limit Attempts</span>
</label>
<p class="setting-description">Restrict the number of attempts allowed for this qualifier</p>
</div>
{#if limitAttemptsEnabled}

View file

@ -1,4 +1,4 @@
interface BeatSaverMap {
export interface BeatSaverMap {
id: string;
name: string;
description: string;
@ -58,6 +58,7 @@ interface BeatSaverMap {
}>;
downloadURL: string;
previewURL: string;
coverURL: string;
}>;
}
@ -117,7 +118,22 @@ export async function fetchMapsByHashesOrKeys(identifiers: string[]): Promise<Be
}
// 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[][] = [];
for (let i = 0; i < hashes.length; 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) {
try {
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) {
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 TAServerPlayerPort = createPersistedStore('TAServerPlayerPort', "8675");
// In the future, use a store for TAClient, since svelte is neat :)))
export const client = new TAClient();

View file

@ -134,11 +134,11 @@
<div class="footer-section contact">
<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>
support@tournamentassistant.net
support@beatkhana.com
</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>
Join our Discord
</a>
@ -146,7 +146,7 @@
</div>
<div class="footer-bottom">
<div class="copyright">
&copy; {new Date().getFullYear()} Tournament Assistant. All rights reserved.
&copy; {new Date().getFullYear()} Luna & Moon. All rights reserved.
</div>
</div>
</footer>

View file

@ -3,7 +3,7 @@
import { onMount, onDestroy } from 'svelte';
import Notification from '$lib/components/notifications/Popup.svelte';
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 { bufferToImageUrl } from '$lib/services/taImages';
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) {
// Get the authorised users for the tournament
let authorisedUsers = [];
@ -213,14 +206,7 @@
try {
creating = true;
// Connect to the TA server
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;
}
if(!client.isConnected) return;
// Create the tournament object
let tournament: Tournament = {
@ -241,6 +227,12 @@
pools: [],
allowUnauthorizedView: true
},
server: {
name: `${$TAServerUrl}:${$TAServerPort}`,
address: $TAServerUrl,
port: $TAServerPlayerPort,
websocketPort: $TAServerPort
}
};
// Convert image to Uint8Array if an image was provided

View file

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

View file

@ -149,16 +149,22 @@
}
async function handleMapUpdated(event: CustomEvent) {
const res = await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
console.log(res)
await client.updateTournamentPoolMap(tournamentGuid, poolGuid, event.detail.map);
fetchMapPoolData();
showSuccessNotification = true;
setTimeout(() => {
showSuccessNotification = false;
}, 2500);
successMessage = "Map successfully updated!";
}
function handleMapAdded(event: CustomEvent) {
async function handleMapAdded(event: CustomEvent) {
await client.addTournamentPoolMaps(tournamentGuid, poolGuid, [event.detail.map]);
fetchMapPoolData();
showSuccessNotification = true;
setTimeout(() => {
showSuccessNotification = false;
}, 2500);
successMessage = "Map successfully added!";
}
@ -267,18 +273,18 @@
function getActiveModifiersCompact(gameplayModifiers: number): 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)
.filter((value): value is number =>
typeof value === 'number' &&
value !== GameplayModifiers_GameOptions.None
value !== GameplayModifiers_GameOptions.None &&
value > 0
)
.sort((a, b) => a - b);
console.log(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];
if (name && name.trim() !== "") {
activeModifiers.push(name);
@ -290,11 +296,11 @@
}
function getModifierArray(map: any): string[] {
if (map.taData.gameplayParameters?.gameplayModifiers.options == 0) {
if (map.taData.gameplayParameters?.gameplayModifiers?.options == 0) {
return [];
}
return getActiveModifiersCompact(map.taData.gameplayParameters.gameplayModifiers);
return getActiveModifiersCompact(map.taData.gameplayParameters?.gameplayModifiers?.options || 0);
}
onMount(async() => {
@ -414,6 +420,18 @@
{:else}
<span class="no-modifiers">No modifiers active</span>
{/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>

View file

@ -258,7 +258,6 @@
}
}
// API functions - implement these based on your TA client
async function kickPlayerFromMatch(playerGuid: string) {
const response = await client.removeUserFromMatch(tournamentGuid, matchGuid, playerGuid);
console.log('Kicking player:', playerGuid);
@ -280,6 +279,13 @@
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() => {
if ($authTokenStore) {
// Fetch the match data that we need to display
@ -287,6 +293,7 @@
// Using stateManger listen to global changes to our match
client.stateManager.on('matchDeleted', handleMatchDeleted);
client.stateManager.on('matchUpdated', handleMatchUpdated);
client.stateManager.on('userDisconnected', handleUserDisconnected)
} else {
window.location.href = "/discordAuth"
}
@ -296,6 +303,7 @@
// Remove the listeners that were added on mount
client.stateManager.removeListener('matchUpdated', handleMatchUpdated);
client.stateManager.removeListener('matchDeleted', handleMatchDeleted);
client.stateManager.removeListener('userDisconnected', handleUserDisconnected);
});
</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
}
}