This repository has been archived on 2023-11-06. You can view files and clone it, but cannot push or open issues or pull requests.
beatsaber-overlay/src/pages/overlay.js

490 lines
12 KiB
JavaScript
Raw Normal View History

2022-10-19 20:15:18 +00:00
import { Link, Spinner } from "@nextui-org/react";
2022-10-21 09:56:50 +00:00
import { NextSeo } from "next-seo";
2022-10-14 19:00:47 +00:00
import { Component } from "react";
2022-10-23 20:47:57 +00:00
import PlayerStats from "../../src/components/PlayerStats";
import ScoreStats from "../../src/components/ScoreStats";
import SongInfo from "../../src/components/SongInfo";
import LeaderboardType from "../../src/consts/LeaderboardType";
import Utils from "../../src/utils/utils";
2022-10-17 11:58:07 +00:00
2022-10-14 19:00:47 +00:00
import styles from "../styles/overlay.module.css";
2022-10-10 08:34:38 +00:00
2022-10-10 12:14:25 +00:00
export default class Overlay extends Component {
2022-10-10 08:34:38 +00:00
#_beatSaverURL = "";
constructor(props) {
super(props);
this.cutData = [];
this.cutData.SaberA = {
count: [0, 0, 0],
totalScore: [0, 0, 0],
};
this.cutData.SaberB = {
count: [0, 0, 0],
totalScore: [0, 0, 0],
};
this._mounted = false;
2022-10-10 08:34:38 +00:00
this.state = {
2022-10-19 18:12:42 +00:00
hasError: false,
2022-10-19 10:03:42 +00:00
loadingPlayerData: true,
2022-10-11 13:22:01 +00:00
isConnectedToSocket: false,
2022-10-10 08:34:38 +00:00
id: undefined,
2022-10-17 11:58:07 +00:00
isValidSteamId: false,
2022-10-10 08:34:38 +00:00
websiteType: "ScoreSaber",
data: undefined,
showPlayerStats: true,
showScore: false,
showSongInfo: false,
2022-10-20 14:04:49 +00:00
loadedDuringSong: false,
2022-10-10 08:34:38 +00:00
socket: undefined,
isVisible: false,
songInfo: undefined,
beatSaverData: undefined,
currentSongTime: 0,
paused: true,
currentScore: 0,
percentage: "100.00%",
failed: false,
2022-10-20 14:04:49 +00:00
mapStarCount: undefined,
2022-10-21 11:39:42 +00:00
SaberA: {
cutDistanceScore: 0.0,
averagePreSwing: 0.0,
averagePostSwing: 0.0,
2022-10-10 08:34:38 +00:00
},
2022-10-21 11:39:42 +00:00
SaberB: {
cutDistanceScore: 0.0,
averagePreSwing: 0.0,
averagePostSwing: 0.0,
2022-10-14 19:00:47 +00:00
},
};
2022-10-10 08:34:38 +00:00
this.setupTimer();
}
2022-10-19 18:12:42 +00:00
async componentDidMount() {
if (this._mounted === true) {
return;
}
this._mounted = true;
2022-10-19 18:12:42 +00:00
if (this.state.hasError) {
// Reload the page if there has been an error
console.log("There has been an error and the page was reloaded.");
2022-10-19 18:12:42 +00:00
return Router.reload(window.location.pathname);
}
2022-10-10 08:34:38 +00:00
2022-10-14 19:00:47 +00:00
console.log("Initializing...");
this.#_beatSaverURL =
document.location.origin + "/api/beatsaver/map?hash=%s";
2022-10-10 08:34:38 +00:00
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
2022-10-10 17:38:06 +00:00
// Check what website the player wants to use
2022-10-19 09:54:17 +00:00
if (params.beatleader === "true" || params.beatLeader === "true") {
2022-10-10 08:34:38 +00:00
this.setState({ websiteType: "BeatLeader" });
}
const id = params.id;
2022-10-14 19:00:47 +00:00
if (!id) {
// Check if the id param is valid
2022-10-20 10:09:41 +00:00
this.setState({ isValidSteamId: false, loadingPlayerData: false });
2022-10-10 08:34:38 +00:00
return;
}
// Checks if the steam id is valid
const isValid = await this.validateSteamId(id);
if (!isValid) {
2022-10-20 10:09:41 +00:00
this.setState({ isValidSteamId: false, loadingPlayerData: false });
return;
}
this.setState({ id: id, isValidSteamId: true });
2022-10-10 08:34:38 +00:00
// Check if the player wants to disable their stats (pp, global pos, etc)
2022-10-14 19:00:47 +00:00
if (params.showPlayerStats === "false" || params.playerstats === "false") {
2022-10-10 08:34:38 +00:00
this.setState({ showPlayerStats: false });
}
2022-10-19 11:05:21 +00:00
if (this.state.showPlayerStats == true || params.playerstats == "true") {
setTimeout(async () => {
await this.updateData(id);
}, 10); // 10ms
}
2022-10-10 08:34:38 +00:00
let shouldConnectSocket = false;
// Check if the player wants to show their current score information
2022-10-14 19:00:47 +00:00
if (params.showScoreInfo === "true" || params.scoreinfo === "true") {
2022-10-10 08:34:38 +00:00
this.setState({ showScore: true });
shouldConnectSocket = true;
}
// Check if the player wants to show the current song
2022-10-14 19:00:47 +00:00
if (params.showSongInfo === "true" || params.songinfo === "true") {
2022-10-10 08:34:38 +00:00
this.setState({ showSongInfo: true });
shouldConnectSocket = true;
}
if (shouldConnectSocket) {
2022-10-11 13:22:01 +00:00
if (this.state.isConnectedToSocket) return;
2022-10-10 08:34:38 +00:00
this.connectSocket(params.socketaddress);
}
}
// Handle Errors
2022-10-19 18:12:42 +00:00
static getDerivedStateFromError(error) {
return this.setState({ hasError: true });
2022-10-19 18:12:42 +00:00
}
componentDidCatch(error, errorInfo) {
console.log({ error, errorInfo });
}
// ---
2022-10-19 18:12:42 +00:00
// I'd love if HTTP Status just gave this data lmao
// HttpSiraStatus(https://github.com/denpadokei/HttpSiraStatus) does give this data.
isCurrentSongTimeProvided = false;
// we don't need to reset this to false because it is highly unlikely for a player to swap mods within a browser session
/**
* Setup the timer for the song time
*/
setupTimer() {
setInterval(() => {
if (this.isCurrentSongTimeProvided) {
return;
}
if (!this.state.paused && this.state.beatSaverData !== undefined) {
this.setState({ currentSongTime: this.state.currentSongTime + 1 });
}
}, 1000);
}
/**
* Update the current song time
*
* @param {[]} data The song data
*/
handleCurrentSongTime(data) {
try {
const time = data.status.performance.currentSongTime;
if (time !== undefined && time != null) {
this.isCurrentSongTimeProvided = true;
this.setState({ currentSongTime: time });
}
} catch (e) {
// do nothing
}
}
2022-10-10 08:34:38 +00:00
/**
* Fetch and update the data from the respective platform
2022-10-14 19:00:47 +00:00
*
2022-10-10 08:34:38 +00:00
* @param {string} id The steam id of the player
2022-10-14 19:00:47 +00:00
* @returns
2022-10-10 08:34:38 +00:00
*/
2022-10-14 19:00:47 +00:00
async updateData(id) {
const data = await fetch(
2022-10-20 14:59:03 +00:00
Utils.getWebsiteApi(this.state.websiteType).ApiUrl.PlayerData.replace(
"%s",
id
),
2022-10-14 19:00:47 +00:00
{
2022-10-17 11:58:07 +00:00
headers: {
"X-Requested-With": "BeatSaber Overlay",
},
2022-10-14 19:00:47 +00:00
}
);
try {
const json = await data.json();
this.setState({
loadingPlayerData: false,
id: id,
data: json,
});
} catch (e) {
// Catch error and use last known data
console.error(e);
2022-10-10 08:34:38 +00:00
}
}
/**
* Checks if the given steam id is valid or not
*
* @param {id} The Steam ID of the player to validate
*/
async validateSteamId(id) {
2022-10-20 10:09:41 +00:00
if (id.length !== 17) {
return false;
}
const data = await fetch(`/api/validateid?steamid=${id}`);
const json = await data.json();
2022-10-20 10:09:41 +00:00
return json.message === "Valid";
2022-10-10 08:34:38 +00:00
}
/**
* Setup the HTTP Status connection
*/
connectSocket(socketAddress) {
2022-10-14 19:00:47 +00:00
socketAddress =
(socketAddress === undefined
? "ws://localhost"
: `ws://${socketAddress}`) + ":6557/socket";
2022-10-11 13:22:01 +00:00
if (this.state.isConnectedToSocket) return;
if (this.state.isVisible) {
this.resetData(false);
}
2022-10-10 08:34:38 +00:00
console.log(`Connecting to ${socketAddress}`);
const socket = new WebSocket(socketAddress);
2022-10-14 19:00:47 +00:00
socket.addEventListener("open", () => {
console.log(`Connected to ${socketAddress}`);
2022-10-11 13:22:01 +00:00
this.setState({ isConnectedToSocket: true });
2022-10-14 19:00:47 +00:00
});
socket.addEventListener("close", () => {
console.log(
2022-10-17 11:58:07 +00:00
"Attempting to re-connect to the HTTP Status socket in 60 seconds."
2022-10-14 19:00:47 +00:00
);
2022-10-20 14:04:49 +00:00
this.resetData(false);
2022-10-11 13:22:01 +00:00
this.setState({ isConnectedToSocket: false });
2022-10-17 11:58:07 +00:00
setTimeout(() => this.connectSocket(), 60_000);
2022-10-10 08:34:38 +00:00
});
2022-10-14 19:00:47 +00:00
socket.addEventListener("message", (message) => {
2022-10-10 08:34:38 +00:00
const json = JSON.parse(message.data);
2022-10-14 19:00:47 +00:00
this.handleCurrentSongTime(json);
2022-10-10 08:34:38 +00:00
if (!this.handlers[json.event]) {
console.log("Unhandled message from HTTP Status. (" + json.event + ")");
return;
}
this.handlers[json.event](json || []);
2022-10-14 19:00:47 +00:00
});
2022-10-10 08:34:38 +00:00
this.setState({ socket: socket });
}
/**
* Set the current songs beat saver url in {@link #_beatSaverURL}
2022-10-14 19:00:47 +00:00
*
* @param {[]} songData
2022-10-10 08:34:38 +00:00
*/
async setBeatSaver(songData) {
2022-10-14 19:00:47 +00:00
console.log("Updating BeatSaver info");
const data = await fetch(
this.#_beatSaverURL.replace("%s", songData.levelId)
);
2022-10-10 08:34:38 +00:00
const json = await data.json();
2022-10-14 19:00:47 +00:00
this.setState({ beatSaverData: json });
2022-10-20 14:04:49 +00:00
const { characteristic, levelId, difficulty } = songData;
let mapHash = levelId.replace("custom_level_", "");
let mapStars = undefined;
if (this.state.websiteType === "BeatLeader") {
mapStars = await LeaderboardType.BeatLeader.getMapStarCount(
2022-10-20 14:04:49 +00:00
mapHash,
2022-10-20 17:27:43 +00:00
difficulty.replace("+", "Plus"),
2022-10-20 14:04:49 +00:00
characteristic
);
}
if (this.state.websiteType === "ScoreSaber") {
mapStars = await LeaderboardType.ScoreSaber.getMapStarCount(
mapHash,
difficulty.replace("+", "Plus"),
characteristic
);
}
this.setState({ mapStarCount: mapStars });
2022-10-10 08:34:38 +00:00
}
/**
* Cleanup the data and get ready for the next song
2022-10-14 19:00:47 +00:00
*
2022-10-10 08:34:38 +00:00
* @param {boolean} visible Whether to show info other than the player stats
*/
2022-10-20 11:40:21 +00:00
async resetData(visible, loadedDuringSong = false) {
if (this.state.showPlayerStats == true) {
setTimeout(async () => {
await this.updateData(this.state.id);
}, 1000); // 1 second
}
this.cutData = [];
this.cutData.SaberA = {
count: [0, 0, 0],
totalScore: [0, 0, 0],
};
this.cutData.SaberB = {
count: [0, 0, 0],
totalScore: [0, 0, 0],
};
2022-10-10 08:34:38 +00:00
this.setState({
2022-10-21 11:55:03 +00:00
SaberA: {
cutDistanceScore: 0.0,
averagePreSwing: 0.0,
averagePostSwing: 0.0,
2022-10-10 08:34:38 +00:00
},
2022-10-21 11:55:03 +00:00
SaberB: {
cutDistanceScore: 0.0,
averagePreSwing: 0.0,
averagePostSwing: 0.0,
2022-10-10 08:34:38 +00:00
},
songInfo: undefined,
beatSaverData: undefined,
currentSongTime: 0,
currentScore: 0,
percentage: "100.00%",
2022-10-14 19:00:47 +00:00
isVisible: visible,
2022-10-20 14:04:49 +00:00
loadedDuringSong: loadedDuringSong,
mapStarCount: undefined,
2022-10-10 08:34:38 +00:00
});
}
// The HTTP Status handlers
handlers = {
2022-10-14 19:00:47 +00:00
hello: (data) => {
2022-10-10 08:34:38 +00:00
console.log("Hello from HTTP Status!");
2022-10-20 14:04:49 +00:00
if (
data.status &&
data.status.beatmap &&
this.state.songData === undefined
) {
console.log("Going into level during song, resetting data.");
this.resetData(true, true);
this.setState({ songData: data, paused: false });
2022-10-21 16:37:31 +00:00
if (this.state.showScore || this.state.showSongInfo) {
this.setBeatSaver(data.status.beatmap);
}
2022-10-10 08:34:38 +00:00
}
},
2022-10-14 19:00:47 +00:00
scoreChanged: (data) => {
2022-10-10 08:34:38 +00:00
const { status } = data;
2022-10-19 20:15:18 +00:00
const { score, relativeScore } = status.performance;
let finalScore = score;
if (finalScore == 0) {
finalScore = this.state.currentScore;
}
const percent = relativeScore * 100;
2022-10-10 08:34:38 +00:00
this.setState({
2022-10-19 20:15:18 +00:00
currentScore: finalScore,
percentage: percent.toFixed(2) + "%",
2022-10-14 19:00:47 +00:00
});
2022-10-10 08:34:38 +00:00
},
2022-10-14 19:00:47 +00:00
noteFullyCut: (data) => {
2022-10-10 08:34:38 +00:00
const { noteCut } = data;
let beforeCutScore = 0.0;
let afterCutScore = 0.0;
let cutDistanceScore = 0.0;
const cutDataSaber = this.cutData[noteCut.saberType];
cutDataSaber.count[0]++;
cutDataSaber.count[1]++;
cutDataSaber.count[2]++;
cutDataSaber.totalScore[0] += noteCut.beforeCutScore;
cutDataSaber.totalScore[1] += noteCut.afterCutScore;
cutDataSaber.totalScore[2] += noteCut.cutDistanceScore;
beforeCutScore = cutDataSaber.totalScore[0] / cutDataSaber.count[0];
afterCutScore = cutDataSaber.totalScore[1] / cutDataSaber.count[1];
cutDistanceScore = cutDataSaber.totalScore[2] / cutDataSaber.count[2];
2022-10-21 11:39:42 +00:00
const cutData = this.state[noteCut.saberType];
cutData.averagePreSwing = beforeCutScore;
cutData.averagePostSwing = afterCutScore;
cutData.cutDistanceScore = cutDistanceScore;
2022-10-21 11:39:42 +00:00
this.setState({ [noteCut.saberType]: cutData });
2022-10-10 08:34:38 +00:00
},
2022-10-14 19:00:47 +00:00
songStart: (data) => {
console.log("Going into level, resetting data.");
2022-10-10 08:34:38 +00:00
this.resetData(true);
2022-10-14 19:00:47 +00:00
this.setState({ songData: data, paused: false });
2022-10-21 16:37:31 +00:00
if (this.state.showScore || this.state.showSongInfo) {
2022-10-19 09:54:17 +00:00
this.setBeatSaver(data.status.beatmap);
}
2022-10-10 08:34:38 +00:00
},
2022-10-14 19:00:47 +00:00
finished: () => {
2022-10-10 08:34:38 +00:00
this.resetData(false);
},
2022-10-14 19:00:47 +00:00
softFail: () => {
2022-10-10 08:34:38 +00:00
this.setState({ failed: true });
},
2022-10-14 19:00:47 +00:00
pause: () => {
2022-10-10 08:34:38 +00:00
this.setState({ paused: true });
},
2022-10-14 19:00:47 +00:00
resume: () => {
2022-10-10 08:34:38 +00:00
this.setState({ paused: false });
},
2022-10-14 19:00:47 +00:00
menu: () => {
2022-10-10 08:34:38 +00:00
this.resetData(false);
},
2022-10-14 19:00:47 +00:00
noteCut: () => {},
noteMissed: () => {},
noteSpawned: () => {},
bombMissed: () => {},
beatmapEvent: () => {},
energyChanged: () => {},
2022-10-21 09:30:27 +00:00
obstacleEnter: () => {},
obstacleExit: () => {},
2022-10-14 19:00:47 +00:00
};
2022-10-10 08:34:38 +00:00
render() {
2022-10-19 10:03:42 +00:00
const {
isValidSteamId,
data,
websiteType,
showPlayerStats,
loadingPlayerData,
2022-10-19 18:12:42 +00:00
id,
2022-10-19 10:03:42 +00:00
} = this.state;
2022-10-10 08:34:38 +00:00
2022-10-19 20:15:18 +00:00
if (loadingPlayerData) {
return <Spinner size="xl" color="white"></Spinner>;
}
2022-10-14 19:00:47 +00:00
return (
2022-10-26 10:52:58 +00:00
<>
2022-10-21 09:56:50 +00:00
<NextSeo title="Overlay"></NextSeo>
<div className={styles.main}>
{!isValidSteamId ? (
<div className={styles.invalidPlayer}>
<h1>Invalid player, please visit the main page.</h1>
<Link href="/">
<a>Go Home</a>
</Link>
</div>
) : (
<div className={styles.overlay}>
{showPlayerStats && !loadingPlayerData ? (
<PlayerStats
2022-10-21 16:28:18 +00:00
pp={data.pp.toLocaleString("en-US", {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
})}
2022-10-21 09:56:50 +00:00
globalPos={data.rank.toLocaleString()}
country={data.country}
countryRank={data.countryRank.toLocaleString()}
websiteType={websiteType}
2022-10-26 10:52:58 +00:00
avatar={`https://cdn.scoresaber.com/avatars/${id}.jpg`}
2022-10-21 09:56:50 +00:00
loadedDuringSong={this.state.loadedDuringSong}
/>
) : (
<></>
)}
{this.state.showScore && this.state.isVisible ? (
<ScoreStats data={this.state} />
) : (
<></>
)}
{this.state.showSongInfo &&
this.state.beatSaverData !== undefined &&
this.state.isVisible ? (
<SongInfo data={this.state} />
) : (
<></>
)}
</div>
)}
</div>
2022-10-26 10:52:58 +00:00
</>
2022-10-14 19:00:47 +00:00
);
2022-10-10 08:34:38 +00:00
}
2022-10-14 19:00:47 +00:00
}