Compare commits
3 Commits
winroar
...
86830adc47
| Author | SHA1 | Date | |
|---|---|---|---|
| 86830adc47 | |||
| a6629f8e95 | |||
| 29ce672646 |
@@ -1,6 +1,6 @@
|
|||||||
# Beat Saber Overlay
|
# Beat Saber Overlay
|
||||||
|
|
||||||
Beat Saber stream overlay, originally based on [twitch.tv/iza_k](https://github.com/ibillingsley/BeatSaber-Overlay) but rewritten in Deno TypeScript. Requires [BeatSaberPlus](https://github.com/hardcpp/BeatSaberPlus)
|
Beat Saber stream overlay, originally based on the [overlay by iza_k](https://github.com/ibillingsley/BeatSaber-Overlay) but rewritten in Deno TypeScript. Requires [BeatSaberPlus](https://github.com/hardcpp/BeatSaberPlus)
|
||||||
|
|
||||||
### Preview
|
### Preview
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect fill="#111111" width="48" height="48"/>
|
<rect fill="#111111" width="48" height="48"/>
|
||||||
<path fill="#ffffff" d="m21.225 29.383c-0.09826-0.28968-0.22135-0.65379-0.3125-1.1086-0.09115-0.45481-0.13672-0.90394-0.13672-1.3474 0-0.69359 0.15625-1.319 0.46875-1.8761 0.32552-0.56852 0.72266-1.0972 1.1914-1.5862 0.48177-0.48892 0.99609-0.96079 1.543-1.4156 0.5599-0.45481 1.0742-0.89826 1.543-1.3303 0.48177-0.43207 0.87891-0.88688 1.1914-1.3644 0.32552-0.47755 0.48828-1.1434 0.48828-1.7944 0-0.58594-0.11719-1.0937-0.35156-1.5234-0.23438-0.44271-0.5599-0.80729-0.97656-1.0937-0.40364-0.29948-0.8724-0.52083-1.4062-0.66406-0.52083-0.14323-1.0807-0.21484-1.6797-0.21484-1.9401 0-3.7484 0.9081-5.5469 2.5977-0.56753 0.53317-1.5583-3.5623 0-4.5117 2.113-1.2875 4.349-1.875 6.6406-1.875 1.0547 0 2.0508 0.13672 2.9883 0.41016 0.9375 0.27344 1.7578 0.67708 2.4609 1.2109 0.70312 0.53386 1.2565 1.1979 1.6602 1.9922s0.60547 1.7187 0.60547 2.7734c0 1.0026-0.16927 1.9936-0.50781 2.6758-0.33854 0.68222-0.76823 1.3133-1.2891 1.8932-0.50781 0.56852-1.0612 1.0745-1.6602 1.5179-0.58594 0.44344-1.1393 0.88688-1.6602 1.3303-0.50781 0.44344-0.93099 0.90394-1.2695 1.3815-0.33854 0.47755-0.50781 1.0063-0.50781 1.5862 0 0.48892 0.07162 0.93236 0.21484 1.3303 0.14323 0.39796 0.29303 0.73083 0.42969 1.0063 0.31581 0.63652-3.8048 0.93258-4.1211-2e-6zm2.2266 8.3427c-0.74219 0-1.3997-0.25391-1.9727-0.76172-0.54688-0.49479-0.82031-1.1068-0.82031-1.8359 0-0.74219 0.27344-1.3542 0.82031-1.8359 0.5599-0.52083 1.2174-0.78125 1.9727-0.78125 0.74219 0 1.3932 0.26042 1.9531 0.78125 0.54688 0.48177 0.82031 1.0938 0.82031 1.8359 0 0.72917-0.27344 1.3411-0.82031 1.8359-0.57292 0.50781-1.224 0.76172-1.9531 0.76172z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 190 B |
@@ -99,7 +99,8 @@ body.loading #requestOverlay {
|
|||||||
|
|
||||||
#requestList {
|
#requestList {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 2.2rem;
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,20 @@ body.loading #requestOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.request-item {
|
.request-item {
|
||||||
display: list-item;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-cover {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.15em;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-title {
|
.request-title {
|
||||||
|
|||||||
+2
-1
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="requestOverlay" aria-live="polite">
|
<div id="requestOverlay" aria-live="polite">
|
||||||
<div id="requestHeader">Song requests</div>
|
<div id="requestHeader">Song requests</div>
|
||||||
<ol id="requestList"></ol>
|
<ul id="requestList"></ul>
|
||||||
<div id="requestEmpty">No pending requests</div>
|
<div id="requestEmpty">No pending requests</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,6 +84,7 @@
|
|||||||
</select></label>
|
</select></label>
|
||||||
<label>Scale (%): <input id="scaleInput" type="number" min="10" max="1000" step="5"></label>
|
<label>Scale (%): <input id="scaleInput" type="number" min="10" max="1000" step="5"></label>
|
||||||
<label>Fade (ms): <input id="fadeInput" type="number" min="0" max="5000" step="10"></label>
|
<label>Fade (ms): <input id="fadeInput" type="number" min="0" max="5000" step="10"></label>
|
||||||
|
<label>Debug: use history for Song requests: <input id="debugUseHistoryForRequestsInput" type="checkbox"></label>
|
||||||
<label>Debug BSR ID: <span class="debugSongIdRow">
|
<label>Debug BSR ID: <span class="debugSongIdRow">
|
||||||
<span class="debugSongIdHint">e.g. <button type="button" id="debugSongIdExample" title="Fill with next example BSR id (cycles)">43239</button></span>
|
<span class="debugSongIdHint">e.g. <button type="button" id="debugSongIdExample" title="Fill with next example BSR id (cycles)">43239</button></span>
|
||||||
<input id="debugSongIdInput" class="debugSongIdInput" type="text" placeholder="e.g. 4f4e4 or 40-char hash" spellcheck="false" autocomplete="off">
|
<input id="debugSongIdInput" class="debugSongIdInput" type="text" placeholder="e.g. 4f4e4 or 40-char hash" spellcheck="false" autocomplete="off">
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ var OVERLAY_SETTINGS_INITIAL = {
|
|||||||
bottom: true,
|
bottom: true,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
fade: 300,
|
fade: 300,
|
||||||
debugSongId: ""
|
debugSongId: "",
|
||||||
|
debugUseHistoryForRequests: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// src/client/beatsaver.ts
|
// src/client/beatsaver.ts
|
||||||
@@ -54,6 +55,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 +290,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 +310,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 +334,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 +392,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 +411,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 +546,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 +578,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 +603,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 +618,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) {
|
||||||
@@ -601,8 +650,11 @@ async function refreshMapFriendScores() {
|
|||||||
window.onhashchange = () => {
|
window.onhashchange = () => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
void refreshConfiguredPlayerAvatar();
|
void refreshConfiguredPlayerAvatar();
|
||||||
|
void loadRequestQueue();
|
||||||
const debugEl = document.getElementById("debugSongIdInput");
|
const debugEl = document.getElementById("debugSongIdInput");
|
||||||
if (debugEl) debugEl.value = settings.debugSongId;
|
if (debugEl) debugEl.value = settings.debugSongId;
|
||||||
|
const debugHistoryEl = document.getElementById("debugUseHistoryForRequestsInput");
|
||||||
|
if (debugHistoryEl) debugHistoryEl.checked = settings.debugUseHistoryForRequests;
|
||||||
if (settings.debugSongId.trim()) void applyDebugSong();
|
if (settings.debugSongId.trim()) void applyDebugSong();
|
||||||
else {
|
else {
|
||||||
mapInfoRequestId += 1;
|
mapInfoRequestId += 1;
|
||||||
@@ -628,7 +680,7 @@ var debugBsrExampleIndex = 0;
|
|||||||
var requestListEl = must("requestList");
|
var requestListEl = must("requestList");
|
||||||
var requestOverlayEl = must("requestOverlay");
|
var requestOverlayEl = must("requestOverlay");
|
||||||
var requestEmptyEl = must("requestEmpty");
|
var requestEmptyEl = must("requestEmpty");
|
||||||
var requestTitleCache = /* @__PURE__ */ new Map();
|
var requestBeatSaverCache = /* @__PURE__ */ new Map();
|
||||||
var requestTitleMisses = /* @__PURE__ */ new Set();
|
var requestTitleMisses = /* @__PURE__ */ new Set();
|
||||||
function loadChatRequestJson() {
|
function loadChatRequestJson() {
|
||||||
const base = new URL("ChatRequest.json", location.href);
|
const base = new URL("ChatRequest.json", location.href);
|
||||||
@@ -648,10 +700,12 @@ function requesterLine(item) {
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return parts.length ? parts.join(" ") : item.rqn || "";
|
return parts.length ? parts.join(" ") : item.rqn || "";
|
||||||
}
|
}
|
||||||
async function enrichRequestTitle(key, titleEl) {
|
async function enrichRequestFromBeatSaver(key, titleEl, coverEl) {
|
||||||
if (requestTitleMisses.has(key)) return;
|
if (requestTitleMisses.has(key)) return;
|
||||||
if (requestTitleCache.has(key)) {
|
const cached = requestBeatSaverCache.get(key);
|
||||||
titleEl.textContent = requestTitleCache.get(key) ?? "";
|
if (cached) {
|
||||||
|
titleEl.textContent = cached.title;
|
||||||
|
coverEl.src = cached.coverUrl || "images/unknown.svg";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -661,12 +715,19 @@ async function enrichRequestTitle(key, titleEl) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = map.metadata?.songName ?? map.name;
|
const name = map.metadata?.songName ?? map.name;
|
||||||
if (name && typeof name === "string") {
|
const title2 = name && typeof name === "string" ? name : "";
|
||||||
requestTitleCache.set(key, name);
|
if (!title2) {
|
||||||
titleEl.textContent = name;
|
requestTitleMisses.add(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
requestTitleMisses.add(key);
|
const rawCover = map.versions?.[0]?.coverURL?.trim();
|
||||||
|
const coverUrl = rawCover && /^https?:\/\//i.test(rawCover) ? rawCover : "";
|
||||||
|
requestBeatSaverCache.set(key, {
|
||||||
|
title: title2,
|
||||||
|
coverUrl
|
||||||
|
});
|
||||||
|
titleEl.textContent = title2;
|
||||||
|
if (coverUrl) coverEl.src = coverUrl;
|
||||||
} catch {
|
} catch {
|
||||||
requestTitleMisses.add(key);
|
requestTitleMisses.add(key);
|
||||||
}
|
}
|
||||||
@@ -677,6 +738,12 @@ function renderRequestList(items) {
|
|||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "request-item";
|
li.className = "request-item";
|
||||||
|
const coverEl = document.createElement("img");
|
||||||
|
coverEl.className = "request-cover";
|
||||||
|
coverEl.src = "images/unknown.svg";
|
||||||
|
coverEl.alt = "";
|
||||||
|
coverEl.decoding = "async";
|
||||||
|
li.appendChild(coverEl);
|
||||||
const titleEl = document.createElement("span");
|
const titleEl = document.createElement("span");
|
||||||
titleEl.className = "request-title";
|
titleEl.className = "request-title";
|
||||||
titleEl.textContent = `!bsr ${item.key}`;
|
titleEl.textContent = `!bsr ${item.key}`;
|
||||||
@@ -689,7 +756,7 @@ function renderRequestList(items) {
|
|||||||
li.appendChild(meta);
|
li.appendChild(meta);
|
||||||
}
|
}
|
||||||
requestListEl.appendChild(li);
|
requestListEl.appendChild(li);
|
||||||
void enrichRequestTitle(item.key, titleEl);
|
void enrichRequestFromBeatSaver(item.key, titleEl, coverEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function loadRequestQueue() {
|
async function loadRequestQueue() {
|
||||||
@@ -697,7 +764,8 @@ async function loadRequestQueue() {
|
|||||||
const data = await loadChatRequestJson();
|
const data = await loadChatRequestJson();
|
||||||
requestEmptyEl.textContent = "No pending requests";
|
requestEmptyEl.textContent = "No pending requests";
|
||||||
requestOverlayEl.classList.remove("request-load-failed");
|
requestOverlayEl.classList.remove("request-load-failed");
|
||||||
const items = (data.queue ?? []).slice(0, MAX_REQUESTS);
|
const source = settings.debugUseHistoryForRequests ? data.history ?? [] : data.queue ?? [];
|
||||||
|
const items = source.slice(0, MAX_REQUESTS);
|
||||||
renderRequestList(items);
|
renderRequestList(items);
|
||||||
} catch {
|
} catch {
|
||||||
requestEmptyEl.textContent = "Request queue unavailable";
|
requestEmptyEl.textContent = "Request queue unavailable";
|
||||||
@@ -729,7 +797,8 @@ async function bootstrap() {
|
|||||||
"time",
|
"time",
|
||||||
"score",
|
"score",
|
||||||
"friends",
|
"friends",
|
||||||
"bsr"
|
"bsr",
|
||||||
|
"debugUseHistoryForRequests"
|
||||||
]) {
|
]) {
|
||||||
const input = must(`${key}Input`);
|
const input = must(`${key}Input`);
|
||||||
input.checked = settings[key];
|
input.checked = settings[key];
|
||||||
@@ -737,6 +806,7 @@ async function bootstrap() {
|
|||||||
settings[key] = input.checked;
|
settings[key] = input.checked;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
if (key === "friends") void refreshMapFriendScores();
|
if (key === "friends") void refreshMapFriendScores();
|
||||||
|
if (key === "debugUseHistoryForRequests") void loadRequestQueue();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const friendModeInput = must("friendModeInput");
|
const friendModeInput = must("friendModeInput");
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"queue": [],
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"key": "4cc2b",
|
||||||
|
"rqt": 1775779098,
|
||||||
|
"rqn": "timmyboi101",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "41d0a",
|
||||||
|
"rqt": 1775088648,
|
||||||
|
"rqn": "raverbeandk",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4eabc",
|
||||||
|
"rqt": 1773187529,
|
||||||
|
"rqn": "whizlol_",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4ea5c",
|
||||||
|
"rqt": 1771109245,
|
||||||
|
"rqn": "kacy121",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4c351",
|
||||||
|
"rqt": 1772402287,
|
||||||
|
"rqn": "blasted246",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3ecc7",
|
||||||
|
"rqt": 1772402477,
|
||||||
|
"rqn": "danielduel",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4f0fd",
|
||||||
|
"rqt": 1772402471,
|
||||||
|
"rqn": "kitties",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4a978",
|
||||||
|
"rqt": 1772402365,
|
||||||
|
"rqn": "blasted246",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "49be2",
|
||||||
|
"rqt": 1772401977,
|
||||||
|
"rqn": "danielduel",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4dc91",
|
||||||
|
"rqt": 1772401902,
|
||||||
|
"rqn": "simpliftr",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4e932",
|
||||||
|
"rqt": 1771109684,
|
||||||
|
"rqn": "kacy121",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4edb9",
|
||||||
|
"rqt": 1772401613,
|
||||||
|
"rqn": "blasted246",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "42b69",
|
||||||
|
"rqt": 1772401712,
|
||||||
|
"rqn": "danielduel",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3a007",
|
||||||
|
"rqt": 1772140150,
|
||||||
|
"rqn": "morlis1002",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4dd71",
|
||||||
|
"rqt": 1771109448,
|
||||||
|
"rqn": "softmonkeh",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4ea0b",
|
||||||
|
"rqt": 1771109231,
|
||||||
|
"rqn": "kacy121",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "722f",
|
||||||
|
"rqt": 1771108991,
|
||||||
|
"rqn": "kacy121",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4ea9e",
|
||||||
|
"rqt": 1771108667,
|
||||||
|
"rqn": "kacy121",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "219cc",
|
||||||
|
"rqt": 1770861875,
|
||||||
|
"rqn": "rosa_360",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ae0e",
|
||||||
|
"rqt": 1770686841,
|
||||||
|
"rqn": "unigamerplays",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3bbb0",
|
||||||
|
"rqt": 1770685631,
|
||||||
|
"rqn": "unigamerplays",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4d158",
|
||||||
|
"rqt": 1770686339,
|
||||||
|
"rqn": "unigamerplays",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4ddac",
|
||||||
|
"rqt": 1770685592,
|
||||||
|
"rqn": "unigamerplays",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "e298",
|
||||||
|
"rqt": 1757616827,
|
||||||
|
"rqn": "$MenuMusic",
|
||||||
|
"npr": "🎵",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3ccc5",
|
||||||
|
"rqt": 1757215085,
|
||||||
|
"rqn": "666isbetter",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3df54",
|
||||||
|
"rqt": 1757213869,
|
||||||
|
"rqn": "mirageplayzzz",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3769c",
|
||||||
|
"rqt": 1757212949,
|
||||||
|
"rqn": "mirageplayzzz",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "345d9",
|
||||||
|
"rqt": 1757212854,
|
||||||
|
"rqn": "666isbetter",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4347c",
|
||||||
|
"rqt": 1757212771,
|
||||||
|
"rqn": "mirageplayzzz",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "1f3bb",
|
||||||
|
"rqt": 1757212225,
|
||||||
|
"rqn": "666isbetter",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "26d75",
|
||||||
|
"rqt": 1757211488,
|
||||||
|
"rqn": "mirageplayzzz",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "44330",
|
||||||
|
"rqt": 1757211481,
|
||||||
|
"rqn": "nowahou",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "312c6",
|
||||||
|
"rqt": 1757211158,
|
||||||
|
"rqn": "666isbetter",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2c2c6",
|
||||||
|
"rqt": 1757210672,
|
||||||
|
"rqn": "666isbetter",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2e8a",
|
||||||
|
"rqt": 1757209938,
|
||||||
|
"rqn": "666isbetter",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4a345",
|
||||||
|
"rqt": 1757210190,
|
||||||
|
"rqn": "abe_vs_theworld",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4a329",
|
||||||
|
"rqt": 1757209712,
|
||||||
|
"rqn": "mrstacker27",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "4a00b",
|
||||||
|
"rqt": 1756592415,
|
||||||
|
"rqn": "mrstacker27",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "46a3e",
|
||||||
|
"rqt": 1748810448,
|
||||||
|
"rqn": "sabersammy0",
|
||||||
|
"npr": "",
|
||||||
|
"msg": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"allowlist": [],
|
||||||
|
"blocklist": [],
|
||||||
|
"bannedusers": [],
|
||||||
|
"bannedmappers": [],
|
||||||
|
"remaps": []
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
+71
-13
@@ -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 map’s 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) {
|
||||||
@@ -493,8 +534,11 @@ async function refreshMapFriendScores() {
|
|||||||
window.onhashchange = () => {
|
window.onhashchange = () => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
void refreshConfiguredPlayerAvatar();
|
void refreshConfiguredPlayerAvatar();
|
||||||
|
void loadRequestQueue();
|
||||||
const debugEl = document.getElementById("debugSongIdInput") as HTMLInputElement | null;
|
const debugEl = document.getElementById("debugSongIdInput") as HTMLInputElement | null;
|
||||||
if (debugEl) debugEl.value = settings.debugSongId;
|
if (debugEl) debugEl.value = settings.debugSongId;
|
||||||
|
const debugHistoryEl = document.getElementById("debugUseHistoryForRequestsInput") as HTMLInputElement | null;
|
||||||
|
if (debugHistoryEl) debugHistoryEl.checked = settings.debugUseHistoryForRequests;
|
||||||
if (settings.debugSongId.trim()) void applyDebugSong();
|
if (settings.debugSongId.trim()) void applyDebugSong();
|
||||||
else {
|
else {
|
||||||
mapInfoRequestId += 1;
|
mapInfoRequestId += 1;
|
||||||
@@ -523,10 +567,10 @@ const DEBUG_BSR_EXAMPLE_IDS = [
|
|||||||
] as const;
|
] as const;
|
||||||
let debugBsrExampleIndex = 0;
|
let debugBsrExampleIndex = 0;
|
||||||
|
|
||||||
const requestListEl = must<HTMLOListElement>("requestList");
|
const requestListEl = must<HTMLUListElement>("requestList");
|
||||||
const requestOverlayEl = must<HTMLElement>("requestOverlay");
|
const requestOverlayEl = must<HTMLElement>("requestOverlay");
|
||||||
const requestEmptyEl = must<HTMLElement>("requestEmpty");
|
const requestEmptyEl = must<HTMLElement>("requestEmpty");
|
||||||
const requestTitleCache = new Map<string, string>();
|
const requestBeatSaverCache = new Map<string, { title: string; coverUrl: string }>();
|
||||||
const requestTitleMisses = new Set<string>();
|
const requestTitleMisses = new Set<string>();
|
||||||
|
|
||||||
function loadChatRequestJson() {
|
function loadChatRequestJson() {
|
||||||
@@ -544,10 +588,12 @@ function requesterLine(item: ChatRequestEntry) {
|
|||||||
return parts.length ? parts.join(" ") : item.rqn || "";
|
return parts.length ? parts.join(" ") : item.rqn || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enrichRequestTitle(key: string, titleEl: HTMLElement) {
|
async function enrichRequestFromBeatSaver(key: string, titleEl: HTMLElement, coverEl: HTMLImageElement) {
|
||||||
if (requestTitleMisses.has(key)) return;
|
if (requestTitleMisses.has(key)) return;
|
||||||
if (requestTitleCache.has(key)) {
|
const cached = requestBeatSaverCache.get(key);
|
||||||
titleEl.textContent = requestTitleCache.get(key) ?? "";
|
if (cached) {
|
||||||
|
titleEl.textContent = cached.title;
|
||||||
|
coverEl.src = cached.coverUrl || "images/unknown.svg";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -557,12 +603,16 @@ async function enrichRequestTitle(key: string, titleEl: HTMLElement) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = map.metadata?.songName ?? map.name;
|
const name = map.metadata?.songName ?? map.name;
|
||||||
if (name && typeof name === "string") {
|
const title = name && typeof name === "string" ? name : "";
|
||||||
requestTitleCache.set(key, name);
|
if (!title) {
|
||||||
titleEl.textContent = name;
|
requestTitleMisses.add(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
requestTitleMisses.add(key);
|
const rawCover = map.versions?.[0]?.coverURL?.trim();
|
||||||
|
const coverUrl = rawCover && /^https?:\/\//i.test(rawCover) ? rawCover : "";
|
||||||
|
requestBeatSaverCache.set(key, { title, coverUrl });
|
||||||
|
titleEl.textContent = title;
|
||||||
|
if (coverUrl) coverEl.src = coverUrl;
|
||||||
} catch {
|
} catch {
|
||||||
requestTitleMisses.add(key);
|
requestTitleMisses.add(key);
|
||||||
}
|
}
|
||||||
@@ -574,6 +624,12 @@ function renderRequestList(items: ChatRequestEntry[]) {
|
|||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "request-item";
|
li.className = "request-item";
|
||||||
|
const coverEl = document.createElement("img");
|
||||||
|
coverEl.className = "request-cover";
|
||||||
|
coverEl.src = "images/unknown.svg";
|
||||||
|
coverEl.alt = "";
|
||||||
|
coverEl.decoding = "async";
|
||||||
|
li.appendChild(coverEl);
|
||||||
const titleEl = document.createElement("span");
|
const titleEl = document.createElement("span");
|
||||||
titleEl.className = "request-title";
|
titleEl.className = "request-title";
|
||||||
titleEl.textContent = `!bsr ${item.key}`;
|
titleEl.textContent = `!bsr ${item.key}`;
|
||||||
@@ -586,7 +642,7 @@ function renderRequestList(items: ChatRequestEntry[]) {
|
|||||||
li.appendChild(meta);
|
li.appendChild(meta);
|
||||||
}
|
}
|
||||||
requestListEl.appendChild(li);
|
requestListEl.appendChild(li);
|
||||||
void enrichRequestTitle(item.key, titleEl);
|
void enrichRequestFromBeatSaver(item.key, titleEl, coverEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,7 +651,8 @@ async function loadRequestQueue() {
|
|||||||
const data = await loadChatRequestJson();
|
const data = await loadChatRequestJson();
|
||||||
requestEmptyEl.textContent = "No pending requests";
|
requestEmptyEl.textContent = "No pending requests";
|
||||||
requestOverlayEl.classList.remove("request-load-failed");
|
requestOverlayEl.classList.remove("request-load-failed");
|
||||||
const items = (data.queue ?? []).slice(0, MAX_REQUESTS);
|
const source = settings.debugUseHistoryForRequests ? (data.history ?? []) : (data.queue ?? []);
|
||||||
|
const items = source.slice(0, MAX_REQUESTS);
|
||||||
renderRequestList(items);
|
renderRequestList(items);
|
||||||
} catch {
|
} catch {
|
||||||
requestEmptyEl.textContent = "Request queue unavailable";
|
requestEmptyEl.textContent = "Request queue unavailable";
|
||||||
@@ -624,13 +681,14 @@ async function bootstrap() {
|
|||||||
|
|
||||||
// Settings UI
|
// Settings UI
|
||||||
|
|
||||||
for (const key of ["cover", "mapInfo", "time", "score", "friends", "bsr"] as const) {
|
for (const key of ["cover", "mapInfo", "time", "score", "friends", "bsr", "debugUseHistoryForRequests"] as const) {
|
||||||
const input = must<HTMLInputElement>(`${key}Input`);
|
const input = must<HTMLInputElement>(`${key}Input`);
|
||||||
input.checked = settings[key];
|
input.checked = settings[key];
|
||||||
input.oninput = () => {
|
input.oninput = () => {
|
||||||
settings[key] = input.checked;
|
settings[key] = input.checked;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
if (key === "friends") void refreshMapFriendScores();
|
if (key === "friends") void refreshMapFriendScores();
|
||||||
|
if (key === "debugUseHistoryForRequests") void loadRequestQueue();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export interface OverlaySettings {
|
|||||||
fade: number;
|
fade: number;
|
||||||
/** Frontend-only: BeatSaver map key or 40-char hash; when set, map UI + BeatLeader use this instead of BS+ WebSocket map info. */
|
/** Frontend-only: BeatSaver map key or 40-char hash; when set, map UI + BeatLeader use this instead of BS+ WebSocket map info. */
|
||||||
debugSongId: string;
|
debugSongId: string;
|
||||||
|
/** When true, show `history` from ChatRequest/Database JSON as the request list instead of `queue`. */
|
||||||
|
debugUseHistoryForRequests: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OVERLAY_SETTINGS_INITIAL: Readonly<OverlaySettings> = {
|
export const OVERLAY_SETTINGS_INITIAL: Readonly<OverlaySettings> = {
|
||||||
@@ -34,6 +36,7 @@ export const OVERLAY_SETTINGS_INITIAL: Readonly<OverlaySettings> = {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
fade: 300,
|
fade: 300,
|
||||||
debugSongId: "",
|
debugSongId: "",
|
||||||
|
debugUseHistoryForRequests: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface HandshakeEvent {
|
export interface HandshakeEvent {
|
||||||
@@ -117,6 +120,7 @@ export interface ChatRequestEntry {
|
|||||||
|
|
||||||
export interface ChatRequestPayload {
|
export interface ChatRequestPayload {
|
||||||
queue: ChatRequestEntry[];
|
queue: ChatRequestEntry[];
|
||||||
|
history?: ChatRequestEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BeatLeaderDifficulty {
|
export interface BeatLeaderDifficulty {
|
||||||
|
|||||||
Reference in New Issue
Block a user