888 lines
30 KiB
JavaScript

// src/client/types.ts
var OVERLAY_SETTINGS_INITIAL = {
cover: true,
mapInfo: true,
time: true,
score: true,
friends: true,
friendMode: "mutual",
bsr: false,
beatLeaderId: "",
right: false,
bottom: true,
scale: 1,
fade: 300,
debugSongId: "",
debugUseHistoryForRequests: false
};
// src/client/beatsaver.ts
var BASE_URL = "https://api.beatsaver.com";
var USE_RUNTIME_PROXY = typeof document !== "undefined";
function beatsaverUrl(path) {
if (USE_RUNTIME_PROXY) {
return `/api/beatsaver?path=${encodeURIComponent(path)}`;
}
return `${BASE_URL}${path}`;
}
async function fetchBeatSaverMeta(hash) {
try {
const response = await fetch(beatsaverUrl(`/maps/hash/${encodeURIComponent(hash)}`));
if (!response.ok) return null;
return await response.json();
} catch {
return null;
}
}
async function fetchBeatSaverMapById(mapId) {
try {
const response = await fetch(beatsaverUrl(`/maps/id/${encodeURIComponent(mapId)}`));
if (!response.ok) return null;
return await response.json();
} catch {
return null;
}
}
// src/client/beatleader.ts
var BASE_URL2 = "https://api.beatleader.com";
var PAGE_SIZE = 100;
var MAX_LEADERBOARD_SCORE_PAGES = 2e3;
var USE_RUNTIME_PROXY2 = typeof document !== "undefined";
function beatleaderUrl(path) {
if (USE_RUNTIME_PROXY2) {
return `/api/beatleader?path=${encodeURIComponent(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) {
const path = `/leaderboards/hash/${encodeURIComponent(hash)}`;
try {
const res = await fetch(beatleaderUrl(path));
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data : Array.isArray(data.leaderboards) ? data.leaderboards : [];
} catch {
return [];
}
}
async function fetchBeatLeaderPlayer(playerId) {
const path = `/player/${encodeURIComponent(playerId)}`;
try {
const res = await fetch(beatleaderUrl(path));
if (!res.ok) return null;
const data = await res.json();
const id = data.id == null ? playerId : String(data.id);
const avatar = typeof data.avatar === "string" ? data.avatar.trim() || null : null;
return {
id,
avatar
};
} catch {
return null;
}
}
async function resolveBeatLeaderPlayerId(playerId) {
const p = await fetchBeatLeaderPlayer(playerId);
return p?.id ?? playerId;
}
async function fetchLeaderboardScoresById(leaderboardId, maxPages = MAX_LEADERBOARD_SCORE_PAGES) {
const scores = [];
const pageSize = PAGE_SIZE;
let page = 1;
for (; ; ) {
if (page > maxPages) break;
const qs = new URLSearchParams({
leaderboardContext: "general",
page: String(page),
sortBy: "rank",
order: "desc",
count: String(pageSize)
});
const path = `/leaderboard/${encodeURIComponent(leaderboardId)}?${qs}`;
const url = beatleaderUrl(path);
let res;
try {
res = await fetch(url);
} catch {
break;
}
if (!res.ok) break;
const payload = await res.json();
const batch = Array.isArray(payload.scores) ? payload.scores : [];
if (batch.length === 0) break;
scores.push(...batch);
if (batch.length < pageSize) break;
page += 1;
}
return scores;
}
async function fetchAllMapScoresByHash(hash, leaderboards, maxPagesPerLeaderboard = MAX_LEADERBOARD_SCORE_PAGES) {
const requests = leaderboards.map((lb) => {
const leaderboardId = lb.id == null ? null : String(lb.id);
if (!leaderboardId) return Promise.resolve([]);
return fetchLeaderboardScoresById(leaderboardId, maxPagesPerLeaderboard);
});
const batches = await Promise.all(requests);
return batches.flat();
}
async function fetchFollowersPage(playerId, type2, page, count) {
const qs = new URLSearchParams({
type: type2,
page: String(page),
count: String(count)
});
const path = `/player/${encodeURIComponent(playerId)}/followers?${qs}`;
const url = beatleaderUrl(path);
try {
const response = await fetch(url);
if (!response.ok) return [];
const data = await response.json();
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
async function fetchAllFollowers(playerId, type2, maxPages = 100) {
const all = [];
for (let page = 1; page <= maxPages; page += 1) {
const batch = await fetchFollowersPage(playerId, type2, page, PAGE_SIZE);
if (batch.length === 0) break;
all.push(...batch);
if (batch.length < PAGE_SIZE) break;
}
return all;
}
function normalizeFollowerEntry(entry) {
return {
...entry,
id: String(entry.id)
};
}
async function fetchFriends(playerId, mode, maxPages = 100) {
const canonicalPlayerId = await resolveBeatLeaderPlayerId(playerId);
const [following, followers] = await Promise.all([
fetchAllFollowers(canonicalPlayerId, "Following", maxPages),
fetchAllFollowers(canonicalPlayerId, "Followers", maxPages)
]);
const followingIds = new Set(following.map((entry) => String(entry.id)));
if (mode === "following") {
return following.map((entry) => normalizeFollowerEntry(entry));
}
if (mode === "followers") {
return followers.map((entry) => normalizeFollowerEntry(entry));
}
return followers.filter((entry) => followingIds.has(String(entry.id))).map((entry) => normalizeFollowerEntry(entry));
}
function normalizeAccuracy(value) {
if (typeof value !== "number" || !Number.isFinite(value)) return null;
return value <= 1 ? value * 100 : value;
}
// src/client/overlay-config.ts
function mergeOverlayConfigResponse(target, body) {
const d = body.defaults;
if (!d) return;
const out = target;
for (const key of Object.keys(d)) {
const v = d[key];
if (v !== void 0) out[key] = v;
}
}
// src/client/index.ts
function must(id) {
const element = document.getElementById(id);
if (!element) throw new Error(`Missing element: ${id}`);
return element;
}
function parseJson(raw) {
return JSON.parse(raw);
}
var settings = structuredClone(OVERLAY_SETTINGS_INITIAL);
var defaults = structuredClone(OVERLAY_SETTINGS_INITIAL);
var style = document.createElement("style");
function loadSettings() {
const params = new URLSearchParams(location.hash.slice(1));
let css = "";
for (const [key, def] of Object.entries(defaults)) {
const value = parseJson(params.get(key) || "null") ?? def;
settings[key] = value;
if (typeof def === "boolean") document.body.classList.toggle(key, Boolean(value));
else css += `--${key}: ${value}; `;
}
style.textContent = `:root { ${css}}`;
}
function saveSettings() {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(settings)) {
if (value !== defaults[key]) params.set(key, JSON.stringify(value));
}
location.replace(`#${params.toString()}`);
}
var beatSaberPlus = {
// https://github.com/hardcpp/BeatSaberPlus/wiki/%5BEN%5D-Song-Overlay
url: "ws://localhost:2947/socket",
onMessage: (e) => {
const data = parseJson(e.data);
switch (data._type) {
case "event":
switch (data._event) {
case "gameState":
document.body.dataset.gameState = data.gameStateChanged;
break;
case "mapInfo":
void updateMapInfo(data.mapInfoChanged);
break;
case "pause":
updateTime(data.pauseTime, true);
break;
case "resume":
updateTime(data.resumeTime, false);
break;
case "score":
updateScore(data.scoreEvent);
break;
}
break;
case "handshake":
currentPlayerPlatformId = data.playerPlatformId || "";
void refreshConfiguredPlayerAvatar();
void refreshMapFriendScores();
break;
default:
break;
}
}
};
var provider = beatSaberPlus;
var retryMs = 1e4;
var currentPlayerPlatformId = "";
function getEffectivePlayerId() {
const configured = settings.beatLeaderId.trim();
const raw = configured || currentPlayerPlatformId;
if (!raw) return "";
const steamIdCandidate = raw.match(/\d{17,20}/)?.[0];
if (steamIdCandidate) return steamIdCandidate;
if (/^\d+$/.test(raw)) return raw;
if (configured) return raw;
return "";
}
var currentMapHash = "";
var friendsRelationCacheKey = "";
var friendsRelationCache = null;
var friendScoreRequestId = 0;
var mapInfoRequestId = 0;
var rawLevelHash = "";
var currentPlayCharacteristic = "";
var currentPlayDifficulty = "";
function resolvedHashFromBeatSaverMap(map, fallback) {
const v = map.versions?.[0]?.hash;
if (typeof v === "string" && v.length > 0) return v.toLowerCase().trim();
return fallback;
}
function looksLikeBeatSaverHash(s) {
return /^[0-9a-f]{40}$/i.test(s.trim());
}
async function fetchBeatSaverMapForDebug(id) {
const t = id.trim();
if (!t) return null;
if (looksLikeBeatSaverHash(t)) return fetchBeatSaverMeta(t.toLowerCase());
return fetchBeatSaverMapById(t);
}
async function applyDebugSong() {
const raw = settings.debugSongId.trim();
if (!raw) return;
const reqId = ++mapInfoRequestId;
beginFriendScoresForNewMapContext();
document.body.classList.add("loading");
try {
const map = await fetchBeatSaverMapForDebug(raw);
if (reqId !== mapInfoRequestId) return;
if (!map?.id) {
rawLevelHash = "";
currentMapHash = "";
cover.src = "images/unknown.svg";
title.textContent = "BeatSaver not found";
subTitle.textContent = raw;
artist.textContent = "";
mapper.textContent = "";
difficulty.textContent = "";
characteristic.src = "images/characteristic/Standard.svg";
difficultyLabel.textContent = "";
type.textContent = "";
bsrKey.textContent = "";
return;
}
const fallbackHash = looksLikeBeatSaverHash(raw) ? raw.toLowerCase().trim() : "";
const resolved = resolvedHashFromBeatSaverMap(map, fallbackHash);
rawLevelHash = resolved || fallbackHash;
currentMapHash = resolved || fallbackHash;
currentPlayCharacteristic = "Standard";
currentPlayDifficulty = "ExpertPlus";
const v0 = map.versions?.[0];
const coverUrl = v0?.coverURL?.trim();
cover.src = coverUrl || "images/unknown.svg";
title.textContent = map.metadata?.songName ?? map.name ?? "";
subTitle.textContent = map.metadata?.songSubName ?? "";
artist.textContent = map.metadata?.songAuthorName ?? "";
mapper.textContent = map.metadata?.levelAuthorName ?? "";
const firstDiff = v0?.diffs?.[0];
difficulty.textContent = firstDiff?.difficulty?.replace("Plus", " +") ?? "\u2014";
characteristic.src = firstDiff ? `images/characteristic/${firstDiff.characteristic}.svg` : "images/characteristic/Standard.svg";
difficultyLabel.textContent = firstDiff?.label ?? "";
type.textContent = "Custom";
bsrKey.textContent = map.id;
timeMultiplier = 1;
duration = 180;
if (reqId === mapInfoRequestId) {
setTime(0);
}
} catch {
if (reqId !== mapInfoRequestId) return;
rawLevelHash = "";
currentMapHash = "";
cover.src = "images/unknown.svg";
title.textContent = "BeatSaver request failed";
subTitle.textContent = raw;
} finally {
if (reqId === mapInfoRequestId) document.body.classList.remove("loading");
if (reqId === mapInfoRequestId) void refreshMapFriendScores();
}
}
function connect() {
const ws = new WebSocket(provider.url);
ws.onmessage = provider.onMessage;
ws.onclose = onClose;
}
function onClose(_e) {
setTimeout(connect, retryMs);
}
connect();
var cover = must("coverImg");
var title = must("title");
var subTitle = must("subTitle");
var artist = must("artist");
var mapper = must("mapper");
var difficulty = must("difficulty");
var characteristic = must("characteristicImg");
var difficultyLabel = must("difficultyLabel");
var type = must("type");
var bsrKey = must("bsrKey");
var timeMultiplier = 1;
var duration = 0;
async function updateMapInfo(data) {
if (settings.debugSongId.trim()) {
void applyDebugSong();
return;
}
currentPlayCharacteristic = data.characteristic;
currentPlayDifficulty = data.difficulty;
const reqId = ++mapInfoRequestId;
const custom = data.level_id.startsWith("custom_level_");
const wip = custom && data.level_id.endsWith("WIP");
rawLevelHash = custom ? data.level_id.substring(13, 53).toLowerCase() : "";
currentMapHash = rawLevelHash;
cover.src = data.coverRaw ? `data:image/jpeg;base64,${data.coverRaw}` : "images/unknown.svg";
title.textContent = data.name || "";
subTitle.textContent = data.sub_name || "";
artist.textContent = data.artist || "";
mapper.textContent = data.mapper || "";
difficulty.textContent = data.difficulty?.replace("Plus", " +") || "";
characteristic.src = `images/characteristic/${data.characteristic}.svg`;
difficultyLabel.textContent = "";
type.textContent = !custom ? "OST" : wip ? "WIP" : "";
bsrKey.textContent = custom && !wip ? "\u2026" : custom ? rawLevelHash || "???" : "???";
timeMultiplier = data.timeMultiplier || 1;
duration = data.duration / 1e3;
beginFriendScoresForNewMapContext();
if (custom && !wip) {
document.body.classList.add("loading");
try {
const map = await fetchBeatSaverMeta(rawLevelHash);
if (reqId !== mapInfoRequestId) return;
if (!map?.id) {
currentMapHash = rawLevelHash;
} else {
const resolved = resolvedHashFromBeatSaverMap(map, rawLevelHash);
currentMapHash = resolved;
bsrKey.textContent = map.id;
mapper.textContent = map.metadata?.levelAuthorName || "";
const diff = map.versions?.[0]?.diffs?.find((d) => d.characteristic === data.characteristic && d.difficulty === data.difficulty);
if (diff?.label) difficultyLabel.textContent = diff.label;
}
} catch {
if (reqId !== mapInfoRequestId) return;
currentMapHash = rawLevelHash;
} finally {
document.body.classList.remove("loading");
if (reqId === mapInfoRequestId) void refreshMapFriendScores();
}
} else {
if (custom && wip) {
bsrKey.textContent = rawLevelHash || "???";
} else {
bsrKey.textContent = "???";
}
difficultyLabel.textContent = "";
void refreshMapFriendScores();
}
}
var timeText = must("timeText");
var timeBar = must("timeBar");
var intervalMs = 500;
var intervalId = 0;
var currentTime = 0;
function updateTime(time, paused) {
if (!settings.time) return;
setTime(time);
clearInterval(intervalId);
if (paused) return;
intervalId = window.setInterval(() => setTime(currentTime + intervalMs * timeMultiplier / 1e3), intervalMs);
}
function setTime(time) {
currentTime = time;
timeText.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
timeBar.style.width = `${currentTime / (duration || Infinity) * 100}%`;
}
function formatTime(t) {
t = Math.floor(t);
const minutes = Math.floor(t / 60);
const seconds = t - minutes * 60;
return `${minutes}:${String(seconds).padStart(2, "0")}`;
}
var accuracy = must("accuracy");
var mistakes = must("mistakes");
var friendScoresPanel = must("friendScores");
var friendScoresList = must("friendScoresList");
var friendScoresEmpty = must("friendScoresEmpty");
var friendScoresHeaderText = must("friendScoresHeaderText");
var friendScoresPlayerAvatar = must("friendScoresPlayerAvatar");
var friendScoresHeaderImg = must("friendScoresHeaderImg");
var cachedConfiguredPlayerAvatarKey = "";
var cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
async function refreshConfiguredPlayerAvatar() {
const key = `${settings.beatLeaderId.trim()}|${currentPlayerPlatformId}`;
const pid = getEffectivePlayerId();
if (!pid) {
cachedConfiguredPlayerAvatarKey = "";
cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
return;
}
if (key === cachedConfiguredPlayerAvatarKey) {
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
return;
}
const profile = await fetchBeatLeaderPlayer(pid);
const keyAfter = `${settings.beatLeaderId.trim()}|${currentPlayerPlatformId}`;
if (keyAfter !== key) return;
cachedConfiguredPlayerAvatarKey = key;
cachedConfiguredPlayerAvatarSrc = profile?.avatar?.trim() || "images/unknown.svg";
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
}
function updateScore(score) {
if (!settings.score) return;
accuracy.textContent = (score.accuracy * 100).toFixed(1);
mistakes.textContent = score.missCount ? String(score.missCount) : "";
accuracy.classList.toggle("failed", score.currentHealth === 0);
}
function avatarFromScore(score) {
if (typeof score.player === "object" && score.player?.avatar) {
return score.player.avatar;
}
const url = score.playerAvatar?.trim();
return url || null;
}
function clearFriendScores(message) {
friendScoresList.replaceChildren();
friendScoresEmpty.textContent = message;
friendScoresHeaderText.textContent = "frenz?";
friendScoresHeaderImg.src = "assets/notlikesteve.webp";
friendScoresPanel.classList.remove("has-items", "is-loading");
}
function renderFriendScores(items) {
friendScoresList.replaceChildren();
friendScoresPanel.classList.toggle("has-items", items.length > 0);
friendScoresPanel.classList.remove("is-loading");
friendScoresEmpty.textContent = items.length ? "" : "No friend scores on this map";
friendScoresHeaderText.textContent = items.length ? "frenz!" : "frenz?";
friendScoresHeaderImg.src = items.length ? "assets/peepohigh.webp" : "assets/notlikesteve.webp";
for (const item of items) {
const li = document.createElement("li");
li.className = "friend-score-item";
const avatar = document.createElement("img");
avatar.className = "friend-avatar";
avatar.alt = "";
avatar.decoding = "async";
avatar.loading = "lazy";
avatar.src = item.avatar?.trim() || "images/unknown.svg";
const name = document.createElement("span");
name.className = "friend-name";
name.textContent = item.name;
const acc = document.createElement("span");
acc.className = "friend-acc";
acc.textContent = `${item.acc.toFixed(2)}%`;
li.append(acc, avatar, name);
friendScoresList.appendChild(li);
}
}
function friendsRelationListKey(playerId) {
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() {
const hash = currentMapHash;
if (!settings.friends) {
clearFriendScores("Disabled in settings");
return;
}
if (!hash) {
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...";
const requestId = ++friendScoreRequestId;
try {
const relKey = friendsRelationListKey(playerId);
const friendsPromise = (async () => {
if (friendsRelationCache !== null && relKey === friendsRelationCacheKey) {
return friendsRelationCache;
}
const fetched = await fetchFriends(playerId, settings.friendMode);
friendsRelationCacheKey = relKey;
friendsRelationCache = fetched;
return fetched;
})();
const [leaderboards, friends] = await Promise.all([
fetchBLLeaderboardsByHash(hash),
friendsPromise
]);
if (requestId !== friendScoreRequestId) return;
if (leaderboards.length === 0) {
clearFriendScores("No BeatLeader leaderboards found");
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 mutualFriendIds = new Set(friends.map((f) => f.id));
if (mutualFriendIds.size === 0) {
const relationLabel = settings.friendMode === "following" ? "No followed BeatLeader players" : settings.friendMode === "followers" ? "No BeatLeader followers" : "No mutual BeatLeader followers";
clearFriendScores(relationLabel);
return;
}
const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
if (requestId !== friendScoreRequestId) return;
const bestByPlayer = /* @__PURE__ */ new Map();
for (const score of scores) {
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
const playerKey = scorePlayerId == null ? "" : String(scorePlayerId);
if (!playerKey || !mutualFriendIds.has(playerKey)) continue;
const acc = normalizeAccuracy(score.accuracy ?? score.acc);
if (acc === null) continue;
const existing = bestByPlayer.get(playerKey);
if (!existing || acc > existing.acc) {
const friendMeta = friendById.get(playerKey);
const playerName = score.playerName || (typeof score.player === "object" ? score.player?.name : typeof score.player === "string" ? score.player : null);
const fromScore = avatarFromScore(score);
const fromFriend = friendMeta?.avatar?.trim() || null;
bestByPlayer.set(playerKey, {
name: playerName || friendMeta?.name || playerKey,
acc,
avatar: fromScore ?? fromFriend
});
}
}
const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc);
renderFriendScores(sorted);
} catch {
if (requestId !== friendScoreRequestId) return;
clearFriendScores("Failed loading BeatLeader scores");
}
}
window.onhashchange = () => {
loadSettings();
void refreshConfiguredPlayerAvatar();
void loadRequestQueue();
const debugEl = document.getElementById("debugSongIdInput");
if (debugEl) debugEl.value = settings.debugSongId;
const debugHistoryEl = document.getElementById("debugUseHistoryForRequestsInput");
if (debugHistoryEl) debugHistoryEl.checked = settings.debugUseHistoryForRequests;
if (settings.debugSongId.trim()) void applyDebugSong();
else {
mapInfoRequestId += 1;
currentMapHash = "";
rawLevelHash = "";
void refreshMapFriendScores();
}
};
var MAX_REQUESTS = 10;
var REQUEST_POLL_MS = 5e3;
var DEBUG_BSR_EXAMPLE_IDS = [
"43239",
"4b55e",
"49201",
"35a5e",
"2c25a",
"3864b",
"2d205",
"41d08",
"e298"
];
var debugBsrExampleIndex = 0;
var requestListEl = must("requestList");
var requestOverlayEl = must("requestOverlay");
var requestEmptyEl = must("requestEmpty");
var requestBeatSaverCache = /* @__PURE__ */ new Map();
var requestTitleMisses = /* @__PURE__ */ new Set();
function loadChatRequestJson() {
const base = new URL("ChatRequest.json", location.href);
const busted = new URL(base.href);
busted.searchParams.set("t", String(Date.now()));
return fetch(busted.href, {
cache: "no-store"
}).then((res) => {
if (!res.ok) throw new Error(String(res.status));
return res.json();
});
}
function requesterLine(item) {
const parts = [
item.npr,
item.rqn
].filter(Boolean);
return parts.length ? parts.join(" ") : item.rqn || "";
}
async function enrichRequestFromBeatSaver(key, titleEl, coverEl) {
if (requestTitleMisses.has(key)) return;
const cached = requestBeatSaverCache.get(key);
if (cached) {
titleEl.textContent = cached.title;
coverEl.src = cached.coverUrl || "images/unknown.svg";
return;
}
try {
const map = await fetchBeatSaverMapById(key);
if (!map) {
requestTitleMisses.add(key);
return;
}
const name = map.metadata?.songName ?? map.name;
const title2 = name && typeof name === "string" ? name : "";
if (!title2) {
requestTitleMisses.add(key);
return;
}
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 {
requestTitleMisses.add(key);
}
}
function renderRequestList(items) {
requestListEl.replaceChildren();
requestOverlayEl.classList.toggle("has-items", items.length > 0);
for (const item of items) {
const li = document.createElement("li");
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");
titleEl.className = "request-title";
titleEl.textContent = `!bsr ${item.key}`;
li.appendChild(titleEl);
const who = requesterLine(item);
if (who) {
const meta = document.createElement("span");
meta.className = "request-meta";
meta.textContent = who;
li.appendChild(meta);
}
requestListEl.appendChild(li);
void enrichRequestFromBeatSaver(item.key, titleEl, coverEl);
}
}
async function loadRequestQueue() {
try {
const data = await loadChatRequestJson();
requestEmptyEl.textContent = "No pending requests";
requestOverlayEl.classList.remove("request-load-failed");
const source = settings.debugUseHistoryForRequests ? data.history ?? [] : data.queue ?? [];
const items = source.slice(0, MAX_REQUESTS);
renderRequestList(items);
} catch {
requestEmptyEl.textContent = "Request queue unavailable";
requestOverlayEl.classList.add("request-load-failed");
renderRequestList([]);
}
}
async function bootstrap() {
Object.assign(defaults, OVERLAY_SETTINGS_INITIAL);
try {
const url = new URL("/api/overlay-config", location.href);
const res = await fetch(url, {
cache: "no-store"
});
if (res.ok) {
const data = await res.json();
mergeOverlayConfigResponse(defaults, data);
}
} catch {
}
loadSettings();
document.head.appendChild(style);
void refreshConfiguredPlayerAvatar();
if (settings.debugSongId.trim()) void applyDebugSong();
else void refreshMapFriendScores();
for (const key of [
"cover",
"mapInfo",
"time",
"score",
"friends",
"bsr",
"debugUseHistoryForRequests"
]) {
const input = must(`${key}Input`);
input.checked = settings[key];
input.oninput = () => {
settings[key] = input.checked;
saveSettings();
if (key === "friends") void refreshMapFriendScores();
if (key === "debugUseHistoryForRequests") void loadRequestQueue();
};
}
const friendModeInput = must("friendModeInput");
friendModeInput.value = settings.friendMode;
friendModeInput.onchange = () => {
settings.friendMode = friendModeInput.value;
saveSettings();
void refreshMapFriendScores();
};
const beatLeaderPlayerInput = must("beatLeaderPlayerInput");
beatLeaderPlayerInput.value = settings.beatLeaderId;
beatLeaderPlayerInput.oninput = () => {
settings.beatLeaderId = beatLeaderPlayerInput.value.trim();
saveSettings();
void refreshConfiguredPlayerAvatar();
void refreshMapFriendScores();
};
must("beatLeaderPlayerExample").onclick = () => {
beatLeaderPlayerInput.value = "76561199407393962";
beatLeaderPlayerInput.dispatchEvent(new Event("input", {
bubbles: true
}));
};
const scale = must("scaleInput");
scale.valueAsNumber = settings.scale * 100;
scale.oninput = () => {
settings.scale = scale.valueAsNumber / 100;
saveSettings();
};
const position = must("positionInput");
position.value = JSON.stringify([
settings.right,
settings.bottom
]);
position.onchange = () => {
[settings.right, settings.bottom] = parseJson(position.value);
saveSettings();
};
const fade = must("fadeInput");
fade.valueAsNumber = settings.fade;
fade.oninput = () => {
settings.fade = fade.valueAsNumber;
saveSettings();
};
const debugSongIdInput = must("debugSongIdInput");
debugSongIdInput.value = settings.debugSongId;
debugSongIdInput.oninput = () => {
settings.debugSongId = debugSongIdInput.value;
saveSettings();
if (!settings.debugSongId.trim()) {
mapInfoRequestId += 1;
currentMapHash = "";
rawLevelHash = "";
void refreshMapFriendScores();
} else {
void applyDebugSong();
}
};
const debugSongIdExampleBtn = must("debugSongIdExample");
const syncDebugBsrExampleButton = () => {
debugSongIdExampleBtn.textContent = DEBUG_BSR_EXAMPLE_IDS[debugBsrExampleIndex];
};
syncDebugBsrExampleButton();
debugSongIdExampleBtn.onclick = () => {
const id = DEBUG_BSR_EXAMPLE_IDS[debugBsrExampleIndex];
debugBsrExampleIndex = (debugBsrExampleIndex + 1) % DEBUG_BSR_EXAMPLE_IDS.length;
debugSongIdInput.value = id;
debugSongIdInput.dispatchEvent(new Event("input", {
bubbles: true
}));
syncDebugBsrExampleButton();
};
document.documentElement.onclick = () => document.body.classList.toggle("preview");
must("settings").onclick = (e) => e.stopPropagation();
void loadRequestQueue();
window.setInterval(() => void loadRequestQueue(), REQUEST_POLL_MS);
}
void bootstrap();