mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-26 01:58:52 -05:00
220 lines
11 KiB
TypeScript
220 lines
11 KiB
TypeScript
|
/*
|
||
|
* Vencord, a Discord client mod
|
||
|
* Copyright (c) 2024 Vendicated and contributors
|
||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
*/
|
||
|
|
||
|
import { Devs } from "@utils/constants";
|
||
|
import definePlugin from "@utils/types";
|
||
|
import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common";
|
||
|
|
||
|
import { Activity, ActivityType, BanchoStatusEnum, GameState, Modes, TosuApi, UserLoginStatus } from "./type";
|
||
|
|
||
|
const socketId = "tosu";
|
||
|
const OSU_APP_ID = "367827983903490050";
|
||
|
const OSU_LARGE_IMAGE = "373344233077211136";
|
||
|
const OSU_STARDARD_SMALL_IMAGE = "373370493127884800";
|
||
|
const OSU_MANIA_SMALL_IMAGE = "373370588703621136";
|
||
|
const OSU_TAIKO_SMALL_IMAGE = "373370519891738624";
|
||
|
const OSU_CATCH_SMALL_IMAGE = "373370543161999361";
|
||
|
|
||
|
const throttledOnMessage = throttle(onMessage, 3000, () => FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" }));
|
||
|
|
||
|
let ws: WebSocket;
|
||
|
let wsReconnect: NodeJS.Timeout;
|
||
|
|
||
|
export default definePlugin({
|
||
|
name: "TosuRPC",
|
||
|
description: "osu! RPC with data from tosu",
|
||
|
authors: [Devs.AutumnVN],
|
||
|
start() {
|
||
|
(function connect() {
|
||
|
ws = new WebSocket("ws://localhost:24050/websocket/v2");
|
||
|
ws.addEventListener("error", () => ws.close());
|
||
|
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
|
||
|
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
|
||
|
})();
|
||
|
},
|
||
|
stop() {
|
||
|
ws.close();
|
||
|
clearTimeout(wsReconnect);
|
||
|
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });
|
||
|
}
|
||
|
});
|
||
|
|
||
|
|
||
|
async function onMessage(data: string) {
|
||
|
const json: TosuApi = JSON.parse(data);
|
||
|
// @ts-ignore
|
||
|
if (json.error) return FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });
|
||
|
|
||
|
const { state, session, profile, beatmap, play, resultsScreen } = json;
|
||
|
|
||
|
const activity: Activity = {
|
||
|
application_id: OSU_APP_ID,
|
||
|
name: "osu!",
|
||
|
type: ActivityType.PLAYING,
|
||
|
assets: {
|
||
|
large_image: OSU_LARGE_IMAGE,
|
||
|
large_text: profile.userStatus.number === UserLoginStatus.Connected ? `${profile.name} | #${profile.globalRank} | ${Math.round(profile.pp)}pp` : undefined,
|
||
|
},
|
||
|
timestamps: {
|
||
|
start: Date.now() - session.playTime
|
||
|
},
|
||
|
flags: 1 << 0,
|
||
|
};
|
||
|
|
||
|
switch (profile.mode.number) {
|
||
|
case Modes.Osu:
|
||
|
activity.assets.small_image = OSU_STARDARD_SMALL_IMAGE;
|
||
|
activity.assets.small_text = "osu!";
|
||
|
break;
|
||
|
case Modes.Mania:
|
||
|
activity.assets.small_image = OSU_MANIA_SMALL_IMAGE;
|
||
|
activity.assets.small_text = "osu!mania";
|
||
|
break;
|
||
|
case Modes.Taiko:
|
||
|
activity.assets.small_image = OSU_TAIKO_SMALL_IMAGE;
|
||
|
activity.assets.small_text = "osu!taiko";
|
||
|
break;
|
||
|
case Modes.Fruits:
|
||
|
activity.assets.small_image = OSU_CATCH_SMALL_IMAGE;
|
||
|
activity.assets.small_text = "osu!catch";
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
let player = "";
|
||
|
let mods = "";
|
||
|
let fc = "";
|
||
|
let combo = "";
|
||
|
let h100 = "";
|
||
|
let h50 = "";
|
||
|
let h0 = "";
|
||
|
let sb = "";
|
||
|
let pp = "";
|
||
|
switch (state.number) {
|
||
|
case GameState.Play:
|
||
|
activity.type = profile.banchoStatus.number === BanchoStatusEnum.Playing ? ActivityType.PLAYING : ActivityType.WATCHING;
|
||
|
|
||
|
player = profile.banchoStatus.number === BanchoStatusEnum.Playing ? "" : `${play.playerName} | `;
|
||
|
mods = play.mods.name ? `+${play.mods.name} ` : "";
|
||
|
activity.name = `${player}${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
|
||
|
|
||
|
combo = play.hits[0] === 0 && play.hits.sliderBreaks === 0
|
||
|
? `${play.combo.current}x`
|
||
|
: `${play.combo.current}x/${play.combo.max}x`;
|
||
|
pp = play.hits[0] === 0 && play.hits.sliderBreaks === 0
|
||
|
? `${Math.round(play.pp.current)}pp`
|
||
|
: `${Math.round(play.pp.current)}pp/${Math.round(play.pp.fc)}pp`;
|
||
|
activity.details = `${play.accuracy.toFixed(2)}% | ${combo} | ${pp}`;
|
||
|
|
||
|
h100 = play.hits[100] > 0 ? `${play.hits[100]}x100` : "";
|
||
|
h50 = play.hits[50] > 0 ? `${play.hits[50]}x50` : "";
|
||
|
h0 = play.hits[0] > 0 ? `${play.hits[0]}xMiss` : "";
|
||
|
sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
|
||
|
activity.state = [h100, h50, h0, sb].filter(Boolean).join(" | ");
|
||
|
|
||
|
const playRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${play.rank.current.toLowerCase().replace("x", "ss")}.png`);
|
||
|
activity.assets.small_image = playRank;
|
||
|
activity.assets.small_text = undefined;
|
||
|
break;
|
||
|
case GameState.ResultScreen:
|
||
|
activity.type = ActivityType.WATCHING;
|
||
|
|
||
|
mods = resultsScreen.mods.name ? `+${resultsScreen.mods.name} ` : "";
|
||
|
activity.name = `${resultsScreen.playerName} | ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
|
||
|
|
||
|
fc = resultsScreen.maxCombo === beatmap.stats.maxCombo ? "FC" : `| ${resultsScreen.maxCombo}x/${beatmap.stats.maxCombo}x`;
|
||
|
pp = !resultsScreen.pp.current ? ""
|
||
|
: Math.round(resultsScreen.pp.current) === Math.round(resultsScreen.pp.fc)
|
||
|
? `| ${Math.round(resultsScreen.pp.current)}pp`
|
||
|
: `| ${Math.round(resultsScreen.pp.current)}pp/${Math.round(resultsScreen.pp.fc)}pp`;
|
||
|
activity.details = `${resultsScreen.accuracy.toFixed(2)}% ${fc} ${pp}`;
|
||
|
|
||
|
h100 = resultsScreen.hits[100] > 0 ? `${resultsScreen.hits[100]}x100` : "";
|
||
|
h50 = resultsScreen.hits[50] > 0 ? `${resultsScreen.hits[50]}x50` : "";
|
||
|
h0 = resultsScreen.hits[0] > 0 ? `${resultsScreen.hits[0]}xMiss` : "";
|
||
|
sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
|
||
|
activity.state = [h100, h50, h0].filter(Boolean).join(" | ");
|
||
|
|
||
|
const resultRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${resultsScreen.rank.toLowerCase().replace("x", "ss")}.png`);
|
||
|
activity.assets.small_image = resultRank;
|
||
|
activity.assets.small_text = undefined;
|
||
|
break;
|
||
|
default:
|
||
|
activity.type = ActivityType.LISTENING;
|
||
|
mods = play.mods.name ? `+${play.mods.name} ` : "";
|
||
|
activity.name = `${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
|
||
|
|
||
|
switch (state.number) {
|
||
|
case GameState.Menu: activity.details = "Main Menu"; break;
|
||
|
case GameState.Edit: activity.details = "Edit"; break;
|
||
|
case GameState.SelectEdit: activity.details = "Song Select (Edit)"; break;
|
||
|
case GameState.SelectPlay: activity.details = "Song Select (Play)"; break;
|
||
|
case GameState.SelectDrawings: activity.details = "Select Drawings"; break;
|
||
|
case GameState.Update: activity.details = "Update"; break;
|
||
|
case GameState.Busy: activity.details = "Busy"; break;
|
||
|
case GameState.Lobby: activity.details = "Lobby"; break;
|
||
|
case GameState.MatchSetup: activity.details = "Match Setup"; break;
|
||
|
case GameState.SelectMulti: activity.details = "Select Multi"; break;
|
||
|
case GameState.RankingVs: activity.details = "Ranking Vs"; break;
|
||
|
case GameState.OnlineSelection: activity.details = "Online Selection"; break;
|
||
|
case GameState.OptionsOffsetWizard: activity.details = "Options Offset Wizard"; break;
|
||
|
case GameState.RankingTagCoop: activity.details = "Ranking Tag Coop"; break;
|
||
|
case GameState.RankingTeam: activity.details = "Ranking Team"; break;
|
||
|
case GameState.BeatmapImport: activity.details = "Beatmap Import"; break;
|
||
|
case GameState.PackageUpdater: activity.details = "Package Updater"; break;
|
||
|
case GameState.Benchmark: activity.details = "Benchmark"; break;
|
||
|
case GameState.Tourney: activity.details = "Tourney"; break;
|
||
|
case GameState.Charts: activity.details = "Charts"; break;
|
||
|
}
|
||
|
|
||
|
switch (profile.banchoStatus.number) {
|
||
|
case BanchoStatusEnum.Idle: activity.state = "Idle"; break;
|
||
|
case BanchoStatusEnum.Afk: activity.state = "AFK"; break;
|
||
|
case BanchoStatusEnum.Playing: activity.state = "Playing"; break;
|
||
|
case BanchoStatusEnum.Editing: activity.state = "Editing"; break;
|
||
|
case BanchoStatusEnum.Modding: activity.state = "Modding"; break;
|
||
|
case BanchoStatusEnum.Multiplayer: activity.state = "Multiplayer"; break;
|
||
|
case BanchoStatusEnum.Watching: activity.state = "Watching"; break;
|
||
|
case BanchoStatusEnum.Testing: activity.state = "Testing"; break;
|
||
|
case BanchoStatusEnum.Submitting: activity.state = "Submitting"; break;
|
||
|
case BanchoStatusEnum.Paused: activity.state = "Paused"; break;
|
||
|
case BanchoStatusEnum.Lobby: activity.state = "Lobby"; break;
|
||
|
case BanchoStatusEnum.Multiplaying: activity.state = "Multiplaying"; break;
|
||
|
case BanchoStatusEnum.OsuDirect: activity.state = "osu!direct"; break;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (beatmap.set > 0) {
|
||
|
const mapBg = await getAsset(`https://assets.ppy.sh/beatmaps/${beatmap.set}/covers/list@2x.jpg`);
|
||
|
const res = await fetch(mapBg.replace(/^mp:/, "https://media.discordapp.net/"), { method: "HEAD" });
|
||
|
if (res.ok) activity.assets.large_image = mapBg;
|
||
|
}
|
||
|
|
||
|
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity, socketId: "tosu" });
|
||
|
}
|
||
|
|
||
|
function throttle<T extends Function>(func: T, limit: number, timedOutCallback?: () => void): T {
|
||
|
let inThrottle: boolean;
|
||
|
let callbackTimeout: NodeJS.Timeout;
|
||
|
return function (this: any, ...args: any[]) {
|
||
|
if (!inThrottle) {
|
||
|
func.apply(this, args);
|
||
|
inThrottle = true;
|
||
|
setTimeout(() => inThrottle = false, limit);
|
||
|
if (timedOutCallback) {
|
||
|
clearTimeout(callbackTimeout);
|
||
|
callbackTimeout = setTimeout(timedOutCallback, limit * 2);
|
||
|
}
|
||
|
}
|
||
|
} as any;
|
||
|
}
|
||
|
|
||
|
async function getAsset(key: string): Promise<string> {
|
||
|
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
|
||
|
return (await ApplicationAssetUtils.fetchAssetIds(OSU_APP_ID, [key]))[0];
|
||
|
}
|