diff --git a/ClientHandler.js b/ClientHandler.js index 283efb4..a94ba30 100644 --- a/ClientHandler.js +++ b/ClientHandler.js @@ -12,7 +12,8 @@ import { TickCounter } from "./internalModules/TickCounter.js" import { WorldTracker } from "./internalModules/WorldTracker.js" import { ServerAgeTracker } from "./internalModules/ServerAgeTracker.js" import { CustomModules } from "./internalModules/CustomModules.js" -//import { ChunkPreloader } from "./internalModules/ChunkPreloader.js" +import { ChunkPreloader } from "./internalModules/ChunkPreloader.js" +import { TabListHandler } from "./internalModules/TabListHandler.js" export class ClientHandler extends EventEmitter { constructor(userClient, proxy, id) { @@ -57,8 +58,9 @@ export class ClientHandler extends EventEmitter { this.betterGameInfo = new BetterGameInfo(this) this.consoleLogger = new ConsoleLogger(this) this.serverAgeTracker = new ServerAgeTracker(this) - //this.chunkPreloader = new ChunkPreloader(this) + this.chunkPreloader = new ChunkPreloader(this) this.customModules = new CustomModules(this) + this.tabListHandler = new TabListHandler(this) this.bindEventListeners() } @@ -211,7 +213,7 @@ function randomSalt() { // Generate two random 32-bit integers let upperInt = Math.floor(Math.random() * 0x80000000); let lowerInt = Math.floor(Math.random() * 0x100000000); - + // Combine them into a 64-bit BigInt let combinedInt = BigInt(upperInt) << 32n | BigInt(lowerInt); diff --git a/commands/commandExit.js b/commands/commandExit.js index 5eee30d..601b756 100644 --- a/commands/commandExit.js +++ b/commands/commandExit.js @@ -1,5 +1,5 @@ import { saveData } from "../data/dataHandler.js" -//import { saveChunks } from "../data/chunkCacheHandler.js" +import { saveChunks } from "../data/chunkCacheHandler.js" export const name = "exit" export const aliases = ["quit", "end", "q"] @@ -9,6 +9,6 @@ export const requireTrust = true export async function run(usageInstance) { usageInstance.reply("§7Exiting...") await saveData() - //await saveChunks() + await saveChunks() process.exit() } \ No newline at end of file diff --git a/config/configHandler.js b/config/configHandler.js index 4478fcf..3653c75 100644 --- a/config/configHandler.js +++ b/config/configHandler.js @@ -7,7 +7,7 @@ export let configFileWorking = false try { let tempConfig = fs.readFileSync("./config.yml", "utf8") tempConfig = YAML.parse(tempConfig) - if (tempConfig["config-version"] !== 3) throw {code: "OUTDATED_CONFIG"} + if (tempConfig["config-version"] !== 4) throw {code: "OUTDATED_CONFIG"} replaceConfig(tempConfig) configFileWorking = true } catch (error) { diff --git a/data/chunkCacheHandler.js b/data/chunkCacheHandler.js index d368ca7..0f7dfcf 100644 --- a/data/chunkCacheHandler.js +++ b/data/chunkCacheHandler.js @@ -1,17 +1,21 @@ import fs from "fs" import fsPromises from "fs/promises" +const currentDropperVersion = "1.0" + export let chunks = {} export let chunksFileWorking = false try { let tempChunks = fs.readFileSync("./chunks.json", "utf8") tempChunks = JSON.parse(tempChunks) + if (tempChunks.dropperVersion !== currentDropperVersion) throw new Error({code: "OUTDATED_CHUNKS"}) replaceChunks(tempChunks) chunksFileWorking = true } catch (error) { console.log("No valid chunks.json found. Creating a new file. (Error code: " + error.code + ")") //create fresh data let tempChunks = { + dropperVersion: currentDropperVersion, versions: {} } replaceChunks(tempChunks) diff --git a/defaultConfig.js b/defaultConfig.js index 80213bb..8b6e6f9 100644 --- a/defaultConfig.js +++ b/defaultConfig.js @@ -1,9 +1,12 @@ export default `# Config version, used to quickly validate the config and make sure it will have all of the necessary information in it. Do not change this unless you know what you're doing. -config-version: 3 +config-version: 4 + # Port to host the server on. Usually you can leave this on 25565, which is Minecraft's default port. server-port: 25565 + # The server's host. Recommended to leave this on 127.0.0.1. server-host: 127.0.0.1 + # Perfect map configurations for automatic requeueing if the maps are incorrect. If /rpm is used with no argument, "default" is chosen. # As with the rest of the config, make sure this stays in the same general format if you modify it otherwise it may break. # There MUST be a default config. @@ -16,10 +19,16 @@ perfect-maps: balloons: - Well, Balloons, Sewer, Floating Islands, Iris - Well, Balloons, Floating Islands, Sewer, Iris + # Displayed in the discordlink command. Change if you wish. discord-link: https://discord.gg/Sqbj9Nb835 -# CHUNK CACHING IS DISABLED TEMPORARILY DUE TO THE RELEASE BREAKING IT -# THIS IS CURRENTLY NON-FUNCTIONAL + +# Dropper Utilities can fetch stats for other players in your game using Aiden (skrrrtt on Discord)'s API. +# Each player's win count will be displayed in tab in Dropper games, letting you get a quick idea of how good each player is. +# Additionally, this contributes to Aiden's database, allowing more accurate leaderboards to be displayed on his website (https://hydropper.info/leaderboard). +# Set this to true if you'd like to opt-in. +fetch-player-stats: false + # Dropper Utilities can give your client a small portion of the world around you when you teleport to a new map, prior to receiving it from Hypixel over the network. # This can reduce load times when switching between maps by ~175ms on my setup, it may vary for you. # Chunks near teleportation spots are saved in the file chunks.json which is placed in the same folder as this program. diff --git a/dropperApi/identifierHandler.js b/dropperApi/identifierHandler.js new file mode 100644 index 0000000..2bec94e --- /dev/null +++ b/dropperApi/identifierHandler.js @@ -0,0 +1,89 @@ +import fetch from "node-fetch" + +let cache = new Map() +let cacheTimes = new Map() +let queue = [] + +export function getStats(uuid) { + if (cache.has(uuid)) { + if (performance.now() - cacheTimes.get(uuid) > 300000) { + //if it's too old, re-fetch user data + cache.delete(uuid) + cacheTimes.delete(uuid) + } else { + return cache.get(uuid) + } + } + + let promise = new Promise(resolve => { + queue.push({ + uuid, + resolve + }) + if (queue.length === 1) { + setTimeout(doFetch, 1000) + } + }) + cache.set(uuid, promise) + return promise +} + +async function doFetch() { + let currentQueue = queue + queue = [] + let uuidsToFetch = currentQueue.map(item => item.uuid) + let resultMap = await fetchPlayers(uuidsToFetch) + let now = performance.now() + for (let uuid of uuidsToFetch) { + cacheTimes.set(uuid, now) + } + if (resultMap === null) { //error of some sort + for (let item of currentQueue) { + cache.set(item.uuid, null) + item.resolve(null) + } + return + } + for (let item of currentQueue) { + let uuid = item.uuid + let value = resultMap.get(uuid) + cache.set(uuid, value) + item.resolve(value) + } +} + +async function fetchPlayers(uuids) { + try { + let response = await fetch("https://api.hydropper.info/user/multiple", { + body: JSON.stringify({ + _id: uuids + }), + method: "POST", + headers: { + "User-Agent": "DropperUtilities", + "Content-Type": "application/json" + } + }) + let json = await response.json() + if (json.error) { + throw new Error(JSON.stringify(json)) + } + let users = new Map() + json.users.forEach(user => { + if (user.wins === null) { + console.log("NULL USER", user.uuid) + users.set(user.uuid, null) + return + } + users.set(user.uuid, { + wins: user.wins, + fails: user.fails + }) + }) + return users + } catch (error) { + console.log("Unexpected Dropper API error - please report to lapisfloof on Discord:") + console.log(error) + return null + } +} \ No newline at end of file diff --git a/internalModules/ChunkPreloader.js b/internalModules/ChunkPreloader.js index 1c3d750..b2ac159 100644 --- a/internalModules/ChunkPreloader.js +++ b/internalModules/ChunkPreloader.js @@ -9,6 +9,8 @@ export class ChunkPreloader { this.userClient = clientHandler.userClient this.proxyClient = clientHandler.proxyClient + this.stateHandler = this.clientHandler.stateHandler + this.active = false this.loadedChunks = new Set() @@ -36,8 +38,15 @@ export class ChunkPreloader { } this.loadedChunks.add(key) if (!this.active) return - if (Math.abs(data.x - this.lastTpChunkX) > 1 || Math.abs(data.z - this.lastTpChunkZ) > 1) return //only save chunks near teleportation spots - this.chunkData[key] = serializeChunkData(data) + if (Math.abs(data.x - this.lastTpChunkX) > 2 || Math.abs(data.z - this.lastTpChunkZ) > 2) return //only save chunks near teleportation spots + if (this.stateHandler.mapset) { + let mapsetChunkData = this.chunkData[this.stateHandler.mapset] + if (!mapsetChunkData) { + mapsetChunkData = {} + this.chunkData[this.stateHandler.mapset] = mapsetChunkData + } + mapsetChunkData[key] = serializeChunkData(data) + } } if (meta.name === "unload_chunk") { let key = data.x + "," + data.z @@ -49,10 +58,18 @@ export class ChunkPreloader { this.lastTpChunkX = chunkX this.lastTpChunkZ = chunkZ if (!this.active || !config["chunk-caching"]) return - for (let relativeX = -1; relativeX <= 1; relativeX++) { - for (let relativeZ = -1; relativeZ < 1; relativeZ++) { + for (let relativeX = -2; relativeX <= 2; relativeX++) { + for (let relativeZ = -2; relativeZ < 2; relativeZ++) { let key = (chunkX + relativeX) + "," + (chunkZ + relativeZ) - let data = this.chunkData[key] + let data + if (this.stateHandler.mapset) { + let mapsetChunkData = this.chunkData[this.stateHandler.mapset] + if (!mapsetChunkData) { + mapsetChunkData = {} + this.chunkData[this.stateHandler.mapset] = mapsetChunkData + } + data = mapsetChunkData[key] + } if (!data) continue if (this.loadedChunks.has(key)) continue this.loadedChunks.add(key) diff --git a/internalModules/StateHandler.js b/internalModules/StateHandler.js index c100b60..d9a1eab 100644 --- a/internalModules/StateHandler.js +++ b/internalModules/StateHandler.js @@ -21,8 +21,20 @@ export class StateHandler extends EventEmitter { this.gameFails = null this.otherFinishCount = null this.realTime = null + this.tryLocrawTimeout = null + this.isFirstLogin = true + this.lastServerLocraw = null + this.locrawRetryCount = 0 + this.requestedLocraw = false + this.mapset = null this.bindEventListeners() + this.bindModifiers() + } + + bindModifiers() { + this.clientHandler.incomingModifiers.push(this.handleIncomingPacketForChat.bind(this)) + this.clientHandler.incomingModifiers.push(this.handleIncomingPacketForActionBar.bind(this)) } //called from ClientHandler once tickCounter has been created @@ -31,233 +43,300 @@ export class StateHandler extends EventEmitter { this.tickCounter = this.clientHandler.tickCounter } - bindEventListeners() { - this.proxyClient.on("packet", (data, meta) => { - let actualMessage - if (meta.name === "chat") { - if (data.position === 2) return - actualMessage = data.message - } else if (meta.name === "system_chat") { - if ("type" in data && data.type !== 1) return - if ("isActionBar" in data && data.isActionBar === true) return - actualMessage = data.content - } else return - let parsedMessage + handleIncomingPacketForChat(data, meta) { + let actualMessage + if (meta.name === "chat") { + if (data.position === 2) return + actualMessage = data.message + } else if (meta.name === "system_chat") { + if ("type" in data && data.type !== 1) return + if ("isActionBar" in data && data.isActionBar === true) return + actualMessage = data.content + } else return + let parsedMessage + try { + parsedMessage = JSON.parse(actualMessage) + } catch (error) { + //invalid JSON, Hypixel sometimes sends invalid JSON with unescaped newlines + return + } + //locraw response + checks: { + if (parsedMessage.extra) break checks + if (parsedMessage.color !== "white") break checks + let content = parsedMessage.text try { - parsedMessage = JSON.parse(actualMessage) + content = JSON.parse(content) } catch (error) { - //invalid JSON, Hypixel sometimes sends invalid JSON with unescaped newlines - return + break checks } - //my user joining a game - checks: { - if (this.state !== "none") break checks - if (parsedMessage.extra?.length !== 14) break checks - if (parsedMessage.extra[4].text !== " has joined (") break checks - if (parsedMessage.extra[4].color !== "yellow") break checks - this.setState("waiting") + if (typeof content?.server !== "string") break checks + if (!this.requestedLocraw) break checks + this.requestedLocraw = false + if (content.server === "limbo" || content.server === this.lastServerLocraw) { + this.locrawRetryCount++ + if (this.locrawRetryCount > 3) { + //give up, we might actually be in limbo + return { //equivalent to breaking and cancelling packet + type: "cancel" + } + } + this.tryLocrawTimeout = setTimeout(() => { + if (this.clientHandler.destroyed) return + this.requestedLocraw = true + this.clientHandler.sendServerCommand("locraw") + }, 500) + return { //equivalent to breaking and cancelling packet + type: "cancel" + } + } else { + this.lastServerLocraw = content.server } - //game started, map list - checks: { - //in rare cases, a join message can be sent after the game starts, meaning this won't work unless this check is removed - //if (this.state !== "waiting") break checks - if (parsedMessage.extra?.length !== 10) break checks - if (parsedMessage.extra[0].text !== "Selected Maps: ") break checks - if (parsedMessage.extra[0].color !== "gray") break checks - let maps = [ - parsedMessage.extra[1].text, - parsedMessage.extra[3].text, - parsedMessage.extra[5].text, - parsedMessage.extra[7].text, - parsedMessage.extra[9].text - ] - this.maps = maps - this.times = [] - this.lastFails = 0 - this.hasSkip = false - this.gameFails = 0 - this.otherFinishCount = 0 - this.setState("game") - this.gameState = "waiting" + if (content.gametype === "ARCADE" && content.mode === "DROPPER") { + if (this.state === "none") this.setState("waiting") + this.mapset = content.map } - //countdown done, glass open - checks: { - if (this.state !== "game") break checks - if (parsedMessage.extra?.length !== 1) break checks - if (parsedMessage.text !== "") break checks - if (parsedMessage.extra[0].text !== "DROP!") break checks - if (parsedMessage.extra[0].bold !== true) break checks - if (parsedMessage.extra[0].color !== "green") break checks + return { + type: "cancel" + } + } + //the following check has been replaced with locraw + /* + //my user joining a game + checks: { + if (this.state !== "none") break checks + if (parsedMessage.extra?.length !== 14) break checks + if (parsedMessage.extra[4].text !== " has joined (") break checks + if (parsedMessage.extra[4].color !== "yellow") break checks + this.setState("waiting") + } + */ + //game started, map list + checks: { + //in rare cases, a join message can be sent after the game starts, meaning this won't work unless this check is removed + //if (this.state !== "waiting") break checks + if (parsedMessage.extra?.length !== 10) break checks + if (parsedMessage.extra[0].text !== "Selected Maps: ") break checks + if (parsedMessage.extra[0].color !== "gray") break checks + let maps = [ + parsedMessage.extra[1].text, + parsedMessage.extra[3].text, + parsedMessage.extra[5].text, + parsedMessage.extra[7].text, + parsedMessage.extra[9].text + ] + this.maps = maps + this.times = [] + this.lastFails = 0 + this.hasSkip = false + this.gameFails = 0 + this.otherFinishCount = 0 + this.setState("game") + this.gameState = "waiting" + } + //countdown done, glass open + checks: { + if (this.state !== "game") break checks + if (parsedMessage.extra?.length !== 1) break checks + if (parsedMessage.text !== "") break checks + if (parsedMessage.extra[0].text !== "DROP!") break checks + if (parsedMessage.extra[0].bold !== true) break checks + if (parsedMessage.extra[0].color !== "green") break checks - this.startTime = performance.now() - this.lastSegmentTime = this.startTime + this.startTime = performance.now() + this.lastSegmentTime = this.startTime - this.gameState = 0 + this.gameState = 0 - this.emit("drop") - } - //map completed - checks: { - if (this.state !== "game") break checks - if (parsedMessage.extra?.length !== 5) break checks - if (parsedMessage.text !== "") break checks - if (!parsedMessage.extra[0].text.startsWith("You finished Map ")) break checks - if (parsedMessage.extra[0].color !== "gray") break checks - this.lastFails = 0 - let time = performance.now() - //saved in a variable for info object - let timeText = parsedMessage.extra[3].text - let split = timeText.split(":") - let minutes = parseInt(split[0]) - let seconds = parseInt(split[1]) - let milliseconds = parseInt(split[2]) - let duration = minutes * 60000 + seconds * 1000 + milliseconds - this.times.push(duration) - this.lastSegmentTime = time + this.emit("drop") + } + //map completed + checks: { + if (this.state !== "game") break checks + if (parsedMessage.extra?.length !== 5) break checks + if (parsedMessage.text !== "") break checks + if (!parsedMessage.extra[0].text.startsWith("You finished Map ")) break checks + if (parsedMessage.extra[0].color !== "gray") break checks + this.lastFails = 0 + let time = performance.now() + //saved in a variable for info object + let timeText = parsedMessage.extra[3].text + let split = timeText.split(":") + let minutes = parseInt(split[0]) + let seconds = parseInt(split[1]) + let milliseconds = parseInt(split[2]) + let duration = minutes * 60000 + seconds * 1000 + milliseconds + this.times.push(duration) + this.lastSegmentTime = time - let mapNumber = this.times.length - 1 - let mapName = this.maps[mapNumber] - let mapDifficulty = ["easy", "easy", "medium", "medium", "hard"][mapNumber] - let infoObject = { - type: "map", - number: mapNumber, - name: mapName, - difficulty: mapDifficulty, - duration, - skipped: false - } - if (!this.clientHandler.disableTickCounter) { - let ticks = this.tickCounter.hypixelMapEnd(mapNumber) - infoObject.ticks = ticks - } - this.emit("time", infoObject) - this.gameState++ + let mapNumber = this.times.length - 1 + let mapName = this.maps[mapNumber] + let mapDifficulty = ["easy", "easy", "medium", "medium", "hard"][mapNumber] + let infoObject = { + type: "map", + number: mapNumber, + name: mapName, + difficulty: mapDifficulty, + duration, + skipped: false } - //game completed, used to extract Hypixel's time - checks: { - if (this.state !== "game") break checks - if (parsedMessage.extra?.length !== 3) break checks - if (parsedMessage.text !== "") break checks - if (parsedMessage.extra[0].text !== "You finished all maps in ") break checks - if (parsedMessage.extra[0].color !== "green") break checks - let timeText = parsedMessage.extra[1].text - let split = timeText.split(":") - let minutes = parseInt(split[0]) - let seconds = parseInt(split[1]) - let milliseconds = parseInt(split[2]) - let time = minutes * 60000 + seconds * 1000 + milliseconds - this.totalTime = time - - let realTime = performance.now() - this.startTime - this.realTime = realTime - let infoObject = { - hypixelTime: time, - realTime, - startTime: this.startTime, - endTime: this.lastSegmentTime, - hasSkip: this.hasSkip, - fails: this.gameFails, - place: this.otherFinishCount - } - if (!this.clientHandler.disableTickCounter) { - infoObject.ticks = this.tickCounter.tickCounts.reduce((partialSum, a) => partialSum + a, 0) - } - this.emit("gameEnd", infoObject) + if (!this.clientHandler.disableTickCounter) { + let ticks = this.tickCounter.hypixelMapEnd(mapNumber) + infoObject.ticks = ticks } - //map skipped - checks: { - if (this.state !== "game") break checks - if (parsedMessage.extra?.length !== 3) break checks - if (parsedMessage.text !== "") break checks - if (!parsedMessage.extra[0].text.startsWith("You have skipped ahead to Map ")) break checks - if (parsedMessage.extra[0].color !== "gray") break checks - this.hasSkip = true - this.lastFails = 0 - let time = performance.now() - //saved in a variable for info object - let startTime = this.lastSegmentTime - let segmentDuration = time - this.lastSegmentTime - this.lastSegmentTime = time - this.times.push(segmentDuration) + this.emit("time", infoObject) + this.gameState++ + } + //game completed, used to extract Hypixel's time + checks: { + if (this.state !== "game") break checks + if (parsedMessage.extra?.length !== 3) break checks + if (parsedMessage.text !== "") break checks + if (parsedMessage.extra[0].text !== "You finished all maps in ") break checks + if (parsedMessage.extra[0].color !== "green") break checks + let timeText = parsedMessage.extra[1].text + let split = timeText.split(":") + let minutes = parseInt(split[0]) + let seconds = parseInt(split[1]) + let milliseconds = parseInt(split[2]) + let time = minutes * 60000 + seconds * 1000 + milliseconds + this.totalTime = time - let mapNumber = this.times.length - 1 - let mapName = this.maps[mapNumber] - let mapDifficulty = ["easy", "easy", "medium", "medium", "hard"][mapNumber] - let infoObject = { - type: "map", - number: mapNumber, - name: mapName, - difficulty: mapDifficulty, - startTime, - endTime: time, - duration: segmentDuration, - skipped: true - } - if (!this.clientHandler.disableTickCounter) { - let ticks = this.tickCounter.hypixelMapEnd(mapNumber) - infoObject.ticks = ticks - } - this.emit("time", infoObject) - this.gameState++ + let realTime = performance.now() - this.startTime + this.realTime = realTime + let infoObject = { + hypixelTime: time, + realTime, + startTime: this.startTime, + endTime: this.lastSegmentTime, + hasSkip: this.hasSkip, + fails: this.gameFails, + place: this.otherFinishCount } - //another user finishing - checks: { - if (this.state !== "game") break checks - if (!parsedMessage.extra) break checks - if (parsedMessage.extra.length < 5) break checks - if (parsedMessage.text !== "") break checks - if (parsedMessage.extra[parsedMessage.extra.length - 1].text !== "!") break checks - if (parsedMessage.extra[parsedMessage.extra.length - 1].color !== "gray") break checks - if (parsedMessage.extra[parsedMessage.extra.length - 2].color !== "gold") break checks - if (parsedMessage.extra[parsedMessage.extra.length - 3].text !== "finished all maps in ") break checks - if (parsedMessage.extra[parsedMessage.extra.length - 3].color !== "gray") break checks - this.otherFinishCount++ + if (!this.clientHandler.disableTickCounter) { + infoObject.ticks = this.tickCounter.tickCounts.reduce((partialSum, a) => partialSum + a, 0) } - }) - this.proxyClient.on("packet", (data, meta) => { - let actualMessage - if (meta.name === "chat") { - if (data.position !== 2) return - actualMessage = data.message - } else if (meta.name === "system_chat") { - if (data.type !== 2 && !data.isActionBar) return - actualMessage = data.content - } else return - let parsedMessage - try { - parsedMessage = JSON.parse(actualMessage) - } catch (error) { - //invalid JSON, Hypixel sometimes sends invalid JSON with unescaped newlines - return + this.emit("gameEnd", infoObject) + } + //map skipped + checks: { + if (this.state !== "game") break checks + if (parsedMessage.extra?.length !== 3) break checks + if (parsedMessage.text !== "") break checks + if (!parsedMessage.extra[0].text.startsWith("You have skipped ahead to Map ")) break checks + if (parsedMessage.extra[0].color !== "gray") break checks + this.hasSkip = true + this.lastFails = 0 + let time = performance.now() + //saved in a variable for info object + let startTime = this.lastSegmentTime + let segmentDuration = time - this.lastSegmentTime + this.lastSegmentTime = time + this.times.push(segmentDuration) + + let mapNumber = this.times.length - 1 + let mapName = this.maps[mapNumber] + let mapDifficulty = ["easy", "easy", "medium", "medium", "hard"][mapNumber] + let infoObject = { + type: "map", + number: mapNumber, + name: mapName, + difficulty: mapDifficulty, + startTime, + endTime: time, + duration: segmentDuration, + skipped: true } - //game info bar, checked for fail count - checks: { - if (this.state !== "game") break checks - if (!parsedMessage.text.startsWith("§fMap Time: §a") && !parsedMessage.text.startsWith("§fTotal Time: §a")) break checks - let split = parsedMessage.text.split(" ") - if (split.length !== 6) break checks - let last = split[split.length - 1] - let noFormatting = removeFormattingCodes(last) - let failCount = parseInt(noFormatting) - if (failCount <= this.lastFails) break checks - let difference = failCount - this.lastFails - this.lastFails = failCount - for (let i = 0; i < difference; i++) { - this.emit("fail") - this.gameFails++ - } + if (!this.clientHandler.disableTickCounter) { + let ticks = this.tickCounter.hypixelMapEnd(mapNumber) + infoObject.ticks = ticks } - }) - this.proxyClient.on("respawn", () => { + this.emit("time", infoObject) + this.gameState++ + } + //another user finishing + checks: { + if (this.state !== "game") break checks + if (!parsedMessage.extra) break checks + if (parsedMessage.extra.length < 5) break checks + if (parsedMessage.text !== "") break checks + if (parsedMessage.extra[parsedMessage.extra.length - 1].text !== "!") break checks + if (parsedMessage.extra[parsedMessage.extra.length - 1].color !== "gray") break checks + if (parsedMessage.extra[parsedMessage.extra.length - 2].color !== "gold") break checks + if (parsedMessage.extra[parsedMessage.extra.length - 3].text !== "finished all maps in ") break checks + if (parsedMessage.extra[parsedMessage.extra.length - 3].color !== "gray") break checks + this.otherFinishCount++ + } + } + + handleIncomingPacketForActionBar(data, meta) { + let actualMessage + if (meta.name === "chat") { + if (data.position !== 2) return + actualMessage = data.message + } else if (meta.name === "system_chat") { + if (data.type !== 2 && !data.isActionBar) return + actualMessage = data.content + } else return + let parsedMessage + try { + parsedMessage = JSON.parse(actualMessage) + } catch (error) { + //invalid JSON, Hypixel sometimes sends invalid JSON with unescaped newlines + return + } + //game info bar, checked for fail count + checks: { + if (this.state !== "game") break checks + if (!parsedMessage.text.startsWith("§fMap Time: §a") && !parsedMessage.text.startsWith("§fTotal Time: §a")) break checks + let split = parsedMessage.text.split(" ") + if (split.length !== 6) break checks + let last = split[split.length - 1] + let noFormatting = removeFormattingCodes(last) + let failCount = parseInt(noFormatting) + if (failCount <= this.lastFails) break checks + let difference = failCount - this.lastFails + this.lastFails = failCount + for (let i = 0; i < difference; i++) { + this.emit("fail") + this.gameFails++ + } + } + } + + bindEventListeners() { + this.clientHandler.on("destroy", () => { this.setState("none") }) - this.clientHandler.on("destroy", () => { + this.proxyClient.on("login", () => { this.setState("none") + if (this.tryLocrawTimeout) { + clearTimeout(this.tryLocrawTimeout) + this.tryLocrawTimeout = null + } + this.locrawRetryCount = 0 + if (this.isFirstLogin) { + this.isFirstLogin = false + this.tryLocrawTimeout = setTimeout(() => { + if (this.clientHandler.destroyed) return + this.requestedLocraw = true + this.clientHandler.sendServerCommand("locraw") + }, 1500) + } else { + this.tryLocrawTimeout = setTimeout(() => { + if (this.clientHandler.destroyed) return + this.requestedLocraw = true + this.clientHandler.sendServerCommand("locraw") + }, 150) + } }) } setState(state) { if (this.state === state) return + if (state === "none") { + this.mapset = null + } this.state = state this.emit("state", state) this.emit(state) diff --git a/internalModules/TabListHandler.js b/internalModules/TabListHandler.js new file mode 100644 index 0000000..a1b5732 --- /dev/null +++ b/internalModules/TabListHandler.js @@ -0,0 +1,180 @@ +import { config } from "../config/configHandler.js" +import { getStats } from "../dropperApi/identifierHandler.js" + +let enabled = config["fetch-player-stats"] + +export class TabListHandler { + constructor(clientHandler) { + this.clientHandler = clientHandler + this.userClient = clientHandler.userClient + this.proxyClient = clientHandler.proxyClient + + this.stateHandler = this.clientHandler.stateHandler + + this.teams = new Map() + this.players = new Map() + + if (enabled) { + this.bindModifiers() + this.bindEventListeners() + } + + /*setInterval(() => { + console.dir(this.teams, {colors: true, depth:100}) + console.dir(this.players, {colors: true, depth:100}) + }, 5000)*/ + } + + bindModifiers() { + this.clientHandler.incomingModifiers.push(this.handleIncomingPacket.bind(this)) + } + + handleIncomingPacket(data, meta) { + if (!([ + "named_entity_spawn", + "player_info", + "entity_metadata", + "scoreboard_objective", + "scoreboard_score", + "scoreboard_display_objective", + "scoreboard_team" + ]).includes(meta.name)) return + //also look at named_entity_spawn, player_info, entity_metadata, scoreboard_objective, scoreboard_score, scoreboard_display_objective, scoreboard_team + //console.dir([meta.name, data], {depth: 100, colors: true}) + } + + bindEventListeners() { + /*this.proxyClient.on("login", () => { + for (let i = 0; i < 1000; i++) console.log("AAAAAAAAAAAAAAAA") + console.clear() + })*/ + + + + + this.proxyClient.on("teams", data => { + this.handleTeamPacket(data) + }) + this.proxyClient.on("scoreboard_team", data => { + this.handleTeamPacket(data) + }) + this.proxyClient.on("player_info", data => { + let action = data.action + for (let playerInfo of data.data) { + if (this.userClient.protocolVersion < 761 && action === 4) { + this.players.delete(playerInfo.UUID) //always uppercase UUID for versions < 761 + } else { + let object + if (playerInfo.uuid) { + object = this.players.get(playerInfo.uuid) + } else { + object = this.players.get(playerInfo.UUID) + } + if (!object) object = {} + if (playerInfo.player !== undefined) object.player = playerInfo.player + if (playerInfo.chatSession !== undefined) object.chatSession = playerInfo.chatSession + if (playerInfo.gamemode !== undefined) object.gamemode = playerInfo.gamemode + if (playerInfo.uuid !== undefined) object.uuid = playerInfo.uuid + if (playerInfo.UUID !== undefined) object.uuid = playerInfo.UUID //use lowercase anyways + if (playerInfo.listed !== undefined) object.listed = playerInfo.listed + if (playerInfo.latency !== undefined) object.latency = playerInfo.latency + if (playerInfo.displayName !== undefined) object.displayName = playerInfo.displayName + if (playerInfo.name !== undefined) object.name = playerInfo.name + if (playerInfo.properties !== undefined) object.properties = playerInfo.properties + if (playerInfo.ping !== undefined) object.ping = playerInfo.ping + if (playerInfo.crypto !== undefined) object.crypto = playerInfo.crypto + if (playerInfo.ping > 1 || playerInfo.latency > 1) object.hadPing = true + this.players.set(object.uuid, object) + } + } + if (this.stateHandler.state !== "none") this.checkPlayerList() + }) + this.proxyClient.on("player_remove", data => { + for (let uuid of data.players) { + this.players.delete(uuid) + } + }) + this.stateHandler.on("state", state => { + if (state === "none") return + this.checkPlayerList() + }) + } + + handleTeamPacket(data) { + let team = data.team + let mode = data.mode + switch (mode) { + case 0: { + let object = {} + if ("name" in data) object.name = data.name + if ("prefix" in data) object.prefix = data.prefix + if ("suffix" in data) object.suffix = data.suffix + if ("friendlyFire" in data) object.friendlyFire = data.friendlyFire + if ("nameTagVisibility" in data) object.nameTagVisibility = data.nameTagVisibility + if ("collisionRule" in data) object.collisionRule = data.collisionRule + if ("color" in data) object.color = data.color + if ("formatting" in data) object.formatting = data.formatting + if ("players" in data && data.players) { + object.players = data.players + } else { + object.players = [] + } + this.teams.set(team, object) + break + } + case 1: { + this.teams.delete(team) + break + } + case 2: { + let object = this.teams.get(team) + if ("name" in data) object.name = data.name + if ("prefix" in data) object.prefix = data.prefix + if ("suffix" in data) object.suffix = data.suffix + if ("friendlyFire" in data) object.friendlyFire = data.friendlyFire + if ("nameTagVisibility" in data) object.nameTagVisibility = data.nameTagVisibility + if ("collisionRule" in data) object.collisionRule = data.collisionRule + if ("color" in data) object.color = data.color + if ("formatting" in data) object.formatting = data.formatting + if ("players" in data && data.players) object.players = data.players + break + } + case 3: { + let object = this.teams.get(team) + object.players.push(...data.players) + break + } + case 4: { + let object = this.teams.get(team) + for (let player of data.players) { + object.players.splice(object.players.indexOf(player), 1) + } + break + } + } + } + + getActualPlayers() { + let players = [] + for (let player of this.players.values()) { + if (!player.gamemode) continue + if ("displayName" in player) continue + if (player.hadPing) continue + players.push(player.uuid) + //console.log(player) + } + return players + } + + checkPlayerList() { + let list = this.getActualPlayers() + for (let uuid of list) { + (async () => { + let userData = await getStats(uuid) + if (this.stateHandler.state === "none") return + if (!this.players.has(uuid)) return + //console.log(uuid, userData) + })() + } + } +} \ No newline at end of file diff --git a/internalModules/WorldTracker.js b/internalModules/WorldTracker.js index 0893e91..600032c 100644 --- a/internalModules/WorldTracker.js +++ b/internalModules/WorldTracker.js @@ -38,10 +38,14 @@ export class WorldTracker { } else { chunk = new this.PrismarineChunk() } - if ("bitMap" in data) { - chunk.load(data.chunkData, data.bitMap) - } else { - chunk.load(data.chunkData) + try { + if ("bitMap" in data) { + chunk.load(data.chunkData, data.bitMap) + } else { + chunk.load(data.chunkData) + } + } catch (error) { + return } this.chunks.set(chunkId, chunk) })