Fix leaderboard fetch logic

This commit is contained in:
pleb 2026-04-13 13:31:38 -07:00
parent d971936445
commit 80539af66e
3 changed files with 116 additions and 2 deletions

View File

@ -54,6 +54,22 @@ function beatleaderUrl(path) {
} }
return `${BASE_URL2}${path}`; return `${BASE_URL2}${path}`;
} }
function normalizeBeatLeaderDifficultyName(value) {
return (value ?? "").toLowerCase().replace(/\s+/g, "").replace("expert+", "expertplus");
}
function normalizeBeatLeaderModeName(value) {
return (value ?? "").toLowerCase().replace(/\s+/g, "");
}
function leaderboardsMatchingPlayMode(leaderboards, characteristic2, difficultyRaw) {
const modeNeedle = normalizeBeatLeaderModeName(characteristic2);
const diffNeedle = normalizeBeatLeaderDifficultyName(difficultyRaw);
if (!modeNeedle || !diffNeedle) return [];
return leaderboards.filter((lb) => {
const mode = normalizeBeatLeaderModeName(lb.difficulty?.modeName);
const diff = normalizeBeatLeaderDifficultyName(lb.difficulty?.difficultyName);
return mode === modeNeedle && diff === diffNeedle;
});
}
async function fetchBLLeaderboardsByHash(hash) { async function fetchBLLeaderboardsByHash(hash) {
const path = `/leaderboards/hash/${encodeURIComponent(hash)}`; const path = `/leaderboards/hash/${encodeURIComponent(hash)}`;
try { try {
@ -273,6 +289,8 @@ var friendsRelationCache = null;
var friendScoreRequestId = 0; var friendScoreRequestId = 0;
var mapInfoRequestId = 0; var mapInfoRequestId = 0;
var rawLevelHash = ""; var rawLevelHash = "";
var currentPlayCharacteristic = "";
var currentPlayDifficulty = "";
function resolvedHashFromBeatSaverMap(map, fallback) { function resolvedHashFromBeatSaverMap(map, fallback) {
const v = map.versions?.[0]?.hash; const v = map.versions?.[0]?.hash;
if (typeof v === "string" && v.length > 0) return v.toLowerCase().trim(); if (typeof v === "string" && v.length > 0) return v.toLowerCase().trim();
@ -291,6 +309,7 @@ async function applyDebugSong() {
const raw = settings.debugSongId.trim(); const raw = settings.debugSongId.trim();
if (!raw) return; if (!raw) return;
const reqId = ++mapInfoRequestId; const reqId = ++mapInfoRequestId;
beginFriendScoresForNewMapContext();
document.body.classList.add("loading"); document.body.classList.add("loading");
try { try {
const map = await fetchBeatSaverMapForDebug(raw); const map = await fetchBeatSaverMapForDebug(raw);
@ -314,6 +333,8 @@ async function applyDebugSong() {
const resolved = resolvedHashFromBeatSaverMap(map, fallbackHash); const resolved = resolvedHashFromBeatSaverMap(map, fallbackHash);
rawLevelHash = resolved || fallbackHash; rawLevelHash = resolved || fallbackHash;
currentMapHash = resolved || fallbackHash; currentMapHash = resolved || fallbackHash;
currentPlayCharacteristic = "Standard";
currentPlayDifficulty = "ExpertPlus";
const v0 = map.versions?.[0]; const v0 = map.versions?.[0];
const coverUrl = v0?.coverURL?.trim(); const coverUrl = v0?.coverURL?.trim();
cover.src = coverUrl || "images/unknown.svg"; cover.src = coverUrl || "images/unknown.svg";
@ -370,6 +391,8 @@ async function updateMapInfo(data) {
void applyDebugSong(); void applyDebugSong();
return; return;
} }
currentPlayCharacteristic = data.characteristic;
currentPlayDifficulty = data.difficulty;
const reqId = ++mapInfoRequestId; const reqId = ++mapInfoRequestId;
const custom = data.level_id.startsWith("custom_level_"); const custom = data.level_id.startsWith("custom_level_");
const wip = custom && data.level_id.endsWith("WIP"); const wip = custom && data.level_id.endsWith("WIP");
@ -387,6 +410,7 @@ async function updateMapInfo(data) {
bsrKey.textContent = custom && !wip ? "\u2026" : custom ? rawLevelHash || "???" : "???"; bsrKey.textContent = custom && !wip ? "\u2026" : custom ? rawLevelHash || "???" : "???";
timeMultiplier = data.timeMultiplier || 1; timeMultiplier = data.timeMultiplier || 1;
duration = data.duration / 1e3; duration = data.duration / 1e3;
beginFriendScoresForNewMapContext();
if (custom && !wip) { if (custom && !wip) {
document.body.classList.add("loading"); document.body.classList.add("loading");
try { try {
@ -521,6 +545,23 @@ function renderFriendScores(items) {
function friendsRelationListKey(playerId) { function friendsRelationListKey(playerId) {
return `${playerId}\0${settings.friendMode}`; return `${playerId}\0${settings.friendMode}`;
} }
function beginFriendScoresForNewMapContext() {
friendScoreRequestId += 1;
if (!settings.friends) return;
if (!currentMapHash) {
clearFriendScores("No map loaded");
return;
}
const playerId = getEffectivePlayerId();
if (!playerId) {
clearFriendScores("Waiting for BeatLeader player id");
return;
}
friendScoresList.replaceChildren();
friendScoresPanel.classList.remove("has-items");
friendScoresPanel.classList.add("is-loading");
friendScoresEmpty.textContent = "Loading mutual friend scores...";
}
async function refreshMapFriendScores() { async function refreshMapFriendScores() {
const hash = currentMapHash; const hash = currentMapHash;
if (!settings.friends) { if (!settings.friends) {
@ -536,6 +577,8 @@ async function refreshMapFriendScores() {
clearFriendScores("Waiting for BeatLeader player id"); clearFriendScores("Waiting for BeatLeader player id");
return; return;
} }
friendScoresList.replaceChildren();
friendScoresPanel.classList.remove("has-items");
friendScoresPanel.classList.add("is-loading"); friendScoresPanel.classList.add("is-loading");
friendScoresEmpty.textContent = "Loading mutual friend scores..."; friendScoresEmpty.textContent = "Loading mutual friend scores...";
const requestId = ++friendScoreRequestId; const requestId = ++friendScoreRequestId;
@ -559,6 +602,11 @@ async function refreshMapFriendScores() {
clearFriendScores("No BeatLeader leaderboards found"); clearFriendScores("No BeatLeader leaderboards found");
return; return;
} }
const forPlayMode = leaderboardsMatchingPlayMode(leaderboards, currentPlayCharacteristic, currentPlayDifficulty);
if (forPlayMode.length === 0) {
clearFriendScores("No BeatLeader leaderboard for this difficulty");
return;
}
const friendById = new Map(friends.map((f) => [ const friendById = new Map(friends.map((f) => [
f.id, f.id,
f f
@ -569,7 +617,7 @@ async function refreshMapFriendScores() {
clearFriendScores(relationLabel); clearFriendScores(relationLabel);
return; return;
} }
const scores = await fetchAllMapScoresByHash(hash, leaderboards); const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
if (requestId !== friendScoreRequestId) return; if (requestId !== friendScoreRequestId) return;
const bestByPlayer = /* @__PURE__ */ new Map(); const bestByPlayer = /* @__PURE__ */ new Map();
for (const score of scores) { for (const score of scores) {

View File

@ -28,6 +28,31 @@ function beatleaderUrl(path: string): string {
return `${BASE_URL}${path}`; return `${BASE_URL}${path}`;
} }
/** Match BS+ / BeatSaver difficulty strings to BeatLeader `difficultyName` (handles Expert+ vs ExpertPlus). */
export function normalizeBeatLeaderDifficultyName(value: string | null | undefined): string {
return (value ?? "").toLowerCase().replace(/\s+/g, "").replace("expert+", "expertplus");
}
function normalizeBeatLeaderModeName(value: string | null | undefined): string {
return (value ?? "").toLowerCase().replace(/\s+/g, "");
}
/** Keep only the leaderboard row for the played characteristic + difficulty (hash can list every diff). */
export function leaderboardsMatchingPlayMode(
leaderboards: BeatLeaderLeaderboard[],
characteristic: string,
difficultyRaw: string,
): BeatLeaderLeaderboard[] {
const modeNeedle = normalizeBeatLeaderModeName(characteristic);
const diffNeedle = normalizeBeatLeaderDifficultyName(difficultyRaw);
if (!modeNeedle || !diffNeedle) return [];
return leaderboards.filter((lb) => {
const mode = normalizeBeatLeaderModeName(lb.difficulty?.modeName);
const diff = normalizeBeatLeaderDifficultyName(lb.difficulty?.difficultyName);
return mode === modeNeedle && diff === diffNeedle;
});
}
export async function fetchBLLeaderboardsByHash(hash: string): Promise<BeatLeaderLeaderboard[]> { export async function fetchBLLeaderboardsByHash(hash: string): Promise<BeatLeaderLeaderboard[]> {
const path = `/leaderboards/hash/${encodeURIComponent(hash)}`; const path = `/leaderboards/hash/${encodeURIComponent(hash)}`;
try { try {

View File

@ -18,6 +18,7 @@ import {
fetchBeatLeaderPlayer, fetchBeatLeaderPlayer,
fetchBLLeaderboardsByHash, fetchBLLeaderboardsByHash,
fetchFriends, fetchFriends,
leaderboardsMatchingPlayMode,
normalizeAccuracy, normalizeAccuracy,
} from "./beatleader.ts"; } from "./beatleader.ts";
import { mergeOverlayConfigResponse, type OverlayConfigApiBody } from "./overlay-config.ts"; import { mergeOverlayConfigResponse, type OverlayConfigApiBody } from "./overlay-config.ts";
@ -120,6 +121,9 @@ let friendScoreRequestId = 0;
let mapInfoRequestId = 0; let mapInfoRequestId = 0;
/** Hex hash from BS+ `level_id` (before BeatSaver version hash). */ /** Hex hash from BS+ `level_id` (before BeatSaver version hash). */
let rawLevelHash = ""; let rawLevelHash = "";
/** BeatLeader friend scores are limited to this characteristic + difficulty (from BS+ mapInfo, or debug BSR defaults). */
let currentPlayCharacteristic = "";
let currentPlayDifficulty = "";
function beatLeaderboardId(lb: BeatLeaderLeaderboard): string { function beatLeaderboardId(lb: BeatLeaderLeaderboard): string {
const id = lb.id ?? lb.leaderboardId; const id = lb.id ?? lb.leaderboardId;
@ -148,6 +152,7 @@ async function applyDebugSong() {
const raw = settings.debugSongId.trim(); const raw = settings.debugSongId.trim();
if (!raw) return; if (!raw) return;
const reqId = ++mapInfoRequestId; const reqId = ++mapInfoRequestId;
beginFriendScoresForNewMapContext();
document.body.classList.add("loading"); document.body.classList.add("loading");
try { try {
const map = await fetchBeatSaverMapForDebug(raw); const map = await fetchBeatSaverMapForDebug(raw);
@ -171,6 +176,9 @@ async function applyDebugSong() {
const resolved = resolvedHashFromBeatSaverMap(map, fallbackHash); const resolved = resolvedHashFromBeatSaverMap(map, fallbackHash);
rawLevelHash = resolved || fallbackHash; rawLevelHash = resolved || fallbackHash;
currentMapHash = resolved || fallbackHash; currentMapHash = resolved || fallbackHash;
// Debug BSR has no BS+ difficulty; assume Standard ExpertPlus for BeatLeader lookup.
currentPlayCharacteristic = "Standard";
currentPlayDifficulty = "ExpertPlus";
const v0 = map.versions?.[0]; const v0 = map.versions?.[0];
const coverUrl = v0?.coverURL?.trim(); const coverUrl = v0?.coverURL?.trim();
@ -237,6 +245,8 @@ async function updateMapInfo(data: MapInfo) {
void applyDebugSong(); void applyDebugSong();
return; return;
} }
currentPlayCharacteristic = data.characteristic;
currentPlayDifficulty = data.difficulty;
const reqId = ++mapInfoRequestId; const reqId = ++mapInfoRequestId;
const custom = data.level_id.startsWith("custom_level_"); const custom = data.level_id.startsWith("custom_level_");
const wip = custom && data.level_id.endsWith("WIP"); const wip = custom && data.level_id.endsWith("WIP");
@ -256,6 +266,8 @@ async function updateMapInfo(data: MapInfo) {
timeMultiplier = data.timeMultiplier || 1; timeMultiplier = data.timeMultiplier || 1;
duration = data.duration / 1000; duration = data.duration / 1000;
beginFriendScoresForNewMapContext();
if (custom && !wip) { if (custom && !wip) {
document.body.classList.add("loading"); document.body.classList.add("loading");
try { try {
@ -409,6 +421,28 @@ function friendsRelationListKey(playerId: string): string {
return `${playerId}\0${settings.friendMode}`; return `${playerId}\0${settings.friendMode}`;
} }
/**
* Call synchronously when map identity / difficulty changes so stale in-flight fetches cannot repaint,
* and the panel does not keep showing the previous maps scores while BeatLeader loads.
*/
function beginFriendScoresForNewMapContext() {
friendScoreRequestId += 1;
if (!settings.friends) return;
if (!currentMapHash) {
clearFriendScores("No map loaded");
return;
}
const playerId = getEffectivePlayerId();
if (!playerId) {
clearFriendScores("Waiting for BeatLeader player id");
return;
}
friendScoresList.replaceChildren();
friendScoresPanel.classList.remove("has-items");
friendScoresPanel.classList.add("is-loading");
friendScoresEmpty.textContent = "Loading mutual friend scores...";
}
async function refreshMapFriendScores() { async function refreshMapFriendScores() {
const hash = currentMapHash; const hash = currentMapHash;
if (!settings.friends) { if (!settings.friends) {
@ -424,6 +458,8 @@ async function refreshMapFriendScores() {
clearFriendScores("Waiting for BeatLeader player id"); clearFriendScores("Waiting for BeatLeader player id");
return; return;
} }
friendScoresList.replaceChildren();
friendScoresPanel.classList.remove("has-items");
friendScoresPanel.classList.add("is-loading"); friendScoresPanel.classList.add("is-loading");
friendScoresEmpty.textContent = "Loading mutual friend scores..."; friendScoresEmpty.textContent = "Loading mutual friend scores...";
const requestId = ++friendScoreRequestId; const requestId = ++friendScoreRequestId;
@ -447,6 +483,11 @@ async function refreshMapFriendScores() {
clearFriendScores("No BeatLeader leaderboards found"); clearFriendScores("No BeatLeader leaderboards found");
return; return;
} }
const forPlayMode = leaderboardsMatchingPlayMode(leaderboards, currentPlayCharacteristic, currentPlayDifficulty);
if (forPlayMode.length === 0) {
clearFriendScores("No BeatLeader leaderboard for this difficulty");
return;
}
const friendById = new Map(friends.map((f) => [f.id, f])); const friendById = new Map(friends.map((f) => [f.id, f]));
const mutualFriendIds = new Set(friends.map((f) => f.id)); const mutualFriendIds = new Set(friends.map((f) => f.id));
if (mutualFriendIds.size === 0) { if (mutualFriendIds.size === 0) {
@ -458,7 +499,7 @@ async function refreshMapFriendScores() {
clearFriendScores(relationLabel); clearFriendScores(relationLabel);
return; return;
} }
const scores = await fetchAllMapScoresByHash(hash, leaderboards); const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
if (requestId !== friendScoreRequestId) return; if (requestId !== friendScoreRequestId) return;
const bestByPlayer = new Map<string, { name: string; acc: number; avatar: string | null }>(); const bestByPlayer = new Map<string, { name: string; acc: number; avatar: string | null }>();
for (const score of scores) { for (const score of scores) {