fix map loading stuff, add bswc score calc stuff

This commit is contained in:
Luna 2025-07-05 18:54:16 +02:00
parent 5ae7c790be
commit 9bb0af358c
7 changed files with 633 additions and 11 deletions

View file

@ -263,7 +263,7 @@
gameplayParameters: { gameplayParameters: {
beatmap: { beatmap: {
name: map.songName, name: map.songName,
levelId: `custom_level_${map.hash}`, levelId: `custom_level_${map.hash.toUpperCase()}`,
difficulty: getDifficultyNumber(map.difficulty), difficulty: getDifficultyNumber(map.difficulty),
characteristic: { characteristic: {
serializedName: 'Standard', serializedName: 'Standard',

View file

@ -287,7 +287,7 @@
gameplayParameters: { gameplayParameters: {
beatmap: { beatmap: {
name: mapName, name: mapName,
levelId: mapBeatmapId, levelId: mapBeatmapId.toUpperCase().replace('CUSTOM_LEVEL_', 'custom_level_'),
difficulty: getDifficultyNumber(selectedDifficulty), difficulty: getDifficultyNumber(selectedDifficulty),
characteristic: { characteristic: {
serializedName: selectedCharacteristic, serializedName: selectedCharacteristic,

View file

@ -213,10 +213,10 @@ export async function fetchMapByHashOrKey(identifier: string): Promise<BeatSaver
*/ */
export function extractHashFromLevelId(levelId: string): string { export function extractHashFromLevelId(levelId: string): string {
const prefix = 'custom_level_'; const prefix = 'custom_level_';
if (!levelId.startsWith(prefix)) { if (!levelId.toLowerCase().startsWith(prefix)) {
throw new Error(`Invalid level ID format. Expected format: ${prefix}[hash]`); throw new Error(`Invalid level ID format. Expected format: ${prefix}[hash]`);
} }
return levelId.substring(prefix.length); return levelId.toLowerCase().substring(prefix.length);
} }
/** /**

View file

@ -0,0 +1,622 @@
<script lang="ts">
import { onMount } from 'svelte';
interface Difficulty {
characteristic: string;
name: string;
}
interface Song {
hash: string;
difficulties: Difficulty[];
}
interface MapPool {
playlistTitle: string;
playlistAuthor: string;
playlistDescription: string;
syncURL: string;
image: string;
songs: Song[];
}
interface BeatSaverMap {
id: string;
name: string;
description: string;
uploader: {
name: string;
};
metadata: {
songName: string;
songAuthorName: string;
levelAuthorName: string;
};
versions: Array<{
hash: string;
diffs: Array<{
difficulty: string;
characteristic: string;
nps: number;
notes: number;
}>;
}>;
}
interface MapData extends BeatSaverMap {
selectedDifficulty: string;
selectedCharacteristic: string;
maxScore: number;
nps: number;
}
let mapPool: MapPool | null = null;
let maps: MapData[] = [];
let selectedMapIndex: number | null = null;
let loading = false;
let resultsInput = '';
let calculatedResults = '';
let copied = false;
let fileInput: HTMLInputElement;
onMount(() => {
const defaultPool: MapPool = {
playlistTitle: "Week 2",
playlistAuthor: "Cube Community",
playlistDescription: "The Bo7 Week 2 Map Pool for the Beat Saber World Cup 2025",
syncURL: "https://api.cube.community/rest/pooling/playlist?poolId=cm94fsme0000ty9u9h3d3par0",
image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
songs: [
{"hash":"bce7949bd6b9432782b543c77014fddc756a6894","difficulties":[{"characteristic":"Standard","name":"expert"}]},
{"hash":"e2e71212e6468506b35ab08986918f317157cddc","difficulties":[{"characteristic":"Standard","name":"easy"}]},
{"hash":"40f74ba3c2a4475acabb8e2a1d5eb9219f36e043","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"55710205349cc4b25e09644d797d119ef964bd1d","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"62d66da2dd4ee601b24ee608847fb3657a78e761","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"9109925c3719d232e3eeae8868c0bf1efd660d51","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"93d5adc1756f488ad85484279831f327d94efe64","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"95d9d9e21c381f2e41073ca3257208174e90b322","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"b907db0d67ab965b4b702d242463a44e01c5ecd3","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"d67821c494b38ff51860766d853ba8f5d82ca104","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]},
{"hash":"a1c6d47059e479ae493d118b4c7e5a1ef1340e99","difficulties":[{"characteristic":"Standard","name":"expertPlus"}]}
]
};
mapPool = defaultPool;
fetchMapsData(defaultPool);
});
async function fetchMapsData(pool: MapPool) {
loading = true;
const mapData: MapData[] = [];
for (const song of pool.songs) {
try {
const response = await fetch(`https://api.beatsaver.com/maps/hash/${song.hash}`);
if (response.ok) {
const data: BeatSaverMap = await response.json();
// Find the selected difficulty
const selectedDiff = song.difficulties[0];
const version = data.versions.find(v => v.hash.toLowerCase() === song.hash.toLowerCase());
if (version) {
const diff = version.diffs.find(d =>
d.difficulty.toLowerCase() === selectedDiff.name.toLowerCase() &&
d.characteristic === selectedDiff.characteristic
);
if (diff) {
const maxScore = diff.notes * 920; // Max score per note in Beat Saber
mapData.push({
...data,
selectedDifficulty: selectedDiff.name,
selectedCharacteristic: selectedDiff.characteristic,
maxScore,
nps: diff.nps
});
}
}
}
} catch (error) {
console.error(`Error fetching map ${song.hash}:`, error);
}
}
maps = mapData;
loading = false;
}
async function handleFileUpload(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
const text = await file.text();
try {
const pool: MapPool = JSON.parse(text);
mapPool = pool;
fetchMapsData(pool);
} catch (error) {
console.error('Error parsing map pool file:', error);
}
}
function calculateResults() {
if (selectedMapIndex === null || selectedMapIndex < 0 || !maps[selectedMapIndex]) return;
const selectedMap = maps[selectedMapIndex];
const maxScore = selectedMap.maxScore;
// Parse results input
const lines = resultsInput.trim().split('\n');
const scores: Array<{ country: string; score: number; accuracy: number }> = [];
let winner = '';
lines.forEach(line => {
const scoreMatch = line.match(/(.+?)\s+Score:\s*([\d,]+)/);
const winnerMatch = line.match(/Winner:\s*(.+)/);
if (scoreMatch) {
const country = scoreMatch[1].trim();
const score = parseInt(scoreMatch[2].replace(/,/g, ''));
const accuracy = (score / maxScore) * 100 / 3; // Divide by 3 for average accuracy
scores.push({ country, score, accuracy });
} else if (winnerMatch) {
winner = winnerMatch[1].trim();
}
});
// Format results
let result = '';
scores.forEach(({ country, score, accuracy }) => {
result += `${country} Score: ${score.toLocaleString()} (${accuracy.toFixed(3)}%)\n`;
});
if (winner) {
result += `Winner: ${winner}`;
}
calculatedResults = result;
}
async function copyResults() {
if (calculatedResults) {
await navigator.clipboard.writeText(calculatedResults);
copied = true;
setTimeout(() => copied = false, 2000);
}
}
function getDifficultyColor(difficulty: string): string {
switch (difficulty.toLowerCase()) {
case 'easy': return 'text-green-400';
case 'normal': return 'text-blue-400';
case 'hard': return 'text-orange-400';
case 'expert': return 'text-red-400';
case 'expertplus': return 'text-purple-400';
default: return 'text-gray-400';
}
}
function selectMap(index: number) {
selectedMapIndex = index;
calculateResults();
}
// Reactive statement to recalculate when inputs change
$: if (resultsInput && selectedMapIndex !== null) {
calculateResults();
}
</script>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-controls">
<label class="file-upload-label">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
Load Map Pool
<input
bind:this={fileInput}
type="file"
accept=".bplist,.json"
on:change={handleFileUpload}
class="file-upload-input"
/>
</label>
{#if loading}
<div class="loading-text">Loading maps...</div>
{/if}
</div>
{#if mapPool}
<div class="map-pool-info">
<img
src={mapPool.image}
alt={mapPool.playlistTitle}
class="map-pool-image"
/>
<div>
<h1 class="map-pool-title">{mapPool.playlistTitle}</h1>
<p class="map-pool-author">by {mapPool.playlistAuthor}</p>
<p class="map-pool-description">{mapPool.playlistDescription}</p>
</div>
</div>
{/if}
</div>
<!-- Maps Grid -->
<div class="maps-grid">
{#each maps as map, index}
<div
class="map-card {selectedMapIndex === index ? 'selected' : ''}"
on:click={() => selectMap(index)}
role="button"
tabindex="0"
on:keydown={(e) => e.key === 'Enter' && selectMap(index)}
>
<div class="map-content">
<h3 class="map-title">
{map.metadata.songName}
</h3>
<p class="map-author">by {map.metadata.songAuthorName}</p>
<p class="map-mapper">mapped by {map.metadata.levelAuthorName}</p>
<div class="map-details">
<div class="map-difficulty-row">
<span class="map-difficulty difficulty-{map.selectedDifficulty.toLowerCase()}">
{map.selectedDifficulty.charAt(0).toUpperCase() + map.selectedDifficulty.slice(1)}
</span>
<span class="map-nps">{map.nps.toFixed(1)} NPS</span>
</div>
<div class="map-score">
Max Score: {map.maxScore.toLocaleString()}
</div>
</div>
</div>
</div>
{/each}
</div>
<!-- Results Section -->
<div class="results-section">
<div class="results-column">
<h2>Enter Results</h2>
<textarea
bind:value={resultsInput}
placeholder="United Kingdom Score: 1,238,773&#10;Poland Score: 1,233,813&#10;Winner: United Kingdom"
class="results-textarea"
></textarea>
</div>
<div class="results-column">
<div class="results-header">
<h2>Calculated Results</h2>
{#if calculatedResults}
<button
on:click={copyResults}
class="copy-button"
>
{#if copied}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Copied!
{:else}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Copy
{/if}
</button>
{/if}
</div>
<textarea
bind:value={calculatedResults}
readonly
class="results-textarea"
></textarea>
</div>
</div>
{#if selectedMapIndex !== null && maps[selectedMapIndex]}
<div class="selected-map-info">
<h3>Currently Selected:</h3>
<p class="selected-map-name">{maps[selectedMapIndex].metadata.songName}</p>
<p class="selected-map-score">Max Score: {maps[selectedMapIndex].maxScore.toLocaleString()}</p>
</div>
{/if}
</div>
</body>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
color: white;
padding: 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
max-width: 1280px;
margin: 0 auto;
}
/* Header */
.header {
margin-bottom: 32px;
}
.header-controls {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.file-upload-label {
display: flex;
align-items: center;
gap: 8px;
background-color: #2563eb;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s;
}
.file-upload-label:hover {
background-color: #1d4ed8;
}
.file-upload-label svg {
width: 20px;
height: 20px;
}
.file-upload-input {
display: none;
}
.loading-text {
color: #60a5fa;
}
.map-pool-info {
background-color: #1f2937;
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 24px;
}
.map-pool-image {
width: 80px;
height: 80px;
border-radius: 8px;
object-fit: cover;
}
.map-pool-title {
font-size: 30px;
font-weight: bold;
color: white;
margin: 0;
}
.map-pool-author {
color: #d1d5db;
margin: 4px 0;
}
.map-pool-description {
color: #9ca3af;
margin-top: 8px;
}
/* Maps Grid */
.maps-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.map-card {
background-color: #1f2937;
border-radius: 12px;
padding: 24px;
cursor: pointer;
transition: all 0.3s;
opacity: 0.8;
display: flex;
flex-direction: column;
height: 100%;
}
.map-card:hover {
opacity: 1;
transform: scale(1.05);
}
.map-card.selected {
opacity: 1;
border: 2px solid #3b82f6;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
}
.map-title {
font-size: 20px;
font-weight: 600;
color: white;
margin-bottom: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.map-author {
color: #d1d5db;
margin-bottom: 4px;
}
.map-mapper {
color: #9ca3af;
margin-bottom: 12px;
}
.map-details {
margin-top: auto;
}
.map-difficulty-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.map-difficulty {
font-weight: 500;
text-transform: capitalize;
}
.difficulty-easy { color: #10b981; }
.difficulty-normal { color: #3b82f6; }
.difficulty-hard { color: #f59e0b; }
.difficulty-expert { color: #ef4444; }
.difficulty-expertplus { color: #8b5cf6; }
.map-nps {
color: #9ca3af;
}
.map-score {
font-size: 14px;
color: #6b7280;
}
/* Results Section */
.results-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.results-column h2 {
font-size: 20px;
font-weight: 600;
margin-bottom: 16px;
}
.results-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.copy-button {
display: flex;
align-items: center;
gap: 8px;
background-color: #059669;
padding: 4px 12px;
border-radius: 8px;
border: none;
color: white;
cursor: pointer;
transition: background-color 0.2s;
}
.copy-button:hover {
background-color: #047857;
}
.copy-button svg {
width: 16px;
height: 16px;
}
.results-textarea {
width: 100%;
height: 192px;
background-color: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
padding: 16px;
color: white;
font-family: inherit;
resize: none;
outline: none;
}
.results-textarea:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
.results-textarea::placeholder {
color: #6b7280;
}
.results-textarea[readonly] {
background-color: #1f2937;
}
/* Selected Map Info */
.selected-map-info {
margin-top: 24px;
padding: 16px;
background-color: #1f2937;
border-radius: 8px;
}
.selected-map-info h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.selected-map-name {
color: #60a5fa;
}
.selected-map-score {
color: #9ca3af;
}
@media (max-width: 1024px) {
.results-section {
grid-template-columns: 1fr;
}
.map-pool-info {
flex-direction: column;
text-align: center;
}
}
@media (max-width: 768px) {
body {
padding: 16px;
}
.maps-grid {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -130,7 +130,7 @@
let authorisedUsers = []; let authorisedUsers = [];
try { try {
const response = await client.getAuthorizedUsers(tournamentGuid); const response = await client.getAuthorizedUsers(tournamentGuid);
console.log(response) console.log("authUsers Found", response);
authorisedUsers = (response as any).details.getAuthorizedUsers.authorizedUsers; authorisedUsers = (response as any).details.getAuthorizedUsers.authorizedUsers;
return authorisedUsers; return authorisedUsers;
} catch (error) { } catch (error) {

View file

@ -245,7 +245,7 @@
} }
async function getMapBeatsaverData(levelId: string): Promise<BeatSaverMap> { async function getMapBeatsaverData(levelId: string): Promise<BeatSaverMap> {
const newLevelId = levelId.replace("custom_level_", ""); const newLevelId = levelId.toLowerCase().replace("custom_level_", "");
const data = await fetch(`https://api.beatsaver.com/maps/hash/${newLevelId}`); const data = await fetch(`https://api.beatsaver.com/maps/hash/${newLevelId}`);

View file

@ -107,9 +107,9 @@
matchPlayers matchPlayers
.filter(x => x.playState == User_PlayStates.InGame) .filter(x => x.playState == User_PlayStates.InGame)
.forEach(player => { .forEach(player => {
const playerKey = `${player.guid}-${currentSong.beatmap.levelId.toLowerCase()}`; const playerKey: string = `${player.guid}-${currentSong.beatmap.levelId.toLowerCase()}`;
if (!activeSongPlayers.has(playerKey)) { if (!activeSongPlayers.has(playerKey as any)) {
activeSongPlayers.add(playerKey); activeSongPlayers.add(playerKey as any);
} }
}); });
} }
@ -424,7 +424,7 @@
const poolCustomMaps: CustomMap[] = pool.maps const poolCustomMaps: CustomMap[] = pool.maps
.map(map => { .map(map => {
const levelId = map.gameplayParameters!.beatmap!.levelId; const levelId = map.gameplayParameters!.beatmap!.levelId;
const beatsaverData = mapDataLookup[(levelId.replace('custom_level_', '').toLowerCase())]; const beatsaverData = mapDataLookup[(levelId.toLowerCase().replace('custom_level_', ''))];
if (!beatsaverData) return null; if (!beatsaverData) return null;
@ -775,7 +775,7 @@
loadingSongForPlayers = true; loadingSongForPlayers = true;
const matchResponse = await client.setMatchMap(tournamentGuid, matchGuid, map.taData); const matchResponse = await client.setMatchMap(tournamentGuid, matchGuid, map.taData);
if(matchResponse.type == Response_ResponseType.Fail) return; if(matchResponse.type == Response_ResponseType.Fail) return;
const loadResponse = await client.loadSong(map.taData.gameplayParameters!.beatmap!.levelId, playingPlayerIds, 3 * 60 * 1000); const loadResponse = await client.loadSong(map.taData.gameplayParameters!.beatmap!.levelId.toUpperCase().replace('CUSTOM_LEVEL_', 'custom_level_'), playingPlayerIds, 3 * 60 * 1000);
failedToLoadSongForPlayers = false; failedToLoadSongForPlayers = false;
const failedLoads = loadResponse.filter(x => x.response.type == Response_ResponseType.Fail); const failedLoads = loadResponse.filter(x => x.response.type == Response_ResponseType.Fail);