class WebSocketManager { constructor(host) { this.version = '0.1.4'; if (host) { this.host = host; } this.createConnection = this.createConnection.bind(this); /** * @type {{ [key: string]: WebSocket }} asd; */ this.sockets = {}; } createConnection(url, callback, filters) { let INTERVAL = ''; const that = this; this.sockets[url] = new WebSocket(`ws://${this.host}${url}?l=${encodeURI(window.COUNTER_PATH)}`); this.sockets[url].onopen = () => { console.log(`[OPEN] ${url}: Connected`); if (INTERVAL) clearInterval(INTERVAL); if (Array.isArray(filters)) { this.sockets[url].send(`applyFilters:${JSON.stringify(filters)}`); } }; this.sockets[url].onclose = (event) => { console.log(`[CLOSED] ${url}: ${event.reason}`); delete this.sockets[url]; INTERVAL = setTimeout(() => { that.createConnection(url, callback, filters); }, 1000); }; this.sockets[url].onerror = (event) => { console.log(`[ERROR] ${url}: ${event.reason}`); }; this.sockets[url].onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.error != null) { console.error(`[MESSAGE_ERROR] ${url}:`, data.error); return; }; if (data.message != null) { if (data.message.error != null) { console.error(`[MESSAGE_ERROR] ${url}:`, data.message.error); return; } }; callback(data); } catch (error) { console.log(`[MESSAGE_ERROR] ${url}: Couldn't parse incomming message`, error); }; }; }; /** * Connects to gosu compatible socket api. * @param {(data: WEBSOCKET_V1) => void} callback The function to handle received messages. * @param {Filters[]} filters */ api_v1(callback, filters) { this.createConnection(`/ws`, callback, filters); }; /** * Connects to tosu advanced socket api. * @param {(data: WEBSOCKET_V2) => void} callback The function to handle received messages. * @param {Filters[]} filters */ api_v2(callback, filters) { this.createConnection(`/websocket/v2`, callback, filters); }; /** * Connects to tosu precise socket api. * @param {(data: WEBSOCKET_V2_PRECISE) => void} callback The function to handle received messages. * @param {Filters[]} filters */ api_v2_precise(callback, filters) { this.createConnection(`/websocket/v2/precise`, callback, filters); }; /** * Calculate custom pp for a current, or specified map * @param {CALCULATE_PP} params * @returns {Promise<CALCULATE_PP_RESPONSE | { error: string }>} */ async calculate_pp(params) { try { if (typeof params != 'object') { return { error: 'Wrong argument type, should be object with params' }; }; const url = new URL(`http://${this.host}/api/calculate/pp`); Object.keys(params) .forEach(key => url.searchParams.append(key, params[key])); const request = await fetch(url, { method: "GET", }); const json = await request.json(); return json; } catch (error) { console.error(error); return { error: error.message, }; }; }; /** * Get beatmap **.osu** file (local) * @param {string} file_path Path to a file **beatmap_folder_name/osu_file_name.osu** * @returns {string | { error: string }} */ async getBeatmapOsuFile(file_path) { try { if (typeof file_path != 'object') { return { error: 'Wrong argument type, should be object with params' }; }; const request = await fetch(`${this.host}/files/beatmap/${file_path}`, { method: "GET", }); const text = await request.text(); return text; } catch (error) { console.error(error); return { error: error.message, }; }; }; /** * Connects to message * @param {(data: { command: string, message: any }) => void} callback The function to handle received messages. */ commands(callback) { this.createConnection(`/websocket/commands`, callback); }; /** * * @param {string} name * @param {string|Object} payload */ sendCommand(name, command, amountOfRetries = 1) { const that = this; if (!this.sockets['/websocket/commands']) { setTimeout(() => { that.sendCommand(name, command, amountOfRetries + 1); }, 100); return; }; try { const payload = typeof command == 'object' ? JSON.stringify(command) : command; this.sockets['/websocket/commands'].send(`${name}:${payload}`); } catch (error) { if (amountOfRetries <= 3) { console.log(`[COMMAND_ERROR] Attempt ${amountOfRetries}`, error); setTimeout(() => { that.sendCommand(name, command, amountOfRetries + 1); }, 1000); return; }; console.error(`[COMMAND_ERROR]`, error); }; }; close(url) { this.host = url; const array = Object.keys(this.sockets); for (let i = 0; i < array.length; i++) { const key = array[i]; const value = this.sockets[key]; if (!value) continue; value.close(); }; }; }; export default WebSocketManager; /** * @typedef {string | { field: string; keys: Filters[] }} Filters */ /** @typedef {object} CALCULATE_PP * @property {string} path Path to .osu file. Example: C:/osu/Songs/beatmap/file.osu * @property {number} mode Osu = 0, Taiko = 1, Catch = 2, Mania = 3 * @property {number} mods Mods id. Example: 64 - DT * @property {number} acc Accuracy % from 0 to 100 * @property {number} nGeki Amount of Geki (300g / MAX) * @property {number} nKatu Amount of Katu (100k / 200) * @property {number} n300 Amount of 300 * @property {number} n100 Amount of 100 * @property {number} n50 Amount of 50 * @property {number} nMisses Amount of Misses * @property {number} combo combo * @property {number} passedObjects Sum of nGeki, nKatu, n300, n100, n50, nMisses * @property {number} clockRate Map rate number. Example: 1.5 = DT */ /** @typedef {object} CALCULATE_PP_RESPONSE * @property {object} difficulty * @property {number} difficulty.mode * @property {number} difficulty.stars * @property {boolean} difficulty.isConvert * @property {number} difficulty.aim * @property {number} difficulty.speed * @property {number} difficulty.flashlight * @property {number} difficulty.sliderFactor * @property {number} difficulty.speedNoteCount * @property {number} difficulty.od * @property {number} difficulty.hp * @property {number} difficulty.nCircles * @property {number} difficulty.nSliders * @property {number} difficulty.nSpinners * @property {number} difficulty.ar * @property {number} difficulty.maxCombo * @property {object} state * @property {number} state.maxCombo * @property {number} state.nGeki * @property {number} state.nKatu * @property {number} state.n300 * @property {number} state.n100 * @property {number} state.n50 * @property {number} state.misses * @property {number} pp * @property {number} ppAim * @property {number} ppFlashlight * @property {number} ppSpeed * @property {number} ppAccuracy * @property {number} effectiveMissCount */ /** @typedef {object} WEBSOCKET_V1 * @property {'stable' | 'lazer'} client * @property {object} settings * @property {boolean} settings.showInterface * @property {object} settings.folders * @property {string} settings.folders.game * @property {string} settings.folders.skin * @property {string} settings.folders.songs * @property {object} menu * @property {object} menu.mainMenu * @property {number} menu.mainMenu.bassDensity * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23} menu.state * @property {0 | 1 | 2 | 3} menu.gameMode * @property {0 | 1} menu.isChatEnabled * @property {object} menu.bm * @property {object} menu.bm.time * @property {number} menu.bm.time.firstObj * @property {number} menu.bm.time.current * @property {number} menu.bm.time.full * @property {number} menu.bm.time.mp3 * @property {number} menu.bm.id * @property {number} menu.bm.set * @property {string} menu.bm.md5 * @property {0 | 1 | 2 | 4 | 5 | 6 | 7} menu.bm.rankedStatus * @property {object} menu.bm.metadata * @property {string} menu.bm.metadata.artist * @property {string} menu.bm.metadata.artistOriginal * @property {string} menu.bm.metadata.title * @property {string} menu.bm.metadata.titleOriginal * @property {string} menu.bm.metadata.mapper * @property {string} menu.bm.metadata.difficulty * @property {object} menu.bm.stats * @property {number} menu.bm.stats.AR * @property {number} menu.bm.stats.CS * @property {number} menu.bm.stats.OD * @property {number} menu.bm.stats.HP * @property {number} menu.bm.stats.SR * @property {object} menu.bm.stats.BPM * @property {number} menu.bm.stats.BPM.realtime * @property {number} menu.bm.stats.BPM.common * @property {number} menu.bm.stats.BPM.min * @property {number} menu.bm.stats.BPM.max * @property {number} menu.bm.stats.circles * @property {number} menu.bm.stats.sliders * @property {number} menu.bm.stats.spinners * @property {number} menu.bm.stats.holds * @property {number} menu.bm.stats.maxCombo * @property {number} menu.bm.stats.fullSR * @property {number} menu.bm.stats.memoryAR * @property {number} menu.bm.stats.memoryCS * @property {number} menu.bm.stats.memoryOD * @property {number} menu.bm.stats.memoryHP * @property {object} menu.bm.path * @property {string} menu.bm.path.full * @property {string} menu.bm.path.folder * @property {string} menu.bm.path.file * @property {string} menu.bm.path.bg * @property {string} menu.bm.path.audio * @property {object} menu.mods * @property {number} menu.mods.num * @property {string} menu.mods.str * @property {object} menu.pp * @property {number} menu.pp.90 * @property {number} menu.pp.91 * @property {number} menu.pp.92 * @property {number} menu.pp.93 * @property {number} menu.pp.94 * @property {number} menu.pp.95 * @property {number} menu.pp.96 * @property {number} menu.pp.97 * @property {number} menu.pp.98 * @property {number} menu.pp.99 * @property {number} menu.pp.100 * @property {number[]} menu.pp.strains * @property {object} menu.pp.strainsAll * @property {object[]} menu.pp.strainsAll.series * @property {'aim' | 'aimNoSliders' | 'flashlight' | 'speed' | 'color' | 'rhythm' | 'stamina' | 'movement' | 'strains'} menu.pp.strainsAll.series.name * @property {number[]} menu.pp.strainsAll.series.data * @property {number[]} menu.pp.strainsAll.xaxis * @property {object} gameplay * @property {0 | 1 | 2 | 3} gameplay.gameMode * @property {string} gameplay.name * @property {number} gameplay.score * @property {number} gameplay.accuracy * @property {object} gameplay.combo * @property {number} gameplay.combo.current * @property {number} gameplay.combo.max * @property {object} gameplay.hp * @property {number} gameplay.hp.normal * @property {number} gameplay.hp.smooth * @property {object} gameplay.hits * @property {number} gameplay.hits.0 * @property {number} gameplay.hits.50 * @property {number} gameplay.hits.100 * @property {number} gameplay.hits.300 * @property {number} gameplay.hits.geki This is also used as the 320's count in the osu!mania ruleset * @property {number} gameplay.hits.katu This is also used as the 200's count in the osu!mania ruleset * @property {number} gameplay.hits.sliderBreaks * @property {object} gameplay.hits.grade * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} gameplay.hits.grade.current * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} gameplay.hits.grade.maxThisPlay * @property {number} gameplay.hits.unstableRate * @property {number[]} gameplay.hits.hitErrorArray * @property {object} gameplay.pp * @property {number} gameplay.pp.current * @property {number} gameplay.pp.fc * @property {number} gameplay.pp.maxThisPlay * @property {object} gameplay.keyOverlay * @property {object} gameplay.keyOverlay.k1 * @property {boolean} gameplay.keyOverlay.k1.isPressed * @property {number} gameplay.keyOverlay.k1.count * @property {object} gameplay.keyOverlay.k2 * @property {boolean} gameplay.keyOverlay.k2.isPressed * @property {number} gameplay.keyOverlay.k2.count * @property {object} gameplay.keyOverlay.m1 * @property {boolean} gameplay.keyOverlay.m1.isPressed * @property {number} gameplay.keyOverlay.m1.count * @property {object} gameplay.keyOverlay.m2 * @property {boolean} gameplay.keyOverlay.m2.isPressed * @property {number} gameplay.keyOverlay.m2.count * @property {object} gameplay.leaderboard * @property {boolean} gameplay.leaderboard.hasLeaderboard * @property {boolean} gameplay.leaderboard.isVisible * @property {object} gameplay.leaderboard.ourplayer * @property {string} gameplay.leaderboard.ourplayer.name * @property {number} gameplay.leaderboard.ourplayer.score * @property {number} gameplay.leaderboard.ourplayer.combo * @property {number} gameplay.leaderboard.ourplayer.maxCombo * @property {string} gameplay.leaderboard.ourplayer.mods * @property {number} gameplay.leaderboard.ourplayer.h300 * @property {number} gameplay.leaderboard.ourplayer.h100 * @property {number} gameplay.leaderboard.ourplayer.h50 * @property {number} gameplay.leaderboard.ourplayer.h0 * @property {number} gameplay.leaderboard.ourplayer.team * @property {number} gameplay.leaderboard.ourplayer.position * @property {number} gameplay.leaderboard.ourplayer.isPassing * @property {object[]} gameplay.leaderboard.slots * @property {string} gameplay.leaderboard.slots.name * @property {number} gameplay.leaderboard.slots.score * @property {number} gameplay.leaderboard.slots.combo * @property {number} gameplay.leaderboard.slots.maxCombo * @property {string} gameplay.leaderboard.slots.mods * @property {number} gameplay.leaderboard.slots.h300 * @property {number} gameplay.leaderboard.slots.h100 * @property {number} gameplay.leaderboard.slots.h50 * @property {number} gameplay.leaderboard.slots.h0 * @property {number} gameplay.leaderboard.slots.team * @property {number} gameplay.leaderboard.slots.position * @property {number} gameplay.leaderboard.slots.isPassing * @property {boolean} gameplay._isReplayUiHidden * @property {object} resultsScreen * @property {number} resultsScreen.0 * @property {number} resultsScreen.50 * @property {number} resultsScreen.100 * @property {number} resultsScreen.300 * @property {0 | 1 | 2 | 3} resultsScreen.mode * @property {string} resultsScreen.name * @property {number} resultsScreen.score * @property {number} resultsScreen.accuracy * @property {number} resultsScreen.maxCombo * @property {object} resultsScreen.mods * @property {number} resultsScreen.mods.num * @property {string} resultsScreen.mods.str * @property {number} resultsScreen.geki This is also used as the 320's count in the osu!mania ruleset * @property {number} resultsScreen.katu This is also used as the 200's count in the osu!mania ruleset * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} resultsScreen.grade * @property {string} resultsScreen.createdAt * @property {object} userProfile * @property {0 | 256 | 257 | 65537 | 65793} userProfile.rawLoginStatus * @property {string} userProfile.name * @property {number} userProfile.accuracy * @property {number} userProfile.rankedScore * @property {number} userProfile.id * @property {number} userProfile.level * @property {number} userProfile.playCount * @property {0 | 1 | 2 | 3} userProfile.playMode * @property {number} userProfile.rank * @property {number} userProfile.countryCode * @property {number} userProfile.performancePoints * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13} userProfile.rawBanchoStatus * @property {string} userProfile.backgroundColour * @property {object} tourney * @property {object} tourney.manager * @property {number} tourney.manager.ipcState * @property {number} tourney.manager.bestOF * @property {object} tourney.manager.teamName * @property {string} tourney.manager.teamName.left * @property {string} tourney.manager.teamName.right * @property {object} tourney.manager.stars * @property {number} tourney.manager.stars.left * @property {number} tourney.manager.stars.right * @property {object} tourney.manager.bools * @property {boolean} tourney.manager.bools.scoreVisible * @property {boolean} tourney.manager.bools.starsVisible * @property {object[]} tourney.manager.chat * @property {string} tourney.manager.chat.team * @property {string} tourney.manager.chat.time * @property {string} tourney.manager.chat.name * @property {string} tourney.manager.chat.messageBody * @property {object} tourney.manager.gameplay * @property {object} tourney.manager.gameplay.score * @property {number} tourney.manager.gameplay.score.left * @property {number} tourney.manager.gameplay.score.right * @property {object[]} tourney.ipcClients * @property {string} tourney.ipcClients.team * @property {object} tourney.ipcClients.spectating * @property {string} tourney.ipcClients.spectating.name * @property {string} tourney.ipcClients.spectating.country * @property {number} tourney.ipcClients.spectating.userID * @property {number} tourney.ipcClients.spectating.accuracy * @property {number} tourney.ipcClients.spectating.rankedScore * @property {number} tourney.ipcClients.spectating.playCount * @property {number} tourney.ipcClients.spectating.globalRank * @property {number} tourney.ipcClients.spectating.totalPP * @property {object} tourney.ipcClients.gameplay * @property {0 | 1 | 2 | 3} tourney.ipcClients.gameplay.gameMode * @property {string} tourney.ipcClients.gameplay.name * @property {number} tourney.ipcClients.gameplay.score * @property {number} tourney.ipcClients.gameplay.accuracy * @property {object} tourney.ipcClients.gameplay.combo * @property {number} tourney.ipcClients.gameplay.combo.current * @property {number} tourney.ipcClients.gameplay.combo.max * @property {object} tourney.ipcClients.gameplay.hp * @property {number} tourney.ipcClients.gameplay.hp.normal * @property {number} tourney.ipcClients.gameplay.hp.smooth * @property {object} tourney.ipcClients.gameplay.hits * @property {number} tourney.ipcClients.gameplay.hits.0 * @property {number} tourney.ipcClients.gameplay.hits.50 * @property {number} tourney.ipcClients.gameplay.hits.100 * @property {number} tourney.ipcClients.gameplay.hits.300 * @property {number} tourney.ipcClients.gameplay.hits.geki This is also used as the 320's count in the osu!mania ruleset * @property {number} tourney.ipcClients.gameplay.hits.katu This is also used as the 200's count in the osu!mania ruleset * @property {number} tourney.ipcClients.gameplay.hits.sliderBreaks * @property {number} tourney.ipcClients.gameplay.hits.unstableRate * @property {number[]} tourney.ipcClients.gameplay.hits.hitErrorArray * @property {object} tourney.ipcClients.gameplay.mods * @property {number} tourney.ipcClients.gameplay.mods.num * @property {string} tourney.ipcClients.gameplay.mods.str */ /** @typedef {object} WEBSOCKET_V2 * @property {'stable' | 'lazer'} client * @property {string} server * @property {object} state * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23} state.number * @property {'menu' | 'edit' | 'play' | 'exit' | 'selectEdit' | 'selectPlay' | 'selectDrawings' | 'resultScreen' | 'update' | 'busy' | 'unknown' | 'lobby' | 'matchSetup' | 'selectMulti' | 'rankingVs' | 'onlineSelection' | 'optionsOffsetWizard' | 'rankingTagCoop' | 'rankingTeam' | 'beatmapImport' | 'packageUpdater' | 'benchmark' | 'tourney' | 'charts'} state.name * @property {object} session * @property {number} session.playTime * @property {number} session.playCount * @property {object} settings * @property {boolean} settings.interfaceVisible * @property {boolean} settings.replayUIVisible * @property {object} settings.chatVisibilityStatus * @property {0 | 1 | 2} settings.chatVisibilityStatus.number * @property {'hidden' | 'visible' | 'visibleWithFriendsList'} settings.chatVisibilityStatus.name * @property {object} settings.leaderboard * @property {boolean} settings.leaderboard.visible * @property {object} settings.leaderboard.type * @property {0 | 1 | 2 | 3 | 4} settings.leaderboard.type.number * @property {'local' | 'global' | 'selectedmods' | 'friends' | 'country'} settings.leaderboard.type.name * @property {object} settings.progressBar * @property {0 | 1 | 2 | 3 | 4} settings.progressBar.number * @property {'off' | 'pie' | 'topRight' | 'bottomRight' | 'bottom'} settings.progressBar.name * @property {number} settings.bassDensity * @property {object} settings.resolution * @property {boolean} settings.resolution.fullscreen * @property {number} settings.resolution.width * @property {number} settings.resolution.height * @property {number} settings.resolution.widthFullscreen * @property {number} settings.resolution.heightFullscreen * @property {object} settings.client * @property {boolean} settings.client.updateAvailable * @property {0 | 1 | 2 | 3} settings.client.branch - 0: Cutting Edge * - 1: Stable * - 2: Beta * - 3: Stable (Fallback) * @property {string} settings.client.version The full build version, e.g. `b20241029cuttingedge` * @property {object} settings.scoreMeter * @property {object} settings.scoreMeter.type * @property {0 | 1 | 2} settings.scoreMeter.type.number * @property {'none' | 'colour' | 'error'} settings.scoreMeter.type.name * @property {number} settings.scoreMeter.size * @property {object} settings.cursor * @property {boolean} settings.cursor.useSkinCursor * @property {boolean} settings.cursor.autoSize * @property {number} settings.cursor.size * @property {object} settings.mouse * @property {boolean} settings.mouse.rawInput * @property {boolean} settings.mouse.disableButtons * @property {boolean} settings.mouse.disableWheel * @property {number} settings.mouse.sensitivity * @property {object} settings.mania * @property {boolean} settings.mania.speedBPMScale * @property {boolean} settings.mania.usePerBeatmapSpeedScale * @property {object} settings.sort * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7} settings.sort.number * @property {'artist' | 'bpm' | 'creator' | 'date' | 'difficulty' | 'length' | 'rank' | 'title'} settings.sort.name * @property {object} settings.group * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19} settings.group.number * @property {'none' | 'artist' | 'bPM' | 'creator' | 'date' | 'difficulty' | 'length' | 'rank' | 'myMaps' | 'search' | 'show_All' | 'title' | 'lastPlayed' | 'onlineFavourites' | 'maniaKeys' | 'mode' | 'collection' | 'rankedStatus'} settings.group.name Note: `search` and `show_All` share the same number - `12` * @property {object} settings.skin * @property {boolean} settings.skin.useDefaultSkinInEditor * @property {boolean} settings.skin.ignoreBeatmapSkins * @property {boolean} settings.skin.tintSliderBall * @property {boolean} settings.skin.useTaikoSkin * @property {string} settings.skin.name * @property {object} settings.mode * @property {0 | 1 | 2 | 3} settings.mode.number * @property {'osu' | 'taiko' | 'fruits' | 'mania'} settings.mode.name * @property {object} settings.audio * @property {boolean} settings.audio.ignoreBeatmapSounds * @property {boolean} settings.audio.useSkinSamples * @property {object} settings.audio.volume * @property {number} settings.audio.volume.master * @property {number} settings.audio.volume.music * @property {number} settings.audio.volume.effect * @property {object} settings.audio.offset * @property {number} settings.audio.offset.universal * @property {object} settings.background * @property {number} settings.background.dim * @property {boolean} settings.background.video * @property {boolean} settings.background.storyboard * @property {object} settings.keybinds * @property {object} settings.keybinds.osu * @property {string} settings.keybinds.osu.k1 * @property {string} settings.keybinds.osu.k2 * @property {string} settings.keybinds.osu.smokeKey * @property {object} settings.keybinds.fruits * @property {string} settings.keybinds.fruits.k1 * @property {string} settings.keybinds.fruits.k2 * @property {string} settings.keybinds.fruits.Dash * @property {object} settings.keybinds.taiko * @property {string} settings.keybinds.taiko.innerLeft * @property {string} settings.keybinds.taiko.innerRight * @property {string} settings.keybinds.taiko.outerLeft * @property {string} settings.keybinds.taiko.outerRight * @property {string} settings.keybinds.quickRetry * @property {object} profile * @property {object} profile.userStatus * @property {0 | 256 | 257 | 65537 | 65793} profile.userStatus.number * @property {'reconnecting' | 'guest' | 'recieving_data' | 'disconnected' | 'connected'} profile.userStatus.name * @property {object} profile.banchoStatus * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13} profile.banchoStatus.number * @property {'idle' | 'afk' | 'playing' | 'editing' | 'modding' | 'multiplayer' | 'watching' | 'unknown' | 'testing' | 'submitting' | 'paused' | 'lobby' | 'multiplaying' | 'osuDirect'} profile.banchoStatus.name * @property {number} profile.id * @property {string} profile.name * @property {object} profile.mode * @property {0 | 1 | 2 | 3} profile.mode.number * @property {'osu' | 'taiko' | 'fruits' | 'mania'} profile.mode.name * @property {number} profile.rankedScore * @property {number} profile.level * @property {number} profile.accuracy * @property {number} profile.pp * @property {number} profile.playCount * @property {number} profile.globalRank * @property {object} profile.countryCode * @property {number} profile.countryCode.number * @property {string} profile.countryCode.name * @property {string} profile.backgroundColour * @property {object} beatmap * @property {boolean} beatmap.isKiai * @property {boolean} beatmap.isBreak * @property {boolean} beatmap.isConvert * @property {object} beatmap.time * @property {number} beatmap.time.live * @property {number} beatmap.time.firstObject * @property {number} beatmap.time.lastObject * @property {number} beatmap.time.mp3Length * @property {object} beatmap.status * @property {0 | 1 | 2 | 4 | 5 | 6 | 7} beatmap.status.number * @property {'unknown' | 'notSubmitted' | 'pending' | 'ranked' | 'approved' | 'qualified' | 'loved'} beatmap.status.name * @property {string} beatmap.checksum * @property {number} beatmap.id * @property {number} beatmap.set * @property {object} beatmap.mode * @property {0 | 1 | 2 | 3} beatmap.mode.number * @property {'osu' | 'taiko' | 'fruits' | 'mania'} beatmap.mode.name * @property {string} beatmap.artist * @property {string} beatmap.artistUnicode * @property {string} beatmap.title * @property {string} beatmap.titleUnicode * @property {string} beatmap.mapper * @property {string} beatmap.version * @property {object} beatmap.stats * @property {object} beatmap.stats.stars * @property {number} beatmap.stats.stars.live * @property {number} [beatmap.stats.stars.aim] This is available only in the osu! ruleset * @property {number} [beatmap.stats.stars.speed] This is available only in the osu! ruleset * @property {number} [beatmap.stats.stars.flashlight] This is available only in the osu! ruleset * @property {number} [beatmap.stats.stars.sliderFactor] This is available only in the osu! ruleset * @property {number} [beatmap.stats.stars.stamina] This is available only in the osu!taiko ruleset * @property {number} [beatmap.stats.stars.rhythm] This is available only in the osu!taiko ruleset * @property {number} [beatmap.stats.stars.color] This is available only in the osu!taiko ruleset * @property {number} [beatmap.stats.stars.peak] This is available only in the osu!taiko ruleset * @property {number} [beatmap.stats.stars.hitWindow] 300's hit window; this is available only in the osu!mania ruleset * @property {number} beatmap.stats.stars.total * @property {object} beatmap.stats.ar * @property {number} beatmap.stats.ar.original * @property {number} beatmap.stats.ar.converted * @property {object} beatmap.stats.cs * @property {number} beatmap.stats.cs.original * @property {number} beatmap.stats.cs.converted * @property {object} beatmap.stats.od * @property {number} beatmap.stats.od.original * @property {number} beatmap.stats.od.converted * @property {object} beatmap.stats.hp * @property {number} beatmap.stats.hp.original * @property {number} beatmap.stats.hp.converted * @property {object} beatmap.stats.bpm * @property {number} beatmap.stats.bpm.realtime * @property {number} beatmap.stats.bpm.common * @property {number} beatmap.stats.bpm.min * @property {number} beatmap.stats.bpm.max * @property {object} beatmap.stats.objects * @property {number} beatmap.stats.objects.circles * @property {number} beatmap.stats.objects.sliders * @property {number} beatmap.stats.objects.spinners * @property {number} beatmap.stats.objects.holds * @property {number} beatmap.stats.objects.total * @property {number} beatmap.stats.maxCombo * @property {object} play * @property {string} play.playerName * @property {object} play.mode * @property {0 | 1 | 2 | 3} play.mode.number * @property {'osu' | 'taiko' | 'fruits' | 'mania'} play.mode.name * @property {number} play.score * @property {number} play.accuracy * @property {object} play.healthBar * @property {number} play.healthBar.normal * @property {number} play.healthBar.smooth * @property {object} play.hits * @property {number} play.hits.0 * @property {number} play.hits.50 * @property {number} play.hits.100 * @property {number} play.hits.300 * @property {number} play.hits.geki This is also used as the 320's count in the osu!mania ruleset * @property {number} play.hits.katu This is also used as the 200's count in the osu!mania ruleset * @property {number} play.hits.sliderBreaks * @property {number} play.hits.sliderEndHits This is populated only when playing osu!(lazer) * @property {number} play.hits.sliderTickHits This is populated only when playing osu!(lazer) * @property {number[]} play.hitErrorArray * @property {object} play.combo * @property {number} play.combo.current * @property {number} play.combo.max * @property {object} play.mods * @property {string} play.mods.checksum * @property {number} play.mods.number * @property {string} play.mods.name * @property {object[]} play.mods.array * @property {string} play.mods.array.acronym * @property {object} [play.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview * @property {number} play.mods.rate * @property {object} play.rank * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} play.rank.current * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} play.rank.maxThisPlay * @property {object} play.pp * @property {number} play.pp.current * @property {number} play.pp.fc * @property {number} play.pp.maxAchievedThisPlay * @property {object} play.pp.detailed * @property {object} play.pp.detailed.current * @property {number} play.pp.detailed.current.aim * @property {number} play.pp.detailed.current.speed * @property {number} play.pp.detailed.current.accuracy * @property {number} play.pp.detailed.current.difficulty * @property {number} play.pp.detailed.current.flashlight * @property {number} play.pp.detailed.current.total * @property {object} play.pp.detailed.fc * @property {number} play.pp.detailed.fc.aim * @property {number} play.pp.detailed.fc.speed * @property {number} play.pp.detailed.fc.accuracy * @property {number} play.pp.detailed.fc.difficulty * @property {number} play.pp.detailed.fc.flashlight * @property {number} play.pp.detailed.fc.total * @property {number} play.unstableRate * @property {object[]} leaderboard * @property {boolean} leaderboard.isFailed * @property {number} leaderboard.position * @property {number} leaderboard.team * @property {number} leaderboard.id * @property {string} leaderboard.name * @property {number} leaderboard.score * @property {number} leaderboard.accuracy * @property {object} leaderboard.hits * @property {number} leaderboard.hits.0 * @property {number} leaderboard.hits.50 * @property {number} leaderboard.hits.100 * @property {number} leaderboard.hits.300 * @property {number} leaderboard.hits.geki This is also used as the 320's count in the osu!mania ruleset * @property {number} leaderboard.hits.katu This is also used as the 200's count in the osu!mania ruleset * @property {object} leaderboard.combo * @property {number} leaderboard.combo.current * @property {number} leaderboard.combo.max * @property {object} leaderboard.mods * @property {string} leaderboard.mods.checksum * @property {number} leaderboard.mods.number * @property {string} leaderboard.mods.name * @property {object[]} leaderboard.mods.array * @property {string} leaderboard.mods.array.acronym * @property {object} [leaderboard.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview * @property {number} leaderboard.mods.rate * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} leaderboard.rank * @property {object} performance * @property {object} performance.accuracy * @property {number} performance.accuracy.90 * @property {number} performance.accuracy.91 * @property {number} performance.accuracy.92 * @property {number} performance.accuracy.93 * @property {number} performance.accuracy.94 * @property {number} performance.accuracy.95 * @property {number} performance.accuracy.96 * @property {number} performance.accuracy.97 * @property {number} performance.accuracy.98 * @property {number} performance.accuracy.99 * @property {number} performance.accuracy.100 * @property {object} performance.graph * @property {object[]} performance.graph.series * @property {'aim' | 'aimNoSliders' | 'flashlight' | 'speed' | 'color' | 'rhythm' | 'stamina' | 'movement' | 'strains'} performance.graph.series.name * @property {number[]} performance.graph.series.data * @property {number[]} performance.graph.xaxis * @property {object} resultsScreen * @property {number} resultsScreen.scoreId * @property {string} resultsScreen.playerName * @property {object} resultsScreen.mode * @property {0 | 1 | 2 | 3} resultsScreen.mode.number * @property {'osu' | 'taiko' | 'fruits' | 'mania'} resultsScreen.mode.name * @property {number} resultsScreen.score * @property {number} resultsScreen.accuracy * @property {object} resultsScreen.hits * @property {number} resultsScreen.hits.0 * @property {number} resultsScreen.hits.50 * @property {number} resultsScreen.hits.100 * @property {number} resultsScreen.hits.300 * @property {number} resultsScreen.hits.geki This is also used as the 320's count in the osu!mania ruleset * @property {number} resultsScreen.hits.katu This is also used as the 200's count in the osu!mania ruleset * @property {number} resultsScreen.hits.sliderEndHits This is populated only when playing osu!(lazer) * @property {number} resultsScreen.hits.sliderTickHits This is populated only when playing osu!(lazer) * @property {object} resultsScreen.mods * @property {string} resultsScreen.mods.checksum * @property {number} resultsScreen.mods.number * @property {string} resultsScreen.mods.name * @property {object[]} resultsScreen.mods.array * @property {string} resultsScreen.mods.array.acronym * @property {object} [resultsScreen.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview * @property {number} resultsScreen.mods.rate * @property {number} resultsScreen.maxCombo * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} resultsScreen.rank * @property {object} resultsScreen.pp * @property {number} resultsScreen.pp.current * @property {number} resultsScreen.pp.fc * @property {string} resultsScreen.createdAt * @property {object} folders * @property {string} folders.game * @property {string} folders.skin * @property {string} folders.songs * @property {string} folders.beatmap * @property {object} files * @property {string} files.beatmap * @property {string} files.background * @property {string} files.audio * @property {object} directPath * @property {string} directPath.beatmapFile * @property {string} directPath.beatmapBackground * @property {string} directPath.beatmapAudio * @property {string} directPath.beatmapFolder * @property {string} directPath.skinFolder * @property {object} tourney * @property {boolean} tourney.scoreVisible * @property {boolean} tourney.starsVisible * @property {number} tourney.ipcState * @property {number} tourney.bestOF * @property {object} tourney.team * @property {string} tourney.team.left * @property {string} tourney.team.right * @property {object} tourney.points * @property {number} tourney.points.left * @property {number} tourney.points.right * @property {object[]} tourney.chat * @property {string} tourney.chat.team * @property {string} tourney.chat.name * @property {string} tourney.chat.message * @property {string} tourney.chat.timestamp * @property {object} tourney.totalScore * @property {number} tourney.totalScore.left * @property {number} tourney.totalScore.right * @property {object[]} tourney.clients * @property {number} tourney.clients.ipcId * @property {'left' | 'right'} tourney.clients.team * @property {object} tourney.clients.user * @property {number} tourney.clients.user.id * @property {string} tourney.clients.user.name * @property {string} tourney.clients.user.country * @property {number} tourney.clients.user.accuracy * @property {number} tourney.clients.user.rankedScore * @property {number} tourney.clients.user.playCount * @property {number} tourney.clients.user.globalRank * @property {number} tourney.clients.user.totalPP * @property {object} tourney.clients.beatmap * @property {object} tourney.clients.beatmap.stats * @property {object} tourney.clients.beatmap.stats.stars * @property {number} tourney.clients.beatmap.stats.stars.live * @property {number} [tourney.clients.beatmap.stats.stars.aim] This is available only in the osu! ruleset * @property {number} [tourney.clients.beatmap.stats.stars.speed] This is available only in the osu! ruleset * @property {number} [tourney.clients.beatmap.stats.stars.flashlight] This is available only in the osu! ruleset * @property {number} [tourney.clients.beatmap.stats.stars.sliderFactor] This is available only in the osu! ruleset * @property {number} [tourney.clients.beatmap.stats.stars.stamina] This is available only in the osu!taiko ruleset * @property {number} [tourney.clients.beatmap.stats.stars.rhythm] This is available only in the osu!taiko ruleset * @property {number} [tourney.clients.beatmap.stats.stars.color] This is available only in the osu!taiko ruleset * @property {number} [tourney.clients.beatmap.stats.stars.peak] This is available only in the osu!taiko ruleset * @property {number} [tourney.clients.beatmap.stats.stars.hitWindow] 300's hit window; this is available only in the osu!mania ruleset * @property {number} tourney.clients.beatmap.stats.stars.total * @property {object} tourney.clients.beatmap.stats.ar * @property {number} tourney.clients.beatmap.stats.ar.original * @property {number} tourney.clients.beatmap.stats.ar.converted * @property {object} tourney.clients.beatmap.stats.cs * @property {number} tourney.clients.beatmap.stats.cs.original * @property {number} tourney.clients.beatmap.stats.cs.converted * @property {object} tourney.clients.beatmap.stats.od * @property {number} tourney.clients.beatmap.stats.od.original * @property {number} tourney.clients.beatmap.stats.od.converted * @property {object} tourney.clients.beatmap.stats.hp * @property {number} tourney.clients.beatmap.stats.hp.original * @property {number} tourney.clients.beatmap.stats.hp.converted * @property {object} tourney.clients.beatmap.stats.bpm * @property {number} tourney.clients.beatmap.stats.bpm.realtime * @property {number} tourney.clients.beatmap.stats.bpm.common * @property {number} tourney.clients.beatmap.stats.bpm.min * @property {number} tourney.clients.beatmap.stats.bpm.max * @property {object} tourney.clients.beatmap.stats.objects * @property {number} tourney.clients.beatmap.stats.objects.circles * @property {number} tourney.clients.beatmap.stats.objects.sliders * @property {number} tourney.clients.beatmap.stats.objects.spinners * @property {number} tourney.clients.beatmap.stats.objects.holds * @property {number} tourney.clients.beatmap.stats.objects.total * @property {number} tourney.clients.beatmap.stats.maxCombo * @property {object} tourney.clients.play * @property {string} tourney.clients.play.playerName * @property {object} tourney.clients.play.mode * @property {0 | 1 | 2 | 3} tourney.clients.play.mode.number * @property {'osu' | 'taiko' | 'fruits' | 'mania'} tourney.clients.play.mode.name * @property {number} tourney.clients.play.score * @property {number} tourney.clients.play.accuracy * @property {object} tourney.clients.play.healthBar * @property {number} tourney.clients.play.healthBar.normal * @property {number} tourney.clients.play.healthBar.smooth * @property {object} tourney.clients.play.hits * @property {number} tourney.clients.play.hits.0 * @property {number} tourney.clients.play.hits.50 * @property {number} tourney.clients.play.hits.100 * @property {number} tourney.clients.play.hits.300 * @property {number} tourney.clients.play.hits.geki This is also used as the 320's count in the osu!mania ruleset * @property {number} tourney.clients.play.hits.katu This is also used as the 200's count in the osu!mania ruleset * @property {number} tourney.clients.play.hits.sliderBreaks * @property {number} tourney.clients.play.hits.sliderEndHits This is populated only when playing osu!(lazer) * @property {number} tourney.clients.play.hits.sliderTickHits This is populated only when playing osu!(lazer) * @property {number[]} tourney.clients.play.hitErrorArray * @property {object} tourney.clients.play.combo * @property {number} tourney.clients.play.combo.current * @property {number} tourney.clients.play.combo.max * @property {object} tourney.clients.play.mods * @property {string} tourney.clients.play.mods.checksum * @property {number} tourney.clients.play.mods.number * @property {string} tourney.clients.play.mods.name * @property {object[]} tourney.clients.play.mods.array * @property {string} tourney.clients.play.mods.array.acronym * @property {object} [tourney.clients.play.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview * @property {number} tourney.clients.play.mods.rate * @property {object} tourney.clients.play.rank * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} tourney.clients.play.rank.current * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} tourney.clients.play.rank.maxThisPlay * @property {object} tourney.clients.play.pp * @property {number} tourney.clients.play.pp.current * @property {number} tourney.clients.play.pp.fc * @property {number} tourney.clients.play.pp.maxAchievedThisPlay * @property {object} tourney.clients.play.pp.detailed * @property {object} tourney.clients.play.pp.detailed.current * @property {number} tourney.clients.play.pp.detailed.current.aim * @property {number} tourney.clients.play.pp.detailed.current.speed * @property {number} tourney.clients.play.pp.detailed.current.accuracy * @property {number} tourney.clients.play.pp.detailed.current.difficulty * @property {number} tourney.clients.play.pp.detailed.current.flashlight * @property {number} tourney.clients.play.pp.detailed.current.total * @property {object} tourney.clients.play.pp.detailed.fc * @property {number} tourney.clients.play.pp.detailed.fc.aim * @property {number} tourney.clients.play.pp.detailed.fc.speed * @property {number} tourney.clients.play.pp.detailed.fc.accuracy * @property {number} tourney.clients.play.pp.detailed.fc.difficulty * @property {number} tourney.clients.play.pp.detailed.fc.flashlight * @property {number} tourney.clients.play.pp.detailed.fc.total * @property {number} tourney.clients.play.unstableRate */ /** @typedef {object} WEBSOCKET_V2_PRECISE * @property {number} currentTime * @property {object} keys * @property {object} keys.k1 * @property {boolean} keys.k1.isPressed * @property {number} keys.k1.count * @property {object} keys.k2 * @property {boolean} keys.k2.isPressed * @property {number} keys.k2.count * @property {object} keys.m1 * @property {boolean} keys.m1.isPressed * @property {number} keys.m1.count * @property {object} keys.m2 * @property {boolean} keys.m2.isPressed * @property {number} keys.m2.count * @property {number[]} hitErrors * @property {object[]} tourney * @property {number} tourney.ipcId * @property {number[]} tourney.hitErrors * @property {object} tourney.keys * @property {object} tourney.keys.k1 * @property {boolean} tourney.keys.k1.isPressed * @property {number} tourney.keys.k1.count * @property {object} tourney.keys.k2 * @property {boolean} tourney.keys.k2.isPressed * @property {number} tourney.keys.k2.count * @property {object} tourney.keys.m1 * @property {boolean} tourney.keys.m1.isPressed * @property {number} tourney.keys.m1.count * @property {object} tourney.keys.m2 * @property {boolean} tourney.keys.m2.isPressed * @property {number} tourney.keys.m2.count */