// 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 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; default: console.log("message", e.data); break; } } }; var provider = beatSaberPlus; var retryMs = 1e4; var retries = 0; function connect() { console.log(`Connecting to ${provider.url} (attempt ${retries++})`); const ws = new WebSocket(provider.url); ws.onopen = onOpen; ws.onmessage = provider.onMessage; ws.onclose = onClose; } function onOpen() { console.log("Connection open."); retries = 0; } function onClose(e) { console.log(`Connection closed. code: ${e.code}, reason: ${e.reason}, clean: ${e.wasClean}`); 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) { const custom = data.level_id.startsWith("custom_level_"); const wip = custom && data.level_id.endsWith("WIP"); 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 = data.BSRKey || "???"; timeMultiplier = data.timeMultiplier || 1; duration = data.duration / 1e3; if (custom && !wip) { document.body.classList.add("loading"); try { const response = await fetch(`https://api.beatsaver.com/maps/hash/${data.level_id.substring(13, 53)}`); const map = await response.json(); if (!map.id) return; 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; } finally { document.body.classList.remove("loading"); } } } 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"); 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); } var settings = { cover: true, mapInfo: true, time: true, score: true, bsr: false, debug: false, right: false, bottom: true, scale: 1, fade: 300 }; var defaults = structuredClone(settings); 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()}`); } window.onhashchange = loadSettings; loadSettings(); document.head.appendChild(style); for (const key of [ "cover", "mapInfo", "time", "score", "bsr", "debug" ]) { const input = must(`${key}Input`); input.checked = settings[key]; input.oninput = () => { settings[key] = input.checked; saveSettings(); if (key === "debug") void loadRequestQueue(); }; } var scale = must("scaleInput"); scale.valueAsNumber = settings.scale * 100; scale.oninput = () => { settings.scale = scale.valueAsNumber / 100; saveSettings(); }; var position = must("positionInput"); position.value = JSON.stringify([ settings.right, settings.bottom ]); position.onchange = () => { [settings.right, settings.bottom] = parseJson(position.value); saveSettings(); }; var fade = must("fadeInput"); fade.valueAsNumber = settings.fade; fade.oninput = () => { settings.fade = fade.valueAsNumber; saveSettings(); }; document.documentElement.onclick = () => document.body.classList.toggle("preview"); must("settings").onclick = (e) => e.stopPropagation(); var MAX_REQUESTS = 10; var REQUEST_POLL_MS = 5e3; var requestListEl = must("requestList"); var requestOverlayEl = must("requestOverlay"); var requestEmptyEl = must("requestEmpty"); var requestTitleCache = /* @__PURE__ */ new Map(); function useRequestHistorySim() { return settings.debug || new URLSearchParams(location.search).get("debug") === "1"; } function requestJsonFilenames() { const explicit = new URLSearchParams(location.search).get("requests"); if (explicit) return [ explicit ]; if (useRequestHistorySim()) return [ "ChatRequest.json", "database.json" ]; return [ "ChatRequest.json" ]; } function loadJsonNextToPage(fileName) { const base = new URL(fileName, location.href); if (base.protocol !== "http:" && base.protocol !== "https:") { throw new Error("not-http"); } 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(); }); } async function loadRequestPayload() { let lastErr; for (const name of requestJsonFilenames()) { try { return await loadJsonNextToPage(name); } catch (e) { lastErr = e; } } throw lastErr instanceof Error ? lastErr : new Error(String(lastErr)); } function requesterLine(item) { const parts = [ item.npr, item.rqn ].filter(Boolean); return parts.length ? parts.join(" ") : item.rqn || ""; } async function enrichRequestTitle(key, titleEl) { if (requestTitleCache.has(key)) { titleEl.textContent = requestTitleCache.get(key) ?? ""; return; } try { const response = await fetch(`https://api.beatsaver.com/maps/id/${encodeURIComponent(key)}`); if (!response.ok) return; const map = await response.json(); const name = map.metadata?.songName ?? map.name; if (name && typeof name === "string") { requestTitleCache.set(key, name); titleEl.textContent = name; } } catch { } } 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 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 enrichRequestTitle(item.key, titleEl); } } async function loadRequestQueue() { try { const data = await loadRequestPayload(); requestEmptyEl.textContent = "No pending requests"; requestOverlayEl.classList.remove("request-load-failed"); const raw = useRequestHistorySim() ? data.history ?? [] : data.queue ?? []; const items = raw.slice(0, MAX_REQUESTS); renderRequestList(items); } catch { requestEmptyEl.textContent = "whupsy, database file missing"; requestOverlayEl.classList.add("request-load-failed"); renderRequestList([]); } } void loadRequestQueue(); window.setInterval(() => void loadRequestQueue(), REQUEST_POLL_MS);