Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df2242f1c | |||
| 3410e3324e | |||
| 86830adc47 | |||
| a6629f8e95 | |||
| 29ce672646 |
@ -26,5 +26,5 @@ Beat Saber Plus itself (game mod) exposes the socket; this repo is only the HTML
|
|||||||
- **Live API calls are proxied** same-origin via `src/server/serve.ts`: `/api/beatleader?path=/...`, `/api/beatsaver?path=/...` (CORS).
|
- **Live API calls are proxied** same-origin via `src/server/serve.ts`: `/api/beatleader?path=/...`, `/api/beatsaver?path=/...` (CORS).
|
||||||
- **Map correlation is hash-based**: resolve BeatSaver map/hash first, then BeatLeader leaderboards via `/leaderboards/hash/{hash}`.
|
- **Map correlation is hash-based**: resolve BeatSaver map/hash first, then BeatLeader leaderboards via `/leaderboards/hash/{hash}`.
|
||||||
- **Friend scores** use `/leaderboard/{leaderboardId}` for stable `playerId` fields.
|
- **Friend scores** use `/leaderboard/{leaderboardId}` for stable `playerId` fields.
|
||||||
- **Mutual friends**: intersection of `/player/{id}/followers?type=Following` and `type=Followers` (paged by `page` + `count`).
|
- **Mutual friends**: intersection of `/player/{id}/followers?type=Following` and `type=Followers` (paged by `page` + `count`). **`all` friends**: union of both lists (deduped).
|
||||||
- **`#friendScores`**: best accuracy per friend for the current map, sorted descending.
|
- **`#friendScores`**: best accuracy per friend for the current map, sorted descending.
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -21,10 +21,12 @@ Configuration is `overlay.toml` in the repo root (copy from `overlay.toml.exampl
|
|||||||
|
|
||||||
- `chat_request_database` — absolute path to Beat Saber Plus `ChatRequest/Database.json`. The server serves that file as `ChatRequest.json` over HTTP (no symlink).
|
- `chat_request_database` — absolute path to Beat Saber Plus `ChatRequest/Database.json`. The server serves that file as `ChatRequest.json` over HTTP (no symlink).
|
||||||
- `beatleader_player_id` — default BeatLeader id for friend scores (the server logs a warning if this is missing).
|
- `beatleader_player_id` — default BeatLeader id for friend scores (the server logs a warning if this is missing).
|
||||||
- Overlay UI defaults — optional toggles and layout matching the settings dialog: `cover`, `map_info`, `time`, `score`, `friends`, `friend_mode` (`mutual` \| `following` \| `followers`), `bsr`, `right`, `bottom`, `scale`, `fade`.
|
- Overlay UI defaults — optional toggles and layout matching the settings dialog: `cover`, `map_info`, `time`, `score`, `friends`, `friend_mode` (`mutual` \| `following` \| `followers` \| `all`), `bsr`, `right`, `bottom`, `scale`, `fade`.
|
||||||
|
|
||||||
Mutual friend mode shows scores from players that you follow and that follow you back on Beatleader. Or in other words, users that are in both your following and follower list.
|
Mutual friend mode shows scores from players that you follow and that follow you back on Beatleader. Or in other words, users that are in both your following and follower list.
|
||||||
|
|
||||||
|
Use `friend_mode = "all"` to include anyone you follow **or** anyone who follows you (union, deduped by player id).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Clone the repo and run `deno task serve` as above.
|
Clone the repo and run `deno task serve` as above.
|
||||||
|
|||||||
@ -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 |
60
index.css
60
index.css
@ -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 {
|
||||||
@ -303,6 +317,48 @@ span:empty {
|
|||||||
max-width: 44rem;
|
max-width: 44rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.personal-best-row {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
opacity: 0.92;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#friendScores.has-personal-best .personal-best-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-best-label {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
opacity: 0.75;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-best-acc {
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-best-delta {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 4.2ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-best-delta.personal-best-delta--ahead {
|
||||||
|
color: #3ddc97;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-best-delta.personal-best-delta--behind {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
#friendScoresHeader {
|
#friendScoresHeader {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@ -39,6 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="friendScores" aria-live="polite">
|
<div id="friendScores" aria-live="polite">
|
||||||
|
<div id="personalBestRow" class="personal-best-row">
|
||||||
|
<span class="personal-best-label">PB</span>
|
||||||
|
<span id="personalBestAcc" class="personal-best-acc"></span>
|
||||||
|
<span id="personalBestDelta" class="personal-best-delta"></span>
|
||||||
|
</div>
|
||||||
<div id="friendScoresHeader"><img id="friendScoresPlayerAvatar" src="images/unknown.svg" alt=""> <span id="friendScoresHeaderText">frenz</span> <img id="friendScoresHeaderImg" src="assets/peepohigh.webp" alt=""></div>
|
<div id="friendScoresHeader"><img id="friendScoresPlayerAvatar" src="images/unknown.svg" alt=""> <span id="friendScoresHeaderText">frenz</span> <img id="friendScoresHeaderImg" src="assets/peepohigh.webp" alt=""></div>
|
||||||
<ol id="friendScoresList"></ol>
|
<ol id="friendScoresList"></ol>
|
||||||
<div id="friendScoresEmpty">No map loaded</div>
|
<div id="friendScoresEmpty">No map loaded</div>
|
||||||
@ -46,7 +51,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>
|
||||||
@ -70,6 +75,7 @@
|
|||||||
<option value="mutual">Followed + follower (mutual)</option>
|
<option value="mutual">Followed + follower (mutual)</option>
|
||||||
<option value="following">Following (I follow them)</option>
|
<option value="following">Following (I follow them)</option>
|
||||||
<option value="followers">Followers (they follow me)</option>
|
<option value="followers">Followers (they follow me)</option>
|
||||||
|
<option value="all">All friends (following or followers)</option>
|
||||||
</select></label>
|
</select></label>
|
||||||
<label id="beatLeaderPlayerSetting">Player id: <span class="beatLeaderPlayerRow">
|
<label id="beatLeaderPlayerSetting">Player id: <span class="beatLeaderPlayerRow">
|
||||||
<span class="debugSongIdHint">e.g. <button type="button" id="beatLeaderPlayerExample" title="Fill with pleb's numeric id">pleb</button></span>
|
<span class="debugSongIdHint">e.g. <button type="button" id="beatLeaderPlayerExample" title="Fill with pleb's numeric id">pleb</button></span>
|
||||||
@ -84,6 +90,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">
|
||||||
|
|||||||
130
index.js
130
index.js
@ -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
|
||||||
@ -187,6 +188,23 @@ async function fetchFriends(playerId, mode, maxPages = 100) {
|
|||||||
if (mode === "followers") {
|
if (mode === "followers") {
|
||||||
return followers.map((entry) => normalizeFollowerEntry(entry));
|
return followers.map((entry) => normalizeFollowerEntry(entry));
|
||||||
}
|
}
|
||||||
|
if (mode === "all") {
|
||||||
|
const seen = /* @__PURE__ */ new Set();
|
||||||
|
const out = [];
|
||||||
|
for (const entry of following) {
|
||||||
|
const id = String(entry.id);
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
out.push(normalizeFollowerEntry(entry));
|
||||||
|
}
|
||||||
|
for (const entry of followers) {
|
||||||
|
const id = String(entry.id);
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
out.push(normalizeFollowerEntry(entry));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
return followers.filter((entry) => followingIds.has(String(entry.id))).map((entry) => normalizeFollowerEntry(entry));
|
return followers.filter((entry) => followingIds.has(String(entry.id))).map((entry) => normalizeFollowerEntry(entry));
|
||||||
}
|
}
|
||||||
function normalizeAccuracy(value) {
|
function normalizeAccuracy(value) {
|
||||||
@ -245,6 +263,9 @@ var beatSaberPlus = {
|
|||||||
switch (data._event) {
|
switch (data._event) {
|
||||||
case "gameState":
|
case "gameState":
|
||||||
document.body.dataset.gameState = data.gameStateChanged;
|
document.body.dataset.gameState = data.gameStateChanged;
|
||||||
|
if (data.gameStateChanged === "Menu") {
|
||||||
|
resetPersonalBestDeltaPlaceholder();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "mapInfo":
|
case "mapInfo":
|
||||||
void updateMapInfo(data.mapInfoChanged);
|
void updateMapInfo(data.mapInfoChanged);
|
||||||
@ -474,6 +495,9 @@ var friendScoresEmpty = must("friendScoresEmpty");
|
|||||||
var friendScoresHeaderText = must("friendScoresHeaderText");
|
var friendScoresHeaderText = must("friendScoresHeaderText");
|
||||||
var friendScoresPlayerAvatar = must("friendScoresPlayerAvatar");
|
var friendScoresPlayerAvatar = must("friendScoresPlayerAvatar");
|
||||||
var friendScoresHeaderImg = must("friendScoresHeaderImg");
|
var friendScoresHeaderImg = must("friendScoresHeaderImg");
|
||||||
|
var personalBestAcc = must("personalBestAcc");
|
||||||
|
var personalBestDelta = must("personalBestDelta");
|
||||||
|
var currentPbAccuracyPercent = null;
|
||||||
var cachedConfiguredPlayerAvatarKey = "";
|
var cachedConfiguredPlayerAvatarKey = "";
|
||||||
var cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
|
var cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
|
||||||
async function refreshConfiguredPlayerAvatar() {
|
async function refreshConfiguredPlayerAvatar() {
|
||||||
@ -496,11 +520,42 @@ async function refreshConfiguredPlayerAvatar() {
|
|||||||
cachedConfiguredPlayerAvatarSrc = profile?.avatar?.trim() || "images/unknown.svg";
|
cachedConfiguredPlayerAvatarSrc = profile?.avatar?.trim() || "images/unknown.svg";
|
||||||
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
|
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
|
||||||
}
|
}
|
||||||
|
function applyPersonalBestRow(pbPercent) {
|
||||||
|
currentPbAccuracyPercent = pbPercent;
|
||||||
|
friendScoresPanel.classList.toggle("has-personal-best", pbPercent !== null);
|
||||||
|
if (pbPercent === null) {
|
||||||
|
personalBestAcc.textContent = "";
|
||||||
|
personalBestDelta.textContent = "";
|
||||||
|
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
personalBestAcc.textContent = `${pbPercent.toFixed(2)}%`;
|
||||||
|
resetPersonalBestDeltaPlaceholder();
|
||||||
|
}
|
||||||
|
function resetPersonalBestDeltaPlaceholder() {
|
||||||
|
if (currentPbAccuracyPercent === null) return;
|
||||||
|
personalBestDelta.textContent = "\u2014";
|
||||||
|
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
|
||||||
|
}
|
||||||
|
function updatePersonalBestDelta(liveAccuracyPercent) {
|
||||||
|
if (currentPbAccuracyPercent === null) return;
|
||||||
|
const delta = liveAccuracyPercent - currentPbAccuracyPercent;
|
||||||
|
personalBestDelta.textContent = `${delta >= 0 ? "+" : ""}${delta.toFixed(2)}%`;
|
||||||
|
personalBestDelta.classList.toggle("personal-best-delta--ahead", delta > 5e-4);
|
||||||
|
personalBestDelta.classList.toggle("personal-best-delta--behind", delta < -5e-4);
|
||||||
|
if (Math.abs(delta) <= 5e-4) {
|
||||||
|
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
|
||||||
|
}
|
||||||
|
}
|
||||||
function updateScore(score) {
|
function updateScore(score) {
|
||||||
if (!settings.score) return;
|
if (settings.score) {
|
||||||
accuracy.textContent = (score.accuracy * 100).toFixed(1);
|
accuracy.textContent = (score.accuracy * 100).toFixed(1);
|
||||||
mistakes.textContent = score.missCount ? String(score.missCount) : "";
|
mistakes.textContent = score.missCount ? String(score.missCount) : "";
|
||||||
accuracy.classList.toggle("failed", score.currentHealth === 0);
|
accuracy.classList.toggle("failed", score.currentHealth === 0);
|
||||||
|
}
|
||||||
|
if (settings.friends) {
|
||||||
|
updatePersonalBestDelta(score.accuracy * 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function avatarFromScore(score) {
|
function avatarFromScore(score) {
|
||||||
if (typeof score.player === "object" && score.player?.avatar) {
|
if (typeof score.player === "object" && score.player?.avatar) {
|
||||||
@ -510,6 +565,7 @@ function avatarFromScore(score) {
|
|||||||
return url || null;
|
return url || null;
|
||||||
}
|
}
|
||||||
function clearFriendScores(message) {
|
function clearFriendScores(message) {
|
||||||
|
applyPersonalBestRow(null);
|
||||||
friendScoresList.replaceChildren();
|
friendScoresList.replaceChildren();
|
||||||
friendScoresEmpty.textContent = message;
|
friendScoresEmpty.textContent = message;
|
||||||
friendScoresHeaderText.textContent = "frenz?";
|
friendScoresHeaderText.textContent = "frenz?";
|
||||||
@ -547,6 +603,7 @@ function friendsRelationListKey(playerId) {
|
|||||||
}
|
}
|
||||||
function beginFriendScoresForNewMapContext() {
|
function beginFriendScoresForNewMapContext() {
|
||||||
friendScoreRequestId += 1;
|
friendScoreRequestId += 1;
|
||||||
|
applyPersonalBestRow(null);
|
||||||
if (!settings.friends) return;
|
if (!settings.friends) return;
|
||||||
if (!currentMapHash) {
|
if (!currentMapHash) {
|
||||||
clearFriendScores("No map loaded");
|
clearFriendScores("No map loaded");
|
||||||
@ -560,7 +617,7 @@ function beginFriendScoresForNewMapContext() {
|
|||||||
friendScoresList.replaceChildren();
|
friendScoresList.replaceChildren();
|
||||||
friendScoresPanel.classList.remove("has-items");
|
friendScoresPanel.classList.remove("has-items");
|
||||||
friendScoresPanel.classList.add("is-loading");
|
friendScoresPanel.classList.add("is-loading");
|
||||||
friendScoresEmpty.textContent = "Loading mutual friend scores...";
|
friendScoresEmpty.textContent = "Loading friend scores...";
|
||||||
}
|
}
|
||||||
async function refreshMapFriendScores() {
|
async function refreshMapFriendScores() {
|
||||||
const hash = currentMapHash;
|
const hash = currentMapHash;
|
||||||
@ -577,10 +634,11 @@ async function refreshMapFriendScores() {
|
|||||||
clearFriendScores("Waiting for BeatLeader player id");
|
clearFriendScores("Waiting for BeatLeader player id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
applyPersonalBestRow(null);
|
||||||
friendScoresList.replaceChildren();
|
friendScoresList.replaceChildren();
|
||||||
friendScoresPanel.classList.remove("has-items");
|
friendScoresPanel.classList.remove("has-items");
|
||||||
friendScoresPanel.classList.add("is-loading");
|
friendScoresPanel.classList.add("is-loading");
|
||||||
friendScoresEmpty.textContent = "Loading mutual friend scores...";
|
friendScoresEmpty.textContent = "Loading friend scores...";
|
||||||
const requestId = ++friendScoreRequestId;
|
const requestId = ++friendScoreRequestId;
|
||||||
try {
|
try {
|
||||||
const relKey = friendsRelationListKey(playerId);
|
const relKey = friendsRelationListKey(playerId);
|
||||||
@ -593,10 +651,12 @@ async function refreshMapFriendScores() {
|
|||||||
friendsRelationCache = fetched;
|
friendsRelationCache = fetched;
|
||||||
return fetched;
|
return fetched;
|
||||||
})();
|
})();
|
||||||
const [leaderboards, friends] = await Promise.all([
|
const [leaderboards, friends, selfProfile] = await Promise.all([
|
||||||
fetchBLLeaderboardsByHash(hash),
|
fetchBLLeaderboardsByHash(hash),
|
||||||
friendsPromise
|
friendsPromise,
|
||||||
|
fetchBeatLeaderPlayer(playerId)
|
||||||
]);
|
]);
|
||||||
|
const myBeatLeaderId = selfProfile?.id ?? playerId;
|
||||||
if (requestId !== friendScoreRequestId) return;
|
if (requestId !== friendScoreRequestId) return;
|
||||||
if (leaderboards.length === 0) {
|
if (leaderboards.length === 0) {
|
||||||
clearFriendScores("No BeatLeader leaderboards found");
|
clearFriendScores("No BeatLeader leaderboards found");
|
||||||
@ -613,12 +673,21 @@ async function refreshMapFriendScores() {
|
|||||||
]));
|
]));
|
||||||
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) {
|
||||||
const relationLabel = settings.friendMode === "following" ? "No followed BeatLeader players" : settings.friendMode === "followers" ? "No BeatLeader followers" : "No mutual BeatLeader followers";
|
const relationLabel = settings.friendMode === "following" ? "No followed BeatLeader players" : settings.friendMode === "followers" ? "No BeatLeader followers" : settings.friendMode === "all" ? "No followed or follower BeatLeader players" : "No mutual BeatLeader followers";
|
||||||
clearFriendScores(relationLabel);
|
clearFriendScores(relationLabel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
|
const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
|
||||||
if (requestId !== friendScoreRequestId) return;
|
if (requestId !== friendScoreRequestId) return;
|
||||||
|
let playerPbAcc = null;
|
||||||
|
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 || playerKey !== String(myBeatLeaderId)) continue;
|
||||||
|
const acc = normalizeAccuracy(score.accuracy ?? score.acc);
|
||||||
|
if (acc === null) continue;
|
||||||
|
if (playerPbAcc === null || acc > playerPbAcc) playerPbAcc = acc;
|
||||||
|
}
|
||||||
const bestByPlayer = /* @__PURE__ */ new Map();
|
const bestByPlayer = /* @__PURE__ */ new Map();
|
||||||
for (const score of scores) {
|
for (const score of scores) {
|
||||||
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
|
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
|
||||||
@ -639,6 +708,10 @@ async function refreshMapFriendScores() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (playerPbAcc !== null) {
|
||||||
|
bestByPlayer.delete(String(myBeatLeaderId));
|
||||||
|
}
|
||||||
|
applyPersonalBestRow(playerPbAcc);
|
||||||
const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc);
|
const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc);
|
||||||
renderFriendScores(sorted);
|
renderFriendScores(sorted);
|
||||||
} catch {
|
} catch {
|
||||||
@ -649,8 +722,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;
|
||||||
@ -676,7 +752,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);
|
||||||
@ -696,10 +772,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 {
|
||||||
@ -709,12 +787,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);
|
||||||
}
|
}
|
||||||
@ -725,6 +810,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}`;
|
||||||
@ -737,7 +828,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() {
|
||||||
@ -745,7 +836,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";
|
||||||
@ -777,7 +869,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];
|
||||||
@ -785,6 +878,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");
|
||||||
|
|||||||
@ -12,7 +12,7 @@ map_info = true
|
|||||||
time = true
|
time = true
|
||||||
score = true
|
score = true
|
||||||
friends = true
|
friends = true
|
||||||
friend_mode = "mutual" # mutual | following | followers
|
friend_mode = "mutual" # mutual | following | followers | all
|
||||||
bsr = false
|
bsr = false
|
||||||
right = false
|
right = false
|
||||||
bottom = true
|
bottom = true
|
||||||
|
|||||||
283
samples/Database.json
Normal file
283
samples/Database.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
@ -196,6 +196,23 @@ export async function fetchFriends(playerId: string, mode: FriendMode, maxPages
|
|||||||
if (mode === "followers") {
|
if (mode === "followers") {
|
||||||
return followers.map((entry) => normalizeFollowerEntry(entry as BeatLeaderFollower));
|
return followers.map((entry) => normalizeFollowerEntry(entry as BeatLeaderFollower));
|
||||||
}
|
}
|
||||||
|
if (mode === "all") {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: BeatLeaderFollower[] = [];
|
||||||
|
for (const entry of following) {
|
||||||
|
const id = String(entry.id);
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
out.push(normalizeFollowerEntry(entry as BeatLeaderFollower));
|
||||||
|
}
|
||||||
|
for (const entry of followers) {
|
||||||
|
const id = String(entry.id);
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
out.push(normalizeFollowerEntry(entry as BeatLeaderFollower));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
return followers
|
return followers
|
||||||
.filter((entry) => followingIds.has(String(entry.id)))
|
.filter((entry) => followingIds.has(String(entry.id)))
|
||||||
.map((entry) => normalizeFollowerEntry(entry as BeatLeaderFollower));
|
.map((entry) => normalizeFollowerEntry(entry as BeatLeaderFollower));
|
||||||
|
|||||||
@ -72,6 +72,9 @@ const beatSaberPlus = {
|
|||||||
switch (data._event) {
|
switch (data._event) {
|
||||||
case "gameState":
|
case "gameState":
|
||||||
document.body.dataset.gameState = data.gameStateChanged;
|
document.body.dataset.gameState = data.gameStateChanged;
|
||||||
|
if (data.gameStateChanged === "Menu") {
|
||||||
|
resetPersonalBestDeltaPlaceholder();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "mapInfo":
|
case "mapInfo":
|
||||||
void updateMapInfo(data.mapInfoChanged);
|
void updateMapInfo(data.mapInfoChanged);
|
||||||
@ -342,6 +345,11 @@ const friendScoresEmpty = must<HTMLElement>("friendScoresEmpty");
|
|||||||
const friendScoresHeaderText = must<HTMLElement>("friendScoresHeaderText");
|
const friendScoresHeaderText = must<HTMLElement>("friendScoresHeaderText");
|
||||||
const friendScoresPlayerAvatar = must<HTMLImageElement>("friendScoresPlayerAvatar");
|
const friendScoresPlayerAvatar = must<HTMLImageElement>("friendScoresPlayerAvatar");
|
||||||
const friendScoresHeaderImg = must<HTMLImageElement>("friendScoresHeaderImg");
|
const friendScoresHeaderImg = must<HTMLImageElement>("friendScoresHeaderImg");
|
||||||
|
const personalBestAcc = must<HTMLElement>("personalBestAcc");
|
||||||
|
const personalBestDelta = must<HTMLElement>("personalBestDelta");
|
||||||
|
|
||||||
|
/** BeatLeader PB (percent) for the current map + difficulty; null if none or friends panel inactive. */
|
||||||
|
let currentPbAccuracyPercent: number | null = null;
|
||||||
|
|
||||||
let cachedConfiguredPlayerAvatarKey = "";
|
let cachedConfiguredPlayerAvatarKey = "";
|
||||||
let cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
|
let cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
|
||||||
@ -367,11 +375,45 @@ async function refreshConfiguredPlayerAvatar() {
|
|||||||
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
|
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyPersonalBestRow(pbPercent: number | null) {
|
||||||
|
currentPbAccuracyPercent = pbPercent;
|
||||||
|
friendScoresPanel.classList.toggle("has-personal-best", pbPercent !== null);
|
||||||
|
if (pbPercent === null) {
|
||||||
|
personalBestAcc.textContent = "";
|
||||||
|
personalBestDelta.textContent = "";
|
||||||
|
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
personalBestAcc.textContent = `${pbPercent.toFixed(2)}%`;
|
||||||
|
resetPersonalBestDeltaPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPersonalBestDeltaPlaceholder() {
|
||||||
|
if (currentPbAccuracyPercent === null) return;
|
||||||
|
personalBestDelta.textContent = "—";
|
||||||
|
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePersonalBestDelta(liveAccuracyPercent: number) {
|
||||||
|
if (currentPbAccuracyPercent === null) return;
|
||||||
|
const delta = liveAccuracyPercent - currentPbAccuracyPercent;
|
||||||
|
personalBestDelta.textContent = `${delta >= 0 ? "+" : ""}${delta.toFixed(2)}%`;
|
||||||
|
personalBestDelta.classList.toggle("personal-best-delta--ahead", delta > 0.0005);
|
||||||
|
personalBestDelta.classList.toggle("personal-best-delta--behind", delta < -0.0005);
|
||||||
|
if (Math.abs(delta) <= 0.0005) {
|
||||||
|
personalBestDelta.classList.remove("personal-best-delta--ahead", "personal-best-delta--behind");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateScore(score: Score) {
|
function updateScore(score: Score) {
|
||||||
if (!settings.score) return;
|
if (settings.score) {
|
||||||
accuracy.textContent = (score.accuracy * 100).toFixed(1);
|
accuracy.textContent = (score.accuracy * 100).toFixed(1);
|
||||||
mistakes.textContent = score.missCount ? String(score.missCount) : "";
|
mistakes.textContent = score.missCount ? String(score.missCount) : "";
|
||||||
accuracy.classList.toggle("failed", score.currentHealth === 0);
|
accuracy.classList.toggle("failed", score.currentHealth === 0);
|
||||||
|
}
|
||||||
|
if (settings.friends) {
|
||||||
|
updatePersonalBestDelta(score.accuracy * 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function avatarFromScore(score: BeatLeaderScore): string | null {
|
function avatarFromScore(score: BeatLeaderScore): string | null {
|
||||||
@ -383,6 +425,7 @@ function avatarFromScore(score: BeatLeaderScore): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearFriendScores(message: string) {
|
function clearFriendScores(message: string) {
|
||||||
|
applyPersonalBestRow(null);
|
||||||
friendScoresList.replaceChildren();
|
friendScoresList.replaceChildren();
|
||||||
friendScoresEmpty.textContent = message;
|
friendScoresEmpty.textContent = message;
|
||||||
friendScoresHeaderText.textContent = "frenz?";
|
friendScoresHeaderText.textContent = "frenz?";
|
||||||
@ -427,6 +470,7 @@ function friendsRelationListKey(playerId: string): string {
|
|||||||
*/
|
*/
|
||||||
function beginFriendScoresForNewMapContext() {
|
function beginFriendScoresForNewMapContext() {
|
||||||
friendScoreRequestId += 1;
|
friendScoreRequestId += 1;
|
||||||
|
applyPersonalBestRow(null);
|
||||||
if (!settings.friends) return;
|
if (!settings.friends) return;
|
||||||
if (!currentMapHash) {
|
if (!currentMapHash) {
|
||||||
clearFriendScores("No map loaded");
|
clearFriendScores("No map loaded");
|
||||||
@ -440,7 +484,7 @@ function beginFriendScoresForNewMapContext() {
|
|||||||
friendScoresList.replaceChildren();
|
friendScoresList.replaceChildren();
|
||||||
friendScoresPanel.classList.remove("has-items");
|
friendScoresPanel.classList.remove("has-items");
|
||||||
friendScoresPanel.classList.add("is-loading");
|
friendScoresPanel.classList.add("is-loading");
|
||||||
friendScoresEmpty.textContent = "Loading mutual friend scores...";
|
friendScoresEmpty.textContent = "Loading friend scores...";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshMapFriendScores() {
|
async function refreshMapFriendScores() {
|
||||||
@ -458,10 +502,11 @@ async function refreshMapFriendScores() {
|
|||||||
clearFriendScores("Waiting for BeatLeader player id");
|
clearFriendScores("Waiting for BeatLeader player id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
applyPersonalBestRow(null);
|
||||||
friendScoresList.replaceChildren();
|
friendScoresList.replaceChildren();
|
||||||
friendScoresPanel.classList.remove("has-items");
|
friendScoresPanel.classList.remove("has-items");
|
||||||
friendScoresPanel.classList.add("is-loading");
|
friendScoresPanel.classList.add("is-loading");
|
||||||
friendScoresEmpty.textContent = "Loading mutual friend scores...";
|
friendScoresEmpty.textContent = "Loading friend scores...";
|
||||||
const requestId = ++friendScoreRequestId;
|
const requestId = ++friendScoreRequestId;
|
||||||
try {
|
try {
|
||||||
const relKey = friendsRelationListKey(playerId);
|
const relKey = friendsRelationListKey(playerId);
|
||||||
@ -474,10 +519,12 @@ async function refreshMapFriendScores() {
|
|||||||
friendsRelationCache = fetched;
|
friendsRelationCache = fetched;
|
||||||
return fetched;
|
return fetched;
|
||||||
})();
|
})();
|
||||||
const [leaderboards, friends] = await Promise.all([
|
const [leaderboards, friends, selfProfile] = await Promise.all([
|
||||||
fetchBLLeaderboardsByHash(hash),
|
fetchBLLeaderboardsByHash(hash),
|
||||||
friendsPromise,
|
friendsPromise,
|
||||||
|
fetchBeatLeaderPlayer(playerId),
|
||||||
]);
|
]);
|
||||||
|
const myBeatLeaderId = selfProfile?.id ?? playerId;
|
||||||
if (requestId !== friendScoreRequestId) return;
|
if (requestId !== friendScoreRequestId) return;
|
||||||
if (leaderboards.length === 0) {
|
if (leaderboards.length === 0) {
|
||||||
clearFriendScores("No BeatLeader leaderboards found");
|
clearFriendScores("No BeatLeader leaderboards found");
|
||||||
@ -495,12 +542,23 @@ async function refreshMapFriendScores() {
|
|||||||
? "No followed BeatLeader players"
|
? "No followed BeatLeader players"
|
||||||
: settings.friendMode === "followers"
|
: settings.friendMode === "followers"
|
||||||
? "No BeatLeader followers"
|
? "No BeatLeader followers"
|
||||||
|
: settings.friendMode === "all"
|
||||||
|
? "No followed or follower BeatLeader players"
|
||||||
: "No mutual BeatLeader followers";
|
: "No mutual BeatLeader followers";
|
||||||
clearFriendScores(relationLabel);
|
clearFriendScores(relationLabel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
|
const scores = await fetchAllMapScoresByHash(hash, forPlayMode);
|
||||||
if (requestId !== friendScoreRequestId) return;
|
if (requestId !== friendScoreRequestId) return;
|
||||||
|
let playerPbAcc: number | null = null;
|
||||||
|
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 || playerKey !== String(myBeatLeaderId)) continue;
|
||||||
|
const acc = normalizeAccuracy(score.accuracy ?? score.acc);
|
||||||
|
if (acc === null) continue;
|
||||||
|
if (playerPbAcc === null || acc > playerPbAcc) playerPbAcc = acc;
|
||||||
|
}
|
||||||
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) {
|
||||||
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
|
const scorePlayerId = score.playerId ?? (typeof score.player === "object" ? score.player?.id : null);
|
||||||
@ -523,6 +581,10 @@ async function refreshMapFriendScores() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (playerPbAcc !== null) {
|
||||||
|
bestByPlayer.delete(String(myBeatLeaderId));
|
||||||
|
}
|
||||||
|
applyPersonalBestRow(playerPbAcc);
|
||||||
const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc);
|
const sorted = Array.from(bestByPlayer.values()).sort((a, b) => b.acc - a.acc);
|
||||||
renderFriendScores(sorted);
|
renderFriendScores(sorted);
|
||||||
} catch {
|
} catch {
|
||||||
@ -534,8 +596,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;
|
||||||
@ -564,10 +629,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() {
|
||||||
@ -585,10 +650,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 {
|
||||||
@ -598,12 +665,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);
|
||||||
}
|
}
|
||||||
@ -615,6 +686,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}`;
|
||||||
@ -627,7 +704,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,7 +713,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";
|
||||||
@ -665,13 +743,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();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { assert } from "jsr:@std/assert";
|
import { assert, assertGreaterOrEqual } from "jsr:@std/assert";
|
||||||
import { fetchFriends } from "./beatleader.ts";
|
import { fetchFriends } from "./beatleader.ts";
|
||||||
|
|
||||||
const PLAYER_ID = "76561199407393962";
|
const PLAYER_ID = "76561199407393962";
|
||||||
@ -12,3 +12,20 @@ Deno.test({
|
|||||||
assert(mutuals.length > 0, `Expected mutual friends for player ${PLAYER_ID}`);
|
assert(mutuals.length > 0, `Expected mutual friends for player ${PLAYER_ID}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "live lookup: all friend mode is superset of mutual",
|
||||||
|
sanitizeOps: false,
|
||||||
|
sanitizeResources: false,
|
||||||
|
async fn() {
|
||||||
|
const [mutuals, allFriends] = await Promise.all([
|
||||||
|
fetchFriends(PLAYER_ID, "mutual", 100),
|
||||||
|
fetchFriends(PLAYER_ID, "all", 100),
|
||||||
|
]);
|
||||||
|
assertGreaterOrEqual(allFriends.length, mutuals.length, "Expected union count >= mutual count");
|
||||||
|
const allIds = new Set(allFriends.map((f) => f.id));
|
||||||
|
for (const m of mutuals) {
|
||||||
|
assert(allIds.has(m.id), `Expected mutual id ${m.id} in all friend list`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export interface OverlayConfigApiBody {
|
|||||||
defaults: Partial<OverlaySettings>;
|
defaults: Partial<OverlaySettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FRIEND_MODES = new Set<FriendMode>(["mutual", "following", "followers"]);
|
const FRIEND_MODES = new Set<FriendMode>(["mutual", "following", "followers", "all"]);
|
||||||
|
|
||||||
/** Merge `/api/overlay-config` into the object used as `defaults` before applying the URL hash. */
|
/** Merge `/api/overlay-config` into the object used as `defaults` before applying the URL hash. */
|
||||||
export function mergeOverlayConfigResponse(
|
export function mergeOverlayConfigResponse(
|
||||||
@ -84,7 +84,7 @@ export function overlayTomlToDefaults(toml: OverlayToml): {
|
|||||||
if (FRIEND_MODES.has(raw as FriendMode)) defaults.friendMode = raw as FriendMode;
|
if (FRIEND_MODES.has(raw as FriendMode)) defaults.friendMode = raw as FriendMode;
|
||||||
else {
|
else {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`overlay.toml: invalid friend_mode "${toml.friend_mode}" (expected mutual, following, or followers)`,
|
`overlay.toml: invalid friend_mode "${toml.friend_mode}" (expected mutual, following, followers, or all)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// https://github.com/hardcpp/BeatSaberPlus/wiki/%5BEN%5D-Song-Overlay
|
// https://github.com/hardcpp/BeatSaberPlus/wiki/%5BEN%5D-Song-Overlay
|
||||||
|
|
||||||
export type FriendMode = "mutual" | "following" | "followers";
|
export type FriendMode = "mutual" | "following" | "followers" | "all";
|
||||||
|
|
||||||
/** Overlay UI + BeatLeader id (URL hash overrides defaults from overlay.toml / `/api/overlay-config`). */
|
/** Overlay UI + BeatLeader id (URL hash overrides defaults from overlay.toml / `/api/overlay-config`). */
|
||||||
export interface OverlaySettings {
|
export interface OverlaySettings {
|
||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user