From 857149d916cc971f8dffda67a7613d708826955c Mon Sep 17 00:00:00 2001 From: Emupedia Date: Tue, 14 May 2024 06:15:24 +0300 Subject: [PATCH] More work --- servers/agarv2/.gitignore | 2 +- servers/agarv2/cli/index.js | 207 ++- servers/agarv2/cli/log-handler.js | 200 ++- servers/agarv2/cli/log-settings.json | 44 +- servers/agarv2/cli/settings.json | 168 +- servers/agarv2/index.js | 36 +- servers/agarv2/src/ServerHandle.js | 350 ++-- servers/agarv2/src/Settings.js | 180 +- servers/agarv2/src/bots/Bot.js | 36 +- servers/agarv2/src/bots/Minion.js | 90 +- servers/agarv2/src/bots/PlayerBot.js | 332 ++-- servers/agarv2/src/cells/Cell.js | 344 ++-- servers/agarv2/src/cells/EjectedCell.js | 100 +- servers/agarv2/src/cells/Mothercell.js | 177 +- servers/agarv2/src/cells/Pellet.js | 99 +- servers/agarv2/src/cells/PlayerCell.js | 207 ++- servers/agarv2/src/cells/Virus.js | 169 +- servers/agarv2/src/commands/CommandList.js | 112 +- .../agarv2/src/commands/DefaultCommands.js | 1584 ++++++++++------- servers/agarv2/src/gamemodes/FFA.js | 118 +- servers/agarv2/src/gamemodes/Gamemode.js | 189 +- servers/agarv2/src/gamemodes/GamemodeList.js | 61 +- .../agarv2/src/gamemodes/LastManStanding.js | 80 +- servers/agarv2/src/gamemodes/Teams.js | 232 +-- servers/agarv2/src/globals.d.ts | 98 +- servers/agarv2/src/primitives/Logger.js | 141 +- servers/agarv2/src/primitives/Misc.js | 182 +- servers/agarv2/src/primitives/QuadTree.js | 517 +++--- servers/agarv2/src/primitives/Reader.js | 189 +- servers/agarv2/src/primitives/Stopwatch.js | 52 +- servers/agarv2/src/primitives/Ticker.js | 147 +- servers/agarv2/src/primitives/Writer.js | 246 +-- .../agarv2/src/protocols/LegacyProtocol.js | 1013 ++++++----- .../agarv2/src/protocols/ModernProtocol.js | 794 +++++---- servers/agarv2/src/protocols/Protocol.js | 226 ++- servers/agarv2/src/protocols/ProtocolStore.js | 56 +- servers/agarv2/src/sockets/ChatChannel.js | 3 + servers/agarv2/src/sockets/Connection.js | 39 +- servers/agarv2/src/sockets/Listener.js | 31 +- servers/agarv2/src/sockets/Router.js | 285 +-- servers/agarv2/src/worlds/Matchmaker.js | 192 +- servers/agarv2/src/worlds/Player.js | 311 ++-- servers/agarv2/src/worlds/World.js | 1504 +++++++++------- 43 files changed, 6412 insertions(+), 4731 deletions(-) diff --git a/servers/agarv2/.gitignore b/servers/agarv2/.gitignore index 2686831a..0feabee5 100644 --- a/servers/agarv2/.gitignore +++ b/servers/agarv2/.gitignore @@ -7,4 +7,4 @@ package-lock.json # IDE settings .vscode -.vs +.vs \ No newline at end of file diff --git a/servers/agarv2/cli/index.js b/servers/agarv2/cli/index.js index d4b9d551..b4404642 100644 --- a/servers/agarv2/cli/index.js +++ b/servers/agarv2/cli/index.js @@ -5,31 +5,37 @@ const { genCommand } = require("../src/commands/CommandList"); const readline = require("readline"); const DefaultCommands = require("../src/commands/DefaultCommands"); + const DefaultProtocols = [ - require("../src/protocols/LegacyProtocol"), - require("../src/protocols/ModernProtocol"), + require("../src/protocols/LegacyProtocol"), + require("../src/protocols/ModernProtocol"), ]; + const DefaultGamemodes = [ - require("../src/gamemodes/FFA"), - require("../src/gamemodes/Teams"), - require("../src/gamemodes/LastManStanding") + require("../src/gamemodes/FFA"), + require("../src/gamemodes/Teams"), + require("../src/gamemodes/LastManStanding") ]; /** @returns {DefaultSettings} */ function readSettings() { - try { return JSON.parse(fs.readFileSync("./settings.json", "utf-8")); } - catch (e) { - console.log("caught error while parsing/reading settings.json:", e.stack); - process.exit(1); - } + try { + return JSON.parse(fs.readFileSync("./settings.json", "utf-8")); + } catch (e) { + console.log("caught error while parsing/reading settings.json:", e.stack); + process.exit(1); + } } + /** @param {DefaultSettings} settings */ function overwriteSettings(settings) { - fs.writeFileSync("./settings.json", JSON.stringify(settings, null, 4), "utf-8"); + fs.writeFileSync("./settings.json", JSON.stringify(settings, null, 4), "utf-8"); +} + +if (!fs.existsSync("./settings.json")) { + overwriteSettings(DefaultSettings); } -if (!fs.existsSync("./settings.json")) - overwriteSettings(DefaultSettings); let settings = readSettings(); const currentHandle = new ServerHandle(settings); @@ -39,99 +45,110 @@ const logger = currentHandle.logger; let commandStreamClosing = false; const commandStream = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: true, - prompt: "", - historySize: 64, - removeHistoryDuplicates: true + input: process.stdin, + output: process.stdout, + terminal: true, + prompt: "", + historySize: 64, + removeHistoryDuplicates: true }); + commandStream.once("SIGINT", () => { - logger.inform("command stream caught SIGINT"); - commandStreamClosing = true; - commandStream.close(); - currentHandle.stop(); - process.exitCode = 0; + logger.inform("command stream caught SIGINT"); + commandStreamClosing = true; + commandStream.close(); + currentHandle.stop(); + process.exitCode = 0; }); - DefaultCommands(currentHandle.commands, currentHandle.chatCommands); currentHandle.protocols.register(...DefaultProtocols); currentHandle.gamemodes.register(...DefaultGamemodes); currentHandle.commands.register( - genCommand({ - name: "start", - args: "", - desc: "start the handle", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (!handle.start()) handle.logger.print("handle already running"); - } - }), - genCommand({ - name: "stop", - args: "", - desc: "stop the handle", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (!handle.stop()) handle.logger.print("handle not started"); - } - }), - genCommand({ - name: "exit", - args: "", - desc: "stop the handle and close the command stream", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - handle.stop(); - commandStream.close(); - commandStreamClosing = true; - } - }), - genCommand({ - name: "reload", - args: "", - desc: "reload the settings from local settings.json", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - handle.setSettings(readSettings()); - logger.print("done"); - } - }), - genCommand({ - name: "save", - args: "", - desc: "save the current settings to settings.json", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - overwriteSettings(handle.settings); - logger.print("done"); - } - }), + genCommand({ + name: "start", + args: "", + desc: "start the handle", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (!handle.start()) { + handle.logger.print("handle already running"); + } + } + }), + genCommand({ + name: "stop", + args: "", + desc: "stop the handle", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (!handle.stop()) { + handle.logger.print("handle not started"); + } + } + }), + genCommand({ + name: "exit", + args: "", + desc: "stop the handle and close the command stream", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + handle.stop(); + commandStream.close(); + commandStreamClosing = true; + } + }), + genCommand({ + name: "reload", + args: "", + desc: "reload the settings from local settings.json", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + handle.setSettings(readSettings()); + logger.print("done"); + } + }), + genCommand({ + name: "save", + args: "", + desc: "save the current settings to settings.json", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + overwriteSettings(handle.settings); + logger.print("done"); + } + }), ); function ask() { - if (commandStreamClosing) return; - commandStream.question("@ ", (input) => { - setTimeout(ask, 0); - if (!(input = input.trim())) return; - logger.printFile(`@ ${input}`); - if (!currentHandle.commands.execute(null, input)) - logger.print(`unknown command`); - }); + if (commandStreamClosing) { + return; + } + commandStream.question("@ ", (input) => { + setTimeout(ask, 0); + if (!(input = input.trim())) { + return; + } + logger.printFile(`@ ${input}`); + if (!currentHandle.commands.execute(null, input)) { + logger.print(`unknown command`); + } + }); } + setTimeout(() => { - logger.debug("command stream open"); - ask(); + logger.debug("command stream open"); + ask(); }, 1000); -currentHandle.start(); + +currentHandle.start(); \ No newline at end of file diff --git a/servers/agarv2/cli/log-handler.js b/servers/agarv2/cli/log-handler.js index e27f996b..2756f80c 100644 --- a/servers/agarv2/cli/log-handler.js +++ b/servers/agarv2/cli/log-handler.js @@ -1,76 +1,84 @@ let settings = { - showingConsole: { - PRINT: true, - FILE: false, - DEBUG: false, - ACCESS: false, - INFO: true, - WARN: true, - ERROR: true, - FATAL: true - }, - showingFile: { - PRINT: true, - FILE: true, - DEBUG: true, - ACCESS: true, - INFO: true, - WARN: true, - ERROR: true, - FATAL: true - }, - fileLogDirectory: "./logs/", - fileLogSaveOld: true + showingConsole: { + PRINT: true, + FILE: false, + DEBUG: false, + ACCESS: false, + INFO: true, + WARN: true, + ERROR: true, + FATAL: true + }, + showingFile: { + PRINT: true, + FILE: true, + DEBUG: true, + ACCESS: true, + INFO: true, + WARN: true, + ERROR: true, + FATAL: true + }, + fileLogDirectory: "./logs/", + fileLogSaveOld: true }; -const { EOL } = require("os"); +const {EOL} = require("os"); const fs = require("fs"); -if (fs.existsSync("./log-settings.json")) - settings = Object.assign(settings, JSON.parse(fs.readFileSync("./log-settings.json", "utf-8"))); +if (fs.existsSync("./log-settings.json")) { + settings = Object.assign(settings, JSON.parse(fs.readFileSync("./log-settings.json", "utf-8"))); +} fs.writeFileSync("./log-settings.json", JSON.stringify(settings, null, 4), "utf-8"); /** * @param {Date=} date */ function dateTime(date) { - const dy = date.getFullYear(); - const dm = ("00" + (date.getMonth() + 1)).slice(-2); - const dd = ("00" + (date.getDate())).slice(-2); - const th = ("00" + (date.getHours())).slice(-2); - const tm = ("00" + (date.getMinutes())).slice(-2); - const ts = ("00" + (date.getSeconds())).slice(-2); - const tz = ("000" + (date.getMilliseconds())).slice(-3); - return `${dy}-${dm}-${dd} ${th}:${tm}:${ts}.${tz}`; + const dy = date.getFullYear(); + const dm = ("00" + (date.getMonth() + 1)).slice(-2); + const dd = ("00" + (date.getDate())).slice(-2); + const th = ("00" + (date.getHours())).slice(-2); + const tm = ("00" + (date.getMinutes())).slice(-2); + const ts = ("00" + (date.getSeconds())).slice(-2); + const tz = ("000" + (date.getMilliseconds())).slice(-3); + return `${dy}-${dm}-${dd} ${th}:${tm}:${ts}.${tz}`; } /** * @param {Date} date */ function filename(date) { - const dy = date.getFullYear(); - const dm = ("00" + (date.getMonth() + 1)).slice(-2); - const dd = ("00" + (date.getDate())).slice(-2); - const th = ("00" + (date.getHours())).slice(-2); - const tm = ("00" + (date.getMinutes())).slice(-2); - const ts = ("00" + (date.getSeconds())).slice(-2); - return `${dy}-${dm}-${dd}T${th}-${tm}-${ts}.log`; + const dy = date.getFullYear(); + const dm = ("00" + (date.getMonth() + 1)).slice(-2); + const dd = ("00" + (date.getDate())).slice(-2); + const th = ("00" + (date.getHours())).slice(-2); + const tm = ("00" + (date.getMinutes())).slice(-2); + const ts = ("00" + (date.getSeconds())).slice(-2); + return `${dy}-${dm}-${dd}T${th}-${tm}-${ts}.log`; } const logFolder = settings.fileLogDirectory; const logFile = `${settings.fileLogDirectory}latest.log`; const oldLogsFolder = settings.fileLogDirectory + "old/"; -if (!fs.existsSync(logFolder)) fs.mkdirSync(logFolder); +if (!fs.existsSync(logFolder)) { + fs.mkdirSync(logFolder); +} if (fs.existsSync(logFile)) { - if (settings.fileLogSaveOld) { - if (!fs.existsSync(oldLogsFolder)) fs.mkdirSync(oldLogsFolder); - const oldLogFile = `${settings.fileLogDirectory}old/${filename(fs.statSync(logFile).ctime)}`; - fs.renameSync(logFile, oldLogFile); - } else fs.unlinkSync(logFile); + if (settings.fileLogSaveOld) { + if (!fs.existsSync(oldLogsFolder)) { + fs.mkdirSync(oldLogsFolder); + } + + const oldLogFile = `${settings.fileLogDirectory}old/${filename(fs.statSync(logFile).ctime)}`; + fs.renameSync(logFile, oldLogFile); + } else { + fs.unlinkSync(logFile); + } } -let fstream = fs.createWriteStream(logFile, { flags: "wx" }); +let fstream = fs.createWriteStream(logFile, {flags: "wx"}); /** @type {string[]} */ let fqueue = []; /** @type {string} */ @@ -84,25 +92,28 @@ let synchronous = false; * @param {string} message */ function formatConsole(date, level, message) { - switch (level) { - case "PRINT": - case "FILE": - return message; - default: return `${dateTime(date)} [${level}] ${message}`; - } + switch (level) { + case "PRINT": + case "FILE": + return message; + default: + return `${dateTime(date)} [${level}] ${message}`; + } } + /** * @param {Date} date * @param {LogEventLevel} level * @param {string} message */ function formatFile(date, level, message) { - switch (level) { - case "PRINT": - case "FILE": - return `${dateTime(date)} ${message}`; - default: return `${dateTime(date)} [${level}] ${message}`; - } + switch (level) { + case "PRINT": + case "FILE": + return `${dateTime(date)} ${message}`; + default: + return `${dateTime(date)} [${level}] ${message}`; + } } /** @@ -111,40 +122,53 @@ function formatFile(date, level, message) { * @param {string} message */ function write(date, level, message) { - if (settings.showingConsole[level]) - console.log(formatConsole(date, level, message)); - if (settings.showingFile[level]) { - fqueue.push(formatFile(date, level, message) + EOL); - if (!fprocessing && !synchronous) fprocess(); - } + if (settings.showingConsole[level]) { + console.log(formatConsole(date, level, message)); + } + + if (settings.showingFile[level]) { + fqueue.push(formatFile(date, level, message) + EOL); + + if (!fprocessing && !synchronous) { + fprocess(); + } + } } + function fprocess() { - fconsuming = null; - if (fqueue.length === 0) - return void (fprocessing = false); - fconsuming = fqueue.join(""); - fstream.write(fconsuming, fprocess); - fqueue.splice(0); - return void (fprocessing = true); + fconsuming = null; + + if (fqueue.length === 0) { + return void (fprocessing = false); + } + + fconsuming = fqueue.join(""); + fstream.write(fconsuming, fprocess); + fqueue.splice(0); + + return void (fprocessing = true); } + function fprocessSync() { - fstream.destroy(); - fstream = null; - const tail = fqueue.join(""); - fs.appendFileSync(logFile, tail, "utf-8"); - fqueue.splice(0); + fstream.destroy(); + fstream = null; + const tail = fqueue.join(""); + fs.appendFileSync(logFile, tail, "utf-8"); + fqueue.splice(0); } -process.once("uncaughtException", function(e) { - synchronous = true; - write(new Date(), "FATAL", e.stack); - fprocessSync(); - process.removeAllListeners("exit"); - process.exit(1); + +process.once("uncaughtException", function (e) { + synchronous = true; + write(new Date(), "FATAL", e.stack); + fprocessSync(); + process.removeAllListeners("exit"); + process.exit(1); }); -process.once("exit", function(code) { - synchronous = true; - write(new Date(), "DEBUG", `process ended with code ${code}`); - fprocessSync(); + +process.once("exit", function (code) { + synchronous = true; + write(new Date(), "DEBUG", `process ended with code ${code}`); + fprocessSync(); }); /** @@ -152,4 +176,4 @@ process.once("exit", function(code) { */ module.exports = (handle) => handle.logger.onlog = write; -const ServerHandle = require("../src/ServerHandle"); +const ServerHandle = require("../src/ServerHandle"); \ No newline at end of file diff --git a/servers/agarv2/cli/log-settings.json b/servers/agarv2/cli/log-settings.json index 49f0aabc..0666ca97 100644 --- a/servers/agarv2/cli/log-settings.json +++ b/servers/agarv2/cli/log-settings.json @@ -1,24 +1,24 @@ { - "showingConsole": { - "PRINT": true, - "FILE": true, - "DEBUG": true, - "ACCESS": true, - "INFO": true, - "WARN": true, - "ERROR": true, - "FATAL": true - }, - "showingFile": { - "PRINT": false, - "FILE": false, - "DEBUG": false, - "ACCESS": false, - "INFO": true, - "WARN": true, - "ERROR": true, - "FATAL": true - }, - "fileLogDirectory": "./logs/", - "fileLogSaveOld": true + "showingConsole": { + "PRINT": true, + "FILE": true, + "DEBUG": true, + "ACCESS": true, + "INFO": true, + "WARN": true, + "ERROR": true, + "FATAL": true + }, + "showingFile": { + "PRINT": false, + "FILE": false, + "DEBUG": false, + "ACCESS": false, + "INFO": true, + "WARN": true, + "ERROR": true, + "FATAL": true + }, + "fileLogDirectory": "./logs/", + "fileLogSaveOld": true } \ No newline at end of file diff --git a/servers/agarv2/cli/settings.json b/servers/agarv2/cli/settings.json index dc6ebb43..99372886 100644 --- a/servers/agarv2/cli/settings.json +++ b/servers/agarv2/cli/settings.json @@ -1,86 +1,86 @@ { - "listenerForbiddenIPs": [], - "listenerAcceptedOrigins": [], - "listenerMaxConnections": 200, - "listenerMaxClientDormancy": 60000, - "listenerMaxConnectionsPerIP": -1, - "listenerUseReCaptcha": true, - "listeningPort": 5000, - "serverFrequency": 25, - "serverName": "Emupedia", - "serverGamemode": "FFA", - "chatEnabled": true, - "chatFilteredPhrases": [], - "chatCooldown": 1000, - "worldMapX": 0, - "worldMapY": 0, - "worldMapW": 7071, - "worldMapH": 7071, - "worldFinderMaxLevel": 16, - "worldFinderMaxItems": 16, - "worldSafeSpawnTries": 64, - "worldSafeSpawnFromEjectedChance": 0.8, - "worldPlayerDisposeDelay": 1500, - "worldEatMult": 1.140175425099138, - "worldEatOverlapDiv": 3, - "worldPlayerBotsPerWorld": 0, - "worldPlayerBotNames": [], - "worldPlayerBotSkins": [], - "worldMinionsPerPlayer": 0, - "worldMaxPlayers": 1000, - "worldMinCount": 0, - "worldMaxCount": 2, - "matchmakerNeedsQueuing": false, - "matchmakerBulkSize": 1, - "minionName": "Minion", - "minionSpawnSize": 32, - "minionEnableERTPControls": false, - "minionEnableQBasedControl": true, - "pelletMinSize": 10, - "pelletMaxSize": 30, - "pelletGrowTicks": 1500, - "pelletCount": 1500, - "virusMinCount": 30, - "virusMaxCount": 90, - "virusSize": 100, - "virusFeedTimes": 7, - "virusPushing": false, - "virusSplitBoost": 780, - "virusPushBoost": 120, - "virusMonotonePops": false, - "ejectedSize": 38, - "ejectingLoss": 38, - "ejectDispersion": 0.3, - "ejectedCellBoost": 780, - "mothercellSize": 149, - "mothercellCount": 1, - "mothercellPassiveSpawnChance": 0.05, - "mothercellActiveSpawnSpeed": 1, - "mothercellPelletBoost": 90, - "mothercellMaxPellets": 96, - "mothercellMaxSize": 1500, - "playerRoamSpeed": 32, - "playerRoamViewScale": 0.4, - "playerViewScaleMult": 1, - "playerMinViewScale": 0.01, - "playerMaxNameLength": 100, - "playerAllowSkinInName": true, - "playerMinSize": 32, - "playerSpawnSize": 32, - "playerMaxSize": 1500, - "playerMinSplitSize": 60, - "playerMinEjectSize": 60, - "playerSplitCap": 255, - "playerEjectDelay": 2, - "playerMaxCells": 32, - "playerMoveMult": 1, - "playerSplitSizeDiv": 1.414213562373095, - "playerSplitDistance": 40, - "playerSplitBoost": 780, - "playerNoCollideDelay": 13, - "playerNoMergeDelay": 15, - "playerMergeVersion": "old", - "playerMergeTime": 1, - "playerMergeTimeIncrease": 0.02, - "playerDecayMult": 0.003 + "listenerForbiddenIPs": [], + "listenerAcceptedOrigins": [], + "listenerMaxConnections": 200, + "listenerMaxClientDormancy": 60000, + "listenerMaxConnectionsPerIP": -1, + "listenerUseReCaptcha": true, + "listeningPort": 5000, + "serverFrequency": 25, + "serverName": "Emupedia", + "serverGamemode": "FFA", + "chatEnabled": true, + "chatFilteredPhrases": [], + "chatCooldown": 1000, + "worldMapX": 0, + "worldMapY": 0, + "worldMapW": 7071, + "worldMapH": 7071, + "worldFinderMaxLevel": 16, + "worldFinderMaxItems": 16, + "worldSafeSpawnTries": 64, + "worldSafeSpawnFromEjectedChance": 0.8, + "worldPlayerDisposeDelay": 1500, + "worldEatMult": 1.140175425099138, + "worldEatOverlapDiv": 3, + "worldPlayerBotsPerWorld": 0, + "worldPlayerBotNames": [], + "worldPlayerBotSkins": [], + "worldMinionsPerPlayer": 0, + "worldMaxPlayers": 1000, + "worldMinCount": 0, + "worldMaxCount": 2, + "matchmakerNeedsQueuing": false, + "matchmakerBulkSize": 1, + "minionName": "Minion", + "minionSpawnSize": 32, + "minionEnableERTPControls": false, + "minionEnableQBasedControl": true, + "pelletMinSize": 10, + "pelletMaxSize": 30, + "pelletGrowTicks": 1500, + "pelletCount": 1500, + "virusMinCount": 30, + "virusMaxCount": 90, + "virusSize": 100, + "virusFeedTimes": 7, + "virusPushing": false, + "virusSplitBoost": 780, + "virusPushBoost": 120, + "virusMonotonePops": false, + "ejectedSize": 38, + "ejectingLoss": 38, + "ejectDispersion": 0.3, + "ejectedCellBoost": 780, + "mothercellSize": 149, + "mothercellCount": 1, + "mothercellPassiveSpawnChance": 0.05, + "mothercellActiveSpawnSpeed": 1, + "mothercellPelletBoost": 90, + "mothercellMaxPellets": 96, + "mothercellMaxSize": 1500, + "playerRoamSpeed": 32, + "playerRoamViewScale": 0.4, + "playerViewScaleMult": 1, + "playerMinViewScale": 0.01, + "playerMaxNameLength": 100, + "playerAllowSkinInName": true, + "playerMinSize": 32, + "playerSpawnSize": 32, + "playerMaxSize": 1500, + "playerMinSplitSize": 60, + "playerMinEjectSize": 60, + "playerSplitCap": 255, + "playerEjectDelay": 2, + "playerMaxCells": 32, + "playerMoveMult": 1, + "playerSplitSizeDiv": 1.414213562373095, + "playerSplitDistance": 40, + "playerSplitBoost": 780, + "playerNoCollideDelay": 13, + "playerNoMergeDelay": 15, + "playerMergeVersion": "old", + "playerMergeTime": 1, + "playerMergeTimeIncrease": 0.02, + "playerDecayMult": 0.003 } \ No newline at end of file diff --git a/servers/agarv2/index.js b/servers/agarv2/index.js index aa98b6a6..65df4af3 100644 --- a/servers/agarv2/index.js +++ b/servers/agarv2/index.js @@ -1,20 +1,20 @@ module.exports = { - ServerHandle: require("./src/ServerHandle"), - Router: require("./src/sockets/Router"), - Protocol: require("./src/protocols/Protocol"), - Command: require("./src/commands/CommandList").Command, - Gamemode: require("./src/gamemodes/Gamemode"), + ServerHandle: require("./src/ServerHandle"), + Router: require("./src/sockets/Router"), + Protocol: require("./src/protocols/Protocol"), + Command: require("./src/commands/CommandList").Command, + Gamemode: require("./src/gamemodes/Gamemode"), - base: { - commands: require("./src/commands/DefaultCommands"), - protocols: [ - require("./src/protocols/LegacyProtocol"), - require("./src/protocols/ModernProtocol") - ], - gamemodes: [ - require("./src/gamemodes/FFA"), - require("./src/gamemodes/Teams"), - require("./src/gamemodes/LastManStanding") - ] - } -}; + base: { + commands: require("./src/commands/DefaultCommands"), + protocols: [ + require("./src/protocols/LegacyProtocol"), + require("./src/protocols/ModernProtocol") + ], + gamemodes: [ + require("./src/gamemodes/FFA"), + require("./src/gamemodes/Teams"), + require("./src/gamemodes/LastManStanding") + ] + } +}; \ No newline at end of file diff --git a/servers/agarv2/src/ServerHandle.js b/servers/agarv2/src/ServerHandle.js index 9c4b75bd..6b0b6431 100644 --- a/servers/agarv2/src/ServerHandle.js +++ b/servers/agarv2/src/ServerHandle.js @@ -1,181 +1,207 @@ const Settings = require("./Settings"); - const { CommandList } = require("./commands/CommandList"); const GamemodeList = require("./gamemodes/GamemodeList"); const ProtocolStore = require("./protocols/ProtocolStore"); - const Stopwatch = require("./primitives/Stopwatch"); const Logger = require("./primitives/Logger"); const Ticker = require("./primitives/Ticker"); const { version } = require("./primitives/Misc"); - const Listener = require("./sockets/Listener"); const Matchmaker = require("./worlds/Matchmaker"); const Player = require("./worlds/Player"); const World = require("./worlds/World"); class ServerHandle { - /** - * @param {Settings} settings - */ - constructor(settings) { - /** @type {Settings} */ - this.settings = Settings; - - this.protocols = new ProtocolStore(); - this.gamemodes = new GamemodeList(this); - /** @type {Gamemode} */ - this.gamemode = null; - this.commands = new CommandList(this); - this.chatCommands = new CommandList(this); - - this.running = false; - /** @type {Date} */ - this.startTime = null; - this.averageTickTime = NaN; - this.tick = NaN; - this.tickDelay = NaN; - this.stepMult = NaN; - - this.ticker = new Ticker(40); - this.ticker.add(this.onTick.bind(this)); - this.stopwatch = new Stopwatch(); - this.logger = new Logger(); - - this.listener = new Listener(this); - this.matchmaker = new Matchmaker(this); - /** @type {Identified} */ - this.worlds = { }; - /** @type {Identified} */ - this.players = { }; - - this.setSettings(settings); - } - - get version() { return version; } - - /** - * @param {Settings} settings - */ - setSettings(settings) { - this.settings = Object.assign({ }, Settings, settings); - this.tickDelay = 1000 / this.settings.serverFrequency; - this.ticker.step = this.tickDelay; - this.stepMult = this.tickDelay / 40; - } - - start() { - if (this.running) return false; - this.logger.inform("starting"); - - this.gamemodes.setGamemode(this.settings.serverGamemode); - this.startTime = new Date(); - this.averageTickTime = this.tick = 0; - this.running = true; - - this.listener.open(); - this.ticker.start(); - this.gamemode.onHandleStart(); - - this.logger.inform("ticker begin"); - this.logger.inform(`OgarII ${this.version}`); - this.logger.inform(`gamemode: ${this.gamemode.name}`); - return true; - } - - stop() { - if (!this.running) return false; - this.logger.inform("stopping"); - - if (this.ticker.running) - this.ticker.stop(); - for (let id in this.worlds) - this.removeWorld(id); - for (let id in this.players) - this.removePlayer(id); - for (let i = 0, l = this.listener.routers; i < l; i++) - this.listener.routers[0].close(); - this.gamemode.onHandleStop(); - this.listener.close(); - - this.startTime = null; - this.averageTickTime = this.tick = NaN; - this.running = false; - - this.logger.inform("ticker stop"); - return true; - } - - /** @returns {World} */ - createWorld() { - let id = 0; - while (this.worlds.hasOwnProperty(++id)) ; - const newWorld = new World(this, id); - this.worlds[id] = newWorld; - this.gamemode.onNewWorld(newWorld); - newWorld.afterCreation(); - this.logger.debug(`added a world with id ${id}`); - return newWorld; - } - - /** - * @param {number} id - * @returns {boolean} - */ - removeWorld(id) { - if (!this.worlds.hasOwnProperty(id)) return false; - this.gamemode.onWorldDestroy(this.worlds[id]); - this.worlds[id].destroy(); - delete this.worlds[id]; - this.logger.debug(`removed world with id ${id}`); - return true; - } - - /** - * @param {Router} router - * @returns {Player} - */ - createPlayer(router) { - let id = 0; - while (this.players.hasOwnProperty(++id)) ; - const newPlayer = new Player(this, id, router); - this.players[id] = newPlayer; - router.player = newPlayer; - this.gamemode.onNewPlayer(newPlayer); - this.logger.debug(`added a player with id ${id}`); - return newPlayer; - } - - /** - * @param {number} id - * @returns {boolean} - */ - removePlayer(id) { - if (!this.players.hasOwnProperty(id)) return false; - this.gamemode.onPlayerDestroy(this.players[id]); - this.players[id].destroy(); - this.players[id].exists = false; - delete this.players[id]; - this.logger.debug(`removed player with id ${id}`); - return true; - } - - onTick() { - this.stopwatch.begin(); - this.tick++; - - for (let id in this.worlds) - this.worlds[id].update(); - this.listener.update(); - this.matchmaker.update(); - this.gamemode.onHandleTick(); - - this.averageTickTime = this.stopwatch.elapsed(); - this.stopwatch.stop(); - } + /** + * @param {Settings} settings + */ + constructor(settings) { + /** @type {Settings} */ + this.settings = Settings; + + this.protocols = new ProtocolStore(); + this.gamemodes = new GamemodeList(this); + /** @type {Gamemode} */ + this.gamemode = null; + this.commands = new CommandList(this); + this.chatCommands = new CommandList(this); + + this.running = false; + /** @type {Date} */ + this.startTime = null; + this.averageTickTime = NaN; + this.tick = NaN; + this.tickDelay = NaN; + this.stepMult = NaN; + + this.ticker = new Ticker(40); + this.ticker.add(this.onTick.bind(this)); + this.stopwatch = new Stopwatch(); + this.logger = new Logger(); + + this.listener = new Listener(this); + this.matchmaker = new Matchmaker(this); + /** @type {Identified} */ + this.worlds = {}; + /** @type {Identified} */ + this.players = {}; + + this.setSettings(settings); + } + get version() { + return version; + } + /** + * @param {Settings} settings + */ + setSettings(settings) { + this.settings = Object.assign({}, Settings, settings); + this.tickDelay = 1000 / this.settings.serverFrequency; + this.ticker.step = this.tickDelay; + this.stepMult = this.tickDelay / 40; + } + start() { + if (this.running) { + return false; + } + + this.logger.inform("starting"); + + this.gamemodes.setGamemode(this.settings.serverGamemode); + this.startTime = new Date(); + this.averageTickTime = this.tick = 0; + this.running = true; + + this.listener.open(); + this.ticker.start(); + this.gamemode.onHandleStart(); + + this.logger.inform("ticker begin"); + this.logger.inform(`OgarII ${this.version}`); + this.logger.inform(`gamemode: ${this.gamemode.name}`); + + return true; + } + stop() { + if (!this.running) { + return false; + } + + this.logger.inform("stopping"); + + if (this.ticker.running) { + this.ticker.stop(); + } + + for (let id in this.worlds) { + this.removeWorld(id); + } + + for (let id in this.players) { + this.removePlayer(id); + } + + for (let i = 0, l = this.listener.routers; i < l; i++) { + this.listener.routers[0].close(); + } + + this.gamemode.onHandleStop(); + this.listener.close(); + + this.startTime = null; + this.averageTickTime = this.tick = NaN; + this.running = false; + + this.logger.inform("ticker stop"); + + return true; + } + /** @returns {World} */ + createWorld() { + let id = 0; + + while (this.worlds.hasOwnProperty(++id)) { + ; + } + + const newWorld = new World(this, id); + this.worlds[id] = newWorld; + this.gamemode.onNewWorld(newWorld); + newWorld.afterCreation(); + this.logger.debug(`added a world with id ${id}`); + + return newWorld; + } + /** + * @param {number} id + * @returns {boolean} + */ + removeWorld(id) { + if (!this.worlds.hasOwnProperty(id)) { + return false; + } + + this.gamemode.onWorldDestroy(this.worlds[id]); + this.worlds[id].destroy(); + delete this.worlds[id]; + this.logger.debug(`removed world with id ${id}`); + + return true; + } + /** + * @param {Router} router + * @returns {Player} + */ + createPlayer(router) { + let id = 0; + + while (this.players.hasOwnProperty(++id)) { + ; + } + + const newPlayer = new Player(this, id, router); + this.players[id] = newPlayer; + router.player = newPlayer; + this.gamemode.onNewPlayer(newPlayer); + this.logger.debug(`added a player with id ${id}`); + + return newPlayer; + } + /** + * @param {number} id + * @returns {boolean} + */ + removePlayer(id) { + if (!this.players.hasOwnProperty(id)) { + return false; + } + + this.gamemode.onPlayerDestroy(this.players[id]); + this.players[id].destroy(); + this.players[id].exists = false; + delete this.players[id]; + this.logger.debug(`removed player with id ${id}`); + + return true; + } + onTick() { + this.stopwatch.begin(); + this.tick++; + + for (let id in this.worlds) { + this.worlds[id].update(); + } + + this.listener.update(); + this.matchmaker.update(); + this.gamemode.onHandleTick(); + + this.averageTickTime = this.stopwatch.elapsed(); + this.stopwatch.stop(); + } } module.exports = ServerHandle; const Router = require("./sockets/Router"); -const Gamemode = require("./gamemodes/Gamemode"); +const Gamemode = require("./gamemodes/Gamemode"); \ No newline at end of file diff --git a/servers/agarv2/src/Settings.js b/servers/agarv2/src/Settings.js index b7e2a1bf..383f28fa 100644 --- a/servers/agarv2/src/Settings.js +++ b/servers/agarv2/src/Settings.js @@ -1,107 +1,107 @@ const value = Object.seal({ - /** @type {IPAddress[]} */ - listenerForbiddenIPs: [], - /** @type {string[]} */ - listenerAcceptedOrigins: [], - listenerMaxConnections: 100, - listenerMaxClientDormancy: 1000 * 60, - listenerMaxConnectionsPerIP: -1, - listenerUseReCaptcha: false, - listeningPort: 443, + /** @type {IPAddress[]} */ + listenerForbiddenIPs: [], + /** @type {string[]} */ + listenerAcceptedOrigins: [], + listenerMaxConnections: 100, + listenerMaxClientDormancy: 1000 * 60, + listenerMaxConnectionsPerIP: -1, + listenerUseReCaptcha: false, + listeningPort: 443, - serverFrequency: 25, - serverName: "An unnamed server", - serverGamemode: "FFA", + serverFrequency: 25, + serverName: "An unnamed server", + serverGamemode: "FFA", - chatEnabled: true, - /** @type {string[]} */ - chatFilteredPhrases: [], - chatCooldown: 1000, + chatEnabled: true, + /** @type {string[]} */ + chatFilteredPhrases: [], + chatCooldown: 1000, - worldMapX: 0, - worldMapY: 0, - worldMapW: 7071, - worldMapH: 7071, - worldFinderMaxLevel: 16, - worldFinderMaxItems: 16, - worldSafeSpawnTries: 64, - worldSafeSpawnFromEjectedChance: 0.8, - worldPlayerDisposeDelay: 25 * 60, + worldMapX: 0, + worldMapY: 0, + worldMapW: 7071, + worldMapH: 7071, + worldFinderMaxLevel: 16, + worldFinderMaxItems: 16, + worldSafeSpawnTries: 64, + worldSafeSpawnFromEjectedChance: 0.8, + worldPlayerDisposeDelay: 25 * 60, - worldEatMult: 1.140175425099138, - worldEatOverlapDiv: 3, + worldEatMult: 1.140175425099138, + worldEatOverlapDiv: 3, - worldPlayerBotsPerWorld: 0, - /** @type {string[]} */ - worldPlayerBotNames: [], - /** @type {string[]} */ - worldPlayerBotSkins: [], - worldMinionsPerPlayer: 0, - worldMaxPlayers: 50, - worldMinCount: 0, - worldMaxCount: 2, - matchmakerNeedsQueuing: false, - matchmakerBulkSize: 1, + worldPlayerBotsPerWorld: 0, + /** @type {string[]} */ + worldPlayerBotNames: [], + /** @type {string[]} */ + worldPlayerBotSkins: [], + worldMinionsPerPlayer: 0, + worldMaxPlayers: 50, + worldMinCount: 0, + worldMaxCount: 2, + matchmakerNeedsQueuing: false, + matchmakerBulkSize: 1, - minionName: "Minion", - minionSpawnSize: 32, - minionEnableERTPControls: false, - minionEnableQBasedControl: true, + minionName: "Minion", + minionSpawnSize: 32, + minionEnableERTPControls: false, + minionEnableQBasedControl: true, - pelletMinSize: 10, - pelletMaxSize: 20, - pelletGrowTicks: 25 * 60, - pelletCount: 1000, + pelletMinSize: 10, + pelletMaxSize: 20, + pelletGrowTicks: 25 * 60, + pelletCount: 1000, - virusMinCount: 30, - virusMaxCount: 90, - virusSize: 100, - virusFeedTimes: 7, - virusPushing: false, - virusSplitBoost: 780, - virusPushBoost: 120, - virusMonotonePops: false, + virusMinCount: 30, + virusMaxCount: 90, + virusSize: 100, + virusFeedTimes: 7, + virusPushing: false, + virusSplitBoost: 780, + virusPushBoost: 120, + virusMonotonePops: false, - ejectedSize: 38, - ejectingLoss: 43, - ejectDispersion: 0.3, - ejectedCellBoost: 780, + ejectedSize: 38, + ejectingLoss: 43, + ejectDispersion: 0.3, + ejectedCellBoost: 780, - mothercellSize: 149, - mothercellCount: 0, - mothercellPassiveSpawnChance: 0.05, - mothercellActiveSpawnSpeed: 1, - mothercellPelletBoost: 90, - mothercellMaxPellets: 96, - mothercellMaxSize: 65535, + mothercellSize: 149, + mothercellCount: 0, + mothercellPassiveSpawnChance: 0.05, + mothercellActiveSpawnSpeed: 1, + mothercellPelletBoost: 90, + mothercellMaxPellets: 96, + mothercellMaxSize: 65535, - playerRoamSpeed: 32, - playerRoamViewScale: 0.4, - playerViewScaleMult: 1, - playerMinViewScale: 0.01, - playerMaxNameLength: 16, - playerAllowSkinInName: true, + playerRoamSpeed: 32, + playerRoamViewScale: 0.4, + playerViewScaleMult: 1, + playerMinViewScale: 0.01, + playerMaxNameLength: 16, + playerAllowSkinInName: true, - playerMinSize: 32, - playerSpawnSize: 32, - playerMaxSize: 1500, - playerMinSplitSize: 60, - playerMinEjectSize: 60, - playerSplitCap: 255, - playerEjectDelay: 2, - playerMaxCells: 16, + playerMinSize: 32, + playerSpawnSize: 32, + playerMaxSize: 1500, + playerMinSplitSize: 60, + playerMinEjectSize: 60, + playerSplitCap: 255, + playerEjectDelay: 2, + playerMaxCells: 16, - playerMoveMult: 1, - playerSplitSizeDiv: 1.414213562373095, - playerSplitDistance: 40, - playerSplitBoost: 780, - playerNoCollideDelay: 13, - playerNoMergeDelay: 15, - /** @type {"old" | "new"} */ - playerMergeVersion: "old", - playerMergeTime: 30, - playerMergeTimeIncrease: 0.02, - playerDecayMult: 0.001 + playerMoveMult: 1, + playerSplitSizeDiv: 1.414213562373095, + playerSplitDistance: 40, + playerSplitBoost: 780, + playerNoCollideDelay: 13, + playerNoMergeDelay: 15, + /** @type {"old" | "new"} */ + playerMergeVersion: "old", + playerMergeTime: 30, + playerMergeTimeIncrease: 0.02, + playerDecayMult: 0.001 }); module.exports = value; \ No newline at end of file diff --git a/servers/agarv2/src/bots/Bot.js b/servers/agarv2/src/bots/Bot.js index f36b5a97..f8666d10 100644 --- a/servers/agarv2/src/bots/Bot.js +++ b/servers/agarv2/src/bots/Bot.js @@ -2,27 +2,29 @@ const Router = require("../sockets/Router"); /** * @abstract -*/ + */ class Bot extends Router { - /** - * @param {World} world - */ - constructor(world) { - super(world.handle.listener); - this.createPlayer(); - world.addPlayer(this.player); - } + /** + * @param {World} world + */ + constructor(world) { + super(world.handle.listener); + this.createPlayer(); + world.addPlayer(this.player); + } - static get isExternal() { return false; } + static get isExternal() { + return false; + } - close() { - super.close(); - this.listener.handle.removePlayer(this.player.id); - this.disconnected = true; - this.disconnectionTick = this.listener.handle.tick; - } + close() { + super.close(); + this.listener.handle.removePlayer(this.player.id); + this.disconnected = true; + this.disconnectionTick = this.listener.handle.tick; + } } module.exports = Bot; -const World = require("../worlds/World"); +const World = require("../worlds/World"); \ No newline at end of file diff --git a/servers/agarv2/src/bots/Minion.js b/servers/agarv2/src/bots/Minion.js index 733f4507..d217ef12 100644 --- a/servers/agarv2/src/bots/Minion.js +++ b/servers/agarv2/src/bots/Minion.js @@ -1,53 +1,51 @@ const Bot = require("./Bot"); class Minion extends Bot { - /** - * @param {Connection} following - */ - constructor(following) { - super(following.player.world); - - this.following = following; - following.minions.push(this); - } - - static get type() { return "minion"; } - static get separateInTeams() { return false; } - - close() { - super.close(); - this.following.minions.splice(this.following.minions.indexOf(this), 1); - } - - get shouldClose() { - return !this.hasPlayer - || !this.player.exists - || !this.player.hasWorld - || this.following.socketDisconnected - || this.following.disconnected - || !this.following.hasPlayer - || !this.following.player.exists - || this.following.player.world !== this.player.world; - } - update() { - const name = - this.listener.settings.minionName === "*" - ? `*${this.following.player.leaderboardName}` - : this.listener.settings.minionName; - const player = this.player; - if (player.state === -1 && this.following.player.state === 0) { - this.spawningName = name; - this.onSpawnRequest(); - this.spawningName = null; - } else { - player.cellName = name; - player.leaderboardName = name; - } - this.mouseX = this.following.minionsFrozen ? this.player.viewArea.x : this.following.mouseX; - this.mouseY = this.following.minionsFrozen ? this.player.viewArea.y : this.following.mouseY; - } + /** + * @param {Connection} following + */ + constructor(following) { + super(following.player.world); + + this.following = following; + following.minions.push(this); + } + + static get type() { + return "minion"; + } + + static get separateInTeams() { + return false; + } + + close() { + super.close(); + this.following.minions.splice(this.following.minions.indexOf(this), 1); + } + + get shouldClose() { + return !this.hasPlayer || !this.player.exists || !this.player.hasWorld || this.following.socketDisconnected || this.following.disconnected || !this.following.hasPlayer || !this.following.player.exists || this.following.player.world !== this.player.world; + } + + update() { + const name = this.listener.settings.minionName === "*" ? `*${this.following.player.leaderboardName}` : this.listener.settings.minionName; + const player = this.player; + + if (player.state === -1 && this.following.player.state === 0) { + this.spawningName = name; + this.onSpawnRequest(); + this.spawningName = null; + } else { + player.cellName = name; + player.leaderboardName = name; + } + + this.mouseX = this.following.minionsFrozen ? this.player.viewArea.x : this.following.mouseX; + this.mouseY = this.following.minionsFrozen ? this.player.viewArea.y : this.following.mouseY; + } } module.exports = Minion; -const Connection = require("../sockets/Connection"); +const Connection = require("../sockets/Connection"); \ No newline at end of file diff --git a/servers/agarv2/src/bots/PlayerBot.js b/servers/agarv2/src/bots/PlayerBot.js index 4842bdb6..da042125 100644 --- a/servers/agarv2/src/bots/PlayerBot.js +++ b/servers/agarv2/src/bots/PlayerBot.js @@ -1,156 +1,194 @@ const Bot = require("./Bot"); class PlayerBot extends Bot { - /** - * @param {World} world - */ - constructor(world) { - super(world); - - this.splitCooldownTicks = 0; - /** @type {Cell} */ - this.target = null; - } - - static get type() { return "playerbot"; } - static get separateInTeams() { return true; } - - get shouldClose() { - return !this.hasPlayer - || !this.player.exists - || !this.player.hasWorld; - } - update() { - if (this.splitCooldownTicks > 0) this.splitCooldownTicks--; - else this.target = null; - - this.player.updateVisibleCells(); - const player = this.player; - if (player.state === -1) { - const names = this.listener.settings.worldPlayerBotNames; - const skins = this.listener.settings.worldPlayerBotSkins; - /** @type {string} */ - this.spawningName = names[~~(Math.random() * names.length)] || "Player bot"; - if (this.spawningName.indexOf("<*>") !== -1) - this.spawningName = this.spawningName.replace("<*>", `<${skins[~~(Math.random() * skins.length)]}>`); - this.onSpawnRequest(); - this.spawningName = null; - } - - /** @type {PlayerCell} */ - let cell = null; - for (let i = 0, l = player.ownedCells.length; i < l; i++) - if (cell === null || player.ownedCells[i].size > cell.size) - cell = player.ownedCells[i]; - if (cell === null) return; - - if (this.target != null) { - if (!this.target.exists || !this.canEat(cell.size, this.target.size)) - this.target = null; - else { - this.mouseX = this.target.x; - this.mouseY = this.target.y; - return; - } - } - - const atMaxCells = player.ownedCells.length >= this.listener.settings.playerMaxCells; - const willingToSplit = player.ownedCells.length <= 2; - const cellCount = Object.keys(player.visibleCells).length; - - let mouseX = 0; - let mouseY = 0; - let bestPrey = null; - let splitkillObstacleNearby = false; - - for (let id in player.visibleCells) { - const check = player.visibleCells[id]; - const truncatedInfluence = Math.log10(cell.squareSize); - let dx = check.x - cell.x; - let dy = check.y - cell.y; - let dSplit = Math.max(1, Math.sqrt(dx * dx + dy * dy)); - let d = Math.max(1, dSplit - cell.size - check.size); - let influence = 0; - switch (check.type) { - case 0: - if (player.id === check.owner.id) break; - if (player.team !== null && player.team === check.owner.team) break; - if (this.canEat(cell.size, check.size)) { - influence = truncatedInfluence; - if (!this.canSplitkill(cell.size, check.size, dSplit)) break; - if (bestPrey === null || check.size > bestPrey.size) - bestPrey = check; - } else { - influence = this.canEat(check.size, cell.size) ? -truncatedInfluence * cellCount : -1; - splitkillObstacleNearby = true; - } - break; - case 1: influence = 1; break; - case 2: - if (atMaxCells) influence = truncatedInfluence; - else if (this.canEat(cell.size, check.size)) { - influence = -1 * cellCount; - if (this.canSplitkill(cell.size, check.size, dSplit)) - splitkillObstacleNearby = true; - } - break; - case 3: if (this.canEat(cell.size, check.size)) influence = truncatedInfluence * cellCount; break; - case 4: - if (this.canEat(check.size, cell.size)) influence = -1; - else if (this.canEat(cell.size, check.size)) { - if (atMaxCells) influence = truncatedInfluence * cellCount; - else influence = -1; - } - break; - } - - if (influence === 0) continue; - if (d === 0) d = 1; - dx /= d; dy /= d; - mouseX += dx * influence / d; - mouseY += dy * influence / d; - } - - if ( - willingToSplit && !splitkillObstacleNearby && this.splitCooldownTicks <= 0 && - bestPrey !== null && bestPrey.size * 2 > cell.size - ) { - this.target = bestPrey; - this.mouseX = bestPrey.x; - this.mouseY = bestPrey.y; - this.splitAttempts++; - this.splitCooldownTicks = 25; - } else { - const d = Math.max(1, Math.sqrt(mouseX * mouseX + mouseY * mouseY)); - this.mouseX = cell.x + mouseX / d * player.viewArea.w; - this.mouseY = cell.y + mouseY / d * player.viewArea.h; - } - } - - /** - * @param {number} aSize - * @param {number} bSize - */ - canEat(aSize, bSize) { - return aSize > bSize * this.listener.settings.worldEatMult; - } - /** - * @param {number} aSize - * @param {number} bSize - * @param {number} d - */ - canSplitkill(aSize, bSize, d) { - const splitDistance = Math.max( - 2 * aSize / this.listener.settings.playerSplitSizeDiv / 2, - this.listener.settings.playerSplitBoost - ); - return aSize / this.listener.settings.playerSplitSizeDiv > bSize * this.listener.settings.worldEatMult && - d - splitDistance <= aSize - bSize / this.listener.settings.worldEatOverlapDiv; - } + /** + * @param {World} world + */ + constructor(world) { + super(world); + + this.splitCooldownTicks = 0; + /** @type {Cell} */ + this.target = null; + } + + static get type() { + return "playerbot"; + } + + static get separateInTeams() { + return true; + } + + get shouldClose() { + return !this.hasPlayer || !this.player.exists || !this.player.hasWorld; + } + + update() { + if (this.splitCooldownTicks > 0) { + this.splitCooldownTicks--; + } else { + this.target = null; + } + + this.player.updateVisibleCells(); + const player = this.player; + + if (player.state === -1) { + const names = this.listener.settings.worldPlayerBotNames; + const skins = this.listener.settings.worldPlayerBotSkins; + /** @type {string} */ + this.spawningName = names[~~(Math.random() * names.length)] || "Player bot"; + + if (this.spawningName.indexOf("<*>") !== -1) { + this.spawningName = this.spawningName.replace("<*>", `<${skins[~~(Math.random() * skins.length)]}>`); + } + + this.onSpawnRequest(); + this.spawningName = null; + } + + /** @type {PlayerCell} */ + let cell = null; + + for (let i = 0, l = player.ownedCells.length; i < l; i++) { + if (cell === null || player.ownedCells[i].size > cell.size) { + cell = player.ownedCells[i]; + } + } + + if (cell === null) { + return; + } + + if (this.target != null) { + if (!this.target.exists || !this.canEat(cell.size, this.target.size)) { + this.target = null; + } else { + this.mouseX = this.target.x; + this.mouseY = this.target.y; + return; + } + } + + const atMaxCells = player.ownedCells.length >= this.listener.settings.playerMaxCells; + const willingToSplit = player.ownedCells.length <= 2; + const cellCount = Object.keys(player.visibleCells).length; + + let mouseX = 0; + let mouseY = 0; + let bestPrey = null; + let splitkillObstacleNearby = false; + + for (let id in player.visibleCells) { + const check = player.visibleCells[id]; + const truncatedInfluence = Math.log10(cell.squareSize); + let dx = check.x - cell.x; + let dy = check.y - cell.y; + let dSplit = Math.max(1, Math.sqrt(dx * dx + dy * dy)); + let d = Math.max(1, dSplit - cell.size - check.size); + let influence = 0; + + switch (check.type) { + case 0: + if (player.id === check.owner.id) { + break; + } + if (player.team !== null && player.team === check.owner.team) { + break; + } + if (this.canEat(cell.size, check.size)) { + influence = truncatedInfluence; + if (!this.canSplitkill(cell.size, check.size, dSplit)) { + break; + } + if (bestPrey === null || check.size > bestPrey.size) { + bestPrey = check; + } + } else { + influence = this.canEat(check.size, cell.size) ? -truncatedInfluence * cellCount : -1; + splitkillObstacleNearby = true; + } + break; + case 1: + influence = 1; + break; + case 2: + if (atMaxCells) { + influence = truncatedInfluence; + } else if (this.canEat(cell.size, check.size)) { + influence = -1 * cellCount; + if (this.canSplitkill(cell.size, check.size, dSplit)) { + splitkillObstacleNearby = true; + } + } + break; + case 3: + if (this.canEat(cell.size, check.size)) { + influence = truncatedInfluence * cellCount; + } + break; + case 4: + if (this.canEat(check.size, cell.size)) { + influence = -1; + } else if (this.canEat(cell.size, check.size)) { + if (atMaxCells) { + influence = truncatedInfluence * cellCount; + } else { + influence = -1; + } + } + break; + } + + if (influence === 0) { + continue; + } + + if (d === 0) { + d = 1; + } + + dx /= d; + dy /= d; + mouseX += dx * influence / d; + mouseY += dy * influence / d; + } + + if (willingToSplit && !splitkillObstacleNearby && this.splitCooldownTicks <= 0 && bestPrey !== null && bestPrey.size * 2 > cell.size) { + this.target = bestPrey; + this.mouseX = bestPrey.x; + this.mouseY = bestPrey.y; + this.splitAttempts++; + this.splitCooldownTicks = 25; + } else { + const d = Math.max(1, Math.sqrt(mouseX * mouseX + mouseY * mouseY)); + this.mouseX = cell.x + mouseX / d * player.viewArea.w; + this.mouseY = cell.y + mouseY / d * player.viewArea.h; + } + } + + /** + * @param {number} aSize + * @param {number} bSize + */ + canEat(aSize, bSize) { + return aSize > bSize * this.listener.settings.worldEatMult; + } + + /** + * @param {number} aSize + * @param {number} bSize + * @param {number} d + */ + canSplitkill(aSize, bSize, d) { + const splitDistance = Math.max(2 * aSize / this.listener.settings.playerSplitSizeDiv / 2, this.listener.settings.playerSplitBoost); + return aSize / this.listener.settings.playerSplitSizeDiv > bSize * this.listener.settings.worldEatMult && d - splitDistance <= aSize - bSize / this.listener.settings.worldEatOverlapDiv; + } } module.exports = PlayerBot; const World = require("../worlds/World"); const Cell = require("../cells/Cell"); -const PlayerCell = require("../cells/PlayerCell"); +const PlayerCell = require("../cells/PlayerCell"); \ No newline at end of file diff --git a/servers/agarv2/src/cells/Cell.js b/servers/agarv2/src/cells/Cell.js index 878cc064..582b4169 100644 --- a/servers/agarv2/src/cells/Cell.js +++ b/servers/agarv2/src/cells/Cell.js @@ -2,153 +2,207 @@ const { throwIfBadNumber, throwIfBadOrNegativeNumber } = require("../primitives/ /** @abstract */ class Cell { - /** - * @param {World} world - * @param {number} x - * @param {number} y - * @param {number} size - * @param {number} color - */ - constructor(world, x, y, size, color) { - this.world = world; - - this.id = world.nextCellId; - this.birthTick = world.handle.tick; - this.exists = false; - /** @type {Cell} */ - this.eatenBy = null; - /** @type {Rect} */ - this.range = null; - this.isBoosting = false; - /** @type {Boost} */ - this.boost = { - dx: 0, - dy: 0, - d: 0 - }; - - /** @type {Player} */ - this.owner = null; - - this.x = x; - this.y = y; - this.size = size; - this.color = color; - this.name = null; - this.skin = null; - - this.posChanged = - this.sizeChanged = - this.colorChanged = - this.nameChanged = - this.skinChanged = - false; - } - - /** - * @abstract - * @returns {number} - */ - get type() { throw new Error("Must be overriden"); } - /** - * @abstract - * @returns {boolean} - */ - get isSpiked() { throw new Error("Must be overriden"); } - /** - * @abstract - * @returns {boolean} - */ - get isAgitated() { throw new Error("Must be overriden"); } - /** - * @abstract - * @returns {boolean} - */ - get avoidWhenSpawning() { throw new Error("Must be overriden"); } - /** - * @virtual - */ - get shouldUpdate() { - return this.posChanged || this.sizeChanged || - this.colorChanged || this.nameChanged || this.skinChanged; - } - - get age() { return (this.world.handle.tick - this.birthTick) * this.world.handle.stepMult; } - /** @type {number} */ - get x() { return this._x; } - /** @type {number} */ - get y() { return this._y; } - set x(value) { throwIfBadNumber(value); this._x = value; this.posChanged = true; } - set y(value) { throwIfBadNumber(value); this._y = value; this.posChanged = true; } - - /** @type {number} */ - get size() { return this._size; } - set size(value) { throwIfBadOrNegativeNumber(value); this._size = value; this.sizeChanged = true; } - - /** @type {number} */ - get squareSize() { return this.size * this.size; } - set squareSize(value) { this.size = Math.sqrt(value); } - - /** @type {number} */ - get mass() { return this.size * this.size / 100; } - set mass(value) { this.size = Math.sqrt(100 * value); } - - /** @type {number} */ - get color() { return this._color; } - set color(value) { this._color = value; this.colorChanged = true; } - - /** @type {string} */ - get name() { return this._name; } - set name(value) { this._name = value; this.nameChanged = true; } - - /** @type {string} */ - get skin() { return this._skin; } - set skin(value) { this._skin = value; this.skinChanged = true; } - - /** - * @param {Cell} other - * @returns {CellEatResult} - */ - getEatResult(other) { - throw new Error("Must be overriden"); - } - - /** - * @virtual - */ - onSpawned() { } - /** - * @virtual - */ - onTick() { - this.posChanged = - this.sizeChanged = - this.colorChanged = - this.nameChanged = - this.skinChanged = - false; - } - /** - * @param {Cell} other - * @virtual - */ - whenAte(other) { - this.squareSize += other.squareSize; - } - /** - * @param {Cell} other - * @virtual - */ - whenEatenBy(other) { - this.eatenBy = other; - } - /** - * @virtual - */ - onRemoved() { } + /** + * @param {World} world + * @param {number} x + * @param {number} y + * @param {number} size + * @param {number} color + */ + constructor(world, x, y, size, color) { + this.world = world; + + this.id = world.nextCellId; + this.birthTick = world.handle.tick; + this.exists = false; + /** @type {Cell} */ + this.eatenBy = null; + /** @type {Rect} */ + this.range = null; + this.isBoosting = false; + /** @type {Boost} */ + this.boost = { dx: 0, dy: 0, d: 0 }; + + /** @type {Player} */ + this.owner = null; + + this.x = x; + this.y = y; + this.size = size; + this.color = color; + this.name = null; + this.skin = null; + + this.posChanged = this.sizeChanged = this.colorChanged = this.nameChanged = this.skinChanged = false; + } + + /** + * @abstract + * @returns {number} + */ + get type() { + throw new Error("Must be overriden"); + } + + /** + * @abstract + * @returns {boolean} + */ + get isSpiked() { + throw new Error("Must be overriden"); + } + + /** + * @abstract + * @returns {boolean} + */ + get isAgitated() { + throw new Error("Must be overriden"); + } + + /** + * @abstract + * @returns {boolean} + */ + get avoidWhenSpawning() { + throw new Error("Must be overriden"); + } + + /** + * @virtual + */ + get shouldUpdate() { + return this.posChanged || this.sizeChanged || this.colorChanged || this.nameChanged || this.skinChanged; + } + + get age() { + return (this.world.handle.tick - this.birthTick) * this.world.handle.stepMult; + } + + /** @type {number} */ + get x() { + return this._x; + } + + /** @type {number} */ + get y() { + return this._y; + } + + set x(value) { + throwIfBadNumber(value); + this._x = value; + this.posChanged = true; + } + + set y(value) { + throwIfBadNumber(value); + this._y = value; + this.posChanged = true; + } + + /** @type {number} */ + get size() { + return this._size; + } + + set size(value) { + throwIfBadOrNegativeNumber(value); + this._size = value; + this.sizeChanged = true; + } + + /** @type {number} */ + get squareSize() { + return this.size * this.size; + } + + set squareSize(value) { + this.size = Math.sqrt(value); + } + + /** @type {number} */ + get mass() { + return this.size * this.size / 100; + } + + set mass(value) { + this.size = Math.sqrt(100 * value); + } + + /** @type {number} */ + get color() { + return this._color; + } + + set color(value) { + this._color = value; + this.colorChanged = true; + } + + /** @type {string} */ + get name() { + return this._name; + } + + set name(value) { + this._name = value; + this.nameChanged = true; + } + + /** @type {string} */ + get skin() { + return this._skin; + } + + set skin(value) { + this._skin = value; + this.skinChanged = true; + } + + /** + * @param {Cell} other + * @returns {CellEatResult} + */ + getEatResult(other) { + throw new Error("Must be overriden"); + } + + /** + * @virtual + */ + onSpawned() {} + + /** + * @virtual + */ + onTick() { + this.posChanged = this.sizeChanged = this.colorChanged = this.nameChanged = this.skinChanged = false; + } + + /** + * @param {Cell} other + * @virtual + */ + whenAte(other) { + this.squareSize += other.squareSize; + } + + /** + * @param {Cell} other + * @virtual + */ + whenEatenBy(other) { + this.eatenBy = other; + } + + /** + * @virtual + */ + onRemoved() {} } module.exports = Cell; const World = require("../worlds/World"); -const Player = require("../worlds/Player"); +const Player = require("../worlds/Player"); \ No newline at end of file diff --git a/servers/agarv2/src/cells/EjectedCell.js b/servers/agarv2/src/cells/EjectedCell.js index 85b1239c..49411933 100644 --- a/servers/agarv2/src/cells/EjectedCell.js +++ b/servers/agarv2/src/cells/EjectedCell.js @@ -1,47 +1,69 @@ const Cell = require("./Cell"); class EjectedCell extends Cell { - /** - * @param {World} world - * @param {Player} owner - * @param {number} x - * @param {number} y - * @param {number} color - */ - constructor(world, owner, x, y, color) { - const size = world.settings.ejectedSize; - super(world, x, y, size, color); - this.owner = owner; - } - - get type() { return 3; } - get isSpiked() { return false; } - get isAgitated() { return false; } - get avoidWhenSpawning() { return false; } - - /** - * @param {Cell} other - * @returns {CellEatResult} - */ - getEatResult(other) { - if (other.type === 2) return other.getEjectedEatResult(false); - if (other.type === 4) return 3; - if (other.type === 3) { - if (!other.isBoosting) other.world.setCellAsBoosting(other); - return 1; - } - return 0; - } - - onSpawned() { - this.world.ejectedCells.push(this); - } - onRemoved() { - this.world.ejectedCells.splice(this.world.ejectedCells.indexOf(this), 1); - } + /** + * @param {World} world + * @param {Player} owner + * @param {number} x + * @param {number} y + * @param {number} color + */ + constructor(world, owner, x, y, color) { + const size = world.settings.ejectedSize; + super(world, x, y, size, color); + this.owner = owner; + } + + get type() { + return 3; + } + + get isSpiked() { + return false; + } + + get isAgitated() { + return false; + } + + get avoidWhenSpawning() { + return false; + } + + /** + * @param {Cell} other + * @returns {CellEatResult} + */ + getEatResult(other) { + if (other.type === 2) { + return other.getEjectedEatResult(false); + } + + if (other.type === 4) { + return 3; + } + + if (other.type === 3) { + if (!other.isBoosting) { + other.world.setCellAsBoosting(other); + } + + return 1; + } + + return 0; + } + + onSpawned() { + this.world.ejectedCells.push(this); + } + + onRemoved() { + this.world.ejectedCells.splice(this.world.ejectedCells.indexOf(this), 1); + } } module.exports = EjectedCell; const World = require("../worlds/World"); -const Player = require("../worlds/Player"); +const Player = require("../worlds/Player"); \ No newline at end of file diff --git a/servers/agarv2/src/cells/Mothercell.js b/servers/agarv2/src/cells/Mothercell.js index 1695f1f2..049d8582 100644 --- a/servers/agarv2/src/cells/Mothercell.js +++ b/servers/agarv2/src/cells/Mothercell.js @@ -5,83 +5,108 @@ const Pellet = require("./Pellet"); * @implements {Spawner} */ class Mothercell extends Cell { - /** - * @param {World} world - */ - constructor(world, x, y) { - const size = world.settings.mothercellSize; - super(world, x, y, size, 0xCE6363); - - this.pelletCount = 0; - this.activePelletFormQueue = 0; - this.passivePelletFormQueue = 0; - } - - get type() { return 4; } - get isSpiked() { return true; } - get isAgitated() { return false; } - get avoidWhenSpawning() { return true; } - - /** - * @param {Cell} other - * @returns {CellEatResult} - */ - getEatResult(other) { return 0; } - - onTick() { - const settings = this.world.settings; - const mothercellSize = settings.mothercellSize; - const pelletSize = settings.pelletMinSize; - const minSpawnSqSize = mothercellSize * mothercellSize + pelletSize * pelletSize; - - this.activePelletFormQueue += settings.mothercellActiveSpawnSpeed * this.world.handle.stepMult; - this.passivePelletFormQueue += Math.random() * settings.mothercellPassiveSpawnChance * this.world.handle.stepMult; - - while (this.activePelletFormQueue > 0) { - if (this.squareSize > minSpawnSqSize) - this.spawnPellet(), this.squareSize -= pelletSize * pelletSize; - else if (this.size > mothercellSize) - this.size = mothercellSize; - this.activePelletFormQueue--; - } - while (this.passivePelletFormQueue > 0) { - if (this.pelletCount < settings.mothercellMaxPellets) - this.spawnPellet(); - this.passivePelletFormQueue--; - } - } - spawnPellet() { - const angle = Math.random() * 2 * Math.PI; - const x = this.x + this.size * Math.sin(angle); - const y = this.y + this.size * Math.cos(angle); - const pellet = new Pellet(this.world, this, x, y); - pellet.boost.dx = Math.sin(angle); - pellet.boost.dy = Math.cos(angle); - const d = this.world.settings.mothercellPelletBoost; - pellet.boost.d = d / 2 + Math.random() * d / 2; - this.world.addCell(pellet); - this.world.setCellAsBoosting(pellet); - } - - onSpawned() { - this.world.mothercellCount++; - } - whenAte(cell) { - super.whenAte(cell); - this.size = Math.min(this.size, this.world.settings.mothercellMaxSize); - } - /** - * @param {Cell} cell - */ - whenEatenBy(cell) { - super.whenEatenBy(cell); - if (cell.type === 0) this.world.popPlayerCell(cell); - } - onRemoved() { - this.world.mothercellCount--; - } + /** + * @param {World} world + */ + constructor(world, x, y) { + const size = world.settings.mothercellSize; + super(world, x, y, size, 0xCE6363); + + this.pelletCount = 0; + this.activePelletFormQueue = 0; + this.passivePelletFormQueue = 0; + } + + get type() { + return 4; + } + + get isSpiked() { + return true; + } + + get isAgitated() { + return false; + } + + get avoidWhenSpawning() { + return true; + } + + /** + * @param {Cell} other + * @returns {CellEatResult} + */ + getEatResult(other) { + return 0; + } + + onTick() { + const settings = this.world.settings; + const mothercellSize = settings.mothercellSize; + const pelletSize = settings.pelletMinSize; + const minSpawnSqSize = mothercellSize * mothercellSize + pelletSize * pelletSize; + + this.activePelletFormQueue += settings.mothercellActiveSpawnSpeed * this.world.handle.stepMult; + this.passivePelletFormQueue += Math.random() * settings.mothercellPassiveSpawnChance * this.world.handle.stepMult; + + while (this.activePelletFormQueue > 0) { + if (this.squareSize > minSpawnSqSize) { + this.spawnPellet(), this.squareSize -= pelletSize * pelletSize; + } else if (this.size > mothercellSize) { + this.size = mothercellSize; + } + + this.activePelletFormQueue--; + } + + while (this.passivePelletFormQueue > 0) { + if (this.pelletCount < settings.mothercellMaxPellets) { + this.spawnPellet(); + } + + this.passivePelletFormQueue--; + } + } + + spawnPellet() { + const angle = Math.random() * 2 * Math.PI; + const x = this.x + this.size * Math.sin(angle); + const y = this.y + this.size * Math.cos(angle); + const pellet = new Pellet(this.world, this, x, y); + pellet.boost.dx = Math.sin(angle); + pellet.boost.dy = Math.cos(angle); + const d = this.world.settings.mothercellPelletBoost; + pellet.boost.d = d / 2 + Math.random() * d / 2; + this.world.addCell(pellet); + this.world.setCellAsBoosting(pellet); + } + + onSpawned() { + this.world.mothercellCount++; + } + + whenAte(cell) { + super.whenAte(cell); + this.size = Math.min(this.size, this.world.settings.mothercellMaxSize); + } + + /** + * @param {Cell} cell + */ + whenEatenBy(cell) { + super.whenEatenBy(cell); + + if (cell.type === 0) { + this.world.popPlayerCell(cell); + } + } + + onRemoved() { + this.world.mothercellCount--; + } } module.exports = Mothercell; -const World = require("../worlds/World"); +const World = require("../worlds/World"); \ No newline at end of file diff --git a/servers/agarv2/src/cells/Pellet.js b/servers/agarv2/src/cells/Pellet.js index 72bb06db..9f867349 100644 --- a/servers/agarv2/src/cells/Pellet.js +++ b/servers/agarv2/src/cells/Pellet.js @@ -2,47 +2,66 @@ const Misc = require("../primitives/Misc"); const Cell = require("./Cell"); class Pellet extends Cell { - /** - * @param {World} world - * @param {Spawner} spawner - * @param {number} x - * @param {number} y - */ - constructor(world, spawner, x, y) { - const size = world.settings.pelletMinSize; - super(world, x, y, size, Misc.randomColor()); - - this.spawner = spawner; - this.lastGrowTick = this.birthTick; - } - - get type() { return 1; } - get isSpiked() { return false; } - get isAgitated() { return false; } - get avoidWhenSpawning() { return false; } - - /** - * @param {Cell} other - * @returns {CellEatResult} - */ - getEatResult() { return 0; } - - onTick() { - super.onTick(); - if (this.size >= this.world.settings.pelletMaxSize) return; - if (this.world.handle.tick - this.lastGrowTick > this.world.settings.pelletGrowTicks / this.world.handle.stepMult) { - this.lastGrowTick = this.world.handle.tick; - this.mass++; - } - } - onSpawned() { - this.spawner.pelletCount++; - } - onRemoved() { - this.spawner.pelletCount--; - } + /** + * @param {World} world + * @param {Spawner} spawner + * @param {number} x + * @param {number} y + */ + constructor(world, spawner, x, y) { + const size = world.settings.pelletMinSize; + super(world, x, y, size, Misc.randomColor()); + + this.spawner = spawner; + this.lastGrowTick = this.birthTick; + } + + get type() { + return 1; + } + + get isSpiked() { + return false; + } + + get isAgitated() { + return false; + } + + get avoidWhenSpawning() { + return false; + } + + /** + * @param {Cell} other + * @returns {CellEatResult} + */ + getEatResult() { + return 0; + } + + onTick() { + super.onTick(); + + if (this.size >= this.world.settings.pelletMaxSize) { + return; + } + + if (this.world.handle.tick - this.lastGrowTick > this.world.settings.pelletGrowTicks / this.world.handle.stepMult) { + this.lastGrowTick = this.world.handle.tick; + this.mass++; + } + } + + onSpawned() { + this.spawner.pelletCount++; + } + + onRemoved() { + this.spawner.pelletCount--; + } } module.exports = Pellet; -const World = require("../worlds/World"); +const World = require("../worlds/World"); \ No newline at end of file diff --git a/servers/agarv2/src/cells/PlayerCell.js b/servers/agarv2/src/cells/PlayerCell.js index 3a3b2701..725fc67f 100644 --- a/servers/agarv2/src/cells/PlayerCell.js +++ b/servers/agarv2/src/cells/PlayerCell.js @@ -1,91 +1,130 @@ const Cell = require("./Cell"); class PlayerCell extends Cell { - /** - * @param {Player} owner - * @param {number} x - * @param {number} y - * @param {number} size - * @param {number} color - */ - constructor(owner, x, y, size) { - super(owner.world, x, y, size, owner.cellColor); - this.owner = owner; - this.name = owner.cellName || ""; - this.skin = owner.cellSkin || ""; - this._canMerge = false; - } - - get moveSpeed() { - return 88 * Math.pow(this.size, -0.4396754) * this.owner.settings.playerMoveMult; - } - get canMerge() { return this._canMerge; } - - get type() { return 0; } - get isSpiked() { return false; } - get isAgitated() { return false; } - get avoidWhenSpawning() { return true; } - - /** - * @param {Cell} other - * @returns {CellEatResult} - */ - getEatResult(other) { - if (other.type === 0) { - const delay = this.world.settings.playerNoCollideDelay; - if (other.owner.id === this.owner.id) { - if (other.age < delay || this.age < delay) return 0; - if (this.canMerge && other.canMerge) return 2; - return 1; - } - if (other.owner.team === this.owner.team && this.owner.team !== null) - return (other.age < delay || this.age < delay) ? 0 : 1; - return this.getDefaultEatResult(other); - } - if (other.type === 4 && other.size > this.size * this.world.settings.worldEatMult) return 3; - if (other.type === 1) return 2; - return this.getDefaultEatResult(other); - } - /** - * @param {Cell} other - */ - getDefaultEatResult(other) { - return other.size * this.world.settings.worldEatMult > this.size ? 0 : 2; - } - - onTick() { - super.onTick(); - - if (this.name !== this.owner.cellName) - this.name = this.owner.cellName; - if (this.skin !== this.owner.cellSkin) - this.skin = this.owner.cellSkin; - if (this.color !== this.owner.cellColor) - this.color = this.owner.cellColor; - - const settings = this.world.settings; - let delay = settings.playerNoMergeDelay; - if (settings.playerMergeTime > 0) { - const initial = Math.round(25 * settings.playerMergeTime); - const increase = Math.round(25 * this.size * settings.playerMergeTimeIncrease); - delay = Math.max(delay, settings.playerMergeVersion === "new" ? Math.max(initial, increase) : initial + increase); - } - this._canMerge = this.age >= delay; - } - - onSpawned() { - this.owner.router.onNewOwnedCell(this); - this.owner.ownedCells.push(this); - this.world.playerCells.unshift(this); - } - - onRemoved() { - this.world.playerCells.splice(this.world.playerCells.indexOf(this), 1); - this.owner.ownedCells.splice(this.owner.ownedCells.indexOf(this), 1); - this.owner.updateState(-1); - } + /** + * @param {Player} owner + * @param {number} x + * @param {number} y + * @param {number} size + * @param {number} color + */ + constructor(owner, x, y, size) { + super(owner.world, x, y, size, owner.cellColor); + this.owner = owner; + this.name = owner.cellName || ""; + this.skin = owner.cellSkin || ""; + this._canMerge = false; + } + + get moveSpeed() { + return 88 * Math.pow(this.size, -0.4396754) * this.owner.settings.playerMoveMult; + } + + get canMerge() { + return this._canMerge; + } + + get type() { + return 0; + } + + get isSpiked() { + return false; + } + + get isAgitated() { + return false; + } + + get avoidWhenSpawning() { + return true; + } + + /** + * @param {Cell} other + * @returns {CellEatResult} + */ + getEatResult(other) { + if (other.type === 0) { + const delay = this.world.settings.playerNoCollideDelay; + + if (other.owner.id === this.owner.id) { + if (other.age < delay || this.age < delay) { + return 0; + } + + if (this.canMerge && other.canMerge) { + return 2; + } + + return 1; + } + + if (other.owner.team === this.owner.team && this.owner.team !== null) { + return (other.age < delay || this.age < delay) ? 0 : 1; + } + + return this.getDefaultEatResult(other); + } + + if (other.type === 4 && other.size > this.size * this.world.settings.worldEatMult) { + return 3; + } + + if (other.type === 1) { + return 2; + } + + return this.getDefaultEatResult(other); + } + + /** + * @param {Cell} other + */ + getDefaultEatResult(other) { + return other.size * this.world.settings.worldEatMult > this.size ? 0 : 2; + } + + onTick() { + super.onTick(); + + if (this.name !== this.owner.cellName) { + this.name = this.owner.cellName; + } + + if (this.skin !== this.owner.cellSkin) { + this.skin = this.owner.cellSkin; + } + + if (this.color !== this.owner.cellColor) { + this.color = this.owner.cellColor; + } + + const settings = this.world.settings; + let delay = settings.playerNoMergeDelay; + + if (settings.playerMergeTime > 0) { + const initial = Math.round(25 * settings.playerMergeTime); + const increase = Math.round(25 * this.size * settings.playerMergeTimeIncrease); + delay = Math.max(delay, settings.playerMergeVersion === "new" ? Math.max(initial, increase) : initial + increase); + } + + this._canMerge = this.age >= delay; + } + + onSpawned() { + this.owner.router.onNewOwnedCell(this); + this.owner.ownedCells.push(this); + this.world.playerCells.unshift(this); + } + + onRemoved() { + this.world.playerCells.splice(this.world.playerCells.indexOf(this), 1); + this.owner.ownedCells.splice(this.owner.ownedCells.indexOf(this), 1); + this.owner.updateState(-1); + } } module.exports = PlayerCell; -const Player = require("../worlds/Player"); +const Player = require("../worlds/Player"); \ No newline at end of file diff --git a/servers/agarv2/src/cells/Virus.js b/servers/agarv2/src/cells/Virus.js index 4d0196ca..ac72ada3 100644 --- a/servers/agarv2/src/cells/Virus.js +++ b/servers/agarv2/src/cells/Virus.js @@ -1,79 +1,104 @@ const Cell = require("./Cell"); class Virus extends Cell { - /** - * @param {World} world - * @param {number} x - * @param {number} y - */ - constructor(world, x, y) { - const size = world.settings.virusSize; - super(world, x, y, size, 0x33FF33); - - this.fedTimes = 0; - this.splitAngle = NaN; - } - - get type() { return 2; } - get isSpiked() { return true; } - get isAgitated() { return false; } - get avoidWhenSpawning() { return true; } - - /** - * @param {Cell} other - * @returns {CellEatResult} - */ - getEatResult(other) { - if (other.type === 3) return this.getEjectedEatResult(true); - if (other.type === 4) return 3; - return 0; - } - /** - * @param {boolean} isSelf - * @returns {CellEatResult} - */ - getEjectedEatResult(isSelf) { - return this.world.virusCount >= this.world.settings.virusMaxCount ? 0 : isSelf ? 2 : 3; - } - - onSpawned() { - this.world.virusCount++; - } - - /** - * @param {Cell} cell - */ - whenAte(cell) { - const settings = this.world.settings; - if (settings.virusPushing) { - const newD = this.boost.d + settings.virusPushBoost; - this.boost.dx = (this.boost.dx * this.boost.d + cell.boost.dx * settings.virusPushBoost) / newD; - this.boost.dy = (this.boost.dy * this.boost.d + cell.boost.dy * settings.virusPushBoost) / newD; - this.boost.d = newD; - this.world.setCellAsBoosting(this); - } else { - this.splitAngle = Math.atan2(cell.boost.dx, cell.boost.dy); - if (++this.fedTimes >= settings.virusFeedTimes) { - this.fedTimes = 0; - this.size = settings.virusSize; - this.world.splitVirus(this); - } else super.whenAte(cell); - } - } - - /** - * @param {Cell} cell - */ - whenEatenBy(cell) { - super.whenEatenBy(cell); - if (cell.type === 0) this.world.popPlayerCell(cell); - } - - onRemoved() { - this.world.virusCount--; - } + /** + * @param {World} world + * @param {number} x + * @param {number} y + */ + constructor(world, x, y) { + const size = world.settings.virusSize; + super(world, x, y, size, 0x33FF33); + + this.fedTimes = 0; + this.splitAngle = NaN; + } + + get type() { + return 2; + } + + get isSpiked() { + return true; + } + + get isAgitated() { + return false; + } + + get avoidWhenSpawning() { + return true; + } + + /** + * @param {Cell} other + * @returns {CellEatResult} + */ + getEatResult(other) { + if (other.type === 3) { + return this.getEjectedEatResult(true); + } + + if (other.type === 4) { + return 3; + } + + return 0; + } + + /** + * @param {boolean} isSelf + * @returns {CellEatResult} + */ + getEjectedEatResult(isSelf) { + return this.world.virusCount >= this.world.settings.virusMaxCount ? 0 : isSelf ? 2 : 3; + } + + onSpawned() { + this.world.virusCount++; + } + + /** + * @param {Cell} cell + */ + whenAte(cell) { + const settings = this.world.settings; + + if (settings.virusPushing) { + const newD = this.boost.d + settings.virusPushBoost; + this.boost.dx = (this.boost.dx * this.boost.d + cell.boost.dx * settings.virusPushBoost) / newD; + this.boost.dy = (this.boost.dy * this.boost.d + cell.boost.dy * settings.virusPushBoost) / newD; + this.boost.d = newD; + this.world.setCellAsBoosting(this); + } else { + this.splitAngle = Math.atan2(cell.boost.dx, cell.boost.dy); + + if (++this.fedTimes >= settings.virusFeedTimes) { + this.fedTimes = 0; + this.size = settings.virusSize; + this.world.splitVirus(this); + } else { + super.whenAte(cell); + } + } + } + + /** + * @param {Cell} cell + */ + whenEatenBy(cell) { + super.whenEatenBy(cell); + + if (cell.type === 0) { + this.world.popPlayerCell(cell); + } + } + + onRemoved() { + this.world.virusCount--; + } } module.exports = Virus; -const World = require("../worlds/World"); +const World = require("../worlds/World"); \ No newline at end of file diff --git a/servers/agarv2/src/commands/CommandList.js b/servers/agarv2/src/commands/CommandList.js index d103d406..8efcf8f8 100644 --- a/servers/agarv2/src/commands/CommandList.js +++ b/servers/agarv2/src/commands/CommandList.js @@ -2,68 +2,80 @@ * @template T */ class Command { - /** - * @param {string} name - * @param {string} description - * @param {string} args - * @param {(handle: ServerHandle, context: T, args: string[]) => void} executor - */ - constructor(name, description, args, executor) { - this.name = name.toLowerCase(); - this.description = description; - this.args = args; - this.executor = executor; - } + /** + * @param {string} name + * @param {string} description + * @param {string} args + * @param {(handle: ServerHandle, context: T, args: string[]) => void} executor + */ + constructor(name, description, args, executor) { + this.name = name.toLowerCase(); + this.description = description; + this.args = args; + this.executor = executor; + } - toString() { - return `${this.name}${!this.args ? "" : " " + this.args} - ${this.description}`; - } + toString() { + return `${this.name}${!this.args ? "" : " " + this.args} - ${this.description}`; + } } /** * @template T */ class CommandList { - constructor(handle) { - this.handle = handle; - /** @type {{[commandName: string]: Command}} */ - this.list = {}; - } + constructor(handle) { + this.handle = handle; + /** @type {{[commandName: string]: Command}} */ + this.list = {}; + } - /** - * @param {Command[]} commands - */ - register(...commands) { - for (let i = 0, l = commands.length; i < l; i++) { - const command = commands[i]; - if (this.list.hasOwnProperty(command)) throw new Error("command conflicts with another already registered one"); - this.list[command.name] = command; - } - } + /** + * @param {Command[]} commands + */ + register(...commands) { + for (let i = 0, l = commands.length; i < l; i++) { + const command = commands[i]; - /** - * @param {T} context - * @param {string} input - */ - execute(context, input) { - const split = input.split(" "); - if (split.length === 0) return false; - if (!this.list.hasOwnProperty(split[0].toLowerCase())) return false; - this.list[split[0].toLowerCase()].executor(this.handle, context, split.slice(1)); - return true; - } + if (this.list.hasOwnProperty(command)) { + throw new Error("command conflicts with another already registered one"); + } + + this.list[command.name] = command; + } + } + + /** + * @param {T} context + * @param {string} input + */ + execute(context, input) { + const split = input.split(" "); + + if (split.length === 0) { + return false; + } + + if (!this.list.hasOwnProperty(split[0].toLowerCase())) { + return false; + } + + this.list[split[0].toLowerCase()].executor(this.handle, context, split.slice(1)); + + return true; + } } module.exports = { - Command: Command, - CommandList: CommandList, - /** - * @template T - * @param {{ args: string, desc: string, name: string, exec: (handle: ServerHandle, context: T, args: string[]) => void }} info - */ - genCommand(info) { - return new Command(info.name, info.desc, info.args, info.exec); - } + Command: Command, + CommandList: CommandList, + /** + * @template T + * @param {{ args: string, desc: string, name: string, exec: (handle: ServerHandle, context: T, args: string[]) => void }} info + */ + genCommand(info) { + return new Command(info.name, info.desc, info.args, info.exec); + } }; const ServerHandle = require("../ServerHandle"); \ No newline at end of file diff --git a/servers/agarv2/src/commands/DefaultCommands.js b/servers/agarv2/src/commands/DefaultCommands.js index 4c0f21dc..f977f6de 100644 --- a/servers/agarv2/src/commands/DefaultCommands.js +++ b/servers/agarv2/src/commands/DefaultCommands.js @@ -13,15 +13,16 @@ const IPvalidate = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){ * @param {number} len */ function padRight(str, pad, len) { - return str + new Array(Math.max(len - str.length, 0)).fill(pad).join(""); + return str + new Array(Math.max(len - str.length, 0)).fill(pad).join(""); } + /** * @param {string} str * @param {string} pad * @param {number} len */ function padLeft(str, pad, len) { - return new Array(Math.max(len - str.length, 0)).fill(pad).join("") + str; + return new Array(Math.max(len - str.length, 0)).fill(pad).join("") + str; } /** @@ -29,107 +30,177 @@ function padLeft(str, pad, len) { * @param {string} eol */ function table(contents, eol) { - const columnSizes = []; - let all = "", i, j, rowText, row, col, size; - for (i = 0; i < contents.columns.length; i++) { - col = contents.columns[i]; - size = col.text.length; - for (j = 0; j < contents.rows.length; j++) - size = Math.max(size, contents.rows[j][i] ? contents.rows[j][i].length : 0); - columnSizes.push(size); - } - for (i = 0, rowText = ""; i < contents.columns.length; i++) { - col = contents.columns[i]; - rowText += (i === 0 ? "" : col.separated ? " | " : " ") + padRight(col.text, col.headPad, columnSizes[i]); - } - all += rowText + eol; - for (i = 0, rowText = ""; i < contents.rows.length; i++, rowText = "") { - for (j = 0; j < contents.rows[i].length; j++) { - row = contents.rows[i][j] || ""; - col = contents.columns[j]; - rowText += (j === 0 ? "" : col.separated ? " | " : " ") + padRight(row, row ? col.rowPad : col.emptyPad, columnSizes[j]); - } - for (; j < contents.columns.length; j++) { - col = contents.columns[j]; - rowText += (j === 0 ? "" : col.separated ? " | " : " ") + padRight("", col.emptyPad, columnSizes[j]); - } - all += rowText + eol; - } - return all; + const columnSizes = []; + let all = "", i, j, rowText, row, col, size; + + for (i = 0; i < contents.columns.length; i++) { + col = contents.columns[i]; + size = col.text.length; + + for (j = 0; j < contents.rows.length; j++) { + size = Math.max(size, contents.rows[j][i] ? contents.rows[j][i].length : 0); + } + + columnSizes.push(size); + } + + for (i = 0, rowText = ""; i < contents.columns.length; i++) { + col = contents.columns[i]; + rowText += (i === 0 ? "" : col.separated ? " | " : " ") + padRight(col.text, col.headPad, columnSizes[i]); + } + + all += rowText + eol; + + for (i = 0, rowText = ""; i < contents.rows.length; i++, rowText = "") { + for (j = 0; j < contents.rows[i].length; j++) { + row = contents.rows[i][j] || ""; + col = contents.columns[j]; + rowText += (j === 0 ? "" : col.separated ? " | " : " ") + padRight(row, row ? col.rowPad : col.emptyPad, columnSizes[j]); + } + + for (; j < contents.columns.length; j++) { + col = contents.columns[j]; + rowText += (j === 0 ? "" : col.separated ? " | " : " ") + padRight("", col.emptyPad, columnSizes[j]); + } + + all += rowText + eol; + } + + return all; } + /** * @param {SettingIdType} id */ function splitSettingId(id) { - let items = [], reg, i = 0; - while ((reg = /([a-z]+)|([A-Z][a-z]+)|([A-Z])/.exec(id)) != null && ++i < 10) { - const capture = reg[1] || reg[2] || reg[3]; - items.push(capture.toLowerCase()), id = id.replace(capture, ""); - } - return items; + let items = [], reg, i = 0; + + while ((reg = /([a-z]+)|([A-Z][a-z]+)|([A-Z])/.exec(id)) != null && ++i < 10) { + const capture = reg[1] || reg[2] || reg[3]; + items.push(capture.toLowerCase()), id = id.replace(capture, ""); + } + + return items; } + /** * @param {string[]} a * @param {string[]} b */ function getSplitSettingHits(a, b) { - let hits = 0; - for (let i = 0, l = b.length; i < l; i++) - if (a.indexOf(b[i]) !== -1) hits++; - return hits; + let hits = 0; + + for (let i = 0, l = b.length; i < l; i++) { + if (a.indexOf(b[i]) !== -1) { + hits++; + } + } + + return hits; } /** @param {number} value */ function prettyMemory(value) { - const units = ["B", "kiB", "MiB", "GiB", "TiB"]; let i = 0; - for (; i < units.length && value / 1024 > 1; i++) - value /= 1024; - return `${value.toFixed(1)} ${units[i]}`; + const units = ["B", "kiB", "MiB", "GiB", "TiB"]; + let i = 0; + + for (; i < units.length && value / 1024 > 1; i++) { + value /= 1024; + } + + return `${value.toFixed(1)} ${units[i]}`; } + /** @param {NodeJS.MemoryUsage} value */ function prettyMemoryData(value) { - return { - heapUsed: prettyMemory(value.heapUsed), - heapTotal: prettyMemory(value.heapTotal), - rss: prettyMemory(value.rss), - external: prettyMemory(value.external) - } + return { + heapUsed: prettyMemory(value.heapUsed), + heapTotal: prettyMemory(value.heapTotal), + rss: prettyMemory(value.rss), + external: prettyMemory(value.external) + } } + /** @param {number} seconds */ function prettyTime(seconds) { - seconds = ~~seconds; + seconds = ~~seconds; - let minutes = ~~(seconds / 60); - if (minutes < 1) return `${seconds} seconds`; - if (seconds === 60) return `1 minute`; + let minutes = ~~(seconds / 60); - let hours = ~~(minutes / 60); - if (hours < 1) return `${minutes} minute${minutes === 1 ? "" : "s"} ${seconds % 60} second${seconds === 1 ? "" : "s"}`; - if (minutes === 60) return `1 hour`; + if (minutes < 1) { + return `${seconds} seconds`; + } - let days = ~~(hours / 24); - if (days < 1) return `${hours} hour${hours === 1 ? "" : "s"} ${minutes % 60} minute${minutes === 1 ? "" : "s"}`; - if (hours === 24) return `1 day`; - return `${days} day${days === 1 ? "" : "s"} ${hours % 24} hour${hours === 1 ? "" : "s"}`; + if (seconds === 60) { + return `1 minute`; + } + + let hours = ~~(minutes / 60); + + if (hours < 1) { + return `${minutes} minute${minutes === 1 ? "" : "s"} ${seconds % 60} second${seconds === 1 ? "" : "s"}`; + } + + if (minutes === 60) { + return `1 hour`; + } + + let days = ~~(hours / 24); + + if (days < 1) { + return `${hours} hour${hours === 1 ? "" : "s"} ${minutes % 60} minute${minutes === 1 ? "" : "s"}`; + } + + if (hours === 24) { + return `1 day`; + } + + return `${days} day${days === 1 ? "" : "s"} ${hours % 24} hour${hours === 1 ? "" : "s"}`; } + /** @param {number} seconds */ function shortPrettyTime(milliseconds) { - let seconds = ~~(milliseconds / 1000); - if (seconds < 1) return `${milliseconds}ms`; - if (milliseconds === 1000) return `1s`; - - let minutes = ~~(seconds / 60); - if (minutes < 1) return `${seconds}s`; - if (seconds === 60) return `1m`; - - let hours = ~~(minutes / 60); - if (hours < 1) return `${minutes}m`; - if (minutes === 60) return `1h`; - - let days = ~~(hours / 24); - if (days < 1) return `${hours}h`; - if (hours === 24) return `1d`; - return `${days}d`; + let seconds = ~~(milliseconds / 1000); + + if (seconds < 1) { + return `${milliseconds}ms`; + } + + if (milliseconds === 1000) { + return `1s`; + } + + let minutes = ~~(seconds / 60); + + if (minutes < 1) { + return `${seconds}s`; + } + + if (seconds === 60) { + return `1m`; + } + + let hours = ~~(minutes / 60); + + if (hours < 1) { + return `${minutes}m`; + } + + if (minutes === 60) { + return `1h`; + } + + let days = ~~(hours / 24); + + if (days < 1) { + return `${hours}h`; + } + + if (hours === 24) { + return `1d`; + } + + return `${days}d`; } /** @@ -139,17 +210,27 @@ function shortPrettyTime(milliseconds) { * @param {boolean} needAlive */ function getPlayerByID(args, handle, index, needAlive) { - if (args.length <= index) - return handle.logger.print("missing player id"), false; - const id = parseInt(args[index]); - if (isNaN(id)) - return handle.logger.print("invalid number for player id"), false; - if (!handle.players.hasOwnProperty(id)) - return handle.logger.print("no player has this id"), false; - if (handle.players[id].state !== 0 && needAlive) - return handle.logger.print("player is not alive"), false; - return handle.players[id]; + if (args.length <= index) { + return handle.logger.print("missing player id"), false; + } + + const id = parseInt(args[index]); + + if (isNaN(id)) { + return handle.logger.print("invalid number for player id"), false; + } + + if (!handle.players.hasOwnProperty(id)) { + return handle.logger.print("no player has this id"), false; + } + + if (handle.players[id].state !== 0 && needAlive) { + return handle.logger.print("player is not alive"), false; + } + + return handle.players[id]; } + /** * @param {string[]} args * @param {ServerHandle} handle @@ -157,16 +238,25 @@ function getPlayerByID(args, handle, index, needAlive) { * @param {boolean} needRunning */ function getWorldByID(args, handle, index, needRunning) { - if (args.length <= index) - return handle.logger.print("missing world id"), false; - const id = parseInt(args[index]); - if (isNaN(id)) - return handle.logger.print("invalid number for world id"), false; - if (!handle.worlds.hasOwnProperty(id)) - return handle.logger.print("no world has this id"), false; - if (handle.worlds[id].frozen && needRunning) - return handle.logger.print("world is frozen"), false; - return handle.worlds[id]; + if (args.length <= index) { + return handle.logger.print("missing world id"), false; + } + + const id = parseInt(args[index]); + + if (isNaN(id)) { + return handle.logger.print("invalid number for world id"), false; + } + + if (!handle.worlds.hasOwnProperty(id)) { + return handle.logger.print("no world has this id"), false; + } + + if (handle.worlds[id].frozen && needRunning) { + return handle.logger.print("world is frozen"), false; + } + + return handle.worlds[id]; } /** @@ -176,13 +266,19 @@ function getWorldByID(args, handle, index, needRunning) { * @param {string} argName */ function getFloat(args, handle, index, argName) { - if (args.length <= index) - return handle.logger.print(`missing ${argName}`), false; - const value = parseFloat(args[index]); - if (isNaN(value)) - return handle.logger.print(`invalid number for ${argName}`), false; - return value; + if (args.length <= index) { + return handle.logger.print(`missing ${argName}`), false; + } + + const value = parseFloat(args[index]); + + if (isNaN(value)) { + return handle.logger.print(`invalid number for ${argName}`), false; + } + + return value; } + /** * @param {string[]} args * @param {ServerHandle} handle @@ -190,13 +286,19 @@ function getFloat(args, handle, index, argName) { * @param {string} argName */ function getInt(args, handle, index, argName) { - if (args.length <= index) - return handle.logger.print(`missing ${argName}`), false; - const value = parseInt(args[index]); - if (isNaN(value)) - return handle.logger.print(`invalid number for ${argName}`), false; - return value; + if (args.length <= index) { + return handle.logger.print(`missing ${argName}`), false; + } + + const value = parseInt(args[index]); + + if (isNaN(value)) { + return handle.logger.print(`invalid number for ${argName}`), false; + } + + return value; } + /** * @param {string[]} args * @param {ServerHandle} handle @@ -204,12 +306,17 @@ function getInt(args, handle, index, argName) { * @param {string} argName */ function getString(args, handle, index, argName) { - if (args.length <= index) - return handle.logger.print(`missing ${argName}`), false; - const value = args[index].trim(); - if (value.length === 0) - return handle.logger.print(`invalid string for ${argName}`), false; - return value; + if (args.length <= index) { + return handle.logger.print(`missing ${argName}`), false; + } + + const value = args[index].trim(); + + if (value.length === 0) { + return handle.logger.print(`invalid string for ${argName}`), false; + } + + return value; } /** @@ -217,548 +324,713 @@ function getString(args, handle, index, argName) { * @param {CommandList} chatCommands */ module.exports = (commands, chatCommands) => { - commands.register( - genCommand({ - name: "help", - args: "", - desc: "display all registered commands and their relevant information", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const list = handle.commands.list; - const keys = Object.keys(list).sort(); - handle.logger.print(table({ - columns: [ - { text: "NAME", headPad: " ", emptyPad: " ", rowPad: " ", separated: false }, - { text: "ARGUMENTS", headPad: " ", emptyPad: " ", rowPad: " ", separated: false }, - { text: "DESCRIPTION", headPad: " ", emptyPad: " ", rowPad: " ", separated: true } - ], - rows: keys.map(v => { - return [ - list[v].name, - list[v].args, - list[v].description - ] - }) - }, EOL)); - } - }), - genCommand({ - name: "routers", - args: "[router type]", - desc: "display information about routers and their players", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const matchingType = args.length >= 1 ? args[0] : null; - const routers = handle.listener.routers - .filter(v => matchingType === null || v.type == matchingType); - handle.logger.print(table({ - columns: [ - { text: "INDEX", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "TYPE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "SOURCE", headPad: " ", emptyPad: "/", rowPad: " ", separated: true }, - { text: "ACTIVE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "DORMANT", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "PROTOCOL", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "PID", headPad: " ", emptyPad: "/", rowPad: " ", separated: false } - ], - rows: routers.map((v, i) => [ - i.toString(), - v.type, - v.type === "connection" ? v.remoteAddress : null, - v.type === "connection" ? shortPrettyTime(Date.now() - v.connectTime) : null, - v.type === "connection" ? shortPrettyTime(Date.now() - v.lastActivityTime) : null, - v.protocol ? v.protocol.subtype : null, - v.hasPlayer ? v.player.id.toString() : null, - ]) - }, EOL)); - } - }), - genCommand({ - name: "players", - args: "[world id or \"any\"] [router type]", - desc: "display information about players", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const worldId = args.length >= 1 ? parseInt(args[0]) || null : null; - const routerType = args.length === 2 ? args[1] : null; - const players = handle.listener.routers - .filter(v => v.hasPlayer && (routerType == null || v.type == routerType)) - .map(v => v.player) - .filter(v => worldId == null || (v.hasWorld && v.world.id === worldId)); - handle.logger.print(table({ - columns: [ - { text: "ID", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "WORLD", headPad: " ", emptyPad: "/", rowPad: " ", separated: true }, - { text: "FOLLOWING", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "STATE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "SCORE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false }, - { text: "NAME", headPad: " ", emptyPad: "/", rowPad: " ", separated: false } - ], - rows: players.map((v) => { - let ret = [ - v.id.toString(), - v.hasWorld ? v.world.id.toString() : null, - ]; - if (v.type === "minion") ret.push(v.following.player.id.toString()); - else if (v.hasWorld && v.state === 1) ret.push(v.world.largestPlayer.id.toString()); - else ret.push(null); - - switch (v.state) { - case -1: ret.push("idle"); break; - case 0: - ret.push("alive"); - ret.push(Math.round(v.score).toString()); - ret.push(v.leaderboardName); - break; - case 1: ret.push("spec"); break; - case 2: ret.push("roam"); break; - } - return ret; - }) - }, EOL)); - } - }), - genCommand({ - name: "stats", - args: "", - desc: "display critical information about the server", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const logger = handle.logger; - const memory = prettyMemoryData(process.memoryUsage()); - const external = handle.listener.connections.length; - const internal = handle.listener.routers.length - external; - if (!handle.running) - return void logger.print("not running"); - logger.print(`load: ${handle.averageTickTime.toFixed(4)} ms / ${handle.tickDelay} ms`); - logger.print(`memory: ${memory.heapUsed} / ${memory.heapTotal} / ${memory.rss} / ${memory.external}`); - logger.print(`uptime: ${prettyTime(Math.floor((Date.now() - handle.startTime.getTime()) / 1000))}`); - logger.print(`routers: ${external} external, ${internal} internal, ${external + internal} total`) - logger.print(`players: ${Object.keys(handle.players).length}`); - for (let id in handle.worlds) { - const world = handle.worlds[id], stats = world.stats, - cells = [ world.cells.length, world.playerCells.length, world.pelletCount, world.virusCount, world.ejectedCells.length, world.mothercellCount], - statsF = [ stats.external, stats.internal, stats.limit, stats.playing, stats.spectating ]; - logger.print(`world ${id}: ${cells[0]} cells - ${cells[1]}P/${cells[2]}p/${cells[3]}v/${cells[4]}e/${cells[5]}m`); - logger.print(` ${statsF[0]} / ${statsF[1]} / ${statsF[2]} players - ${statsF[3]}p/${statsF[4]}s`); - } - } - }), - genCommand({ - name: "setting", - args: " [value]", - desc: "change/print the value of a setting", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (args.length < 1) - return void handle.logger.print("no setting name provided"); - const settingName = args[0]; - if (!handle.settings.hasOwnProperty(settingName)) { - const settingIdSplit = splitSettingId(settingName); - const possible = Object.keys(handle.settings) - .map(v => { return { name: v, hits: getSplitSettingHits(splitSettingId(v), settingIdSplit) }; }) - .sort((a, b) => b.hits - a.hits) - .filter((v) => v.hits > 0) - .filter((v, i, array) => array[0].hits === v.hits) - .map(v => v.name); - let printing = "no such setting"; - if (possible.length > 0) { - printing += `; did you mean ${possible.slice(0, 3).join(", ")}` - if (possible.length > 3) printing += `, ${possible.length - 3} other`; - printing += "?" - } - return void handle.logger.print(printing); - } - if (args.length >= 2) { - const settingValue = JSON.parse(args.slice(1).join(" ")); - const newSettings = Object.assign({ }, handle.settings); - newSettings[settingName] = settingValue; - handle.setSettings(newSettings); - } - handle.logger.print(handle.settings[settingName]); - } - }), - genCommand({ - name: "eval", - args: "", - desc: "evaluate javascript code in a function bound to server handle", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const result = (function() { - try { return eval(args.join(" ")); } - catch (e) { return !e ? e : (e.stack || e); } - }).bind(handle)(); - handle.logger.print(inspect(result, true, 1, false)); - } - }), - genCommand({ - name: "test", - args: "", - desc: "test command", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => handle.logger.print("success successful") - }), - genCommand({ - name: "crash", - args: "", - desc: "manually force an error throw", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { throw new Error("manual crash"); } - }), - genCommand({ - name: "restart", - args: "", - desc: "stop then immediately start the handle", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (!handle.stop()) return void handle.logger.print("handle not started"); - handle.start(); - } - }), - genCommand({ - name: "pause", - args: "", - desc: "toggle handle pause", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (!handle.running) return void handle.logger.print("handle not started"); - if (handle.ticker.running) - handle.ticker.stop(); - else handle.ticker.start(); - } - }), - genCommand({ - name: "mass", - args: " ", - desc: "set cell mass to all of a player's cells", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const player = getPlayerByID(args, handle, 0, true); - const mass = getFloat(args, handle, 1, "mass"); - if (player === false || mass === false) - return; - const l = player.ownedCells.length; - for (let i = 0; i < l; i++) player.ownedCells[i].mass = mass; - handle.logger.print(`player now has ${mass * l} mass`); - } - }), - genCommand({ - name: "merge", - args: "", - desc: "instantly merge a player", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const player = getPlayerByID(args, handle, 0, true); - if (player === false) - return; - const l = player.ownedCells.length; - let sqSize = 0; - for (let i = 0; i < l; i++) sqSize += player.ownedCells[i].squareSize; - player.ownedCells[0].squareSize = sqSize; - player.ownedCells[0].x = player.viewArea.x; - player.ownedCells[0].y = player.viewArea.y; - for (let i = 1; i < l; i++) player.world.removeCell(player.ownedCells[1]); - handle.logger.print(`merged player from ${l} cells and ${Math.round(sqSize / 100)} mass`); - } - }), - genCommand({ - name: "kill", - args: "", - desc: "instantly kill a player", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const player = getPlayerByID(args, handle, 0, true); - if (player === false) - return; - for (let i = 0, l = player.ownedCells.length; i < l; i++) - player.world.removeCell(player.ownedCells[0]); - handle.logger.print("player killed"); - } - }), - genCommand({ - name: "explode", - args: "", - desc: "instantly explode a player's first cell", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const player = getPlayerByID(args, handle, 0, true); - if (player === false) - return; - player.world.popPlayerCell(player.ownedCells[0]); - handle.logger.print("player exploded"); - } - }), - genCommand({ - name: "addminion", - args: " [count]", - desc: "assign minions to a player", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (args.length === 1) args[1] = "1"; - const player = getPlayerByID(args, handle, 0, false); - const count = getInt(args, handle, 1, "count"); - if (player === false || count === false) - return; - if (!player.router.isExternal) - return void handle.logger.print("player is not external"); - if (!player.hasWorld) - return void handle.logger.print("player is not in a world"); - for (let i = 0; i < count; i++) new Minion(player.router); - handle.logger.print(`added ${count} minions to player`); - } - }), - genCommand({ - name: "rmminion", - args: " [count]", - desc: "remove assigned minions from a player", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (args.length === 1) args[1] = "1"; - const player = getPlayerByID(args, handle, 0, false); - const count = getInt(args, handle, 1, "count"); - if (player === false || count === false) - return; - if (!player.router.isExternal) - return void handle.logger.print("player is not external"); - if (!player.hasWorld) - return void handle.logger.print("player is not in a world"); - let realCount = 0; - for (let i = 0; i < count && player.router.minions.length > 0; i++) { - player.router.minions[0].close(); - realCount++; - } - handle.logger.print(`removed ${realCount} minions from player`); - } - }), - genCommand({ - name: "killall", - args: "", - desc: "instantly kill all players in a world", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - const world = getWorldByID(args, handle, 0, false); - if (world === false) - return; - const players = world.players; - for (let i = 0; i < players.length; i++) { - const player = players[i]; - if (player.state !== 0) continue; - for (let j = 0, l = player.ownedCells.length; j < l; j++) - player.world.removeCell(player.ownedCells[0]); - } - handle.logger.print(`${players.length} player${players.length === 1 ? "" : "s"} killed`); - } - }), - genCommand({ - name: "addbot", - args: " [count=1]", - desc: "assign player bots to a world", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (args.length === 1) args[1] = "1"; - - const world = getWorldByID(args, handle, 0, false); - const count = getInt(args, handle, 1, "count"); - if (world === false || count === false) - return; - for (let i = 0; i < count; i++) new PlayerBot(world); - handle.logger.print(`added ${count} player bots to world`); - } - }), - genCommand({ - name: "rmbot", - args: " [count=1]", - desc: "remove player bots from a world", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (args.length === 1) args[1] = "1"; - - const world = getWorldByID(args, handle, 0, false); - const count = getInt(args, handle, 1, "count"); - if (world === false || count === false) - return; - let realCount = 0; - for (let i = 0, l = world.players.length; i < l && realCount < count; i++) { - if (world.players[i].router.type !== "playerbot") continue; - world.players[i].router.close(); - realCount++; i--; l--; - } - handle.logger.print(`removed ${realCount} player bots from world`); - } - }), - genCommand({ - name: "forbid", - args: "", - desc: "forbid (ban) specified IP or a connected player", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (args.length < 1) - return void handle.logger.print(""); - const id = getString(args, handle, 0, "IP address / player id"); - if (id === false) - return; - let ip; - if (!IPvalidate.test(ip = id)) { - if (!handle.players.hasOwnProperty(id)) - return void handle.logger.print("no player has this id"); - const player = handle.players[id]; - if (!player.router.isExternal) - return void handle.logger.print("player is not external"); - ip = player.router.remoteAddress; - } - handle.settings.listenerForbiddenIPs.push(ip); - handle.logger.print(`IP address ${ip} is now forbidden`); - } - }), - genCommand({ - name: "pardon", - args: "", - desc: "pardon (unban) specified IP", - /** - * @param {ServerHandle} context - */ - exec: (handle, context, args) => { - if (args.length < 1) - return void handle.logger.print(""); - const id = getString(args, handle, 0, "IP address"); - if (id === false) - return; - if (!IPvalidate.test(ip = id)) - return void handle.logger.print("invalid IP address"); - const index = handle.settings.listenerForbiddenIPs.indexOf(ip); - if (index === -1) - return void handle.logger.print("specified IP address is not forbidden"); - handle.settings.listenerForbiddenIPs.splice(index, 1); - handle.logger.print(`IP address ${ip} has been pardoned`); - } - }) - ); - chatCommands.register( - genCommand({ - name: "help", - args: "", - desc: "display all registered commands and their relevant information", - /** - * @param {Connection} context - */ - exec: (handle, context, args) => { - const list = handle.chatCommands.list; - handle.listener.globalChat.directMessage(null, context, "available commands:"); - for (let name in list) - handle.listener.globalChat.directMessage(null, context, `${name}${list[name].args.length > 0 ? " " : ""}${list[name].args} - ${list[name].description}`); - } - }), - genCommand({ - name: "id", - args: "", - desc: "get your id", - /** - * @param {Connection} context - */ - exec: (handle, context, args) => { - handle.listener.globalChat.directMessage(null, context, context.hasPlayer ? `your ID is ${context.player.id}` : "you don't have a player associated with yourself"); - } - }), - genCommand({ - name: "worldid", - args: "", - desc: "get your world's id", - /** - * @param {Connection} context - */ - exec: (handle, context, args) => { - const chat = handle.listener.globalChat; - if (!context.hasPlayer) - return void chat.directMessage(null, context, "you don't have a player associated with yourself"); - if (!context.player.hasWorld) - return void chat.directMessage(null, context, "you're not in a world"); - chat.directMessage(null, context, `your world ID is ${context.player.world.id}`); - } - }), - genCommand({ - name: "leaveworld", - args: "", - desc: "leave your world", - /** - * @param {Connection} context - */ - exec: (handle, context, args) => { - const chat = handle.listener.globalChat; - return void chat.directMessage(null, context, "disabled for now"); - if (!context.hasPlayer) - return void chat.directMessage(null, context, "you don't have a player associated with yourself"); - if (!context.player.hasWorld) - return void chat.directMessage(null, context, "you're not in a world"); - context.player.world.removePlayer(context.player); - } - }), - genCommand({ - name: "joinworld", - args: "", - desc: "try to join a world", - /** - * @param {Connection} context - */ - exec: (handle, context, args) => { - const chat = handle.listener.globalChat; - return void chat.directMessage(null, context, "disabled for now"); - if (args.length === 0) - return void chat.directMessage(null, context, "missing world id argument"); - const id = parseInt(args[0]); - if (isNaN(id)) - return void chat.directMessage(null, context, "invalid world id number format"); - if (!context.hasPlayer) - return void chat.directMessage(null, context, "you don't have a player instance associated with yourself"); - if (context.player.hasWorld) - return void chat.directMessage(null, context, "you're already in a world"); - if (!handle.worlds.hasOwnProperty(id)) - return void chat.directMessage(null, context, "this world doesn't exist"); - if (!handle.gamemode.canJoinWorld(handle.worlds[id])) - return void chat.directMessage(null, context, "you can't join this world"); - handle.worlds[id].addPlayer(context.player); - } - }) - ); + commands.register( + genCommand({ + name: "help", + args: "", + desc: "display all registered commands and their relevant information", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const list = handle.commands.list; + const keys = Object.keys(list).sort(); + + handle.logger.print(table({ + columns: [ + { text: "NAME", headPad: " ", emptyPad: " ", rowPad: " ", separated: false }, + { text: "ARGUMENTS", headPad: " ", emptyPad: " ", rowPad: " ", separated: false }, + { text: "DESCRIPTION", headPad: " ", emptyPad: " ", rowPad: " ", separated: true } + ], + rows: keys.map(v => { + return [ + list[v].name, + list[v].args, + list[v].description + ] + }) + }, EOL)); + } + }), + genCommand({ + name: "routers", + args: "[router type]", + desc: "display information about routers and their players", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const matchingType = args.length >= 1 ? args[0] : null; + const routers = handle.listener.routers.filter(v => matchingType === null || v.type == matchingType); + + handle.logger.print(table({ + columns: [ + {text: "INDEX", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "TYPE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "SOURCE", headPad: " ", emptyPad: "/", rowPad: " ", separated: true}, + {text: "ACTIVE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "DORMANT", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "PROTOCOL", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "PID", headPad: " ", emptyPad: "/", rowPad: " ", separated: false} + ], + rows: routers.map((v, i) => [ + i.toString(), + v.type, + v.type === "connection" ? v.remoteAddress : null, + v.type === "connection" ? shortPrettyTime(Date.now() - v.connectTime) : null, + v.type === "connection" ? shortPrettyTime(Date.now() - v.lastActivityTime) : null, + v.protocol ? v.protocol.subtype : null, + v.hasPlayer ? v.player.id.toString() : null, + ]) + }, EOL)); + } + }), + genCommand({ + name: "players", + args: "[world id or \"any\"] [router type]", + desc: "display information about players", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const worldId = args.length >= 1 ? parseInt(args[0]) || null : null; + const routerType = args.length === 2 ? args[1] : null; + const players = handle.listener.routers.filter(v => v.hasPlayer && (routerType == null || v.type == routerType)).map(v => v.player).filter(v => worldId == null || (v.hasWorld && v.world.id === worldId)); + + handle.logger.print(table({ + columns: [ + {text: "ID", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "WORLD", headPad: " ", emptyPad: "/", rowPad: " ", separated: true}, + {text: "FOLLOWING", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "STATE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "SCORE", headPad: " ", emptyPad: "/", rowPad: " ", separated: false}, + {text: "NAME", headPad: " ", emptyPad: "/", rowPad: " ", separated: false} + ], + rows: players.map((v) => { + let ret = [ + v.id.toString(), + v.hasWorld ? v.world.id.toString() : null, + ]; + + if (v.type === "minion") { + ret.push(v.following.player.id.toString()); + } else if (v.hasWorld && v.state === 1) { + ret.push(v.world.largestPlayer.id.toString()); + } else { + ret.push(null); + } + + switch (v.state) { + case -1: + ret.push("idle"); + break; + case 0: + ret.push("alive"); + ret.push(Math.round(v.score).toString()); + ret.push(v.leaderboardName); + break; + case 1: + ret.push("spec"); + break; + case 2: + ret.push("roam"); + break; + } + return ret; + }) + }, EOL)); + } + }), + genCommand({ + name: "stats", + args: "", + desc: "display critical information about the server", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const logger = handle.logger; + const memory = prettyMemoryData(process.memoryUsage()); + const external = handle.listener.connections.length; + const internal = handle.listener.routers.length - external; + + if (!handle.running) { + return void logger.print("not running"); + } + + logger.print(`load: ${handle.averageTickTime.toFixed(4)} ms / ${handle.tickDelay} ms`); + logger.print(`memory: ${memory.heapUsed} / ${memory.heapTotal} / ${memory.rss} / ${memory.external}`); + logger.print(`uptime: ${prettyTime(Math.floor((Date.now() - handle.startTime.getTime()) / 1000))}`); + logger.print(`routers: ${external} external, ${internal} internal, ${external + internal} total`) + logger.print(`players: ${Object.keys(handle.players).length}`); + + for (let id in handle.worlds) { + const world = handle.worlds[id], stats = world.stats, cells = [world.cells.length, world.playerCells.length, world.pelletCount, world.virusCount, world.ejectedCells.length, world.mothercellCount], statsF = [stats.external, stats.internal, stats.limit, stats.playing, stats.spectating]; + logger.print(`world ${id}: ${cells[0]} cells - ${cells[1]}P/${cells[2]}p/${cells[3]}v/${cells[4]}e/${cells[5]}m`); + logger.print(` ${statsF[0]} / ${statsF[1]} / ${statsF[2]} players - ${statsF[3]}p/${statsF[4]}s`); + } + } + }), + genCommand({ + name: "setting", + args: " [value]", + desc: "change/print the value of a setting", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (args.length < 1) { + return void handle.logger.print("no setting name provided"); + } + + const settingName = args[0]; + + if (!handle.settings.hasOwnProperty(settingName)) { + const settingIdSplit = splitSettingId(settingName); + const possible = Object.keys(handle.settings).map(v => { + return {name: v, hits: getSplitSettingHits(splitSettingId(v), settingIdSplit)}; + }).sort((a, b) => b.hits - a.hits).filter((v) => v.hits > 0).filter((v, i, array) => array[0].hits === v.hits).map(v => v.name); + + let printing = "no such setting"; + + if (possible.length > 0) { + printing += `; did you mean ${possible.slice(0, 3).join(", ")}` + + if (possible.length > 3) { + printing += `, ${possible.length - 3} other`; + } + + printing += "?" + } + + return void handle.logger.print(printing); + } + + if (args.length >= 2) { + const settingValue = JSON.parse(args.slice(1).join(" ")); + const newSettings = Object.assign({}, handle.settings); + newSettings[settingName] = settingValue; + handle.setSettings(newSettings); + } + + handle.logger.print(handle.settings[settingName]); + } + }), + genCommand({ + name: "eval", + args: "", + desc: "evaluate javascript code in a function bound to server handle", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const result = (function () { + try { + return eval(args.join(" ")); + } catch (e) { + return !e ? e : (e.stack || e); + } + }).bind(handle)(); + + handle.logger.print(inspect(result, true, 1, false)); + } + }), + genCommand({ + name: "test", + args: "", + desc: "test command", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => handle.logger.print("success successful") + }), + genCommand({ + name: "crash", + args: "", + desc: "manually force an error throw", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + throw new Error("manual crash"); + } + }), + genCommand({ + name: "restart", + args: "", + desc: "stop then immediately start the handle", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (!handle.stop()) { + return void handle.logger.print("handle not started"); + } + + handle.start(); + } + }), + genCommand({ + name: "pause", + args: "", + desc: "toggle handle pause", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (!handle.running) { + return void handle.logger.print("handle not started"); + } + + if (handle.ticker.running) { + handle.ticker.stop(); + } else { + handle.ticker.start(); + } + } + }), + genCommand({ + name: "mass", + args: " ", + desc: "set cell mass to all of a player's cells", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const player = getPlayerByID(args, handle, 0, true); + const mass = getFloat(args, handle, 1, "mass"); + + if (player === false || mass === false) { + return; + } + + const l = player.ownedCells.length; + + for (let i = 0; i < l; i++) { + player.ownedCells[i].mass = mass; + } + + handle.logger.print(`player now has ${mass * l} mass`); + } + }), + genCommand({ + name: "merge", + args: "", + desc: "instantly merge a player", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const player = getPlayerByID(args, handle, 0, true); + + if (player === false) { + return; + } + + const l = player.ownedCells.length; + + let sqSize = 0; + + for (let i = 0; i < l; i++) { + sqSize += player.ownedCells[i].squareSize; + } + + player.ownedCells[0].squareSize = sqSize; + player.ownedCells[0].x = player.viewArea.x; + player.ownedCells[0].y = player.viewArea.y; + + for (let i = 1; i < l; i++) { + player.world.removeCell(player.ownedCells[1]); + } + + handle.logger.print(`merged player from ${l} cells and ${Math.round(sqSize / 100)} mass`); + } + }), + genCommand({ + name: "kill", + args: "", + desc: "instantly kill a player", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const player = getPlayerByID(args, handle, 0, true); + + if (player === false) { + return; + } + + for (let i = 0, l = player.ownedCells.length; i < l; i++) { + player.world.removeCell(player.ownedCells[0]); + } + + handle.logger.print("player killed"); + } + }), + genCommand({ + name: "explode", + args: "", + desc: "instantly explode a player's first cell", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const player = getPlayerByID(args, handle, 0, true); + + if (player === false) { + return; + } + + player.world.popPlayerCell(player.ownedCells[0]); + handle.logger.print("player exploded"); + } + }), + genCommand({ + name: "addminion", + args: " [count]", + desc: "assign minions to a player", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (args.length === 1) { + args[1] = "1"; + } + + const player = getPlayerByID(args, handle, 0, false); + const count = getInt(args, handle, 1, "count"); + + if (player === false || count === false) { + return; + } + + if (!player.router.isExternal) { + return void handle.logger.print("player is not external"); + } + + if (!player.hasWorld) { + return void handle.logger.print("player is not in a world"); + } + + for (let i = 0; i < count; i++) { + new Minion(player.router); + } + + handle.logger.print(`added ${count} minions to player`); + } + }), + genCommand({ + name: "rmminion", + args: " [count]", + desc: "remove assigned minions from a player", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (args.length === 1) { + args[1] = "1"; + } + + const player = getPlayerByID(args, handle, 0, false); + const count = getInt(args, handle, 1, "count"); + + if (player === false || count === false) { + return; + } + + if (!player.router.isExternal) { + return void handle.logger.print("player is not external"); + } + + if (!player.hasWorld) { + return void handle.logger.print("player is not in a world"); + } + + let realCount = 0; + + for (let i = 0; i < count && player.router.minions.length > 0; i++) { + player.router.minions[0].close(); + realCount++; + } + + handle.logger.print(`removed ${realCount} minions from player`); + } + }), + genCommand({ + name: "killall", + args: "", + desc: "instantly kill all players in a world", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + const world = getWorldByID(args, handle, 0, false); + + if (world === false) { + return; + } + + const players = world.players; + + for (let i = 0; i < players.length; i++) { + const player = players[i]; + + if (player.state !== 0) { + continue; + } + + for (let j = 0, l = player.ownedCells.length; j < l; j++) { + player.world.removeCell(player.ownedCells[0]); + } + } + + handle.logger.print(`${players.length} player${players.length === 1 ? "" : "s"} killed`); + } + }), + genCommand({ + name: "addbot", + args: " [count=1]", + desc: "assign player bots to a world", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (args.length === 1) { + args[1] = "1"; + } + + const world = getWorldByID(args, handle, 0, false); + const count = getInt(args, handle, 1, "count"); + + if (world === false || count === false) { + return; + } + + for (let i = 0; i < count; i++) { + new PlayerBot(world); + } + + handle.logger.print(`added ${count} player bots to world`); + } + }), + genCommand({ + name: "rmbot", + args: " [count=1]", + desc: "remove player bots from a world", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (args.length === 1) { + args[1] = "1"; + } + + const world = getWorldByID(args, handle, 0, false); + const count = getInt(args, handle, 1, "count"); + + if (world === false || count === false) { + return; + } + + let realCount = 0; + + for (let i = 0, l = world.players.length; i < l && realCount < count; i++) { + if (world.players[i].router.type !== "playerbot") { + continue; + } + + world.players[i].router.close(); + realCount++; + i--; + l--; + } + + handle.logger.print(`removed ${realCount} player bots from world`); + } + }), + genCommand({ + name: "forbid", + args: "", + desc: "forbid (ban) specified IP or a connected player", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (args.length < 1) { + return void handle.logger.print(""); + } + + const id = getString(args, handle, 0, "IP address / player id"); + + if (id === false) { + return; + } + + let ip; + + if (!IPvalidate.test(ip = id)) { + if (!handle.players.hasOwnProperty(id)) { + return void handle.logger.print("no player has this id"); + } + + const player = handle.players[id]; + + if (!player.router.isExternal) { + return void handle.logger.print("player is not external"); + } + + ip = player.router.remoteAddress; + } + + handle.settings.listenerForbiddenIPs.push(ip); + handle.logger.print(`IP address ${ip} is now forbidden`); + } + }), + genCommand({ + name: "pardon", + args: "", + desc: "pardon (unban) specified IP", + /** + * @param {ServerHandle} context + */ + exec: (handle, context, args) => { + if (args.length < 1) { + return void handle.logger.print(""); + } + + const id = getString(args, handle, 0, "IP address"); + + if (id === false) { + return; + } + + if (!IPvalidate.test(ip = id)) { + return void handle.logger.print("invalid IP address"); + } + + const index = handle.settings.listenerForbiddenIPs.indexOf(ip); + + if (index === -1) { + return void handle.logger.print("specified IP address is not forbidden"); + } + + handle.settings.listenerForbiddenIPs.splice(index, 1); + handle.logger.print(`IP address ${ip} has been pardoned`); + } + }) + ); + chatCommands.register( + genCommand({ + name: "help", + args: "", + desc: "display all registered commands and their relevant information", + /** + * @param {Connection} context + */ + exec: (handle, context, args) => { + const list = handle.chatCommands.list; + handle.listener.globalChat.directMessage(null, context, "available commands:"); + + for (let name in list) { + handle.listener.globalChat.directMessage(null, context, `${name}${list[name].args.length > 0 ? " " : ""}${list[name].args} - ${list[name].description}`); + } + } + }), + genCommand({ + name: "id", + args: "", + desc: "get your id", + /** + * @param {Connection} context + */ + exec: (handle, context, args) => { + handle.listener.globalChat.directMessage(null, context, context.hasPlayer ? `your ID is ${context.player.id}` : "you don't have a player associated with yourself"); + } + }), + genCommand({ + name: "worldid", + args: "", + desc: "get your world's id", + /** + * @param {Connection} context + */ + exec: (handle, context, args) => { + const chat = handle.listener.globalChat; + + if (!context.hasPlayer) { + return void chat.directMessage(null, context, "you don't have a player associated with yourself"); + } + + if (!context.player.hasWorld) { + return void chat.directMessage(null, context, "you're not in a world"); + } + + chat.directMessage(null, context, `your world ID is ${context.player.world.id}`); + } + }), + genCommand({ + name: "leaveworld", + args: "", + desc: "leave your world", + /** + * @param {Connection} context + */ + exec: (handle, context, args) => { + const chat = handle.listener.globalChat; + return void chat.directMessage(null, context, "disabled for now"); + + if (!context.hasPlayer) { + return void chat.directMessage(null, context, "you don't have a player associated with yourself"); + } + + if (!context.player.hasWorld) { + return void chat.directMessage(null, context, "you're not in a world"); + } + + context.player.world.removePlayer(context.player); + } + }), + genCommand({ + name: "joinworld", + args: "", + desc: "try to join a world", + /** + * @param {Connection} context + */ + exec: (handle, context, args) => { + const chat = handle.listener.globalChat; + return void chat.directMessage(null, context, "disabled for now"); + + if (args.length === 0) { + return void chat.directMessage(null, context, "missing world id argument"); + } + + const id = parseInt(args[0]); + + if (isNaN(id)) { + return void chat.directMessage(null, context, "invalid world id number format"); + } + + if (!context.hasPlayer) { + return void chat.directMessage(null, context, "you don't have a player instance associated with yourself"); + } + + if (context.player.hasWorld) { + return void chat.directMessage(null, context, "you're already in a world"); + } + + if (!handle.worlds.hasOwnProperty(id)) { + return void chat.directMessage(null, context, "this world doesn't exist"); + } + + if (!handle.gamemode.canJoinWorld(handle.worlds[id])) { + return void chat.directMessage(null, context, "you can't join this world"); + } + + handle.worlds[id].addPlayer(context.player); + } + }) + ); }; const { CommandList } = require("./CommandList"); diff --git a/servers/agarv2/src/gamemodes/FFA.js b/servers/agarv2/src/gamemodes/FFA.js index 82c18ed8..a5f817c4 100644 --- a/servers/agarv2/src/gamemodes/FFA.js +++ b/servers/agarv2/src/gamemodes/FFA.js @@ -7,64 +7,80 @@ const Misc = require("../primitives/Misc"); * @param {number} index */ function getLeaderboardData(player, requesting, index) { - return { - name: player.leaderboardName, - highlighted: requesting.id === player.id, - cellId: typeof player.ownedCells !== 'undefined' && typeof player.ownedCells[0] !== 'undefined' && typeof player.ownedCells[0]['id'] !== 'undefined' ? player.ownedCells[0]['id'] : undefined, - position: 1 + index - }; + return { + name: player.leaderboardName, + highlighted: requesting.id === player.id, + cellId: typeof player.ownedCells !== 'undefined' && typeof player.ownedCells[0] !== 'undefined' && typeof player.ownedCells[0]['id'] !== 'undefined' ? player.ownedCells[0]['id'] : undefined, + position: 1 + index + }; } class FFA extends Gamemode { - /** - * @param {ServerHandle} handle - */ - constructor(handle) { - super(handle); - } + /** + * @param {ServerHandle} handle + */ + constructor(handle) { + super(handle); + } - static get type() { return 0; } - static get name() { return "FFA"; } + static get type() { + return 0; + } - /** - * @param {Player} player - * @param {string} name - * @param {string} skin - */ - onPlayerSpawnRequest(player, name, skin) { - if (player.state === 0 || !player.hasWorld) return; - const size = player.router.type === "minion" ? - this.handle.settings.minionSpawnSize : - this.handle.settings.playerSpawnSize; - const spawnInfo = player.world.getPlayerSpawn(size); - const color = spawnInfo.color || Misc.randomColor(); - player.cellName = player.chatName = player.leaderboardName = name; - player.cellSkin = skin; - player.chatColor = player.cellColor = color; - player.world.spawnPlayer(player, spawnInfo.pos, size, name, null); - } + static get name() { + return "FFA"; + } - /** - * @param {World} world - */ - compileLeaderboard(world) { - world.leaderboard = world.players.slice(0).filter((v) => !isNaN(v.score)).sort((a, b) => b.score - a.score); - } + /** + * @param {Player} player + * @param {string} name + * @param {string} skin + */ + onPlayerSpawnRequest(player, name, skin) { + if (player.state === 0 || !player.hasWorld) { + return; + } - /** - * @param {Connection} connection - */ - sendLeaderboard(connection) { - if (!connection.hasPlayer) return; - const player = connection.player; - if (!player.hasWorld) return; - if (player.world.frozen) return; - /** @type {Player[]} */ - const leaderboard = player.world.leaderboard; - const data = leaderboard.map((v, i) => getLeaderboardData(v, player, i)); - const selfData = isNaN(player.score) ? null : data[leaderboard.indexOf(player)]; - connection.protocol.onLeaderboardUpdate("ffa", data.slice(0, 10), selfData); - } + const size = player.router.type === "minion" ? this.handle.settings.minionSpawnSize : this.handle.settings.playerSpawnSize; + const spawnInfo = player.world.getPlayerSpawn(size); + const color = spawnInfo.color || Misc.randomColor(); + player.cellName = player.chatName = player.leaderboardName = name; + player.cellSkin = skin; + player.chatColor = player.cellColor = color; + player.world.spawnPlayer(player, spawnInfo.pos, size, name, null); + } + + /** + * @param {World} world + */ + compileLeaderboard(world) { + world.leaderboard = world.players.slice(0).filter((v) => !isNaN(v.score)).sort((a, b) => b.score - a.score); + } + + /** + * @param {Connection} connection + */ + sendLeaderboard(connection) { + if (!connection.hasPlayer) { + return; + } + + const player = connection.player; + + if (!player.hasWorld) { + return; + } + + if (player.world.frozen) { + return; + } + + /** @type {Player[]} */ + const leaderboard = player.world.leaderboard; + const data = leaderboard.map((v, i) => getLeaderboardData(v, player, i)); + const selfData = isNaN(player.score) ? null : data[leaderboard.indexOf(player)]; + connection.protocol.onLeaderboardUpdate("ffa", data.slice(0, 10), selfData); + } } module.exports = FFA; diff --git a/servers/agarv2/src/gamemodes/Gamemode.js b/servers/agarv2/src/gamemodes/Gamemode.js index ab2c6a1f..1b8b0fc7 100644 --- a/servers/agarv2/src/gamemodes/Gamemode.js +++ b/servers/agarv2/src/gamemodes/Gamemode.js @@ -1,79 +1,118 @@ /** @abstract */ class Gamemode { - /** @param {ServerHandle} handle */ - constructor(handle) { - this.handle = handle; - } - - /** @returns {number} @abstract */ - static get type() { throw new Error("Must be overriden"); } - /** @returns {number} */ - get type() { return this.constructor.type; } - /** @returns {string} @abstract */ - static get name() { throw new Error("Must be overriden"); } - /** @returns {string} */ - get name() { return this.constructor.name; } - - /** @virtual */ - onHandleStart() { } - /** @virtual */ - onHandleTick() { } - /** @virtual */ - onHandleStop() { } - - /** @param {World} world @virtual */ - canJoinWorld(world) { return !world.frozen; } - /** @param {Player} player @param {World} world @virtual */ - onPlayerJoinWorld(player, world) { } - /** @param {Player} player @param {World} world @virtual */ - onPlayerLeaveWorld(player, world) { } - - /** @param {World} world @virtual */ - onNewWorld(world) { } - /** @param {World} world @virtual */ - onWorldTick(world) { } - /** @param {World} world @abstract */ - compileLeaderboard(world) { - throw new Error("Must be overriden"); - } - /** @param {Connection} connection @abstract */ - sendLeaderboard(connection) { - throw new Error("Must be overriden"); - } - /** @param {World} world @virtual */ - onWorldDestroy(world) { } - - /** @param {Player} player @virtual */ - onNewPlayer(player) { } - /** @param {Player} player @virtual */ - whenPlayerPressQ(player) { - player.updateState(2); - } - /** @param {Player} player @virtual */ - whenPlayerEject(player) { - if (!player.hasWorld) return; - player.world.ejectFromPlayer(player); - } - /** @param {Player} player @virtual */ - whenPlayerSplit(player) { - if (!player.hasWorld) return; - player.world.splitPlayer(player); - } - /** @param {Player} player @param {string} name @param {string} skin @abstract */ - onPlayerSpawnRequest(player, name, skin) { - throw new Error("Must be overriden"); - } - /** @param {Player} player @virtual */ - onPlayerDestroy(player) { } - - /** @param {Cell} cell @virtual */ - onNewCell(cell) { } - /** @param {Cell} a @param {Cell} b @virtual */ - canEat(a, b) { return true; } - /** @param {PlayerCell} cell @virtual */ - getDecayMult(cell) { return cell.world.settings.playerDecayMult; } - /** @param {Cell} cell @virtual */ - onCellRemove(cell) { } + /** @param {ServerHandle} handle */ + constructor(handle) { + this.handle = handle; + } + + /** @returns {number} @abstract */ + static get type() { + throw new Error("Must be overriden"); + } + + /** @returns {number} */ + get type() { + return this.constructor.type; + } + + /** @returns {string} @abstract */ + static get name() { + throw new Error("Must be overriden"); + } + + /** @returns {string} */ + get name() { + return this.constructor.name; + } + + /** @virtual */ + onHandleStart() {} + + /** @virtual */ + onHandleTick() {} + + /** @virtual */ + onHandleStop() {} + + /** @param {World} world @virtual */ + canJoinWorld(world) { + return !world.frozen; + } + + /** @param {Player} player @param {World} world @virtual */ + onPlayerJoinWorld(player, world) {} + + /** @param {Player} player @param {World} world @virtual */ + onPlayerLeaveWorld(player, world) {} + + /** @param {World} world @virtual */ + onNewWorld(world) {} + + /** @param {World} world @virtual */ + onWorldTick(world) {} + + /** @param {World} world @abstract */ + compileLeaderboard(world) { + throw new Error("Must be overriden"); + } + + /** @param {Connection} connection @abstract */ + sendLeaderboard(connection) { + throw new Error("Must be overriden"); + } + + /** @param {World} world @virtual */ + onWorldDestroy(world) {} + + /** @param {Player} player @virtual */ + onNewPlayer(player) {} + + /** @param {Player} player @virtual */ + whenPlayerPressQ(player) { + player.updateState(2); + } + + /** @param {Player} player @virtual */ + whenPlayerEject(player) { + if (!player.hasWorld) { + return; + } + + player.world.ejectFromPlayer(player); + } + + /** @param {Player} player @virtual */ + whenPlayerSplit(player) { + if (!player.hasWorld) { + return; + } + + player.world.splitPlayer(player); + } + + /** @param {Player} player @param {string} name @param {string} skin @abstract */ + onPlayerSpawnRequest(player, name, skin) { + throw new Error("Must be overriden"); + } + + /** @param {Player} player @virtual */ + onPlayerDestroy(player) {} + + /** @param {Cell} cell @virtual */ + onNewCell(cell) {} + + /** @param {Cell} a @param {Cell} b @virtual */ + canEat(a, b) { + return true; + } + + /** @param {PlayerCell} cell @virtual */ + getDecayMult(cell) { + return cell.world.settings.playerDecayMult; + } + + /** @param {Cell} cell @virtual */ + onCellRemove(cell) {} } module.exports = Gamemode; @@ -83,4 +122,4 @@ const World = require("../worlds/World"); const Connection = require("../sockets/Connection"); const Player = require("../worlds/Player"); const Cell = require("../cells/Cell"); -const PlayerCell = require("../cells/PlayerCell"); +const PlayerCell = require("../cells/PlayerCell"); \ No newline at end of file diff --git a/servers/agarv2/src/gamemodes/GamemodeList.js b/servers/agarv2/src/gamemodes/GamemodeList.js index 531eece4..8e78c1c6 100644 --- a/servers/agarv2/src/gamemodes/GamemodeList.js +++ b/servers/agarv2/src/gamemodes/GamemodeList.js @@ -1,36 +1,41 @@ class GamemodeList { - /** - * @param {ServerHandle} handle - */ - constructor(handle) { - this.handle = handle; - /** @type {Indexed} */ - this.store = { }; - } + /** + * @param {ServerHandle} handle + */ + constructor(handle) { + this.handle = handle; + /** @type {Indexed} */ + this.store = {}; + } - /** - * @param {typeof Gamemode[]} gamemodes - */ - register(...gamemodes) { - for (let i = 0, l = gamemodes.length; i < l; i++) { - const next = gamemodes[i]; - if (this.store.hasOwnProperty(next.name)) - throw new Error(`gamemode ${next.name} conflicts with another already registered one`); - this.store[next.name] = next; - } - } + /** + * @param {typeof Gamemode[]} gamemodes + */ + register(...gamemodes) { + for (let i = 0, l = gamemodes.length; i < l; i++) { + const next = gamemodes[i]; - /** - * @param {string} name - */ - setGamemode(name) { - if (!this.store.hasOwnProperty(name)) - throw new Error("unknown gamemode"); - this.handle.gamemode = new (this.store[name])(this.handle); - } + if (this.store.hasOwnProperty(next.name)) { + throw new Error(`gamemode ${next.name} conflicts with another already registered one`); + } + + this.store[next.name] = next; + } + } + + /** + * @param {string} name + */ + setGamemode(name) { + if (!this.store.hasOwnProperty(name)) { + throw new Error("unknown gamemode"); + } + + this.handle.gamemode = new (this.store[name])(this.handle); + } } module.exports = GamemodeList; const Gamemode = require("./Gamemode"); -const ServerHandle = require("../ServerHandle"); +const ServerHandle = require("../ServerHandle"); \ No newline at end of file diff --git a/servers/agarv2/src/gamemodes/LastManStanding.js b/servers/agarv2/src/gamemodes/LastManStanding.js index 0c632576..868d616f 100644 --- a/servers/agarv2/src/gamemodes/LastManStanding.js +++ b/servers/agarv2/src/gamemodes/LastManStanding.js @@ -1,42 +1,54 @@ const FFA = require("./FFA"); class LastManStanding extends FFA { - static get name() { return "Last Man Standing"; } - static get type() { return 0; } - - /** - * @param {World} world - */ - canJoinWorld(world) { - return world.hadPlayers; - } - /** - * @param {World} world - */ - onNewWorld(world) { - world.hadPlayers = false; - } - /** - * @param {Player} player - * @param {World} world - */ - onPlayerJoinWorld(player, world) { - world.hadPlayers = true; - if (player.router.isExternal) - player.life = 0; - } - /** - * @param {Player} player - * @param {string} name - */ - onPlayerSpawnRequest(player, name) { - if (player.router.isExternal && player.life++ > 0) - return void this.handle.listener.globalChat.directMessage(null, player.router, "You cannot spawn anymore."); - super.onPlayerSpawnRequest(player, name); - } + static get name() { + return "Last Man Standing"; + } + + static get type() { + return 0; + } + + /** + * @param {World} world + */ + canJoinWorld(world) { + return world.hadPlayers; + } + + /** + * @param {World} world + */ + onNewWorld(world) { + world.hadPlayers = false; + } + + /** + * @param {Player} player + * @param {World} world + */ + onPlayerJoinWorld(player, world) { + world.hadPlayers = true; + + if (player.router.isExternal) { + player.life = 0; + } + } + + /** + * @param {Player} player + * @param {string} name + */ + onPlayerSpawnRequest(player, name) { + if (player.router.isExternal && player.life++ > 0) { + return void this.handle.listener.globalChat.directMessage(null, player.router, "You cannot spawn anymore."); + } + + super.onPlayerSpawnRequest(player, name); + } } module.exports = LastManStanding; const World = require("../worlds/World"); -const Player = require("../worlds/Player"); +const Player = require("../worlds/Player"); \ No newline at end of file diff --git a/servers/agarv2/src/gamemodes/Teams.js b/servers/agarv2/src/gamemodes/Teams.js index 2bdd5e9e..b79e9dbe 100644 --- a/servers/agarv2/src/gamemodes/Teams.js +++ b/servers/agarv2/src/gamemodes/Teams.js @@ -1,119 +1,149 @@ const Gamemode = require("./Gamemode"); const Misc = require("../primitives/Misc"); -const highlightBase = 231, - lowlightBase = 23, - highlightDiff = 24, - lowlightDiff = 24; +const highlightBase = 231, lowlightBase = 23, highlightDiff = 24, lowlightDiff = 24; + const teamColors = [ - { r: highlightBase, g: lowlightBase, b: lowlightBase }, - { r: lowlightBase, g: highlightBase, b: lowlightBase }, - { r: lowlightBase, g: lowlightBase, b: highlightBase } + { r: highlightBase, g: lowlightBase, b: lowlightBase }, + { r: lowlightBase, g: highlightBase, b: lowlightBase }, + { r: lowlightBase, g: lowlightBase, b: highlightBase } ]; + const teamColorsInt = [ - 0xFF0000, - 0x00FF00, - 0x0000FF + 0xFF0000, + 0x00FF00, + 0x0000FF ]; + const teamCount = teamColors.length; /** * @param {number} index */ function getTeamColor(index) { - const random = Math.random(); - const highlight = highlightBase + ~~(random * highlightDiff); - const lowlight = lowlightBase - ~~(random * lowlightDiff); - const r = teamColors[index].r === highlightBase ? highlight : lowlight; - const g = teamColors[index].g === highlightBase ? highlight : lowlight; - const b = teamColors[index].b === highlightBase ? highlight : lowlight; - return (r << 16) | (g << 8) | b; + const random = Math.random(); + const highlight = highlightBase + ~~(random * highlightDiff); + const lowlight = lowlightBase - ~~(random * lowlightDiff); + const r = teamColors[index].r === highlightBase ? highlight : lowlight; + const g = teamColors[index].g === highlightBase ? highlight : lowlight; + const b = teamColors[index].b === highlightBase ? highlight : lowlight; + + return (r << 16) | (g << 8) | b; } class Teams extends Gamemode { - /** @param {ServerHandle} handle */ - constructor(handle) { - super(handle); - } - - static get name() { return "Teams"; } - static get type() { return 2; } - - /** - * @param {World} world - */ - onNewWorld(world) { - world.teams = { }; - for (let i = 0; i < teamCount; i++) - world.teams[i] = []; - } - /** - * @param {Player} player - * @param {World} world - */ - onPlayerJoinWorld(player, world) { - if (!player.router.separateInTeams) return; - let team = 0; - for (let i = 0; i < teamCount; i++) - team = world.teams[i].length < world.teams[team].length ? i : team; - world.teams[team].push(player); - player.team = team; - player.chatColor = getTeamColor(player.team); - } - /** - * @param {Player} player - * @param {World} world - */ - onPlayerLeaveWorld(player, world) { - if (!player.router.separateInTeams) return; - world.teams[player.team].splice(world.teams[player.team].indexOf(player), 1); - player.team = null; - } - - /** - * @param {Player} player - * @param {string} name - * @param {string} skin - */ - onPlayerSpawnRequest(player, name, skin) { - if (player.state === 0 || !player.hasWorld) return; - const size = player.router.type === "minion" ? - this.handle.settings.minionSpawnSize : - this.handle.settings.playerSpawnSize; - const pos = player.world.getSafeSpawnPos(size); - const color = player.router.separateInTeams ? getTeamColor(player.team) : Misc.randomColor(); - player.cellName = player.chatName = player.leaderboardName = name; - player.cellSkin = null; - player.chatColor = player.cellColor = color; - player.world.spawnPlayer(player, pos, size); - } - - /** - * @param {World} world - */ - compileLeaderboard(world) { - /** @type {PieLeaderboardEntry[]} */ - const teams = world.leaderboard = []; - for (let i = 0; i < teamCount; i++) - teams.push({ - weight: 0, - color: teamColorsInt[i] - }); - let sum = 0; - for (let i = 0; i < world.playerCells.length; i++) { - const cell = world.playerCells[i]; - if (cell.owner.team === null) continue; - teams[cell.owner.team].weight += cell.squareSize; - sum += cell.squareSize; - } - for (let i = 0; i < teamCount; i++) - teams[i].weight /= sum; - } - - /** @param {Connection} connection */ - sendLeaderboard(connection) { - connection.protocol.onLeaderboardUpdate("pie", connection.player.world.leaderboard); - } + /** @param {ServerHandle} handle */ + constructor(handle) { + super(handle); + } + + static get name() { + return "Teams"; + } + + static get type() { + return 2; + } + + /** + * @param {World} world + */ + onNewWorld(world) { + world.teams = {}; + + for (let i = 0; i < teamCount; i++) { + world.teams[i] = []; + } + } + + /** + * @param {Player} player + * @param {World} world + */ + onPlayerJoinWorld(player, world) { + if (!player.router.separateInTeams) { + return; + } + + let team = 0; + + for (let i = 0; i < teamCount; i++) { + team = world.teams[i].length < world.teams[team].length ? i : team; + } + + world.teams[team].push(player); + player.team = team; + player.chatColor = getTeamColor(player.team); + } + + /** + * @param {Player} player + * @param {World} world + */ + onPlayerLeaveWorld(player, world) { + if (!player.router.separateInTeams) { + return; + } + + world.teams[player.team].splice(world.teams[player.team].indexOf(player), 1); + player.team = null; + } + + /** + * @param {Player} player + * @param {string} name + * @param {string} skin + */ + onPlayerSpawnRequest(player, name, skin) { + if (player.state === 0 || !player.hasWorld) { + return; + } + + const size = player.router.type === "minion" ? this.handle.settings.minionSpawnSize : this.handle.settings.playerSpawnSize; + const pos = player.world.getSafeSpawnPos(size); + const color = player.router.separateInTeams ? getTeamColor(player.team) : Misc.randomColor(); + player.cellName = player.chatName = player.leaderboardName = name; + player.cellSkin = null; + player.chatColor = player.cellColor = color; + player.world.spawnPlayer(player, pos, size); + } + + /** + * @param {World} world + */ + compileLeaderboard(world) { + /** @type {PieLeaderboardEntry[]} */ + const teams = world.leaderboard = []; + + for (let i = 0; i < teamCount; i++) { + teams.push({ + weight: 0, + color: teamColorsInt[i] + }); + } + + let sum = 0; + + for (let i = 0; i < world.playerCells.length; i++) { + const cell = world.playerCells[i]; + + if (cell.owner.team === null) { + continue; + } + + teams[cell.owner.team].weight += cell.squareSize; + sum += cell.squareSize; + } + + for (let i = 0; i < teamCount; i++) { + teams[i].weight /= sum; + } + } + + /** @param {Connection} connection */ + sendLeaderboard(connection) { + connection.protocol.onLeaderboardUpdate("pie", connection.player.world.leaderboard); + } } module.exports = Teams; @@ -121,4 +151,4 @@ module.exports = Teams; const ServerHandle = require("../ServerHandle"); const World = require("../worlds/World"); const Connection = require("../sockets/Connection"); -const Player = require("../worlds/Player"); +const Player = require("../worlds/Player"); \ No newline at end of file diff --git a/servers/agarv2/src/globals.d.ts b/servers/agarv2/src/globals.d.ts index df4a7b77..1ed2840c 100644 --- a/servers/agarv2/src/globals.d.ts +++ b/servers/agarv2/src/globals.d.ts @@ -1,68 +1,76 @@ interface Point { - x: number; - y: number; + x: number; + y: number; } + interface Rect extends Point { - w: number; - h: number; + w: number; + h: number; } + interface Quadrant { - t: boolean; - b: boolean; - l: boolean; - r: boolean; + t: boolean; + b: boolean; + l: boolean; + r: boolean; } + interface ViewArea extends Rect { - s: number; + s: number; } + interface Boost { - dx: number; - dy: number; - d: number; + dx: number; + dy: number; + d: number; } + interface Spawner { - pelletCount: number; + pelletCount: number; } interface ChatSource { - name: string; - isServer: string; - color: number; + name: string; + isServer: string; + color: number; } + interface FFALeaderboardEntry { - name: string; - highlighted: boolean; - cellId: number; - position: number; + name: string; + highlighted: boolean; + cellId: number; + position: number; } + interface PieLeaderboardEntry { - weight: number; - color: number; + weight: number; + color: number; } + interface WorldStats { - limit: number; - internal: number; - external: number; - playing: number; - spectating: number; - name: string; - gamemode: string; - loadTime: number; - uptime: number; + limit: number; + internal: number; + external: number; + playing: number; + spectating: number; + name: string; + gamemode: string; + loadTime: number; + uptime: number; } interface GenCommandTable { - columns: { - text: string; - headPad: string; - emptyPad: string; - rowPad: string; - separated: boolean; - }[]; - rows: string[][]; + columns: { + text: string; + headPad: string; + emptyPad: string; + rowPad: string; + separated: boolean; + }[]; + rows: string[][]; } -declare type LogEventLevel = "DEBUG" | "ACCESS" | "INFO" | "WARN" | "ERROR" | "FATAL"; +declare type LogEventLevel = 'DEBUG' | 'ACCESS' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL'; declare type LogEvent = (date: Date, level: LogEventLevel, message: string) => void; declare type LogMessageData = any[]; @@ -75,14 +83,14 @@ declare type CellEatResult = 0 | 1 | 2 | 3; */ declare type PlayerState = -1 | 0 | 1 | 2; -declare type LeaderboardType = "ffa" | "pie" | "text"; +declare type LeaderboardType = 'ffa' | 'pie' | 'text'; declare type LeaderboardDataType = { - "ffa": FFALeaderboardEntry, - "pie": PieLeaderboardEntry, - "text": string + 'ffa': FFALeaderboardEntry, + 'pie': PieLeaderboardEntry, + 'text': string }; declare type IPAddress = string; declare type Indexed = { [name: string]: T }; declare type Identified = { [id: number]: T }; -declare type Counter = { [item: string]: number }; +declare type Counter = { [item: string]: number }; \ No newline at end of file diff --git a/servers/agarv2/src/primitives/Logger.js b/servers/agarv2/src/primitives/Logger.js index 1a36c93e..13ebd247 100644 --- a/servers/agarv2/src/primitives/Logger.js +++ b/servers/agarv2/src/primitives/Logger.js @@ -1,80 +1,89 @@ const util = require("util"); class Logger { - constructor() { - /** @type {LogEvent} */ - this._onLog = null; - } + constructor() { + /** @type {LogEvent} */ + this._onLog = null; + } - get onlog() { return this._onLog; } - /** - * @param {LogEvent} value - */ - set onlog(value) { - if (!(value instanceof Function) || value.length !== 3) throw new Error("bad value"); - this._onLog = value; - } + get onlog() { + return this._onLog; + } - /** - * @param {LogMessageData} message - * @private - */ - _formatMessage(...message) { return util.format.apply(null, ...message); } + /** + * @param {LogEvent} value + */ + set onlog(value) { + if (!(value instanceof Function) || value.length !== 3) { + throw new Error("bad value"); + } - /** - * @param {LogMessageData} message - */ - print(...message) { - this._onLog && this._onLog(new Date(), "PRINT", this._formatMessage(message)); - } - /** - * @param {LogMessageData} message - */ - printFile(...message) { - this._onLog && this._onLog(new Date(), "FILE", this._formatMessage(message)); - } + this._onLog = value; + } - /** - * @param {LogMessageData} message - */ - debug(...message) { - this._onLog && this._onLog(new Date(), "DEBUG", this._formatMessage(message)); - } + /** + * @param {LogMessageData} message + * @private + */ + _formatMessage(...message) { + return util.format.apply(null, ...message); + } - /** - * @param {LogMessageData} message - */ - onAccess(...message) { - this._onLog && this._onLog(new Date(), "ACCESS", this._formatMessage(message)); - } + /** + * @param {LogMessageData} message + */ + print(...message) { + this._onLog && this._onLog(new Date(), "PRINT", this._formatMessage(message)); + } - /** - * @param {LogMessageData} message - */ - inform(...message) { - this._onLog && this._onLog(new Date(), "INFO", this._formatMessage(message)); - } + /** + * @param {LogMessageData} message + */ + printFile(...message) { + this._onLog && this._onLog(new Date(), "FILE", this._formatMessage(message)); + } - /** - * @param {LogMessageData} message - */ - warn(...message) { - this._onLog && this._onLog(new Date(), "WARN", this._formatMessage(message)); - } + /** + * @param {LogMessageData} message + */ + debug(...message) { + this._onLog && this._onLog(new Date(), "DEBUG", this._formatMessage(message)); + } - /** - * @param {LogMessageData} message - */ - onError(...message) { - this._onLog && this._onLog(new Date(), "ERROR", this._formatMessage(message)); - } + /** + * @param {LogMessageData} message + */ + onAccess(...message) { + this._onLog && this._onLog(new Date(), "ACCESS", this._formatMessage(message)); + } - /** - * @param {LogMessageData} message - */ - onFatal(...message) { - this._onLog && this._onLog(new Date(), "FATAL", this._formatMessage(message)); - } + /** + * @param {LogMessageData} message + */ + inform(...message) { + this._onLog && this._onLog(new Date(), "INFO", this._formatMessage(message)); + } + + /** + * @param {LogMessageData} message + */ + warn(...message) { + this._onLog && this._onLog(new Date(), "WARN", this._formatMessage(message)); + } + + /** + * @param {LogMessageData} message + */ + onError(...message) { + this._onLog && this._onLog(new Date(), "ERROR", this._formatMessage(message)); + } + + /** + * @param {LogMessageData} message + */ + onFatal(...message) { + this._onLog && this._onLog(new Date(), "FATAL", this._formatMessage(message)); + } } -module.exports = Logger; +module.exports = Logger; \ No newline at end of file diff --git a/servers/agarv2/src/primitives/Misc.js b/servers/agarv2/src/primitives/Misc.js index 0287cade..878c75ce 100644 --- a/servers/agarv2/src/primitives/Misc.js +++ b/servers/agarv2/src/primitives/Misc.js @@ -1,93 +1,101 @@ const IPv4MappedValidate = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/; module.exports = { - randomColor() { - switch (~~(Math.random() * 6)) { - case 0: return (~~(Math.random() * 0x100) << 16) | (0xFF << 8) | 0x10; - case 1: return (~~(Math.random() * 0x100) << 16) | (0x10 << 8) | 0xFF; - case 2: return (0xFF << 16) | (~~(Math.random() * 0x100) << 8) | 0x10; - case 3: return (0x10 << 16) | (~~(Math.random() * 0x100) << 8) | 0xFF; - case 4: return (0x10 << 16) | (0xFF << 8) | ~~(Math.random() * 0x100); - case 5: return (0xFF << 16) | (0x10 << 8) | ~~(Math.random() * 0x100); - } - }, - /** - * @param {number=} color - */ - grayscaleColor(color) { - /** @type {number} */ - let weight; - if (color) weight = ~~(0.299 * (color & 0xFF) + 0.587 * ((color.g >> 8) & 0xFF) + 0.114 * (color.b >> 16)); - else weight = 0x7F + ~~(Math.random() * 0x80); - return (weight << 16) | (weight << 8) | weight; - }, - /** @param {number[]} n */ - throwIfBadNumber(...n) { - for (let i = 0; i < n.length; i++) - if (isNaN(n[i]) || !isFinite(n[i]) || n[i] == null) - throw new Error(`bad number (${n[i]}, index ${i})`); - }, - /** @param {number[]} n */ - throwIfBadOrNegativeNumber(...n) { - for (let i = 0; i < n.length; i++) - if (isNaN(n[i]) || !isFinite(n[i]) || n[i] == null || n[i] < 0) - throw new Error(`bad or negative number (${n[i]}, index ${i})`); - }, + randomColor() { + switch (~~(Math.random() * 6)) { + case 0: + return (~~(Math.random() * 0x100) << 16) | (0xFF << 8) | 0x10; + case 1: + return (~~(Math.random() * 0x100) << 16) | (0x10 << 8) | 0xFF; + case 2: + return (0xFF << 16) | (~~(Math.random() * 0x100) << 8) | 0x10; + case 3: + return (0x10 << 16) | (~~(Math.random() * 0x100) << 8) | 0xFF; + case 4: + return (0x10 << 16) | (0xFF << 8) | ~~(Math.random() * 0x100); + case 5: + return (0xFF << 16) | (0x10 << 8) | ~~(Math.random() * 0x100); + } + }, + /** + * @param {number=} color + */ + grayscaleColor(color) { + /** @type {number} */ + let weight; - /** - * @param {Rect} a - * @param {Rect} b - */ - intersects(a, b) { - return a.x - a.w <= b.x + b.w && - a.x + a.w >= b.x - b.w && - a.y - a.h <= b.y + b.h && - a.y + a.h >= b.y - b.h; - }, - /** - * @param {Rect} a - * @param {Rect} b - */ - fullyIntersects(a, b) { - return a.x - a.w >= b.x + b.w && - a.x + a.w <= b.x - b.w && - a.y - a.h >= b.y + b.h && - a.y + a.h <= b.y - b.h; - }, - /** - * @param {Rect} a - * @param {Rect} b - * @returns {Quadrant} - */ - getQuadIntersect(a, b) { - return { - t: a.y - a.h < b.y || a.y + a.h < b.y, - b: a.y - a.h > b.y || a.y + a.h > b.y, - l: a.x - a.w < b.x || a.x + a.w < b.x, - r: a.x - a.w > b.x || a.x + a.w > b.x - }; - }, - /** - * @param {Rect} a - * @param {Rect} b - * @returns {Quadrant} - */ - getQuadFullIntersect(a, b) { - return { - t: a.y - a.h < b.y && a.y + a.h < b.y, - b: a.y - a.h > b.y && a.y + a.h > b.y, - l: a.x - a.w < b.x && a.x + a.w < b.x, - r: a.x - a.w > b.x && a.x + a.w > b.x - }; - }, + if (color) { + weight = ~~(0.299 * (color & 0xFF) + 0.587 * ((color.g >> 8) & 0xFF) + 0.114 * (color.b >> 16)); + } else { + weight = 0x7F + ~~(Math.random() * 0x80); + } - /** - * @param {string} a - */ - filterIPAddress(a) { - const unmapped = IPv4MappedValidate.exec(a); - return unmapped ? unmapped[1] : a; - }, + return (weight << 16) | (weight << 8) | weight; + }, + /** @param {number[]} n */ + throwIfBadNumber(...n) { + for (let i = 0; i < n.length; i++) { + if (isNaN(n[i]) || !isFinite(n[i]) || n[i] == null) { + throw new Error(`bad number (${n[i]}, index ${i})`); + } + } + }, + /** @param {number[]} n */ + throwIfBadOrNegativeNumber(...n) { + for (let i = 0; i < n.length; i++) { + if (isNaN(n[i]) || !isFinite(n[i]) || n[i] == null || n[i] < 0) { + throw new Error(`bad or negative number (${n[i]}, index ${i})`); + } + } + }, + /** + * @param {Rect} a + * @param {Rect} b + */ + intersects(a, b) { + return a.x - a.w <= b.x + b.w && a.x + a.w >= b.x - b.w && a.y - a.h <= b.y + b.h && a.y + a.h >= b.y - b.h; + }, + /** + * @param {Rect} a + * @param {Rect} b + */ + fullyIntersects(a, b) { + return a.x - a.w >= b.x + b.w && a.x + a.w <= b.x - b.w && a.y - a.h >= b.y + b.h && a.y + a.h <= b.y - b.h; + }, + /** + * @param {Rect} a + * @param {Rect} b + * @returns {Quadrant} + */ + getQuadIntersect(a, b) { + return { + t: a.y - a.h < b.y || a.y + a.h < b.y, + b: a.y - a.h > b.y || a.y + a.h > b.y, + l: a.x - a.w < b.x || a.x + a.w < b.x, + r: a.x - a.w > b.x || a.x + a.w > b.x + }; + }, + /** + * @param {Rect} a + * @param {Rect} b + * @returns {Quadrant} + */ + getQuadFullIntersect(a, b) { + return { + t: a.y - a.h < b.y && a.y + a.h < b.y, + b: a.y - a.h > b.y && a.y + a.h > b.y, + l: a.x - a.w < b.x && a.x + a.w < b.x, + r: a.x - a.w > b.x && a.x + a.w > b.x + }; + }, - version: "1.3.6" -}; + /** + * @param {string} a + */ + filterIPAddress(a) { + const unmapped = IPv4MappedValidate.exec(a); + return unmapped ? unmapped[1] : a; + }, + + version: "1.3.6" +}; \ No newline at end of file diff --git a/servers/agarv2/src/primitives/QuadTree.js b/servers/agarv2/src/primitives/QuadTree.js index 0dc047d3..afb0df24 100644 --- a/servers/agarv2/src/primitives/QuadTree.js +++ b/servers/agarv2/src/primitives/QuadTree.js @@ -11,208 +11,321 @@ const { intersects, fullyIntersects, getQuadIntersect, getQuadFullIntersect } = * @template T */ class QuadTree { - /** - * @param {Rect} range - * @param {number} maxLevel - * @param {number} maxItems - * @param {QuadTree=} root - */ - constructor(range, maxLevel, maxItems, root) { - this.root = root; - /** @type {number} */ - this.level = root ? root.level + 1 : 1; - - this.maxLevel = maxLevel; - this.maxItems = maxItems; - this.range = range; - - /** @type {QuadItem[]} */ - this.items = []; - this.hasSplit = false; - } - - destroy() { - for (let i = 0, l = this.items.length; i < l; i++) - delete this.items[i].__root; - if (!this.hasSplit) return; - for (let i = 0; i < 4; i++) this.branches[i].destroy(); - } - /** - * @param {QuadItem} item - */ - insert(item) { - let quad = this; - while (true) { - if (!quad.hasSplit) break; - const quadrant = quad.getQuadrant(item.range); - if (quadrant === -1) break; - quad = quad.branches[quadrant]; - } - item.__root = quad; - quad.items.push(item); - quad.split(); - } - /** - * @param {QuadItem} item - */ - update(item) { - const oldQuad = item.__root; - let newQuad = item.__root; - while (true) { - if (!newQuad.root) break; - newQuad = newQuad.root; - if (fullyIntersects(newQuad.range, item.range)) break; - } - while (true) { - if (!newQuad.hasSplit) break; - const quadrant = newQuad.getQuadrant(item.range); - if (quadrant === -1) break; - newQuad = newQuad.branches[quadrant]; - } - if (oldQuad === newQuad) return; - oldQuad.items.splice(oldQuad.items.indexOf(item), 1); - newQuad.items.push(item); - item.__root = newQuad; - oldQuad.merge(); - newQuad.split(); - } - /** - * @param {QuadItem} item - */ - remove(item) { - const quad = item.__root; - quad.items.splice(quad.items.indexOf(item), 1); - delete item.__root; - quad.merge(); - } - - /** - * @private - */ - split() { - if (this.hasSplit || this.level > this.maxLevel || this.items.length < this.maxItems) return; - this.hasSplit = true; - const x = this.range.x; - const y = this.range.y; - const hw = this.range.w / 2; - const hh = this.range.h / 2; - /** @type {QuadTree[]} */ - this.branches = [ - new QuadTree({ x: x - hw, y: y - hh, w: hw, h: hh }, this.maxLevel, this.maxItems, this), - new QuadTree({ x: x + hw, y: y - hh, w: hw, h: hh }, this.maxLevel, this.maxItems, this), - new QuadTree({ x: x - hw, y: y + hh, w: hw, h: hh }, this.maxLevel, this.maxItems, this), - new QuadTree({ x: x + hw, y: y + hh, w: hw, h: hh }, this.maxLevel, this.maxItems, this) - ]; - for (let i = 0, l = this.items.length, quadrant; i < l; i++) { - quadrant = this.getQuadrant(this.items[i].range); - if (quadrant === -1) continue; - delete this.items[i].__root; - this.branches[quadrant].insert(this.items[i]); - this.items.splice(i, 1); i--; l--; - } - } - /** - * @private - */ - merge() { - let quad = this; - while (quad != null) { - if (!quad.hasSplit) { quad = quad.root; continue; } - for (let i = 0, branch; i < 4; i++) - if ((branch = quad.branches[i]).hasSplit || branch.items.length > 0) - return; - quad.hasSplit = false; - delete quad.branches; - } - } - - /** - * @param {Rect} range - * @param {(item: QuadItem) => void} callback - */ - search(range, callback) { - for (let i = 0, l = this.items.length, item; i < l; i++) - if (intersects(range, (item = this.items[i]).range)) callback(item); - if (!this.hasSplit) return; - const quad = getQuadIntersect(range, this.range); - if (quad.t) { - if (quad.l) this.branches[0].search(range, callback); - if (quad.r) this.branches[1].search(range, callback); - } - if (quad.b) { - if (quad.l) this.branches[2].search(range, callback); - if (quad.r) this.branches[3].search(range, callback); - } - } - /** - * @param {Rect} range - * @param {(item: QuadItem) => boolean} selector - * @returns {boolean} - */ - containsAny(range, selector) { - for (let i = 0, l = this.items.length, item; i < l; i++) - if (intersects(range, (item = this.items[i]).range) && (!selector || selector(item))) - return true; - if (!this.hasSplit) return false; - const quad = getQuadIntersect(range, this.range); - if (quad.t) { - if (quad.l && this.branches[0].containsAny(range, selector)) return true; - if (quad.r && this.branches[1].containsAny(range, selector)) return true; - } - if (quad.b) { - if (quad.l && this.branches[2].containsAny(range, selector)) return true; - if (quad.r && this.branches[3].containsAny(range, selector)) return true; - } - return false; - } - - /** @returns {QuadItem[]} */ - getItemCount() { - if (!this.hasSplit) return this.items.length; - else return this.items.length + - this.branches[0].getItemCount() + - this.branches[1].getItemCount() + - this.branches[2].getItemCount() + - this.branches[3].getItemCount(); - } - /** @returns {number} */ - getBranchCount() { - if (this.hasSplit) - return 1 + - this.branches[0].getBranchCount() + this.branches[1].getBranchCount() + - this.branches[2].getBranchCount() + this.branches[3].getBranchCount(); - return 1; - } - /** - * @returns {string} - */ - debugStr() { - let str = `items ${this.items.length}/${this.maxItems}/${this.getItemCount()} level ${this.level} x ${this.range.x} y ${this.range.y} w ${this.range.w} h ${this.range.h}\n`; - if (this.hasSplit) { - str += new Array(1 + this.level * 2).join(" ") + this.branches[0].debugStr(); - str += new Array(1 + this.level * 2).join(" ") + this.branches[1].debugStr(); - str += new Array(1 + this.level * 2).join(" ") + this.branches[2].debugStr(); - str += new Array(1 + this.level * 2).join(" ") + this.branches[3].debugStr(); - } - return str; - } - - /** - * @param {Rect} a - * @returns {DefiniteQuad} - */ - getQuadrant(a) { - const quad = getQuadFullIntersect(a, this.range); - if (quad.t) { - if (quad.l) return 0; - if (quad.r) return 1; - } - if (quad.b) { - if (quad.l) return 2; - if (quad.r) return 3; - } - return -1; - } + /** + * @param {Rect} range + * @param {number} maxLevel + * @param {number} maxItems + * @param {QuadTree=} root + */ + constructor(range, maxLevel, maxItems, root) { + this.root = root; + /** @type {number} */ + this.level = root ? root.level + 1 : 1; + + this.maxLevel = maxLevel; + this.maxItems = maxItems; + this.range = range; + + /** @type {QuadItem[]} */ + this.items = []; + this.hasSplit = false; + } + + destroy() { + for (let i = 0, l = this.items.length; i < l; i++) { + delete this.items[i].__root; + } + + if (!this.hasSplit) { + return; + } + + for (let i = 0; i < 4; i++) { + this.branches[i].destroy(); + } + } + + /** + * @param {QuadItem} item + */ + insert(item) { + let quad = this; + + while (true) { + if (!quad.hasSplit) { + break; + } + + const quadrant = quad.getQuadrant(item.range); + + if (quadrant === -1) { + break; + } + + quad = quad.branches[quadrant]; + } + + item.__root = quad; + quad.items.push(item); + quad.split(); + } + + /** + * @param {QuadItem} item + */ + update(item) { + const oldQuad = item.__root; + let newQuad = item.__root; + + while (true) { + if (!newQuad.root) { + break; + } + + newQuad = newQuad.root; + + if (fullyIntersects(newQuad.range, item.range)) { + break; + } + } + + while (true) { + if (!newQuad.hasSplit) { + break; + } + + const quadrant = newQuad.getQuadrant(item.range); + + if (quadrant === -1) { + break; + } + + newQuad = newQuad.branches[quadrant]; + } + + if (oldQuad === newQuad) { + return; + } + + oldQuad.items.splice(oldQuad.items.indexOf(item), 1); + newQuad.items.push(item); + item.__root = newQuad; + oldQuad.merge(); + newQuad.split(); + } + + /** + * @param {QuadItem} item + */ + remove(item) { + const quad = item.__root; + quad.items.splice(quad.items.indexOf(item), 1); + delete item.__root; + quad.merge(); + } + + /** + * @private + */ + split() { + if (this.hasSplit || this.level > this.maxLevel || this.items.length < this.maxItems) { + return; + } + + this.hasSplit = true; + const x = this.range.x; + const y = this.range.y; + const hw = this.range.w / 2; + const hh = this.range.h / 2; + + /** @type {QuadTree[]} */ + this.branches = [ + new QuadTree({x: x - hw, y: y - hh, w: hw, h: hh}, this.maxLevel, this.maxItems, this), + new QuadTree({x: x + hw, y: y - hh, w: hw, h: hh}, this.maxLevel, this.maxItems, this), + new QuadTree({x: x - hw, y: y + hh, w: hw, h: hh}, this.maxLevel, this.maxItems, this), + new QuadTree({x: x + hw, y: y + hh, w: hw, h: hh}, this.maxLevel, this.maxItems, this) + ]; + + for (let i = 0, l = this.items.length, quadrant; i < l; i++) { + quadrant = this.getQuadrant(this.items[i].range); + + if (quadrant === -1) { + continue; + } + + delete this.items[i].__root; + this.branches[quadrant].insert(this.items[i]); + this.items.splice(i, 1); + i--; + l--; + } + } + + /** + * @private + */ + merge() { + let quad = this; + + while (quad != null) { + if (!quad.hasSplit) { + quad = quad.root; + continue; + } + + for (let i = 0, branch; i < 4; i++) { + if ((branch = quad.branches[i]).hasSplit || branch.items.length > 0) { + return; + } + } + + quad.hasSplit = false; + delete quad.branches; + } + } + + /** + * @param {Rect} range + * @param {(item: QuadItem) => void} callback + */ + search(range, callback) { + for (let i = 0, l = this.items.length, item; i < l; i++) { + if (intersects(range, (item = this.items[i]).range)) { + callback(item); + } + } + + if (!this.hasSplit) { + return; + } + + const quad = getQuadIntersect(range, this.range); + + if (quad.t) { + if (quad.l) { + this.branches[0].search(range, callback); + } + + if (quad.r) { + this.branches[1].search(range, callback); + } + } + + if (quad.b) { + if (quad.l) { + this.branches[2].search(range, callback); + } + + if (quad.r) { + this.branches[3].search(range, callback); + } + } + } + + /** + * @param {Rect} range + * @param {(item: QuadItem) => boolean} selector + * @returns {boolean} + */ + containsAny(range, selector) { + for (let i = 0, l = this.items.length, item; i < l; i++) { + if (intersects(range, (item = this.items[i]).range) && (!selector || selector(item))) { + return true; + } + } + + if (!this.hasSplit) { + return false; + } + + const quad = getQuadIntersect(range, this.range); + + if (quad.t) { + if (quad.l && this.branches[0].containsAny(range, selector)) { + return true; + } + + if (quad.r && this.branches[1].containsAny(range, selector)) { + return true; + } + } + + if (quad.b) { + if (quad.l && this.branches[2].containsAny(range, selector)) { + return true; + } + + if (quad.r && this.branches[3].containsAny(range, selector)) { + return true; + } + } + + return false; + } + + /** @returns {QuadItem[]} */ + getItemCount() { + if (!this.hasSplit) { + return this.items.length; + } else { + return this.items.length + this.branches[0].getItemCount() + this.branches[1].getItemCount() + this.branches[2].getItemCount() + this.branches[3].getItemCount(); + } + } + + /** @returns {number} */ + getBranchCount() { + if (this.hasSplit) { + return 1 + this.branches[0].getBranchCount() + this.branches[1].getBranchCount() + this.branches[2].getBranchCount() + this.branches[3].getBranchCount(); + } + + return 1; + } + + /** + * @returns {string} + */ + debugStr() { + let str = `items ${this.items.length}/${this.maxItems}/${this.getItemCount()} level ${this.level} x ${this.range.x} y ${this.range.y} w ${this.range.w} h ${this.range.h}\n`; + + if (this.hasSplit) { + str += new Array(1 + this.level * 2).join(" ") + this.branches[0].debugStr(); + str += new Array(1 + this.level * 2).join(" ") + this.branches[1].debugStr(); + str += new Array(1 + this.level * 2).join(" ") + this.branches[2].debugStr(); + str += new Array(1 + this.level * 2).join(" ") + this.branches[3].debugStr(); + } + + return str; + } + + /** + * @param {Rect} a + * @returns {DefiniteQuad} + */ + getQuadrant(a) { + const quad = getQuadFullIntersect(a, this.range); + + if (quad.t) { + if (quad.l) { + return 0; + } + + if (quad.r) { + return 1; + } + } + + if (quad.b) { + if (quad.l) { + return 2; + } + + if (quad.r) { + return 3; + } + } + + return -1; + } } module.exports = QuadTree; \ No newline at end of file diff --git a/servers/agarv2/src/primitives/Reader.js b/servers/agarv2/src/primitives/Reader.js index 8f6494c1..deaf18e0 100644 --- a/servers/agarv2/src/primitives/Reader.js +++ b/servers/agarv2/src/primitives/Reader.js @@ -1,82 +1,113 @@ class Reader { - /** - * @param {Buffer} data - * @param {number=} offset - */ - constructor(data, offset) { - this.data = data; - this.offset = offset || 0; - this.length = data.length; - } - - readUInt8() { - return this.data[this.offset++]; - } - readInt8() { - const a = this.data[this.offset++]; - return a < 0x7F ? a : -a + 0x7F; - } - readUInt16() { - const a = this.data.readUInt16LE(this.offset); - this.offset += 2; - return a; - } - readInt16() { - const a = this.data.readInt16LE(this.offset); - this.offset += 2; - return a; - } - readUInt24() { - const a = this.data.readUIntLE(this.offset, 3); - this.offset += 3; - return a; - } - readInt24() { - const a = this.data.readIntLE(this.offset, 3); - this.offset += 3; - return a; - } - readUInt32() { - const a = this.data.readUInt32LE(this.offset); - this.offset += 4; - return a; - } - readInt32() { - const a = this.data.readInt32LE(this.offset); - this.offset += 4; - return a; - } - readFloat32() { - const a = this.data.readFloatLE(this.offset); - this.offset += 4; - return a; - } - readFloat64() { - const a = this.data.readDoubleLE(this.offset); - this.offset += 8; - return a; - } - /** - * @param {number} count - */ - skip(count) { - this.offset += count; - } - readZTStringUCS2() { - let start = this.offset, index = this.offset; - while (index + 2 < this.length && this.readUInt16() !== 0) index += 2; - return this.data.slice(start, index).toString("ucs2"); - } - readZTStringUTF8() { - let start = this.offset, index = this.offset; - while (index + 1 < this.length && this.readUInt8() !== 0) index++; - return this.data.slice(start, index).toString("utf-8"); - } - readColor() { - const a = this.data.readUIntLE(this.offset, 3); - this.offset += 3; - return (a & 0xFF) | ((a >> 8) & 0xFF) | (a >> 16); - } + /** + * @param {Buffer} data + * @param {number=} offset + */ + constructor(data, offset) { + this.data = data; + this.offset = offset || 0; + this.length = data.length; + } + + readUInt8() { + return this.data[this.offset++]; + } + + readInt8() { + const a = this.data[this.offset++]; + + return a < 0x7F ? a : -a + 0x7F; + } + + readUInt16() { + const a = this.data.readUInt16LE(this.offset); + this.offset += 2; + + return a; + } + + readInt16() { + const a = this.data.readInt16LE(this.offset); + this.offset += 2; + + return a; + } + + readUInt24() { + const a = this.data.readUIntLE(this.offset, 3); + this.offset += 3; + + return a; + } + + readInt24() { + const a = this.data.readIntLE(this.offset, 3); + this.offset += 3; + + return a; + } + + readUInt32() { + const a = this.data.readUInt32LE(this.offset); + this.offset += 4; + + return a; + } + + readInt32() { + const a = this.data.readInt32LE(this.offset); + this.offset += 4; + + return a; + } + + readFloat32() { + const a = this.data.readFloatLE(this.offset); + this.offset += 4; + + return a; + } + + readFloat64() { + const a = this.data.readDoubleLE(this.offset); + this.offset += 8; + + return a; + } + + /** + * @param {number} count + */ + skip(count) { + this.offset += count; + } + + readZTStringUCS2() { + let start = this.offset, index = this.offset; + + while (index + 2 < this.length && this.readUInt16() !== 0) { + index += 2; + } + + return this.data.slice(start, index).toString("ucs2"); + } + + readZTStringUTF8() { + let start = this.offset, index = this.offset; + + while (index + 1 < this.length && this.readUInt8() !== 0) { + index++; + } + + return this.data.slice(start, index).toString("utf-8"); + } + + readColor() { + const a = this.data.readUIntLE(this.offset, 3); + this.offset += 3; + + return (a & 0xFF) | ((a >> 8) & 0xFF) | (a >> 16); + } } -module.exports = Reader; +module.exports = Reader; \ No newline at end of file diff --git a/servers/agarv2/src/primitives/Stopwatch.js b/servers/agarv2/src/primitives/Stopwatch.js index 20b806a1..85d4c7d7 100644 --- a/servers/agarv2/src/primitives/Stopwatch.js +++ b/servers/agarv2/src/primitives/Stopwatch.js @@ -1,24 +1,34 @@ class Stopwatch { - constructor() { } - begin() { - if (this._start) return; - this._start = this._lap = process.hrtime(); - } - lap() { - const diff = process.hrtime(this._lap); - this._lap = process.hrtime(); - return diff[0] * 1e3 + diff[1] / 1e6; - } - elapsed() { - const diff = process.hrtime(this._start); - return diff[0] * 1e3 + diff[1] / 1e6; - } - stop() { - this._start = this._lap = undefined; - } - reset() { - this._start = this._lap = process.hrtime(); - } + constructor() {} + + begin() { + if (this._start) { + return; + } + + this._start = this._lap = process.hrtime(); + } + + lap() { + const diff = process.hrtime(this._lap); + this._lap = process.hrtime(); + + return diff[0] * 1e3 + diff[1] / 1e6; + } + + elapsed() { + const diff = process.hrtime(this._start); + + return diff[0] * 1e3 + diff[1] / 1e6; + } + + stop() { + this._start = this._lap = undefined; + } + + reset() { + this._start = this._lap = process.hrtime(); + } } -module.exports = Stopwatch; +module.exports = Stopwatch; \ No newline at end of file diff --git a/servers/agarv2/src/primitives/Ticker.js b/servers/agarv2/src/primitives/Ticker.js index ed6c8ef1..ac244648 100644 --- a/servers/agarv2/src/primitives/Ticker.js +++ b/servers/agarv2/src/primitives/Ticker.js @@ -1,60 +1,93 @@ class Ticker { - /** - * @param {number} step - */ - constructor(step) { - this.step = step; - this.running = false; - /** @type {Function[]} */ - this.callbacks = []; - } - /** - * @param {Function} callback - */ - add(callback) { - if (!(callback instanceof Function)) - throw new TypeError("given object isn't a function"); - this.callbacks.push(callback); - return this; - } - /** - * @param {Function} callback - */ - remove(callback) { - if (!(callback instanceof Function)) - throw new TypeError("given object isn't a function"); - const i = this.callbacks.indexOf(callback); - if (i === -1) throw new Error("given function wasn't added"); - this.callback.splice(i, 1); - return this; - } - start() { - if (this.running) throw new Error("The ticker has already started"); - this._bind = this._tick.bind(this); - this.running = true; - this._virtualTime = Date.now(); - this._timeoutId = setTimeout(this._bind, this.step); - this.running = true; - return this; - } - _tick() { - if (!this.running) return; - for (let i = 0, l = this.callbacks.length; i < l; i++) - this.callbacks[i](); - this._virtualTime += this.step; - const delta = (this._virtualTime + this.step) - Date.now(); - if (delta < 0) this._virtualTime -= delta; - this._timeoutId = setTimeout(this._bind, delta); - } - stop() { - if (!this.running) throw new Error("The ticker hasn't started"); - clearTimeout(this._timeoutId); - delete this._timeoutId; - delete this._virtualTime; - delete this._bind; - this.running = false; - return this; - } + /** + * @param {number} step + */ + constructor(step) { + this.step = step; + this.running = false; + /** @type {Function[]} */ + this.callbacks = []; + } + + /** + * @param {Function} callback + */ + add(callback) { + if (!(callback instanceof Function)) { + throw new TypeError("given object isn't a function"); + } + + this.callbacks.push(callback); + + return this; + } + + /** + * @param {Function} callback + */ + remove(callback) { + if (!(callback instanceof Function)) { + throw new TypeError("given object isn't a function"); + } + + const i = this.callbacks.indexOf(callback); + + if (i === -1) { + throw new Error("given function wasn't added"); + } + + this.callback.splice(i, 1); + + return this; + } + + start() { + if (this.running) { + throw new Error("The ticker has already started"); + } + + this._bind = this._tick.bind(this); + this.running = true; + this._virtualTime = Date.now(); + this._timeoutId = setTimeout(this._bind, this.step); + this.running = true; + + return this; + } + + _tick() { + if (!this.running) { + return; + } + + for (let i = 0, l = this.callbacks.length; i < l; i++) { + this.callbacks[i](); + } + + this._virtualTime += this.step; + + const delta = (this._virtualTime + this.step) - Date.now(); + + if (delta < 0) { + this._virtualTime -= delta; + } + + this._timeoutId = setTimeout(this._bind, delta); + } + + stop() { + if (!this.running) { + throw new Error("The ticker hasn't started"); + } + + clearTimeout(this._timeoutId); + delete this._timeoutId; + delete this._virtualTime; + delete this._bind; + this.running = false; + + return this; + } } -module.exports = Ticker; +module.exports = Ticker; \ No newline at end of file diff --git a/servers/agarv2/src/primitives/Writer.js b/servers/agarv2/src/primitives/Writer.js index 78c0d6c1..42b52150 100644 --- a/servers/agarv2/src/primitives/Writer.js +++ b/servers/agarv2/src/primitives/Writer.js @@ -3,118 +3,138 @@ const sharedBuf = Buffer.allocUnsafe(poolSize); let offset = 0; class Writer { - constructor() { - offset = 0; - } - get offset() { return offset; } - - /** - * @param {number} a - */ - writeUInt8(a) { - sharedBuf[offset++] = a; - } - /** - * @param {number} a - */ - writeInt8(a) { - sharedBuf[offset++] = a; - } - /** - * @param {number} a - */ - writeUInt16(a) { - sharedBuf.writeUInt16LE(a, offset); - offset += 2; - } - /** - * @param {number} a - */ - writeInt16(a) { - sharedBuf.writeInt16LE(a, offset); - offset += 2; - } - /** - * @param {number} a - */ - writeUInt24(a) { - sharedBuf.writeUIntLE(a, offset, 3); - offset += 3; - } - /** - * @param {number} a - */ - writeInt24(a) { - sharedBuf.writeUIntLE(a, offset, 3); - offset += 3; - } - /** - * @param {number} a - */ - writeUInt32(a) { - sharedBuf.writeUInt32LE(a, offset); - offset += 4; - } - /** - * @param {number} a - */ - writeInt32(a) { - sharedBuf.writeInt32LE(a, offset); - offset += 4; - } - /** - * @param {number} a - */ - writeFloat32(a) { - sharedBuf.writeFloatLE(a, offset); - offset += 4; - } - /** - * @param {number} a - */ - writeFloat64(a) { - sharedBuf.writeDoubleLE(a, offset); - offset += 8; - } - /** - * @param {string} a - */ - writeZTStringUCS2(a) { - if (a) { - const tbuf = Buffer.from(a, "ucs2"); - offset += tbuf.copy(sharedBuf, offset); - } - sharedBuf[offset++] = 0; - sharedBuf[offset++] = 0; - } - /** - * @param {string} a - */ - writeZTStringUTF8(a) { - if (a) { - const tbuf = Buffer.from(a, "utf-8"); - offset += tbuf.copy(sharedBuf, offset); - } - sharedBuf[offset++] = 0; - } - /** - * @param {number} a - */ - writeColor(a) { - sharedBuf.writeUIntLE(((a & 0xFF) << 16) | (((a >> 8) & 0xFF) << 8) | (a >> 16), offset, 3); - offset += 3; - } - /** - * @param {Buffer} a - */ - writeBytes(a) { - offset += a.copy(sharedBuf, offset, 0, a.length); - } - finalize() { - const a = Buffer.allocUnsafe(offset); - sharedBuf.copy(a, 0, 0, offset); - return a; - } + constructor() { + offset = 0; + } + + get offset() { + return offset; + } + + /** + * @param {number} a + */ + writeUInt8(a) { + sharedBuf[offset++] = a; + } + + /** + * @param {number} a + */ + writeInt8(a) { + sharedBuf[offset++] = a; + } + + /** + * @param {number} a + */ + writeUInt16(a) { + sharedBuf.writeUInt16LE(a, offset); + offset += 2; + } + + /** + * @param {number} a + */ + writeInt16(a) { + sharedBuf.writeInt16LE(a, offset); + offset += 2; + } + + /** + * @param {number} a + */ + writeUInt24(a) { + sharedBuf.writeUIntLE(a, offset, 3); + offset += 3; + } + + /** + * @param {number} a + */ + writeInt24(a) { + sharedBuf.writeUIntLE(a, offset, 3); + offset += 3; + } + + /** + * @param {number} a + */ + writeUInt32(a) { + sharedBuf.writeUInt32LE(a, offset); + offset += 4; + } + + /** + * @param {number} a + */ + writeInt32(a) { + sharedBuf.writeInt32LE(a, offset); + offset += 4; + } + + /** + * @param {number} a + */ + writeFloat32(a) { + sharedBuf.writeFloatLE(a, offset); + offset += 4; + } + + /** + * @param {number} a + */ + writeFloat64(a) { + sharedBuf.writeDoubleLE(a, offset); + offset += 8; + } + + /** + * @param {string} a + */ + writeZTStringUCS2(a) { + if (a) { + const tbuf = Buffer.from(a, "ucs2"); + offset += tbuf.copy(sharedBuf, offset); + } + + sharedBuf[offset++] = 0; + sharedBuf[offset++] = 0; + } + + /** + * @param {string} a + */ + writeZTStringUTF8(a) { + if (a) { + const tbuf = Buffer.from(a, "utf-8"); + offset += tbuf.copy(sharedBuf, offset); + } + + sharedBuf[offset++] = 0; + } + + /** + * @param {number} a + */ + writeColor(a) { + sharedBuf.writeUIntLE(((a & 0xFF) << 16) | (((a >> 8) & 0xFF) << 8) | (a >> 16), offset, 3); + offset += 3; + } + + /** + * @param {Buffer} a + */ + writeBytes(a) { + offset += a.copy(sharedBuf, offset, 0, a.length); + } + + finalize() { + const a = Buffer.allocUnsafe(offset); + sharedBuf.copy(a, 0, 0, offset); + + return a; + } } -module.exports = Writer; +module.exports = Writer; \ No newline at end of file diff --git a/servers/agarv2/src/protocols/LegacyProtocol.js b/servers/agarv2/src/protocols/LegacyProtocol.js index a189cf0c..ac88f649 100644 --- a/servers/agarv2/src/protocols/LegacyProtocol.js +++ b/servers/agarv2/src/protocols/LegacyProtocol.js @@ -3,257 +3,327 @@ const Reader = require("../primitives/Reader"); const Writer = require("../primitives/Writer"); class LegacyProtocol extends Protocol { - /** - * @param {Connection} connection - */ - constructor(connection) { - super(connection); - this.gotProtocol = false; - this.protocol = NaN; - this.gotKey = false; - this.key = NaN; - - this.lastLeaderboardType = null; - } - - static get type() { return "legacy"; } - get subtype() { return `l${!isNaN(this.protocol) ? ("00" + this.protocol).slice(-2) : "//"}`; } - - /** - * @param {Reader} reader - */ - distinguishes(reader) { - if (reader.length < 5) return false; - if (reader.readUInt8() !== 254) return false; - this.gotProtocol = true; - this.protocol = reader.readUInt32(); - if (this.protocol < 4) { - this.protocol = 4; - this.logger.debug(`legacy protocol: got version ${this.protocol}, which is lower than 4`); - } - return true; - } - - /** - * @param {Reader} reader - */ - onSocketMessage(reader) { - const messageId = reader.readUInt8(); - if (!this.gotKey) { - if (messageId !== 255) return; - if (reader.length < 5) return void this.fail("Unexpected message format"); - this.gotKey = true; - this.key = reader.readUInt32(); - this.connection.createPlayer(); - return; - } - switch (messageId) { - case 0: - this.connection.spawningName = readZTString(reader, this.protocol); - break; - case 1: - this.connection.requestingSpectate = true; - break; - case 16: - switch (reader.length) { - case 13: - this.connection.mouseX = reader.readInt32(); - this.connection.mouseY = reader.readInt32(); - break; - case 9: - this.connection.mouseX = reader.readInt16(); - this.connection.mouseY = reader.readInt16(); - break; - case 21: - this.connection.mouseX = ~~reader.readFloat64(); - this.connection.mouseY = ~~reader.readFloat64(); - break; - default: return void this.fail(1003, "Unexpected message format"); - } - break; - case 17: - if (this.connection.controllingMinions) - for (let i = 0, l = this.connection.minions.length; i < l; i++) - this.connection.minions[i].splitAttempts++; - else this.connection.splitAttempts++; - break; - case 18: this.connection.isPressingQ = true; break; - case 19: this.connection.isPressingQ = this.hasProcessedQ = false; break; - case 21: - if (this.connection.controllingMinions) - for (let i = 0, l = this.connection.minions.length; i < l; i++) - this.connection.minions[i].ejectAttempts++; - else this.connection.ejectAttempts++; - break; - case 22: - if (!this.gotKey || !this.settings.minionEnableERTPControls) break; - for (let i = 0, l = this.connection.minions.length; i < l; i++) - this.connection.minions[i].splitAttempts++; - break; - case 23: - if (!this.gotKey || !this.settings.minionEnableERTPControls) break; - for (let i = 0, l = this.connection.minions.length; i < l; i++) - this.connection.minions[i].ejectAttempts++; - break; - case 24: - if (!this.gotKey || !this.settings.minionEnableERTPControls) break; - this.connection.minionsFrozen = !this.connection.minionsFrozen; - break; - case 99: - if (reader.length < 2) - return void this.fail(1003, "Bad message format"); - const flags = reader.readUInt8(); - const skipLen = 2 * ((flags & 2) + (flags & 4) + (flags & 8)) - if (reader.length < 2 + skipLen) - return void this.fail(1003, "Unexpected message format"); - reader.skip(skipLen); - const message = readZTString(reader, this.protocol); - this.logger.inform(`[${this.connection.remoteAddress}][${this.connection.verifyScore}][${this.connection.player.cellSkin ? this.connection.player.cellSkin.split('|').slice(-1) : ''}] ${this.connection.player.chatName}: ${message} [${this.connection.player.cellSkin ? this.connection.player.cellSkin.split('|')[0] : ''}]`); - this.connection.onChatMessage(message); - break; - case 254: - if (this.connection.hasPlayer && this.connection.player.hasWorld) - this.onStatsRequest(); - break; - case 255: return void this.fail(1003, "Unexpected message"); - default: return void this.fail(1003, "Unknown message type"); - } - } - - /** - * @param {ChatSource} source - * @param {string} message - */ - onChatMessage(source, message) { - const writer = new Writer(); - writer.writeUInt8(99); - writer.writeUInt8(source.isServer * 128); - writer.writeColor(source.color); - writeZTString(writer, source.name, this.protocol); - writeZTString(writer, message, this.protocol); - this.send(writer.finalize()); - } - - onStatsRequest() { - const writer = new Writer(); - writer.writeUInt8(254); - const stats = this.connection.player.world.stats; - const legacy = { - mode: stats.gamemode, - update: stats.loadTime, - playersTotal: stats.external, - playersAlive: stats.playing, - playersSpect: stats.spectating, - playersLimit: stats.limit - }; - writeZTString(writer, JSON.stringify(Object.assign({}, legacy, stats)), this.protocol); - this.send(writer.finalize()); - } - - /** - * @param {PlayerCell} cell - */ - onNewOwnedCell(cell) { - const writer = new Writer(); - writer.writeUInt8(32); - writer.writeUInt32(cell.id); - this.send(writer.finalize()); - } - - /** - * @param {Rect} range - * @param {boolean} includeServerInfo - */ - onNewWorldBounds(range, includeServerInfo) { - const writer = new Writer(); - writer.writeUInt8(64); - writer.writeFloat64(range.x - range.w); - writer.writeFloat64(range.y - range.h); - writer.writeFloat64(range.x + range.w); - writer.writeFloat64(range.y + range.h); - if (includeServerInfo) { - writer.writeUInt32(this.handle.gamemode.type); - writeZTString(writer, `OgarII ${this.handle.version}`, this.protocol); - } - this.send(writer.finalize()); - } - - onWorldReset() { - const writer = new Writer(); - writer.writeUInt8(18); - this.send(writer.finalize()); - if (this.lastLeaderboardType !== null) { - this.onLeaderboardUpdate(this.lastLeaderboardType, [], null); - this.lastLeaderboardType = null; - } - } - - /** - * @param {LeaderboardType} type - * @param {LeaderboardDataType[type][]} data - * @param {LeaderboardDataType[type]=} selfData - */ - onLeaderboardUpdate(type, data, selfData) { - this.lastLeaderboardType = type; - const writer = new Writer(); - switch (type) { - case "ffa": ffaLeaderboard[this.protocol](writer, data, selfData, this.protocol); break; - case "pie": pieLeaderboard[this.protocol](writer, data, selfData, this.protocol); break; - case "text": textBoard[this.protocol](writer, data, this.protocol); break; - } - this.send(writer.finalize()); - } - - /** - * @param {ViewArea} viewArea - */ - onSpectatePosition(viewArea) { - const writer = new Writer(); - writer.writeUInt8(17); - writer.writeFloat32(viewArea.x); - writer.writeFloat32(viewArea.y); - writer.writeFloat32(viewArea.s); - this.send(writer.finalize()); - } - - /** - * @abstract - * @param {Cell[]} add - * @param {Cell[]} upd - * @param {Cell[]} eat - * @param {Cell[]} del - */ - onVisibleCellUpdate(add, upd, eat, del) { - const source = this.connection.player; - const writer = new Writer(); - writer.writeUInt8(16); - let i, l, cell; - - l = eat.length; - writer.writeUInt16(l); - for (i = 0; i < l; i++) { - cell = eat[i]; - writer.writeUInt32(cell.eatenBy.id); - writer.writeUInt32(cell.id); - } - - for (i = 0, l = add.length; i < l; i++) { - cell = add[i]; - writeCellData[this.protocol](writer, source, this.protocol, cell, - true, true, true, true, true, true); - } - for (i = 0, l = upd.length; i < l; i++) { - cell = upd[i]; - writeCellData[this.protocol](writer, source, this.protocol, cell, - false, cell.sizeChanged, cell.posChanged, cell.colorChanged, cell.nameChanged, cell.skinChanged); - } - writer.writeUInt32(0); - - l = del.length; - writer[this.protocol < 6 ? "writeUInt32" : "writeUInt16"](l); - for (i = 0; i < l; i++) writer.writeUInt32(del[i].id); - this.send(writer.finalize()); - } + /** + * @param {Connection} connection + */ + constructor(connection) { + super(connection); + this.gotProtocol = false; + this.protocol = NaN; + this.gotKey = false; + this.key = NaN; + + this.lastLeaderboardType = null; + } + + static get type() { + return "legacy"; + } + + get subtype() { + return `l${!isNaN(this.protocol) ? ("00" + this.protocol).slice(-2) : "//"}`; + } + + /** + * @param {Reader} reader + */ + distinguishes(reader) { + if (reader.length < 5) { + return false; + } + + if (reader.readUInt8() !== 254) { + return false; + } + + this.gotProtocol = true; + this.protocol = reader.readUInt32(); + + if (this.protocol < 4) { + this.protocol = 4; + this.logger.debug(`legacy protocol: got version ${this.protocol}, which is lower than 4`); + } + + return true; + } + + /** + * @param {Reader} reader + */ + onSocketMessage(reader) { + const messageId = reader.readUInt8(); + + if (!this.gotKey) { + if (messageId !== 255) { + return; + } + + if (reader.length < 5) { + return void this.fail("Unexpected message format"); + } + + this.gotKey = true; + this.key = reader.readUInt32(); + this.connection.createPlayer(); + + return; + } + + switch (messageId) { + case 0: + this.connection.spawningName = readZTString(reader, this.protocol); + break; + case 1: + this.connection.requestingSpectate = true; + break; + case 16: + switch (reader.length) { + case 13: + this.connection.mouseX = reader.readInt32(); + this.connection.mouseY = reader.readInt32(); + break; + case 9: + this.connection.mouseX = reader.readInt16(); + this.connection.mouseY = reader.readInt16(); + break; + case 21: + this.connection.mouseX = ~~reader.readFloat64(); + this.connection.mouseY = ~~reader.readFloat64(); + break; + default: + return void this.fail(1003, "Unexpected message format"); + } + break; + case 17: + if (this.connection.controllingMinions) { + for (let i = 0, l = this.connection.minions.length; i < l; i++) { + this.connection.minions[i].splitAttempts++; + } + } else { + this.connection.splitAttempts++; + } + break; + case 18: + this.connection.isPressingQ = true; + break; + case 19: + this.connection.isPressingQ = this.hasProcessedQ = false; + break; + case 21: + if (this.connection.controllingMinions) { + for (let i = 0, l = this.connection.minions.length; i < l; i++) { + this.connection.minions[i].ejectAttempts++; + } + } else { + this.connection.ejectAttempts++; + } + break; + case 22: + if (!this.gotKey || !this.settings.minionEnableERTPControls) { + break; + } + + for (let i = 0, l = this.connection.minions.length; i < l; i++) { + this.connection.minions[i].splitAttempts++; + } + break; + case 23: + if (!this.gotKey || !this.settings.minionEnableERTPControls) { + break; + } + + for (let i = 0, l = this.connection.minions.length; i < l; i++) { + this.connection.minions[i].ejectAttempts++; + } + break; + case 24: + if (!this.gotKey || !this.settings.minionEnableERTPControls) { + break; + } + + this.connection.minionsFrozen = !this.connection.minionsFrozen; + break; + case 99: + if (reader.length < 2) { + return void this.fail(1003, "Bad message format"); + } + + const flags = reader.readUInt8(); + const skipLen = 2 * ((flags & 2) + (flags & 4) + (flags & 8)) + + if (reader.length < 2 + skipLen) { + return void this.fail(1003, "Unexpected message format"); + } + + reader.skip(skipLen); + const message = readZTString(reader, this.protocol); + this.logger.inform(`[${this.connection.remoteAddress}][${this.connection.verifyScore}][${this.connection.player.cellSkin ? this.connection.player.cellSkin.split('|').slice(-1) : ''}] ${this.connection.player.chatName}: ${message} [${this.connection.player.cellSkin ? this.connection.player.cellSkin.split('|')[0] : ''}]`); + this.connection.onChatMessage(message); + break; + case 254: + if (this.connection.hasPlayer && this.connection.player.hasWorld) { + this.onStatsRequest(); + } + break; + case 255: + return void this.fail(1003, "Unexpected message"); + default: + return void this.fail(1003, "Unknown message type"); + } + } + + /** + * @param {ChatSource} source + * @param {string} message + */ + onChatMessage(source, message) { + const writer = new Writer(); + writer.writeUInt8(99); + writer.writeUInt8(source.isServer * 128); + writer.writeColor(source.color); + writeZTString(writer, source.name, this.protocol); + writeZTString(writer, message, this.protocol); + this.send(writer.finalize()); + } + + onStatsRequest() { + const writer = new Writer(); + writer.writeUInt8(254); + const stats = this.connection.player.world.stats; + + const legacy = { + mode: stats.gamemode, + update: stats.loadTime, + playersTotal: stats.external, + playersAlive: stats.playing, + playersSpect: stats.spectating, + playersLimit: stats.limit + }; + + writeZTString(writer, JSON.stringify(Object.assign({}, legacy, stats)), this.protocol); + this.send(writer.finalize()); + } + + /** + * @param {PlayerCell} cell + */ + onNewOwnedCell(cell) { + const writer = new Writer(); + writer.writeUInt8(32); + writer.writeUInt32(cell.id); + this.send(writer.finalize()); + } + + /** + * @param {Rect} range + * @param {boolean} includeServerInfo + */ + onNewWorldBounds(range, includeServerInfo) { + const writer = new Writer(); + writer.writeUInt8(64); + writer.writeFloat64(range.x - range.w); + writer.writeFloat64(range.y - range.h); + writer.writeFloat64(range.x + range.w); + writer.writeFloat64(range.y + range.h); + + if (includeServerInfo) { + writer.writeUInt32(this.handle.gamemode.type); + writeZTString(writer, `OgarII ${this.handle.version}`, this.protocol); + } + + this.send(writer.finalize()); + } + + onWorldReset() { + const writer = new Writer(); + writer.writeUInt8(18); + this.send(writer.finalize()); + + if (this.lastLeaderboardType !== null) { + this.onLeaderboardUpdate(this.lastLeaderboardType, [], null); + this.lastLeaderboardType = null; + } + } + + /** + * @param {LeaderboardType} type + * @param {LeaderboardDataType[type][]} data + * @param {LeaderboardDataType[type]=} selfData + */ + onLeaderboardUpdate(type, data, selfData) { + this.lastLeaderboardType = type; + const writer = new Writer(); + + switch (type) { + case "ffa": + ffaLeaderboard[this.protocol](writer, data, selfData, this.protocol); + break; + case "pie": + pieLeaderboard[this.protocol](writer, data, selfData, this.protocol); + break; + case "text": + textBoard[this.protocol](writer, data, this.protocol); + break; + } + + this.send(writer.finalize()); + } + + /** + * @param {ViewArea} viewArea + */ + onSpectatePosition(viewArea) { + const writer = new Writer(); + writer.writeUInt8(17); + writer.writeFloat32(viewArea.x); + writer.writeFloat32(viewArea.y); + writer.writeFloat32(viewArea.s); + this.send(writer.finalize()); + } + + /** + * @abstract + * @param {Cell[]} add + * @param {Cell[]} upd + * @param {Cell[]} eat + * @param {Cell[]} del + */ + onVisibleCellUpdate(add, upd, eat, del) { + const source = this.connection.player; + const writer = new Writer(); + writer.writeUInt8(16); + let i, l, cell; + + l = eat.length; + writer.writeUInt16(l); + + for (i = 0; i < l; i++) { + cell = eat[i]; + writer.writeUInt32(cell.eatenBy.id); + writer.writeUInt32(cell.id); + } + + for (i = 0, l = add.length; i < l; i++) { + cell = add[i]; + writeCellData[this.protocol](writer, source, this.protocol, cell, true, true, true, true, true, true); + } + + for (i = 0, l = upd.length; i < l; i++) { + cell = upd[i]; + writeCellData[this.protocol](writer, source, this.protocol, cell, false, cell.sizeChanged, cell.posChanged, cell.colorChanged, cell.nameChanged, cell.skinChanged); + } + + writer.writeUInt32(0); + + l = del.length; + writer[this.protocol < 6 ? "writeUInt32" : "writeUInt16"](l); + + for (i = 0; i < l; i++) { + writer.writeUInt32(del[i].id); + } + + this.send(writer.finalize()); + } } module.exports = LegacyProtocol; @@ -262,26 +332,27 @@ module.exports = LegacyProtocol; * @type {{ [protocol: number]: (writer: Writer, data: LeaderboardDataType["pie"][], selfData: LeaderboardDataType["pie"], protocol: number) => void }} */ const pieLeaderboard = { - 4: pieLeaderboard4, - 5: pieLeaderboard4, - 6: pieLeaderboard4, - 7: pieLeaderboard4, - 8: pieLeaderboard4, - 9: pieLeaderboard4, - 10: pieLeaderboard4, - 11: pieLeaderboard4, - 12: pieLeaderboard4, - 13: pieLeaderboard4, - 14: pieLeaderboard4, - 15: pieLeaderboard4, - 16: pieLeaderboard4, - 17: pieLeaderboard4, - 18: pieLeaderboard4, - 19: pieLeaderboard4, - 20: pieLeaderboard4, - 21: pieLeaderboard21, - 22: pieLeaderboard21 + 4: pieLeaderboard4, + 5: pieLeaderboard4, + 6: pieLeaderboard4, + 7: pieLeaderboard4, + 8: pieLeaderboard4, + 9: pieLeaderboard4, + 10: pieLeaderboard4, + 11: pieLeaderboard4, + 12: pieLeaderboard4, + 13: pieLeaderboard4, + 14: pieLeaderboard4, + 15: pieLeaderboard4, + 16: pieLeaderboard4, + 17: pieLeaderboard4, + 18: pieLeaderboard4, + 19: pieLeaderboard4, + 20: pieLeaderboard4, + 21: pieLeaderboard21, + 22: pieLeaderboard21 }; + /** * @param {Writer} writer * @param {LeaderboardDataType["pie"][]} data @@ -289,11 +360,14 @@ const pieLeaderboard = { * @param {number} protocol */ function pieLeaderboard4(writer, data, protocol) { - writer.writeUInt8(50); - writer.writeUInt32(data.length); - for (let i = 0, l = data.length; i < l; i++) - writer.writeFloat32(data[i].weight); + writer.writeUInt8(50); + writer.writeUInt32(data.length); + + for (let i = 0, l = data.length; i < l; i++) { + writer.writeFloat32(data[i].weight); + } } + /** * @param {Writer} writer * @param {LeaderboardDataType["pie"][]} data @@ -301,38 +375,40 @@ function pieLeaderboard4(writer, data, protocol) { * @param {number} protocol */ function pieLeaderboard21(writer, data, protocol) { - writer.writeUInt8(50); - writer.writeUInt32(data.length); - for (let i = 0, l = data.length; i < l; i++) { - writer.writeFloat32(data[i].weight); - writer.writeColor(data[i].color); - } + writer.writeUInt8(50); + writer.writeUInt32(data.length); + + for (let i = 0, l = data.length; i < l; i++) { + writer.writeFloat32(data[i].weight); + writer.writeColor(data[i].color); + } } /** * @type {{ [protocol: number]: (writer: Writer, data: LeaderboardDataType["ffa"][], selfData: LeaderboardDataType["ffa"], protocol: number) => void }} */ const ffaLeaderboard = { - 4: ffaLeaderboard4, - 5: ffaLeaderboard4, - 6: ffaLeaderboard4, - 7: ffaLeaderboard4, - 8: ffaLeaderboard4, - 9: ffaLeaderboard4, - 10: ffaLeaderboard4, - 11: ffaLeaderboard11, - 12: ffaLeaderboard11, - 13: ffaLeaderboard11, - 14: ffaLeaderboard11, - 15: ffaLeaderboard11, - 16: ffaLeaderboard11, - 17: ffaLeaderboard11, - 18: ffaLeaderboard11, - 19: ffaLeaderboard11, - 20: ffaLeaderboard11, - 21: ffaLeaderboard11, - 22: ffaLeaderboard11 + 4: ffaLeaderboard4, + 5: ffaLeaderboard4, + 6: ffaLeaderboard4, + 7: ffaLeaderboard4, + 8: ffaLeaderboard4, + 9: ffaLeaderboard4, + 10: ffaLeaderboard4, + 11: ffaLeaderboard11, + 12: ffaLeaderboard11, + 13: ffaLeaderboard11, + 14: ffaLeaderboard11, + 15: ffaLeaderboard11, + 16: ffaLeaderboard11, + 17: ffaLeaderboard11, + 18: ffaLeaderboard11, + 19: ffaLeaderboard11, + 20: ffaLeaderboard11, + 21: ffaLeaderboard11, + 22: ffaLeaderboard11 }; + /** * @param {Writer} writer * @param {LeaderboardDataType["ffa"][]} data @@ -340,16 +416,22 @@ const ffaLeaderboard = { * @param {number} protocol */ function ffaLeaderboard4(writer, data, selfData, protocol) { - writer.writeUInt8(49); - writer.writeUInt32(data.length); - for (let i = 0, l = data.length; i < l; i++) { - const item = data[i]; - if (protocol === 6) - writer.writeUInt32(item.highlighted ? 1 : 0); - else writer.writeUInt32(item.cellId); - writeZTString(writer, item.name, protocol); - } + writer.writeUInt8(49); + writer.writeUInt32(data.length); + + for (let i = 0, l = data.length; i < l; i++) { + const item = data[i]; + + if (protocol === 6) { + writer.writeUInt32(item.highlighted ? 1 : 0); + } else { + writer.writeUInt32(item.cellId); + } + + writeZTString(writer, item.name, protocol); + } } + /** * @param {Writer} writer * @param {LeaderboardDataType["ffa"][]} data @@ -357,90 +439,98 @@ function ffaLeaderboard4(writer, data, selfData, protocol) { * @param {number} protocol */ function ffaLeaderboard11(writer, data, selfData, protocol) { - writer.writeUInt8(protocol >= 14 ? 53 : 51); - for (let i = 0, l = data.length; i < l; i++) { - const item = data[i]; - if (item === selfData) - writer.writeUInt8(8); - else { - writer.writeUInt8(2); - writer.writeZTStringUTF8(item.name); - } - } + writer.writeUInt8(protocol >= 14 ? 53 : 51); + + for (let i = 0, l = data.length; i < l; i++) { + const item = data[i]; + + if (item === selfData) { + writer.writeUInt8(8); + } else { + writer.writeUInt8(2); + writer.writeZTStringUTF8(item.name); + } + } } /** * @type {{ [protocol: number]: (writer: Writer, data: LeaderboardDataType["text"][], protocol: number) => void }} */ const textBoard = { - 4: textBoard4, - 5: textBoard4, - 6: textBoard4, - 7: textBoard4, - 8: textBoard4, - 9: textBoard4, - 10: textBoard4, - 11: textBoard4, - 12: textBoard4, - 13: textBoard4, - 14: textBoard14, - 15: textBoard14, - 16: textBoard14, - 17: textBoard14, - 18: textBoard14, - 19: textBoard14, - 20: textBoard14, - 21: textBoard14, - 22: textBoard14 + 4: textBoard4, + 5: textBoard4, + 6: textBoard4, + 7: textBoard4, + 8: textBoard4, + 9: textBoard4, + 10: textBoard4, + 11: textBoard4, + 12: textBoard4, + 13: textBoard4, + 14: textBoard14, + 15: textBoard14, + 16: textBoard14, + 17: textBoard14, + 18: textBoard14, + 19: textBoard14, + 20: textBoard14, + 21: textBoard14, + 22: textBoard14 }; + /** * @param {Writer} writer * @param {LeaderboardDataType["text"][]} data * @param {number} protocol */ function textBoard4(writer, data, protocol) { - writer.writeUInt8(48); - writer.writeUInt32(data.length); - for (let i = 0, l = data.length; i < l; i++) - writer.writeZTString(data[i], protocol); + writer.writeUInt8(48); + writer.writeUInt32(data.length); + + for (let i = 0, l = data.length; i < l; i++) { + writer.writeZTString(data[i], protocol); + } } + /** * @param {Writer} writer * @param {LeaderboardDataType["text"][]} data * @param {number} protocol */ function textBoard14(writer, data, protocol) { - writer.writeUInt8(53); - for (let i = 0, l = data.length; i < l; i++) { - writer.writeUInt8(2); - writer.writeZTStringUTF8(data[i]); - } + writer.writeUInt8(53); + + for (let i = 0, l = data.length; i < l; i++) { + writer.writeUInt8(2); + writer.writeZTStringUTF8(data[i]); + } } /** * @type {{ [protocol: number]: (writer: Writer, source: Player, protocol: number, cell: Cell, includeType: boolean, includeSize: boolean, includePos: boolean, includeColor: boolean, includeName: boolean, includeSkin: boolean) => void }} */ const writeCellData = { - 4: writeCellData4, - 5: writeCellData4, - 6: writeCellData6, - 7: writeCellData6, - 8: writeCellData6, - 9: writeCellData6, - 10: writeCellData6, - 11: writeCellData11, - 12: writeCellData11, - 13: writeCellData11, - 14: writeCellData11, - 15: writeCellData11, - 16: writeCellData11, - 17: writeCellData11, - 18: writeCellData11, - 19: writeCellData11, - 20: writeCellData11, - 21: writeCellData11, - 22: writeCellData11 + 4: writeCellData4, + 5: writeCellData4, + 6: writeCellData6, + 7: writeCellData6, + 8: writeCellData6, + 9: writeCellData6, + 10: writeCellData6, + 11: writeCellData11, + 12: writeCellData11, + 13: writeCellData11, + 14: writeCellData11, + 15: writeCellData11, + 16: writeCellData11, + 17: writeCellData11, + 18: writeCellData11, + 19: writeCellData11, + 20: writeCellData11, + 21: writeCellData11, + 22: writeCellData11 }; + /** * @param {Writer} writer * @param {Player} source @@ -454,23 +544,43 @@ const writeCellData = { * @param {boolean} includeSkin */ function writeCellData4(writer, source, protocol, cell, includeType, includeSize, includePos, includeColor, includeName, includeSkin) { - writer.writeUInt32(cell.id); - writer[protocol === 4 ? "writeInt16" : "writeInt32"](cell.x); - writer[protocol === 4 ? "writeInt16" : "writeInt32"](cell.y); - writer.writeUInt16(cell.size); - writer.writeColor(cell.color); - - let flags = 0; - if (cell.isSpiked) flags |= 0x01; - if (includeSkin) flags |= 0x04; - if (cell.isAgitated) flags |= 0x10; - if (cell.type === 3) flags |= 0x20; - writer.writeUInt8(flags); - - if (includeSkin) writer.writeZTStringUTF8(cell.skin); - if (includeName) writer.writeZTStringUCS2(cell.name); - else writer.writeUInt16(0); + writer.writeUInt32(cell.id); + writer[protocol === 4 ? "writeInt16" : "writeInt32"](cell.x); + writer[protocol === 4 ? "writeInt16" : "writeInt32"](cell.y); + writer.writeUInt16(cell.size); + writer.writeColor(cell.color); + + let flags = 0; + + if (cell.isSpiked) { + flags |= 0x01; + } + + if (includeSkin) { + flags |= 0x04; + } + + if (cell.isAgitated) { + flags |= 0x10; + } + + if (cell.type === 3) { + flags |= 0x20; + } + + writer.writeUInt8(flags); + + if (includeSkin) { + writer.writeZTStringUTF8(cell.skin); + } + + if (includeName) { + writer.writeZTStringUCS2(cell.name); + } else { + writer.writeUInt16(0); + } } + /** * @param {Writer} writer * @param {Player} source @@ -484,24 +594,52 @@ function writeCellData4(writer, source, protocol, cell, includeType, includeSize * @param {boolean} includeSkin */ function writeCellData6(writer, source, protocol, cell, includeType, includeSize, includePos, includeColor, includeName, includeSkin) { - writer.writeUInt32(cell.id); - writer.writeInt32(cell.x); - writer.writeInt32(cell.y); - writer.writeUInt16(cell.size); - - let flags = 0; - if (cell.isSpiked) flags |= 0x01; - if (includeColor) flags |= 0x02; - if (includeSkin) flags |= 0x04; - if (includeName) flags |= 0x08; - if (cell.isAgitated) flags |= 0x10; - if (cell.type === 3) flags |= 0x20; - writer.writeUInt8(flags); - - if (includeColor) writer.writeColor(cell.color); - if (includeSkin) writer.writeZTStringUTF8(cell.skin); - if (includeName) writer.writeZTStringUTF8(cell.name); + writer.writeUInt32(cell.id); + writer.writeInt32(cell.x); + writer.writeInt32(cell.y); + writer.writeUInt16(cell.size); + + let flags = 0; + + if (cell.isSpiked) { + flags |= 0x01; + } + + if (includeColor) { + flags |= 0x02; + } + + if (includeSkin) { + flags |= 0x04; + } + + if (includeName) { + flags |= 0x08; + } + + if (cell.isAgitated) { + flags |= 0x10; + } + + if (cell.type === 3) { + flags |= 0x20; + } + + writer.writeUInt8(flags); + + if (includeColor) { + writer.writeColor(cell.color); + } + + if (includeSkin) { + writer.writeZTStringUTF8(cell.skin); + } + + if (includeName) { + writer.writeZTStringUTF8(cell.name); + } } + /** * @param {Writer} writer * @param {Player} source @@ -515,26 +653,60 @@ function writeCellData6(writer, source, protocol, cell, includeType, includeSize * @param {boolean} includeSkin */ function writeCellData11(writer, source, protocol, cell, includeType, includeSize, includePos, includeColor, includeName, includeSkin) { - writer.writeUInt32(cell.id); - writer.writeInt32(cell.x); - writer.writeInt32(cell.y); - writer.writeUInt16(cell.size); - - let flags = 0; - if (cell.isSpiked) flags |= 0x01; - if (includeColor) flags |= 0x02; - if (includeSkin) flags |= 0x04; - if (includeName) flags |= 0x08; - if (cell.isAgitated) flags |= 0x10; - if (cell.type === 3) flags |= 0x20; - if (cell.type === 3 && cell.owner !== source) flags |= 0x40; - if (includeType && cell.type === 1) flags |= 0x80; - writer.writeUInt8(flags); - if (includeType && cell.type === 1) writer.writeUInt8(1); - - if (includeColor) writer.writeColor(cell.color); - if (includeSkin) writer.writeZTStringUTF8(cell.skin); - if (includeName) writer.writeZTStringUTF8(cell.name); + writer.writeUInt32(cell.id); + writer.writeInt32(cell.x); + writer.writeInt32(cell.y); + writer.writeUInt16(cell.size); + + let flags = 0; + + if (cell.isSpiked) { + flags |= 0x01; + } + + if (includeColor) { + flags |= 0x02; + } + + if (includeSkin) { + flags |= 0x04; + } + + if (includeName) { + flags |= 0x08; + } + + if (cell.isAgitated) { + flags |= 0x10; + } + + if (cell.type === 3) { + flags |= 0x20; + } + + if (cell.type === 3 && cell.owner !== source) { + flags |= 0x40; + } + + if (includeType && cell.type === 1) { + flags |= 0x80; + } + + writer.writeUInt8(flags); + + if (includeType && cell.type === 1) { + writer.writeUInt8(1); + } + + if (includeColor) { + writer.writeColor(cell.color); + } + if (includeSkin) { + writer.writeZTStringUTF8(cell.skin); + } + if (includeName) { + writer.writeZTStringUTF8(cell.name); + } } /** @@ -543,15 +715,16 @@ function writeCellData11(writer, source, protocol, cell, includeType, includeSiz * @returns {string} */ function readZTString(reader, protocol) { - return reader[protocol < 6 ? "readZTStringUCS2" : "readZTStringUTF8"](); + return reader[protocol < 6 ? "readZTStringUCS2" : "readZTStringUTF8"](); } + /** * @param {Writer} writer * @param {string} value * @param {number} protocol */ function writeZTString(writer, value, protocol) { - writer[protocol < 6 ? "writeZTStringUCS2" : "writeZTStringUTF8"](value); + writer[protocol < 6 ? "writeZTStringUCS2" : "writeZTStringUTF8"](value); } const Cell = require("../cells/Cell"); diff --git a/servers/agarv2/src/protocols/ModernProtocol.js b/servers/agarv2/src/protocols/ModernProtocol.js index f0bd64d1..3e468ac3 100644 --- a/servers/agarv2/src/protocols/ModernProtocol.js +++ b/servers/agarv2/src/protocols/ModernProtocol.js @@ -5,330 +5,482 @@ const Writer = require("../primitives/Writer"); const PingReturn = Buffer.from(new Uint8Array([2])); class ModernProtocol extends Protocol { - /** - * @param {Connection} connection - */ - constructor(connection) { - super(connection); - this.protocol = NaN; - - this.leaderboardPending = false; - /** @type {LeaderboardType} */ - this.leaderboardType = null; - /** @type {LeaderboardDataType[LeaderboardType][]} */ - this.leaderboardData = null; - /** @type {LeaderboardDataType[LeaderboardType]} */ - this.leaderboardSelfData = null; - - /** @type {{ source: ChatSource, message: string }[]} */ - this.chatPending = []; - /** @type {Rect} */ - this.worldBorderPending = null; - /** @type {ViewArea} */ - this.spectateAreaPending = null; - this.serverInfoPending = false; - this.worldStatsPending = false; - this.clearCellsPending = false; - } - - static get type() { return "modern"; } - get subtype() { return `m${!isNaN(this.protocol) ? ("00" + this.protocol).slice(-2) : "//"}`; } - - /** - * @param {Reader} reader - */ - distinguishes(reader) { - if (reader.length < 5) return false; - if (reader.readUInt8() !== 1) return false; - this.gotProtocol = true; - this.protocol = reader.readUInt32(); - if (this.protocol !== 3) return void this.fail(1003, "Unsupported protocol version"); - this.connection.createPlayer(); - return true; - } - - /** - * @param {Reader} reader - */ - onSocketMessage(reader) { - const messageId = reader.readUInt8(); - switch (messageId) { - case 2: - this.send(PingReturn); - this.worldStatsPending = true; - break; - case 3: - if (reader.length < 12) - return void this.fail(1003, "Unexpected message format"); - let i, l, count; - this.connection.mouseX = reader.readInt32(); - this.connection.mouseY = reader.readInt32(); - this.connection.splitAttempts += reader.readUInt8(); - count = reader.readUInt8(); - for (i = 0, l = this.connection.minions.length; count > 0 && i < l; i++) - this.connection.minions[i].splitAttempts += count; - - const globalFlags = reader.readUInt8(); - if (globalFlags & 1) { - if (reader.length < 13) - return void this.fail(1003, "Unexpected message format"); - this.connection.spawningName = reader.readZTStringUTF8(); - } - if (globalFlags & 2) this.connection.requestingSpectate = true; - if (globalFlags & 4) this.connection.isPressingQ = true; - if (globalFlags & 8) this.connection.isPressingQ = this.connection.hasProcessedQ = false; - if (globalFlags & 16) this.connection.ejectAttempts++; - if (globalFlags & 32) - for (i = 0, l = this.connection.minions.length; i < l; i++) - this.connection.minions[i].ejectAttempts++; - if (globalFlags & 64) this.connection.minionsFrozen = !this.connection.minionsFrozen; - if (globalFlags & 128) { - if (reader.length < 13 + (globalFlags & 1)) - return void this.fail(1003, "Unexpected message format"); - count = reader.readUInt8(); - if (reader.length < 13 + (globalFlags & 1) + count) - return void this.fail(1003, "Unexpected message format"); - for (let i = 0; i < count; i++) - this.connection.onChatMessage(reader.readZTStringUTF8()); - } - break; - default: return void this.fail(1003, "Unknown message type"); - } - } - - /** - * @param {ChatSource} source - * @param {string} message - */ - onChatMessage(source, message) { - this.chatPending.push({ - source: source, - message: message - }); - } - - /** - * @param {PlayerCell} cell - */ - onNewOwnedCell(cell) { /* ignored */ } - - /** - * @param {Rect} range - * @param {boolean} includeServerInfo - */ - onNewWorldBounds(range, includeServerInfo) { - this.worldBorderPending = range; - this.serverInfoPending = includeServerInfo; - } - - onWorldReset() { - this.clearCellsPending = true; - this.worldBorderPending = false; - this.worldStatsPending = false; - this.onVisibleCellUpdate([], [], [], []); - } - - /** - * @param {LeaderboardType} type - * @param {LeaderboardDataType[type][]} data - * @param {LeaderboardDataType[type]=} selfData - */ - onLeaderboardUpdate(type, data, selfData) { - this.leaderboardPending = true; - this.leaderboardType = type; - this.leaderboardData = data; - this.leaderboardSelfData = selfData; - } - - /** - * @param {ViewArea} viewArea - */ - onSpectatePosition(viewArea) { - this.spectateAreaPending = viewArea; - } - - /** - * @abstract - * @param {Cell[]} add - * @param {Cell[]} upd - * @param {Cell[]} eat - * @param {Cell[]} del - */ - onVisibleCellUpdate(add, upd, eat, del) { - let globalFlags = 0, hitSelfData, flags, item, i, l; - - if (this.spectateAreaPending != null) globalFlags |= 1; - if (this.worldBorderPending != null) globalFlags |= 2; - if (this.serverInfoPending) globalFlags |= 4; - if (this.connection.hasPlayer && this.connection.player.hasWorld && this.worldStatsPending) - globalFlags |= 8; - if (this.chatPending.length > 0) globalFlags |= 16; - if (this.leaderboardPending) globalFlags |= 32; - if (this.clearCellsPending) globalFlags |= 64, - this.clearCellsPending = false; - if (add.length > 0) globalFlags |= 128; - if (upd.length > 0) globalFlags |= 256; - if (eat.length > 0) globalFlags |= 512; - if (del.length > 0) globalFlags |= 1024; - - if (globalFlags === 0) return; - - const writer = new Writer(); - writer.writeUInt8(3); - writer.writeUInt16(globalFlags); - - if (this.spectateAreaPending != null) { - writer.writeFloat32(this.spectateAreaPending.x); - writer.writeFloat32(this.spectateAreaPending.y); - writer.writeFloat32(this.spectateAreaPending.s); - this.spectateAreaPending = null; - } - if (this.worldBorderPending != null) { - item = this.worldBorderPending; - writer.writeFloat32(item.x - item.w); - writer.writeFloat32(item.x + item.w); - writer.writeFloat32(item.y - item.h); - writer.writeFloat32(item.y + item.h); - this.worldBorderPending = null; - } - if (this.serverInfoPending) { - writer.writeUInt8(this.handle.gamemode.type); - item = this.handle.version.split("."); - writer.writeUInt8(parseInt(item[0])); - writer.writeUInt8(parseInt(item[1])); - writer.writeUInt8(parseInt(item[2])); - this.serverInfoPending = false; - } - if (this.worldStatsPending) { - item = this.connection.player.world.stats; - writer.writeZTStringUTF8(item.name); - writer.writeZTStringUTF8(item.gamemode); - writer.writeFloat32(item.loadTime / this.handle.tickDelay); - writer.writeUInt32(item.uptime); - writer.writeUInt16(item.limit); - writer.writeUInt16(item.external); - writer.writeUInt16(item.internal); - writer.writeUInt16(item.playing); - writer.writeUInt16(item.spectating); - this.worldStatsPending = false; - } - if ((l = this.chatPending.length) > 0) { - writer.writeUInt16(l); - for (i = 0; i < l; i++) { - item = this.chatPending[i]; - writer.writeZTStringUTF8(item.source.name); - writer.writeColor(item.source.color); - writer.writeUInt8(item.source.isServer ? 1 : 0); - writer.writeZTStringUTF8(item.message); - } - this.chatPending.splice(0, l); - } - if (this.leaderboardPending) { - l = this.leaderboardData.length; - switch (this.leaderboardType) { - case "ffa": - writer.writeUInt8(1); - for (i = 0; i < l; i++) { - item = this.leaderboardData[i]; - flags = 0; - if (item.highlighted) flags |= 1; - if (item === this.leaderboardSelfData) - flags |= 2, hitSelfData = true; - writer.writeUInt16(item.position); - writer.writeUInt8(flags); - writer.writeZTStringUTF8(item.name); - } - if (!hitSelfData && (item = this.leaderboardSelfData) != null) { - writer.writeUInt16(item.position); - flags = item.highlighted ? 1 : 0; - writer.writeUInt8(flags); - writer.writeZTStringUTF8(item.name); - } - writer.writeUInt16(0); - break; - case "pie": - writer.writeUInt8(2); - writer.writeUInt16(l); - for (i = 0; i < l; i++) - writer.writeFloat32(this.leaderboardData[i]); - break; - case "text": - writer.writeUInt8(3); - writer.writeUInt16(l); - for (i = 0; i < l; i++) - writer.writeZTStringUTF8(this.leaderboardData[i]); - break; - } - - this.leaderboardPending = false; - this.leaderboardType = null; - this.leaderboardData = null; - this.leaderboardSelfData = null; - } - - if ((l = add.length) > 0) { - for (i = 0; i < l; i++) { - item = add[i]; - writer.writeUInt32(item.id); - writer.writeUInt8(item.type); - writer.writeFloat32(item.x); - writer.writeFloat32(item.y); - writer.writeUInt16(item.size); - writer.writeColor(item.color); - flags = 0; - if (item.type === 0 && item.owner === this.connection.player) - flags |= 1; - if (!!item.name) flags |= 2; - if (!!item.skin) flags |= 4; - writer.writeUInt8(flags); - if (!!item.name) writer.writeZTStringUTF8(item.name); - if (!!item.skin) writer.writeZTStringUTF8(item.skin); - } - writer.writeUInt32(0); - } - if ((l = upd.length) > 0) { - for (i = 0; i < l; i++) { - item = upd[i]; - flags = 0; - if (item.posChanged) flags |= 1; - if (item.sizeChanged) flags |= 2; - if (item.colorChanged) flags |= 4; - if (item.nameChanged) flags |= 8; - if (item.skinChanged) flags |= 16; - writer.writeUInt32(item.id); - writer.writeUInt8(flags); - if (item.posChanged) { - writer.writeFloat32(item.x); - writer.writeFloat32(item.y); - } - if (item.sizeChanged) - writer.writeUInt16(item.size); - if (item.colorChanged) writer.writeColor(item.color); - if (item.nameChanged) writer.writeZTStringUTF8(item.name); - if (item.skinChanged) writer.writeZTStringUTF8(item.skin); - } - writer.writeUInt32(0); - } - if ((l = eat.length) > 0) { - for (i = 0; i < l; i++) { - item = eat[i]; - writer.writeUInt32(item.id); - writer.writeUInt32(item.eatenBy.id); - } - writer.writeUInt32(0); - } - if ((l = del.length) > 0) { - for (i = 0; i < l; i++) - writer.writeUInt32(del[i].id); - writer.writeUInt32(0); - } - - this.send(writer.finalize()); - } + /** + * @param {Connection} connection + */ + constructor(connection) { + super(connection); + this.protocol = NaN; + + this.leaderboardPending = false; + /** @type {LeaderboardType} */ + this.leaderboardType = null; + /** @type {LeaderboardDataType[LeaderboardType][]} */ + this.leaderboardData = null; + /** @type {LeaderboardDataType[LeaderboardType]} */ + this.leaderboardSelfData = null; + + /** @type {{ source: ChatSource, message: string }[]} */ + this.chatPending = []; + /** @type {Rect} */ + this.worldBorderPending = null; + /** @type {ViewArea} */ + this.spectateAreaPending = null; + this.serverInfoPending = false; + this.worldStatsPending = false; + this.clearCellsPending = false; + } + + static get type() { + return "modern"; + } + + get subtype() { + return `m${!isNaN(this.protocol) ? ("00" + this.protocol).slice(-2) : "//"}`; + } + + /** + * @param {Reader} reader + */ + distinguishes(reader) { + if (reader.length < 5) { + return false; + } + + if (reader.readUInt8() !== 1) { + return false; + } + + this.gotProtocol = true; + this.protocol = reader.readUInt32(); + + if (this.protocol !== 3) { + return void this.fail(1003, "Unsupported protocol version"); + } + + this.connection.createPlayer(); + + return true; + } + + /** + * @param {Reader} reader + */ + onSocketMessage(reader) { + const messageId = reader.readUInt8(); + + switch (messageId) { + case 2: + this.send(PingReturn); + this.worldStatsPending = true; + break; + case 3: + if (reader.length < 12) { + return void this.fail(1003, "Unexpected message format"); + } + + let i, l, count; + this.connection.mouseX = reader.readInt32(); + this.connection.mouseY = reader.readInt32(); + this.connection.splitAttempts += reader.readUInt8(); + count = reader.readUInt8(); + + for (i = 0, l = this.connection.minions.length; count > 0 && i < l; i++) { + this.connection.minions[i].splitAttempts += count; + } + + const globalFlags = reader.readUInt8(); + + if (globalFlags & 1) { + if (reader.length < 13) { + return void this.fail(1003, "Unexpected message format"); + } + + this.connection.spawningName = reader.readZTStringUTF8(); + } + + if (globalFlags & 2) { + this.connection.requestingSpectate = true; + } + + if (globalFlags & 4) { + this.connection.isPressingQ = true; + } + + if (globalFlags & 8) { + this.connection.isPressingQ = this.connection.hasProcessedQ = false; + } + + if (globalFlags & 16) { + this.connection.ejectAttempts++; + } + + if (globalFlags & 32) { + for (i = 0, l = this.connection.minions.length; i < l; i++) { + this.connection.minions[i].ejectAttempts++; + } + } + + if (globalFlags & 64) { + this.connection.minionsFrozen = !this.connection.minionsFrozen; + } + + if (globalFlags & 128) { + if (reader.length < 13 + (globalFlags & 1)) { + return void this.fail(1003, "Unexpected message format"); + } + + count = reader.readUInt8(); + + if (reader.length < 13 + (globalFlags & 1) + count) { + return void this.fail(1003, "Unexpected message format"); + } + + for (let i = 0; i < count; i++) { + this.connection.onChatMessage(reader.readZTStringUTF8()); + } + } + break; + default: + return void this.fail(1003, "Unknown message type"); + } + } + + /** + * @param {ChatSource} source + * @param {string} message + */ + onChatMessage(source, message) { + this.chatPending.push({ + source: source, + message: message + }); + } + + /** + * @param {PlayerCell} cell + */ + onNewOwnedCell(cell) {/* ignored */} + + /** + * @param {Rect} range + * @param {boolean} includeServerInfo + */ + onNewWorldBounds(range, includeServerInfo) { + this.worldBorderPending = range; + this.serverInfoPending = includeServerInfo; + } + + onWorldReset() { + this.clearCellsPending = true; + this.worldBorderPending = false; + this.worldStatsPending = false; + this.onVisibleCellUpdate([], [], [], []); + } + + /** + * @param {LeaderboardType} type + * @param {LeaderboardDataType[type][]} data + * @param {LeaderboardDataType[type]=} selfData + */ + onLeaderboardUpdate(type, data, selfData) { + this.leaderboardPending = true; + this.leaderboardType = type; + this.leaderboardData = data; + this.leaderboardSelfData = selfData; + } + + /** + * @param {ViewArea} viewArea + */ + onSpectatePosition(viewArea) { + this.spectateAreaPending = viewArea; + } + + /** + * @abstract + * @param {Cell[]} add + * @param {Cell[]} upd + * @param {Cell[]} eat + * @param {Cell[]} del + */ + onVisibleCellUpdate(add, upd, eat, del) { + let globalFlags = 0, hitSelfData, flags, item, i, l; + + if (this.spectateAreaPending != null) { + globalFlags |= 1; + } + + if (this.worldBorderPending != null) { + globalFlags |= 2; + } + + if (this.serverInfoPending) { + globalFlags |= 4; + } + + if (this.connection.hasPlayer && this.connection.player.hasWorld && this.worldStatsPending) { + globalFlags |= 8; + } + + if (this.chatPending.length > 0) { + globalFlags |= 16; + } + + if (this.leaderboardPending) { + globalFlags |= 32; + } + + if (this.clearCellsPending) { + globalFlags |= 64, this.clearCellsPending = false; + } + + if (add.length > 0) { + globalFlags |= 128; + } + + if (upd.length > 0) { + globalFlags |= 256; + } + + if (eat.length > 0) { + globalFlags |= 512; + } + + if (del.length > 0) { + globalFlags |= 1024; + } + + if (globalFlags === 0) { + return; + } + + const writer = new Writer(); + writer.writeUInt8(3); + writer.writeUInt16(globalFlags); + + if (this.spectateAreaPending != null) { + writer.writeFloat32(this.spectateAreaPending.x); + writer.writeFloat32(this.spectateAreaPending.y); + writer.writeFloat32(this.spectateAreaPending.s); + this.spectateAreaPending = null; + } + + if (this.worldBorderPending != null) { + item = this.worldBorderPending; + writer.writeFloat32(item.x - item.w); + writer.writeFloat32(item.x + item.w); + writer.writeFloat32(item.y - item.h); + writer.writeFloat32(item.y + item.h); + this.worldBorderPending = null; + } + + if (this.serverInfoPending) { + writer.writeUInt8(this.handle.gamemode.type); + item = this.handle.version.split("."); + writer.writeUInt8(parseInt(item[0])); + writer.writeUInt8(parseInt(item[1])); + writer.writeUInt8(parseInt(item[2])); + this.serverInfoPending = false; + } + + if (this.worldStatsPending) { + item = this.connection.player.world.stats; + writer.writeZTStringUTF8(item.name); + writer.writeZTStringUTF8(item.gamemode); + writer.writeFloat32(item.loadTime / this.handle.tickDelay); + writer.writeUInt32(item.uptime); + writer.writeUInt16(item.limit); + writer.writeUInt16(item.external); + writer.writeUInt16(item.internal); + writer.writeUInt16(item.playing); + writer.writeUInt16(item.spectating); + this.worldStatsPending = false; + } + + if ((l = this.chatPending.length) > 0) { + writer.writeUInt16(l); + + for (i = 0; i < l; i++) { + item = this.chatPending[i]; + writer.writeZTStringUTF8(item.source.name); + writer.writeColor(item.source.color); + writer.writeUInt8(item.source.isServer ? 1 : 0); + writer.writeZTStringUTF8(item.message); + } + + this.chatPending.splice(0, l); + } + + if (this.leaderboardPending) { + l = this.leaderboardData.length; + + switch (this.leaderboardType) { + case "ffa": + writer.writeUInt8(1); + + for (i = 0; i < l; i++) { + item = this.leaderboardData[i]; + flags = 0; + + if (item.highlighted) { + flags |= 1; + } + + if (item === this.leaderboardSelfData) { + flags |= 2, hitSelfData = true; + } + + writer.writeUInt16(item.position); + writer.writeUInt8(flags); + writer.writeZTStringUTF8(item.name); + } + + if (!hitSelfData && (item = this.leaderboardSelfData) != null) { + writer.writeUInt16(item.position); + flags = item.highlighted ? 1 : 0; + writer.writeUInt8(flags); + writer.writeZTStringUTF8(item.name); + } + + writer.writeUInt16(0); + break; + case "pie": + writer.writeUInt8(2); + writer.writeUInt16(l); + + for (i = 0; i < l; i++) { + writer.writeFloat32(this.leaderboardData[i]); + } + break; + case "text": + writer.writeUInt8(3); + writer.writeUInt16(l); + + for (i = 0; i < l; i++) { + writer.writeZTStringUTF8(this.leaderboardData[i]); + } + break; + } + + this.leaderboardPending = false; + this.leaderboardType = null; + this.leaderboardData = null; + this.leaderboardSelfData = null; + } + + if ((l = add.length) > 0) { + for (i = 0; i < l; i++) { + item = add[i]; + writer.writeUInt32(item.id); + writer.writeUInt8(item.type); + writer.writeFloat32(item.x); + writer.writeFloat32(item.y); + writer.writeUInt16(item.size); + writer.writeColor(item.color); + flags = 0; + + if (item.type === 0 && item.owner === this.connection.player) { + flags |= 1; + } + + if (!!item.name) { + flags |= 2; + } + + if (!!item.skin) { + flags |= 4; + } + + writer.writeUInt8(flags); + + if (!!item.name) { + writer.writeZTStringUTF8(item.name); + } + + if (!!item.skin) { + writer.writeZTStringUTF8(item.skin); + } + } + writer.writeUInt32(0); + } + + if ((l = upd.length) > 0) { + for (i = 0; i < l; i++) { + item = upd[i]; + flags = 0; + + if (item.posChanged) { + flags |= 1; + } + + if (item.sizeChanged) { + flags |= 2; + } + + if (item.colorChanged) { + flags |= 4; + } + + if (item.nameChanged) { + flags |= 8; + } + + if (item.skinChanged) { + flags |= 16; + } + + writer.writeUInt32(item.id); + writer.writeUInt8(flags); + + if (item.posChanged) { + writer.writeFloat32(item.x); + writer.writeFloat32(item.y); + } + + if (item.sizeChanged) { + writer.writeUInt16(item.size); + } + + if (item.colorChanged) { + writer.writeColor(item.color); + } + + if (item.nameChanged) { + writer.writeZTStringUTF8(item.name); + } + + if (item.skinChanged) { + writer.writeZTStringUTF8(item.skin); + } + } + writer.writeUInt32(0); + } + + if ((l = eat.length) > 0) { + for (i = 0; i < l; i++) { + item = eat[i]; + writer.writeUInt32(item.id); + writer.writeUInt32(item.eatenBy.id); + } + + writer.writeUInt32(0); + } + + if ((l = del.length) > 0) { + for (i = 0; i < l; i++) { + writer.writeUInt32(del[i].id); + } + + writer.writeUInt32(0); + } + + this.send(writer.finalize()); + } } module.exports = ModernProtocol; const Cell = require("../cells/Cell"); const PlayerCell = require("../cells/PlayerCell"); -const Connection = require("../sockets/Connection"); +const Connection = require("../sockets/Connection"); \ No newline at end of file diff --git a/servers/agarv2/src/protocols/Protocol.js b/servers/agarv2/src/protocols/Protocol.js index c8380133..6e32eb29 100644 --- a/servers/agarv2/src/protocols/Protocol.js +++ b/servers/agarv2/src/protocols/Protocol.js @@ -2,95 +2,141 @@ * @abstract */ class Protocol { - /** - * @param {Connection} connection - */ - constructor(connection) { - this.connection = connection; - } - - /** - * @abstract - * @returns {string} - */ - static get type() { throw new Error("Must be implemented"); } - get type() { return this.constructor.type; } - /** - * @abstract - * @returns {string} - */ - get subtype() { throw new Error("Must be implemented"); } - - get listener() { return this.connection.listener; } - get handle() { return this.connection.listener.handle; } - get logger() { return this.connection.listener.handle.logger; } - get settings() { return this.connection.listener.handle.settings; } - - /** - * @abstract - * @param {Reader} reader - * @returns {boolean} - */ - distinguishes(reader) { throw new Error("Must be implemented"); } - - /** - * @abstract - * @param {Reader} reader - */ - onSocketMessage(reader) { throw new Error("Must be implemented"); } - - /** - * @abstract - * @param {ChatSource} source - * @param {string} message - */ - onChatMessage(source, message) { throw new Error("Must be implemented"); } - /** - * @abstract - * @param {PlayerCell} cell - */ - onNewOwnedCell(cell) { throw new Error("Must be implemented"); } - /** - * @param {World} world - * @param {boolean} includeServerInfo - */ - onNewWorldBounds(world, includeServerInfo) { throw new Error("Must be implemented"); } - /** - * @abstract - */ - onWorldReset() { throw new Error("Must be implemented"); } - /** - * @abstract - * @param {LeaderboardType} type - * @param {LeaderboardDataType[type][]} data - * @param {LeaderboardDataType[type]=} selfData - */ - onLeaderboardUpdate(type, data, selfData) { throw new Error("Must be implemented"); } - /** - * @abstract - * @param {ViewArea} viewArea - */ - onSpectatePosition(viewArea) { throw new Error("Must be implemented"); } - /** - * @abstract - * @param {Cell[]} add - * @param {Cell[]} upd - * @param {Cell[]} eat - * @param {Cell[]} del - */ - onVisibleCellUpdate(add, upd, eat, del) { throw new Error("Must be implemented"); } - - /** - * @param {Buffer} data - */ - send(data) { this.connection.send(data); } - /** - * @param {number=} code - * @param {string=} reason - */ - fail(code, reason) { - this.connection.closeSocket(code || 1003, reason || "Unspecified protocol fail"); - } + /** + * @param {Connection} connection + */ + constructor(connection) { + this.connection = connection; + } + + /** + * @abstract + * @returns {string} + */ + static get type() { + throw new Error("Must be implemented"); + } + + get type() { + return this.constructor.type; + } + + /** + * @abstract + * @returns {string} + */ + get subtype() { + throw new Error("Must be implemented"); + } + + get listener() { + return this.connection.listener; + } + + get handle() { + return this.connection.listener.handle; + } + + get logger() { + return this.connection.listener.handle.logger; + } + + get settings() { + return this.connection.listener.handle.settings; + } + + /** + * @abstract + * @param {Reader} reader + * @returns {boolean} + */ + distinguishes(reader) { + throw new Error("Must be implemented"); + } + + /** + * @abstract + * @param {Reader} reader + */ + onSocketMessage(reader) { + throw new Error("Must be implemented"); + } + + /** + * @abstract + * @param {ChatSource} source + * @param {string} message + */ + onChatMessage(source, message) { + throw new Error("Must be implemented"); + } + + /** + * @abstract + * @param {PlayerCell} cell + */ + onNewOwnedCell(cell) { + throw new Error("Must be implemented"); + } + + /** + * @param {World} world + * @param {boolean} includeServerInfo + */ + onNewWorldBounds(world, includeServerInfo) { + throw new Error("Must be implemented"); + } + + /** + * @abstract + */ + onWorldReset() { + throw new Error("Must be implemented"); + } + + /** + * @abstract + * @param {LeaderboardType} type + * @param {LeaderboardDataType[type][]} data + * @param {LeaderboardDataType[type]=} selfData + */ + onLeaderboardUpdate(type, data, selfData) { + throw new Error("Must be implemented"); + } + + /** + * @abstract + * @param {ViewArea} viewArea + */ + onSpectatePosition(viewArea) { + throw new Error("Must be implemented"); + } + + /** + * @abstract + * @param {Cell[]} add + * @param {Cell[]} upd + * @param {Cell[]} eat + * @param {Cell[]} del + */ + onVisibleCellUpdate(add, upd, eat, del) { + throw new Error("Must be implemented"); + } + + /** + * @param {Buffer} data + */ + send(data) { + this.connection.send(data); + } + + /** + * @param {number=} code + * @param {string=} reason + */ + fail(code, reason) { + this.connection.closeSocket(code || 1003, reason || "Unspecified protocol fail"); + } } module.exports = Protocol; @@ -98,4 +144,4 @@ module.exports = Protocol; const Reader = require("../primitives/Reader"); const Cell = require("../cells/Cell"); const PlayerCell = require("../cells/PlayerCell"); -const Connection = require("../sockets/Connection"); +const Connection = require("../sockets/Connection"); \ No newline at end of file diff --git a/servers/agarv2/src/protocols/ProtocolStore.js b/servers/agarv2/src/protocols/ProtocolStore.js index 8510f150..c13afa31 100644 --- a/servers/agarv2/src/protocols/ProtocolStore.js +++ b/servers/agarv2/src/protocols/ProtocolStore.js @@ -1,36 +1,38 @@ - class ProtocolStore { - constructor() { - /** @type {typeof Protocol[]} */ - this.store = []; - } + constructor() { + /** @type {typeof Protocol[]} */ + this.store = []; + } + + /** + * @param {typeof Protocol[]} protocols + */ + register(...protocols) { + this.store.splice(this.store.length, 0, ...protocols); + } + + /** + * @param {Connection} connection + * @param {Reader} reader + */ + decide(connection, reader) { + for (let i = 0; i < this.store.length; i++) { + const generated = new this.store[i](connection); + + if (!generated.distinguishes(reader)) { + reader.offset = 0; + continue; + } - /** - * @param {typeof Protocol[]} protocols - */ - register(...protocols) { - this.store.splice(this.store.length, 0, ...protocols); - } + return connection.socketDisconnected ? null : generated; + } - /** - * @param {Connection} connection - * @param {Reader} reader - */ - decide(connection, reader) { - for (let i = 0; i < this.store.length; i++) { - const generated = new this.store[i](connection); - if (!generated.distinguishes(reader)) { - reader.offset = 0; - continue; - } - return connection.socketDisconnected ? null : generated; - } - return null; - } + return null; + } } module.exports = ProtocolStore; const Protocol = require("./Protocol"); const Connection = require("../sockets/Connection"); -const Reader = require("../primitives/Reader"); +const Reader = require("../primitives/Reader"); \ No newline at end of file diff --git a/servers/agarv2/src/sockets/ChatChannel.js b/servers/agarv2/src/sockets/ChatChannel.js index dbbde6a1..d99aa9c9 100644 --- a/servers/agarv2/src/sockets/ChatChannel.js +++ b/servers/agarv2/src/sockets/ChatChannel.js @@ -38,6 +38,7 @@ class ChatChannel { }); } else { this.remove(connection); + this.connections.push({ remoteAddress: connection.remoteAddress, socket: connection @@ -65,6 +66,7 @@ class ChatChannel { for (let i = 0, l = this.settings.chatFilteredPhrases.length; i < l; i++) if (message.indexOf(this.settings.chatFilteredPhrases[i]) !== -1) return true; + return false; } /** @@ -95,6 +97,7 @@ class ChatChannel { if (this.shouldFilter(message)) { return this.directMessage(null, source, "Your message contains banned words."); } + const sourceInfo = source == null ? serverSource : getSourceFromConnection(source); recipient.protocol.onChatMessage(sourceInfo, message); } diff --git a/servers/agarv2/src/sockets/Connection.js b/servers/agarv2/src/sockets/Connection.js index acff4aea..f1cb3f72 100644 --- a/servers/agarv2/src/sockets/Connection.js +++ b/servers/agarv2/src/sockets/Connection.js @@ -9,7 +9,9 @@ class Connection extends Router { */ constructor(listener, webSocket, req) { super(listener); + const ip = typeof req.headers['x-real-ip'] !== 'undefined' ? req.headers['x-real-ip'] : req.socket.remoteAddress; + this.remoteAddress = filterIPAddress(ip); this.verifyScore = -1; this.webSocket = webSocket; @@ -40,7 +42,9 @@ class Connection extends Router { close() { if (!this.socketDisconnected) return void this.closeSocket(1001, "Manual connection close call"); + super.close(); + this.disconnected = true; this.disconnectionTick = this.handle.tick; this.listener.onDisconnection(this, this.closeCode, this.closeReason); @@ -57,6 +61,7 @@ class Connection extends Router { */ onSocketClose(code, reason) { if (this.socketDisconnected) return; + this.logger.debug(`connection from ${this.remoteAddress} has disconnected`); this.socketDisconnected = true; this.closeCode = code; @@ -68,11 +73,15 @@ class Connection extends Router { */ onSocketMessage(data) { if (data instanceof String) return void this.closeSocket(1003, "Unexpected message format"); + if (data.byteLength > 512 || data.byteLength === 0) return void this.closeSocket(1009, "Unexpected message size"); this.lastActivityTime = Date.now(); + const reader = new Reader(Buffer.from(data), 0); + if (this.protocol !== null) this.protocol.onSocketMessage(reader); + else { this.protocol = this.handle.protocols.decide(this, reader); if (this.protocol === null) return void this.closeSocket(1003, "Ambiguous protocol"); @@ -81,13 +90,14 @@ class Connection extends Router { createPlayer() { super.createPlayer(); + if (this.settings.chatEnabled) this.listener.globalChat.add(this); + if (this.settings.matchmakerNeedsQueuing) { this.listener.globalChat.directMessage(null, this, "This server requires players to be queued."); this.listener.globalChat.directMessage(null, this, "Try spawning to enqueue."); - } else - this.handle.matchmaker.toggleQueued(this); + } else this.handle.matchmaker.toggleQueued(this); } /** @@ -107,16 +117,19 @@ class Connection extends Router { if (!this.handle.chatCommands.execute(this, message.slice(1))) { this.listener.globalChat.directMessage(null, this, "unknown command, execute /help for the list of commands"); } + return; } if (message.length <= 1) { this.listener.globalChat.directMessage(null, this, "[AntiSpam] Your message is too short, write longer messages."); + return; } if (message.length >= 10 && !~message.indexOf(' ')) { this.listener.globalChat.directMessage(null, this, "[AntiSpam] Your message doesn't has any spaces, add some spaces."); + return; } @@ -124,6 +137,7 @@ class Connection extends Router { if (lastMessage) { if (lastMessage === message || (~lastMessage.indexOf(message) || ~message.indexOf(lastMessage)) && message.length >= 10) { this.listener.globalChat.directMessage(null, this, "[AntiSpam] Please don't repeat yourself, write something different."); + return; } } @@ -131,6 +145,7 @@ class Connection extends Router { /*if (lastlastMessage) { if (lastlastMessage === message || (~lastlastMessage.indexOf(message) || ~message.indexOf(lastlastMessage)) && message.length >= 10) { this.listener.globalChat.directMessage(null, this, "[AntiSpam] Please don't repeat yourself, write something different."); + return; } } @@ -138,6 +153,7 @@ class Connection extends Router { if (lastlastlastMessage) { if (lastlastlastMessage === message || (~lastlastlastMessage.indexOf(message) || ~message.indexOf(lastlastlastMessage)) && message.length >= 10) { this.listener.globalChat.directMessage(null, this, "[AntiSpam] Please don't repeat yourself, write something different."); + return; } }*/ @@ -154,6 +170,7 @@ class Connection extends Router { } onQPress() { if (!this.hasPlayer) return; + if (this.listener.settings.minionEnableQBasedControl && this.minions.length > 0) this.controllingMinions = !this.controllingMinions; else this.handle.gamemode.whenPlayerPressQ(this.player); @@ -163,39 +180,50 @@ class Connection extends Router { } update() { if (!this.hasPlayer) return; + if (!this.player.hasWorld) { if (this.spawningName !== null) this.handle.matchmaker.toggleQueued(this); + this.spawningName = null; this.splitAttempts = 0; this.ejectAttempts = 0; this.requestingSpectate = false; this.isPressingQ = false; this.hasProcessedQ = false; + return; } + this.player.updateVisibleCells(); const add = [], upd = [], eat = [], del = []; const player = this.player; - const visible = player.visibleCells, - lastVisible = player.lastVisibleCells; + const visible = player.visibleCells, lastVisible = player.lastVisibleCells; + for (let id in visible) { const cell = visible[id]; + if (!lastVisible.hasOwnProperty(id)) add.push(cell); else if (cell.shouldUpdate) upd.push(cell); } + for (let id in lastVisible) { const cell = lastVisible[id]; + if (visible.hasOwnProperty(id)) continue; + if (cell.eatenBy !== null) eat.push(cell); + del.push(cell); } if (player.state === 1 || player.state === 2) this.protocol.onSpectatePosition(player.viewArea); + if (this.handle.tick % 4 === 0) this.handle.gamemode.sendLeaderboard(this); + this.protocol.onVisibleCellUpdate(add, upd, eat, del); } onWorldSet() { @@ -208,10 +236,10 @@ class Connection extends Router { onWorldReset() { this.protocol.onWorldReset(); } - /** @param {Buffer} data */ send(data) { if (this.socketDisconnected) return; + this.webSocket.send(data); } /** @@ -220,6 +248,7 @@ class Connection extends Router { */ closeSocket(code, reason) { if (this.socketDisconnected) return; + this.socketDisconnected = true; this.closeCode = code; this.closeReason = reason; diff --git a/servers/agarv2/src/sockets/Listener.js b/servers/agarv2/src/sockets/Listener.js index c51633af..9340134d 100644 --- a/servers/agarv2/src/sockets/Listener.js +++ b/servers/agarv2/src/sockets/Listener.js @@ -15,7 +15,6 @@ class Listener { this.listenerSocket = null; this.handle = handle; this.globalChat = new ChatChannel(this); - /** @type {Router[]} */ this.routers = []; /** @type {Connection[]} */ @@ -23,28 +22,31 @@ class Listener { /** @type {Counter} */ this.connectionsByIP = { }; } - get settings() { return this.handle.settings; } get logger() { return this.handle.logger; } - open() { if (this.listenerSocket !== null) return false; + this.logger.debug(`listener opening at ${this.settings.listeningPort}`); + this.listenerSocket = new WebSocketServer({ port: this.settings.listeningPort, verifyClient: this.verifyClient.bind(this) }, this.onOpen.bind(this)); + this.listenerSocket.on("connection", this.onConnection.bind(this)); + return true; } close() { if (this.listenerSocket === null) return false; + this.logger.debug("listener closing"); this.listenerSocket.close(); this.listenerSocket = null; + return true; } - /** * @param {{req: any, origin: string}} info * @param {*} response @@ -53,23 +55,33 @@ class Listener { const ip = typeof info.req.headers['x-real-ip'] !== 'undefined' ? info.req.headers['x-real-ip'] : info.req.socket.remoteAddress; const address = filterIPAddress(ip); this.logger.onAccess(`REQUEST FROM ${address}, ${info.secure ? "" : "not "}secure, Origin: ${info.origin}`); + if (this.connections.length > this.settings.listenerMaxConnections) { this.logger.inform("listenerMaxConnections reached, dropping new connections"); + return void response(false, 503, "Service Unavailable"); } + const acceptedOrigins = this.settings.listenerAcceptedOrigins; + if (acceptedOrigins.length > 0 && acceptedOrigins.indexOf(info.origin) === -1) { this.logger.inform(`listenerAcceptedOrigins doesn't contain ${info.origin}`); + return void response(false, 403, "Forbidden"); } + if (this.settings.listenerForbiddenIPs.indexOf(address) !== -1) { this.logger.inform(`listenerForbiddenIPs contains ${address}, dropping connection`); + return void response(false, 403, "Forbidden"); } + if (this.settings.listenerMaxConnectionsPerIP > 0) { const count = this.connectionsByIP[address]; + if (count && count >= this.settings.listenerMaxConnectionsPerIP) { this.logger.inform(`listenerMaxConnectionsPerIP reached for '${address}', dropping its new connections`); + return void response(false, 403, "Forbidden"); } } @@ -80,7 +92,6 @@ class Listener { onOpen() { this.logger.inform(`listener open at ${this.settings.listeningPort}`); } - /** * @param {Router} router */ @@ -93,7 +104,6 @@ class Listener { removeRouter(router) { this.routers.splice(this.routers.indexOf(router), 1); } - /** * @param {WebSocket} webSocket */ @@ -132,22 +142,29 @@ class Listener { */ onDisconnection(connection, code, reason) { this.logger.onAccess(`DISCONNECTION FROM ${connection.remoteAddress} (${code} '${reason}')`); + if (--this.connectionsByIP[connection.remoteAddress] <= 0) delete this.connectionsByIP[connection.remoteAddress]; + this.globalChat.remove(connection); this.connections.splice(this.connections.indexOf(connection), 1); } - update() { let i, l; + for (i = 0, l = this.routers.length; i < l; i++) { const router = this.routers[i]; + if (!router.shouldClose) continue; + router.close(); i--; l--; } + for (i = 0; i < l; i++) this.routers[i].update(); + for (i = 0, l = this.connections.length; i < l; i++) { const connection = this.connections[i]; + if (this.settings.listenerForbiddenIPs.indexOf(connection.remoteAddress) !== -1) connection.closeSocket(1003, "Remote address is forbidden"); else if (Date.now() - connection.lastActivityTime >= this.settings.listenerMaxClientDormancy) diff --git a/servers/agarv2/src/sockets/Router.js b/servers/agarv2/src/sockets/Router.js index 1745ae71..33e8c872 100644 --- a/servers/agarv2/src/sockets/Router.js +++ b/servers/agarv2/src/sockets/Router.js @@ -1,123 +1,178 @@ /** @interface */ class Router { - /** - * @param {Listener} listener - */ - constructor(listener) { - this.listener = listener; - this.disconnected = false; - this.disconnectionTick = NaN; - - this.mouseX = 0; - this.mouseY = 0; - - /** @type {string=} */ - this.spawningName = null; - this.requestingSpectate = false; - this.isPressingQ = false; - this.hasProcessedQ = false; - this.splitAttempts = 0; - this.ejectAttempts = 0; - this.ejectTick = listener.handle.tick; - - this.hasPlayer = false; - /** @type {Player} */ - this.player = null; - - this.listener.addRouter(this); - } - - /** @abstract @returns {string} */ - static get type() { throw new Error("Must be overriden"); } - /** @returns {string} */ - get type() { return this.constructor.type; } - - /** @abstract @returns {boolean} */ - static get isExternal() { throw new Error("Must be overriden"); } - /** @returns {boolean} */ - get isExternal() { return this.constructor.isExternal; } - - /** @abstract @returns {boolean} */ - static get separateInTeams() { throw new Error("Must be overriden"); } - /** @returns {boolean} */ - get separateInTeams() { return this.constructor.separateInTeams; } - - get handle() { return this.listener.handle; } - get logger() { return this.listener.handle.logger; } - get settings() { return this.listener.handle.settings; } - - createPlayer() { - if (this.hasPlayer) return; - this.hasPlayer = true; - this.player = this.listener.handle.createPlayer(this); - } - destroyPlayer() { - if (!this.hasPlayer) return; - this.hasPlayer = false; - this.listener.handle.removePlayer(this.player.id); - this.player = null; - } - - /** @virtual */ - onWorldSet() { } - /** @virtual */ - onWorldReset() { } - /** @param {PlayerCell} cell @virtual */ - onNewOwnedCell(cell) { } - - /** @virtual */ - onSpawnRequest() { - if (!this.hasPlayer) return; - let name = this.spawningName.slice(0, this.settings.playerMaxNameLength); - /** @type {string} */ - let skin; - if (this.settings.playerAllowSkinInName) { - const regex = /\<(.*)\>(.*)/.exec(name); - if (regex !== null) { - name = regex[2]; - skin = regex[1]; - } - } - this.listener.handle.gamemode.onPlayerSpawnRequest(this.player, name, skin); - } - /** @virtual */ - onSpectateRequest() { - if (!this.hasPlayer) return; - this.player.updateState(1); - } - /** @virtual */ - onQPress() { - if (!this.hasPlayer) return; - this.listener.handle.gamemode.whenPlayerPressQ(this.player); - } - /** @virtual */ - attemptSplit() { - if (!this.hasPlayer) return; - this.listener.handle.gamemode.whenPlayerSplit(this.player); - } - /** @virtual */ - attemptEject() { - if (!this.hasPlayer) return; - this.listener.handle.gamemode.whenPlayerEject(this.player); - } - - /** @virtual */ - close() { - this.listener.removeRouter(this); - } - - /** @abstract @returns {boolean} */ - get shouldClose() { - throw new Error("Must be overriden"); - } - /** @abstract */ - update() { - throw new Error("Must be overriden"); - } + /** + * @param {Listener} listener + */ + constructor(listener) { + this.listener = listener; + this.disconnected = false; + this.disconnectionTick = NaN; + + this.mouseX = 0; + this.mouseY = 0; + + /** @type {string=} */ + this.spawningName = null; + this.requestingSpectate = false; + this.isPressingQ = false; + this.hasProcessedQ = false; + this.splitAttempts = 0; + this.ejectAttempts = 0; + this.ejectTick = listener.handle.tick; + + this.hasPlayer = false; + /** @type {Player} */ + this.player = null; + + this.listener.addRouter(this); + } + + /** @abstract @returns {string} */ + static get type() { + throw new Error("Must be overriden"); + } + + /** @returns {string} */ + get type() { + return this.constructor.type; + } + + /** @abstract @returns {boolean} */ + static get isExternal() { + throw new Error("Must be overriden"); + } + + /** @returns {boolean} */ + get isExternal() { + return this.constructor.isExternal; + } + + /** @abstract @returns {boolean} */ + static get separateInTeams() { + throw new Error("Must be overriden"); + } + + /** @returns {boolean} */ + get separateInTeams() { + return this.constructor.separateInTeams; + } + + get handle() { + return this.listener.handle; + } + + get logger() { + return this.listener.handle.logger; + } + + get settings() { + return this.listener.handle.settings; + } + + createPlayer() { + if (this.hasPlayer) { + return; + } + + this.hasPlayer = true; + this.player = this.listener.handle.createPlayer(this); + } + + destroyPlayer() { + if (!this.hasPlayer) { + return; + } + + this.hasPlayer = false; + this.listener.handle.removePlayer(this.player.id); + this.player = null; + } + + /** @virtual */ + onWorldSet() {} + + /** @virtual */ + onWorldReset() {} + + /** @param {PlayerCell} cell @virtual */ + onNewOwnedCell(cell) {} + + /** @virtual */ + onSpawnRequest() { + if (!this.hasPlayer) { + return; + } + + let name = this.spawningName.slice(0, this.settings.playerMaxNameLength); + /** @type {string} */ + let skin; + + if (this.settings.playerAllowSkinInName) { + const regex = /\<(.*)\>(.*)/.exec(name); + + if (regex !== null) { + name = regex[2]; + skin = regex[1]; + } + } + + this.listener.handle.gamemode.onPlayerSpawnRequest(this.player, name, skin); + } + + /** @virtual */ + onSpectateRequest() { + if (!this.hasPlayer) { + return; + } + + this.player.updateState(1); + } + + /** @virtual */ + onQPress() { + if (!this.hasPlayer) { + return; + } + + this.listener.handle.gamemode.whenPlayerPressQ(this.player); + } + + /** @virtual */ + attemptSplit() { + if (!this.hasPlayer) { + return; + } + + this.listener.handle.gamemode.whenPlayerSplit(this.player); + } + + /** @virtual */ + attemptEject() { + if (!this.hasPlayer) { + return; + } + + this.listener.handle.gamemode.whenPlayerEject(this.player); + } + + /** @virtual */ + close() { + this.listener.removeRouter(this); + } + + /** @abstract @returns {boolean} */ + get shouldClose() { + throw new Error("Must be overriden"); + } + + /** @abstract */ + update() { + throw new Error("Must be overriden"); + } } module.exports = Router; const Listener = require("./Listener"); const Player = require("../worlds/Player"); -const PlayerCell = require("../cells/PlayerCell"); +const PlayerCell = require("../cells/PlayerCell"); \ No newline at end of file diff --git a/servers/agarv2/src/worlds/Matchmaker.js b/servers/agarv2/src/worlds/Matchmaker.js index d855b32a..332121e3 100644 --- a/servers/agarv2/src/worlds/Matchmaker.js +++ b/servers/agarv2/src/worlds/Matchmaker.js @@ -1,85 +1,121 @@ class Matchmaker { - /** - * @param {ServerHandle} handle - */ - constructor(handle) { - this.handle = handle; - /** @type {Connection[]} */ - this.queued = []; - } - - /** - * @param {Connection} connection - */ - isInQueue(connection) { - return this.queued.indexOf(connection) !== -1; - } - broadcastQueueLength() { - if (!this.handle.settings.matchmakerNeedsQueuing) return; - const message = `${this.queued.length}/${this.handle.settings.matchmakerBulkSize} are in queue`; - for (let i = 0, l = this.queued.length; i < l; i++) - this.handle.listener.globalChat.directMessage(null, this.queued[i], message); - } - /** - * @param {Connection} connection - */ - toggleQueued(connection) { - this.isInQueue(connection) ? this.dequeue(connection) : this.enqueue(connection); - } - /** - * @param {Connection} connection - */ - enqueue(connection) { - if (this.handle.settings.matchmakerNeedsQueuing) - this.handle.listener.globalChat.directMessage(null, connection, "joined the queue"); - this.queued.push(connection); - this.broadcastQueueLength(); - } - /** - * @param {Connection} connection - */ - dequeue(connection) { - if (this.handle.settings.matchmakerNeedsQueuing) - this.handle.listener.globalChat.directMessage(null, connection, "left the queue"); - this.queued.splice(this.queued.indexOf(connection), 1); - this.broadcastQueueLength(); - } - - update() { - const bulkSize = this.handle.settings.matchmakerBulkSize; - while (true) { - if (this.queued.length < bulkSize) return; - const world = this.getSuitableWorld(); - if (world === null) return; - for (let i = 0; i < bulkSize; i++) { - const next = this.queued.shift(); - if (this.handle.settings.matchmakerNeedsQueuing) - this.handle.listener.globalChat.directMessage(null, next, "match found!"); - world.addPlayer(next.player); - } - } - } - - getSuitableWorld() { - /** @type {World} */ - let bestWorld = null; - for (let id in this.handle.worlds) { - const world = this.handle.worlds[id]; - if (!this.handle.gamemode.canJoinWorld(world)) continue; - if (world.stats.external >= this.handle.settings.worldMaxPlayers) - continue; - if (bestWorld === null || world.stats.external < bestWorld.stats.external) - bestWorld = world; - } - if (bestWorld !== null) return bestWorld; - else if (Object.keys(this.handle.worlds).length < this.handle.settings.worldMaxCount) - return this.handle.createWorld(); - else return null; - } + /** + * @param {ServerHandle} handle + */ + constructor(handle) { + this.handle = handle; + /** @type {Connection[]} */ + this.queued = []; + } + + /** + * @param {Connection} connection + */ + isInQueue(connection) { + return this.queued.indexOf(connection) !== -1; + } + + broadcastQueueLength() { + if (!this.handle.settings.matchmakerNeedsQueuing) { + return; + } + + const message = `${this.queued.length}/${this.handle.settings.matchmakerBulkSize} are in queue`; + + for (let i = 0, l = this.queued.length; i < l; i++) { + this.handle.listener.globalChat.directMessage(null, this.queued[i], message); + } + } + + /** + * @param {Connection} connection + */ + toggleQueued(connection) { + this.isInQueue(connection) ? this.dequeue(connection) : this.enqueue(connection); + } + + /** + * @param {Connection} connection + */ + enqueue(connection) { + if (this.handle.settings.matchmakerNeedsQueuing) { + this.handle.listener.globalChat.directMessage(null, connection, "joined the queue"); + } + + this.queued.push(connection); + this.broadcastQueueLength(); + } + + /** + * @param {Connection} connection + */ + dequeue(connection) { + if (this.handle.settings.matchmakerNeedsQueuing) { + this.handle.listener.globalChat.directMessage(null, connection, "left the queue"); + } + + this.queued.splice(this.queued.indexOf(connection), 1); + this.broadcastQueueLength(); + } + + update() { + const bulkSize = this.handle.settings.matchmakerBulkSize; + + while (true) { + if (this.queued.length < bulkSize) { + return; + } + + const world = this.getSuitableWorld(); + + if (world === null) { + return; + } + + for (let i = 0; i < bulkSize; i++) { + const next = this.queued.shift(); + + if (this.handle.settings.matchmakerNeedsQueuing) { + this.handle.listener.globalChat.directMessage(null, next, "match found!"); + } + + world.addPlayer(next.player); + } + } + } + + getSuitableWorld() { + /** @type {World} */ + let bestWorld = null; + + for (let id in this.handle.worlds) { + const world = this.handle.worlds[id]; + + if (!this.handle.gamemode.canJoinWorld(world)) { + continue; + } + + if (world.stats.external >= this.handle.settings.worldMaxPlayers) { + continue; + } + + if (bestWorld === null || world.stats.external < bestWorld.stats.external) { + bestWorld = world; + } + } + + if (bestWorld !== null) { + return bestWorld; + } else if (Object.keys(this.handle.worlds).length < this.handle.settings.worldMaxCount) { + return this.handle.createWorld(); + } else { + return null; + } + } } module.exports = Matchmaker; const ServerHandle = require("../ServerHandle"); const Connection = require("../sockets/Connection"); -const World = require("../worlds/World"); +const World = require("../worlds/World"); \ No newline at end of file diff --git a/servers/agarv2/src/worlds/Player.js b/servers/agarv2/src/worlds/Player.js index dbeac053..a1e019e4 100644 --- a/servers/agarv2/src/worlds/Player.js +++ b/servers/agarv2/src/worlds/Player.js @@ -5,140 +5,181 @@ const Cell = require("../cells/Cell"); const PlayerCell = require("../cells/PlayerCell"); class Player { - /** - * @param {ServerHandle} handle - * @param {number} id - * @param {Router} router - */ - constructor(handle, id, router) { - this.handle = handle; - this.id = id; - this.router = router; - this.exists = true; - - /** @type {string} */ - this.leaderboardName = null; - /** @type {string} */ - this.cellName = null; - this.chatName = "Spectator"; - /** @type {string} */ - this.cellSkin = null; - /** @type {number} */ - this.cellColor = 0x7F7F7F; - /** @type {number} */ - this.chatColor = 0x7F7F7F; - - /** @type {PlayerState} */ - this.state = -1; - this.hasWorld = false; - /** @type {World} */ - this.world = null; - /** @type {any} */ - this.team = null; - this.score = NaN; - - /** @type {PlayerCell[]} */ - this.ownedCells = []; - /** @type {{[cellId: string]: Cell}} */ - this.visibleCells = { }; - /** @type {{[cellId: string]: Cell}} */ - this.lastVisibleCells = { }; - /** @type {ViewArea} */ - this.viewArea = { - x: 0, - y: 0, - w: 1920 / 2 * handle.settings.playerViewScaleMult, - h: 1080 / 2 * handle.settings.playerViewScaleMult, - s: 1 - }; - } - - get settings() { return this.handle.settings; } - - destroy() { - if (this.hasWorld) this.world.removePlayer(this); - this.exists = false; - } - - /** - * @param {PlayerState} targetState - */ - updateState(targetState) { - if (this.world === null) this.state = -1; - else if (this.ownedCells.length > 0) this.state = 0; - else if (targetState === -1) this.state = -1; - else if (this.world.largestPlayer === null) this.state = 2; - else if (this.state === 1 && targetState === 2) this.state = 2; - else this.state = 1; - } - - updateViewArea() { - if (this.world === null) return; - let s; - switch (this.state) { - case -1: this.score = NaN; break; - case 0: - let x = 0, y = 0, score = 0; s = 0; - const l = this.ownedCells.length; - for (let i = 0; i < l; i++) { - const cell = this.ownedCells[i]; - x += cell.x; - y += cell.y; - s += cell.size; - score += cell.mass; - } - this.viewArea.x = x / l; - this.viewArea.y = y / l; - this.score = score; - s = this.viewArea.s = Math.pow(Math.min(64 / s, 1), 0.4); - this.viewArea.w = 1920 / s / 2 * this.settings.playerViewScaleMult; - this.viewArea.h = 1080 / s / 2 * this.settings.playerViewScaleMult; - break; - case 1: - this.score = NaN; - const spectating = this.world.largestPlayer; - this.viewArea.x = spectating.viewArea.x; - this.viewArea.y = spectating.viewArea.y; - this.viewArea.s = spectating.viewArea.s; - this.viewArea.w = spectating.viewArea.w; - this.viewArea.h = spectating.viewArea.h; - break; - case 2: - this.score = NaN; - let dx = this.router.mouseX - this.viewArea.x; - let dy = this.router.mouseY - this.viewArea.y; - const d = Math.sqrt(dx * dx + dy * dy); - const D = Math.min(d, this.settings.playerRoamSpeed); - if (D < 1) break; dx /= d; dy /= d; - const border = this.world.border; - this.viewArea.x = Math.max(border.x - border.w, Math.min(this.viewArea.x + dx * D, border.x + border.w)); - this.viewArea.y = Math.max(border.y - border.h, Math.min(this.viewArea.y + dy * D, border.y + border.h)); - s = this.viewArea.s = this.settings.playerRoamViewScale; - this.viewArea.w = 1920 / s / 2 * this.settings.playerViewScaleMult; - this.viewArea.h = 1080 / s / 2 * this.settings.playerViewScaleMult; - break; - } - } - - updateVisibleCells() { - if (this.world === null) return; - delete this.lastVisibleCells; - this.lastVisibleCells = this.visibleCells; - let visibleCells = this.visibleCells = { }; - for (let i = 0, l = this.ownedCells.length; i < l; i++) { - const cell = this.ownedCells[i]; - visibleCells[cell.id] = cell; - } - this.world.finder.search(this.viewArea, (cell) => visibleCells[cell.id] = cell); - } - - checkExistence() { - if (!this.router.disconnected) return; - if (this.state !== 0) return void this.handle.removePlayer(this.id); - const disposeDelay = this.settings.worldPlayerDisposeDelay; - if (disposeDelay > 0 && this.handle.tick - this.router.disconnectionTick >= disposeDelay) - this.handle.removePlayer(this.id); - } + /** + * @param {ServerHandle} handle + * @param {number} id + * @param {Router} router + */ + constructor(handle, id, router) { + this.handle = handle; + this.id = id; + this.router = router; + this.exists = true; + + /** @type {string} */ + this.leaderboardName = null; + /** @type {string} */ + this.cellName = null; + this.chatName = "Spectator"; + /** @type {string} */ + this.cellSkin = null; + /** @type {number} */ + this.cellColor = 0x7F7F7F; + /** @type {number} */ + this.chatColor = 0x7F7F7F; + + /** @type {PlayerState} */ + this.state = -1; + this.hasWorld = false; + /** @type {World} */ + this.world = null; + /** @type {any} */ + this.team = null; + this.score = NaN; + + /** @type {PlayerCell[]} */ + this.ownedCells = []; + /** @type {{[cellId: string]: Cell}} */ + this.visibleCells = {}; + /** @type {{[cellId: string]: Cell}} */ + this.lastVisibleCells = {}; + /** @type {ViewArea} */ + this.viewArea = { + x: 0, + y: 0, + w: 1920 / 2 * handle.settings.playerViewScaleMult, + h: 1080 / 2 * handle.settings.playerViewScaleMult, + s: 1 + }; + } + + get settings() { + return this.handle.settings; + } + + destroy() { + if (this.hasWorld) { + this.world.removePlayer(this); + } + + this.exists = false; + } + + /** + * @param {PlayerState} targetState + */ + updateState(targetState) { + if (this.world === null) { + this.state = -1; + } else if (this.ownedCells.length > 0) { + this.state = 0; + } else if (targetState === -1) { + this.state = -1; + } else if (this.world.largestPlayer === null) { + this.state = 2; + } else if (this.state === 1 && targetState === 2) { + this.state = 2; + } else { + this.state = 1; + } + } + + updateViewArea() { + if (this.world === null) { + return; + } + + let s; + + switch (this.state) { + case -1: + this.score = NaN; + break; + case 0: + let x = 0, y = 0, score = 0; + s = 0; + + const l = this.ownedCells.length; + + for (let i = 0; i < l; i++) { + const cell = this.ownedCells[i]; + x += cell.x; + y += cell.y; + s += cell.size; + score += cell.mass; + } + + this.viewArea.x = x / l; + this.viewArea.y = y / l; + this.score = score; + s = this.viewArea.s = Math.pow(Math.min(64 / s, 1), 0.4); + this.viewArea.w = 1920 / s / 2 * this.settings.playerViewScaleMult; + this.viewArea.h = 1080 / s / 2 * this.settings.playerViewScaleMult; + break; + case 1: + this.score = NaN; + const spectating = this.world.largestPlayer; + this.viewArea.x = spectating.viewArea.x; + this.viewArea.y = spectating.viewArea.y; + this.viewArea.s = spectating.viewArea.s; + this.viewArea.w = spectating.viewArea.w; + this.viewArea.h = spectating.viewArea.h; + break; + case 2: + this.score = NaN; + let dx = this.router.mouseX - this.viewArea.x; + let dy = this.router.mouseY - this.viewArea.y; + const d = Math.sqrt(dx * dx + dy * dy); + const D = Math.min(d, this.settings.playerRoamSpeed); + + if (D < 1) { + break; + } + + dx /= d; + dy /= d; + const border = this.world.border; + this.viewArea.x = Math.max(border.x - border.w, Math.min(this.viewArea.x + dx * D, border.x + border.w)); + this.viewArea.y = Math.max(border.y - border.h, Math.min(this.viewArea.y + dy * D, border.y + border.h)); + s = this.viewArea.s = this.settings.playerRoamViewScale; + this.viewArea.w = 1920 / s / 2 * this.settings.playerViewScaleMult; + this.viewArea.h = 1080 / s / 2 * this.settings.playerViewScaleMult; + break; + } + } + + updateVisibleCells() { + if (this.world === null) { + return; + } + + delete this.lastVisibleCells; + this.lastVisibleCells = this.visibleCells; + let visibleCells = this.visibleCells = {}; + + for (let i = 0, l = this.ownedCells.length; i < l; i++) { + const cell = this.ownedCells[i]; + visibleCells[cell.id] = cell; + } + + this.world.finder.search(this.viewArea, (cell) => visibleCells[cell.id] = cell); + } + + checkExistence() { + if (!this.router.disconnected) { + return; + } + + if (this.state !== 0) { + return void this.handle.removePlayer(this.id); + } + + const disposeDelay = this.settings.worldPlayerDisposeDelay; + + if (disposeDelay > 0 && this.handle.tick - this.router.disconnectionTick >= disposeDelay) { + this.handle.removePlayer(this.id); + } + } } -module.exports = Player; +module.exports = Player; \ No newline at end of file diff --git a/servers/agarv2/src/worlds/World.js b/servers/agarv2/src/worlds/World.js index 9941269a..0186c438 100644 --- a/servers/agarv2/src/worlds/World.js +++ b/servers/agarv2/src/worlds/World.js @@ -1,663 +1,883 @@ const QuadTree = require("../primitives/QuadTree"); - const Minion = require("../bots/Minion"); const PlayerBot = require("../bots/PlayerBot"); - const Pellet = require("../cells/Pellet"); const EjectedCell = require("../cells/EjectedCell"); const PlayerCell = require("../cells/PlayerCell"); const Mothercell = require("../cells/Mothercell"); const Virus = require("../cells/Virus"); const ChatChannel = require("../sockets/ChatChannel"); - const { fullyIntersects, SQRT_2 } = require("../primitives/Misc"); /** * @implements {Spawner} */ class World { - /** - * @param {ServerHandle} handle - * @param {number} id - */ - constructor(handle, id) { - this.handle = handle; - this.id = id; - - this.frozen = false; - - this._nextCellId = 1; - /** @type {Cell[]} */ - this.cells = []; - /** @type {Cell[]} */ - this.boostingCells = []; - this.pelletCount = 0; - this.mothercellCount = 0; - this.virusCount = 0; - /** @type {EjectedCell[]} */ - this.ejectedCells = []; - /** @type {PlayerCell[]} */ - this.playerCells = []; - - /** @type {Player[]} */ - this.players = []; - /** @type {Player=} */ - this.largestPlayer = null; - this.worldChat = new ChatChannel(this.handle); - - /** @type {Rect} */ - this.border = { x: NaN, y: NaN, w: NaN, h: NaN }; - /** @type {QuadTree} */ - this.finder = null; - - /** - * @type {WorldStats} - */ - this.stats = { - limit: NaN, - internal: NaN, - external: NaN, - playing: NaN, - spectating: NaN, - name: null, - gamemode: null, - loadTime: NaN, - uptime: NaN - }; - - this.setBorder({ x: this.settings.worldMapX, y: this.settings.worldMapY, w: this.settings.worldMapW, h: this.settings.worldMapH }); - } - - get settings() { return this.handle.settings; } - get nextCellId() { - return this._nextCellId >= 4294967296 ? (this._nextCellId = 1) : this._nextCellId++; - } - - afterCreation() { - for (let i = 0; i < this.settings.worldPlayerBotsPerWorld; i++) - new PlayerBot(this); - } - destroy() { - while (this.players.length > 0) - this.removePlayer(this.players[0]); - while (this.cells.length > 0) - this.removeCell(this.cells[0]); - } - - /** - * @param {Rect} range - */ - setBorder(range) { - this.border.x = range.x; - this.border.y = range.y; - this.border.w = range.w; - this.border.h = range.h; - if (this.finder !== null) this.finder.destroy(); - this.finder = new QuadTree( - this.border, - this.settings.worldFinderMaxLevel, - this.settings.worldFinderMaxItems - ); - for (let i = 0, l = this.cells.length; i < l; i++) { - const cell = this.cells[i]; - if (cell.type === 0) continue; - this.finder.insert(cell); - if (!fullyIntersects(this.border, cell.range)) - this.removeCell(cell); - } - } - - /** @param {Cell} cell */ - addCell(cell) { - cell.exists = true; - cell.range = { - x: cell.x, - y: cell.y, - w: cell.size, - h: cell.size - }; - this.cells.push(cell); - this.finder.insert(cell); - cell.onSpawned(); - this.handle.gamemode.onNewCell(cell); - } - /** @param {Cell} cell */ - setCellAsBoosting(cell) { - if (cell.isBoosting) return false; - cell.isBoosting = true; - this.boostingCells.push(cell); - return true; - } - /** @param {Cell} cell */ - setCellAsNotBoosting(cell) { - if (!cell.isBoosting) return false; - cell.isBoosting = false; - this.boostingCells.splice(this.boostingCells.indexOf(cell), 1); - return true; - } - /** @param {Cell} cell */ - updateCell(cell) { - cell.range.x = cell.x; - cell.range.y = cell.y; - cell.range.w = cell.size; - cell.range.h = cell.size; - this.finder.update(cell); - } - /** @param {Cell} cell */ - removeCell(cell) { - this.handle.gamemode.onCellRemove(cell); - cell.onRemoved(); - this.finder.remove(cell); - delete cell.range; - this.setCellAsNotBoosting(cell); - this.cells.splice(this.cells.indexOf(cell), 1); - cell.exists = false; - } - - /** @param {Player} player */ - addPlayer(player) { - this.players.push(player); - player.world = this; - player.hasWorld = true; - this.worldChat.add(player.router); - this.handle.gamemode.onPlayerJoinWorld(player, this); - player.router.onWorldSet(); - this.handle.logger.debug(`player ${player.id} has been added to world ${this.id}`); - if (!player.router.isExternal) return; - for (let i = 0; i < this.settings.worldMinionsPerPlayer; i++) - new Minion(player.router); - } - /** @param {Player} player */ - removePlayer(player) { - this.players.splice(this.players.indexOf(player), 1); - this.handle.gamemode.onPlayerLeaveWorld(player, this); - player.world = null; - player.hasWorld = false; - this.worldChat.remove(player.router); - while (player.ownedCells.length > 0) - this.removeCell(player.ownedCells[0]); - player.router.onWorldReset(); - this.handle.logger.debug(`player ${player.id} has been removed from world ${this.id}`); - } - - /** - * @param {number} cellSize - * @returns {Point} - */ - getRandomPos(cellSize) { - return { - x: this.border.x - this.border.w + cellSize + Math.random() * (2 * this.border.w - cellSize), - y: this.border.y - this.border.h + cellSize + Math.random() * (2 * this.border.h - cellSize), - }; - } - /** - * @param {Rect} range - */ - isSafeSpawnPos(range) { - return !this.finder.containsAny(range, /** @param {Cell} other */ (item) => item.avoidWhenSpawning); - } - /** - * @param {number} cellSize - * @returns {Point} - */ - getSafeSpawnPos(cellSize) { - let tries = this.settings.worldSafeSpawnTries; - while (--tries >= 0) { - const pos = this.getRandomPos(cellSize); - if (this.isSafeSpawnPos({ x: pos.x, y: pos.y, w: cellSize, h: cellSize })) - return pos; - } - return this.getRandomPos(cellSize); - } - /** - * @param {number} cellSize - * @returns {{ color: number, pos: Point }} - */ - getPlayerSpawn(cellSize) { - if (this.settings.worldSafeSpawnFromEjectedChance > Math.random() && this.ejectedCells.length > 0) { - let tries = this.settings.worldSafeSpawnTries; - while (--tries >= 0) { - const cell = this.ejectedCells[~~(Math.random() * this.ejectedCells.length)]; - if (this.isSafeSpawnPos({ x: cell.x, y: cell.y, w: cellSize, h: cellSize })) { - this.removeCell(cell); - return { color: cell.color, pos: { x: cell.x, y: cell.y } }; - } - } - } - return { color: null, pos: this.getSafeSpawnPos(cellSize) }; - } - - /** - * @param {Player} player - * @param {Point} pos - * @param {number} size - */ - spawnPlayer(player, pos, size) { - const playerCell = new PlayerCell(player, pos.x, pos.y, size); - this.addCell(playerCell); - player.updateState(0); - } - - update() { - this.frozen ? this.frozenUpdate() : this.liveUpdate(); - } - - frozenUpdate() { - for (let i = 0, l = this.players.length; i < l; i++) { - const router = this.players[i].router; - router.splitAttempts = 0; - router.ejectAttempts = 0; - if (router.isPressingQ) { - if (!router.hasProcessedQ) - router.onQPress(); - router.hasProcessedQ = true; - } else router.hasProcessedQ = false; - router.requestingSpectate = false; - router.spawningName = null; - } - } - - liveUpdate() { - this.handle.gamemode.onWorldTick(this); - - /** @type {Cell[]} */ - const eat = []; - /** @type {Cell[]} */ - const rigid = []; - /** @type {number} */ - let i; - /** @type {number} */ - let l; - - for (i = 0, l = this.cells.length; i < l; i++) - this.cells[i].onTick(); - - while (this.pelletCount < this.settings.pelletCount) { - const pos = this.getSafeSpawnPos(this.settings.pelletMinSize); - this.addCell(new Pellet(this, this, pos.x, pos.y)); - } - while (this.virusCount < this.settings.virusMinCount) { - const pos = this.getSafeSpawnPos(this.settings.virusSize); - this.addCell(new Virus(this, pos.x, pos.y)); - } - while (this.mothercellCount < this.settings.mothercellCount) { - const pos = this.getSafeSpawnPos(this.settings.mothercellSize); - this.addCell(new Mothercell(this, pos.x, pos.y)); - } - - for (i = 0, l = this.boostingCells.length; i < l;) { - if (!this.boostCell(this.boostingCells[i])) l--; - else i++; - } - - for (i = 0; i < l; i++) { - const cell = this.boostingCells[i]; - if (cell.type !== 2 && cell.type !== 3) continue; - this.finder.search(cell.range, (other) => { - if (cell.id === other.id) return; - switch (cell.getEatResult(other)) { - case 1: rigid.push(cell, other); break; - case 2: eat.push(cell, other); break; - case 3: eat.push(other, cell); break; - } - }); - } - - for (i = 0, l = this.playerCells.length; i < l; i++) { - const cell = this.playerCells[i]; - this.movePlayerCell(cell); - this.decayPlayerCell(cell); - this.autosplitPlayerCell(cell); - this.bounceCell(cell); - this.updateCell(cell); - } - - for (i = 0, l = this.playerCells.length; i < l; i++) { - const cell = this.playerCells[i]; - this.finder.search(cell.range, (other) => { - if (cell.id === other.id) return; - switch (cell.getEatResult(other)) { - case 1: rigid.push(cell, other); break; - case 2: eat.push(cell, other); break; - case 3: eat.push(other, cell); break; - } - }); - } - - for (i = 0, l = rigid.length; i < l;) - this.resolveRigidCheck(rigid[i++], rigid[i++]); - for (i = 0, l = eat.length; i < l;) - this.resolveEatCheck(eat[i++], eat[i++]); - - this.largestPlayer = null; - for (i = 0, l = this.players.length; i < l; i++) { - const player = this.players[i]; - if (!isNaN(player.score) && (this.largestPlayer === null || player.score > this.largestPlayer.score)) - this.largestPlayer = player; - } - - for (i = 0, l = this.players.length; i < l; i++) { - const player = this.players[i]; - player.checkExistence(); - if (!player.exists) { i--; l--; continue; } - if (player.state === 1 && this.largestPlayer == null) - player.updateState(2); - const router = player.router; - for (let j = 0, k = this.settings.playerSplitCap; j < k && router.splitAttempts > 0; j++) { - router.attemptSplit(); - router.splitAttempts--; - } - const nextEjectTick = this.handle.tick - this.settings.playerEjectDelay; - if (router.ejectAttempts > 0 && nextEjectTick >= router.ejectTick) { - router.attemptEject(); - router.ejectAttempts = 0; - router.ejectTick = this.handle.tick; - } - if (router.isPressingQ) { - if (!router.hasProcessedQ) - router.onQPress(); - router.hasProcessedQ = true; - } else router.hasProcessedQ = false; - if (router.requestingSpectate) { - router.onSpectateRequest(); - router.requestingSpectate = false; - } - if (router.spawningName !== null) { - router.onSpawnRequest(); - router.spawningName = null; - } - player.updateViewArea(); - } - - this.compileStatistics(); - this.handle.gamemode.compileLeaderboard(this); - - if (this.stats.external <= 0 && Object.keys(this.handle.worlds).length > this.settings.worldMinCount) - this.handle.removeWorld(this.id); - } - - /** - * @param {Cell} a - * @param {Cell} b - */ - resolveRigidCheck(a, b) { - let dx = b.x - a.x; - let dy = b.y - a.y; - let d = Math.sqrt(dx * dx + dy * dy); - const m = a.size + b.size - d; - if (m <= 0) return; - if (d === 0) d = 1, dx = 1, dy = 0; - else dx /= d, dy /= d; - const M = a.squareSize + b.squareSize; - const aM = b.squareSize / M; - const bM = a.squareSize / M; - a.x -= dx * m * aM; - a.y -= dy * m * aM; - b.x += dx * m * bM; - b.y += dy * m * bM; - this.bounceCell(a); - this.bounceCell(b); - this.updateCell(a); - this.updateCell(b); - } - - /** - * @param {Cell} a - * @param {Cell} b - */ - resolveEatCheck(a, b) { - if (!a.exists || !b.exists) return; - const dx = b.x - a.x; - const dy = b.y - a.y; - const d = Math.sqrt(dx * dx + dy * dy); - if (d > a.size - b.size / this.settings.worldEatOverlapDiv) return; - if (!this.handle.gamemode.canEat(a, b)) return; - a.whenAte(b); - b.whenEatenBy(a); - this.removeCell(b); - this.updateCell(a); - } - - /** - * @param {Cell} cell - */ - boostCell(cell) { - const d = cell.boost.d / 9 * this.handle.stepMult; - cell.x += cell.boost.dx * d; - cell.y += cell.boost.dy * d; - this.bounceCell(cell, true); - this.updateCell(cell); - if ((cell.boost.d -= d) >= 1) return true; - this.setCellAsNotBoosting(cell); - return false; - } - - /** - * @param {Cell} cell - * @param {boolean=} bounce - */ - bounceCell(cell, bounce) { - const r = cell.size / 2; - const b = this.border; - if (cell.x <= b.x - b.w + r) { - cell.x = b.x - b.w + r; - if (bounce) cell.boost.dx = -cell.boost.dx; - } - if (cell.x >= b.x + b.w - r) { - cell.x = b.x + b.w - r; - if (bounce) cell.boost.dx = -cell.boost.dx; - } - if (cell.y <= b.y - b.h + r) { - cell.y = b.y - b.h + r; - if (bounce) cell.boost.dy = -cell.boost.dy; - } - if (cell.y >= b.y + b.h - r) { - cell.y = b.y + b.h - r; - if (bounce) cell.boost.dy = -cell.boost.dy; - } - } - - /** - * @param {Virus} virus - */ - splitVirus(virus) { - const newVirus = new Virus(this, virus.x, virus.y); - newVirus.boost.dx = Math.sin(virus.splitAngle); - newVirus.boost.dy = Math.cos(virus.splitAngle); - newVirus.boost.d = this.settings.virusSplitBoost; - this.addCell(newVirus); - this.setCellAsBoosting(newVirus); - } - - /** - * @param {PlayerCell} cell - */ - movePlayerCell(cell) { - const router = cell.owner.router; - if (router.disconnected) return; - let dx = router.mouseX - cell.x; - let dy = router.mouseY - cell.y; - const d = Math.sqrt(dx * dx + dy * dy); - if (d < 1) return; dx /= d; dy /= d; - const m = Math.min(cell.moveSpeed, d) * this.handle.stepMult; - cell.x += dx * m; - cell.y += dy * m; - } - /** - * @param {PlayerCell} cell - */ - decayPlayerCell(cell) { - const newSize = cell.size - cell.size * this.handle.gamemode.getDecayMult(cell) / 50 * this.handle.stepMult; - cell.size = Math.max(newSize, this.settings.playerMinSize); - } - /** - * @param {PlayerCell} cell - * @param {number} size - * @param {Boost} boost - */ - launchPlayerCell(cell, size, boost) { - cell.squareSize -= size * size; - const x = cell.x + this.settings.playerSplitDistance * boost.dx; - const y = cell.y + this.settings.playerSplitDistance * boost.dy; - const newCell = new PlayerCell(cell.owner, x, y, size); - newCell.boost.dx = boost.dx; - newCell.boost.dy = boost.dy; - newCell.boost.d = boost.d; - this.addCell(newCell); - this.setCellAsBoosting(newCell); - } - /** - * @param {PlayerCell} cell - */ - autosplitPlayerCell(cell) { - const minSplit = this.settings.playerMaxSize * this.settings.playerMaxSize; - const cellsLeft = 1 + this.settings.playerMaxCells - cell.owner.ownedCells.length; - const overflow = Math.ceil(cell.squareSize / minSplit); - if (overflow === 1 || cellsLeft <= 0) return; - const splitTimes = Math.min(overflow, cellsLeft); - const splitSize = Math.min(Math.sqrt(cell.squareSize / splitTimes), this.settings.playerMaxSize); - for (let i = 1; i < splitTimes; i++) { - const angle = Math.random() * 2 * Math.PI; - this.launchPlayerCell(cell, splitSize, { - dx: Math.sin(angle), - dy: Math.cos(angle), - d: this.settings.playerSplitBoost - }); - } - cell.size = splitSize; - } - - /** - * @param {Player} player - */ - splitPlayer(player) { - const router = player.router; - const l = player.ownedCells.length; - for (let i = 0; i < l; i++) { - if (player.ownedCells.length >= this.settings.playerMaxCells) - break; - const cell = player.ownedCells[i]; - if (cell.size < this.settings.playerMinSplitSize) - continue; - let dx = router.mouseX - cell.x; - let dy = router.mouseY - cell.y; - let d = Math.sqrt(dx * dx + dy * dy); - if (d < 1) dx = 1, dy = 0, d = 1; - else dx /= d, dy /= d; - this.launchPlayerCell(cell, cell.size / this.settings.playerSplitSizeDiv, { - dx: dx, - dy: dy, - d: this.settings.playerSplitBoost - }); - } - } - /** - * @param {Player} player - */ - ejectFromPlayer(player) { - const dispersion = this.settings.ejectDispersion; - const loss = this.settings.ejectingLoss * this.settings.ejectingLoss; - const router = player.router; - const l = player.ownedCells.length; - for (let i = 0; i < l; i++) { - const cell = player.ownedCells[i]; - if (cell.size < this.settings.playerMinEjectSize) - continue; - let dx = router.mouseX - cell.x; - let dy = router.mouseY - cell.y; - let d = Math.sqrt(dx * dx + dy * dy); - if (d < 1) dx = 1, dy = 0, d = 1; - else dx /= d, dy /= d; - const sx = cell.x + dx * cell.size; - const sy = cell.y + dy * cell.size; - const newCell = new EjectedCell(this, player, sx, sy, cell.color); - const a = Math.atan2(dx, dy) - dispersion + Math.random() * 2 * dispersion; - newCell.boost.dx = Math.sin(a); - newCell.boost.dy = Math.cos(a); - newCell.boost.d = this.settings.ejectedCellBoost; - this.addCell(newCell); - this.setCellAsBoosting(newCell); - cell.squareSize -= loss; - this.updateCell(cell); - } - } - - /** - * @param {PlayerCell} cell - */ - popPlayerCell(cell) { - const splits = this.distributeCellMass(cell); - for (let i = 0, l = splits.length; i < l; i++) { - const angle = Math.random() * 2 * Math.PI; - this.launchPlayerCell(cell, Math.sqrt(splits[i] * 100), { - dx: Math.sin(angle), - dy: Math.cos(angle), - d: this.settings.playerSplitBoost - }); - } - } - - /** - * @param {PlayerCell} cell - * @returns {number[]} - */ - distributeCellMass(cell) { - const player = cell.owner; - let cellsLeft = this.settings.playerMaxCells - player.ownedCells.length; - if (cellsLeft <= 0) return []; - let splitMin = this.settings.playerMinSplitSize; - splitMin = splitMin * splitMin / 100; - const cellMass = cell.mass; - if (this.settings.virusMonotonePops) { - const amount = Math.min(Math.floor(cellMass / splitMin), cellsLeft); - const perPiece = cellMass / (amount + 1); - return new Array(amount).fill(perPiece); - } - if (cellMass / cellsLeft < splitMin) { - let amount = 2, perPiece = NaN; - while ((perPiece = cellMass / (amount + 1)) >= splitMin && amount * 2 <= cellsLeft) - amount *= 2; - return new Array(amount).fill(perPiece); - } - const splits = []; - let nextMass = cellMass / 2; - let massLeft = cellMass / 2; - while (cellsLeft > 0) { - if (nextMass / cellsLeft < splitMin) break; - while (nextMass >= massLeft && cellsLeft > 1) - nextMass /= 2; - splits.push(nextMass); - massLeft -= nextMass; - cellsLeft--; - } - nextMass = massLeft / cellsLeft; - return splits.concat(new Array(cellsLeft).fill(nextMass)); - } - - compileStatistics() { - let internal = 0, external = 0, playing = 0, spectating = 0; - for (let i = 0, l = this.players.length; i < l; i++) { - const player = this.players[i]; - if (!player.router.isExternal) { internal++; continue; } - external++; - if (player.state === 0) playing++; - else if (player.state === 1 || player.state === 2) - spectating++; - } - this.stats.limit = this.settings.listenerMaxConnections - this.handle.listener.connections.length + external; - this.stats.internal = internal; - this.stats.external = external; - this.stats.playing = playing; - this.stats.spectating = spectating; - this.stats.name = this.settings.serverName; - this.stats.gamemode = this.handle.gamemode.name; - this.stats.loadTime = this.handle.averageTickTime / this.handle.stepMult; - this.stats.uptime = Math.floor((Date.now() - this.handle.startTime.getTime()) / 1000); - } + /** + * @param {ServerHandle} handle + * @param {number} id + */ + constructor(handle, id) { + this.handle = handle; + this.id = id; + this.frozen = false; + this._nextCellId = 1; + /** @type {Cell[]} */ + this.cells = []; + /** @type {Cell[]} */ + this.boostingCells = []; + this.pelletCount = 0; + this.mothercellCount = 0; + this.virusCount = 0; + /** @type {EjectedCell[]} */ + this.ejectedCells = []; + /** @type {PlayerCell[]} */ + this.playerCells = []; + /** @type {Player[]} */ + this.players = []; + /** @type {Player=} */ + this.largestPlayer = null; + this.worldChat = new ChatChannel(this.handle); + /** @type {Rect} */ + this.border = { x: NaN, y: NaN, w: NaN, h: NaN }; + /** @type {QuadTree} */ + this.finder = null; + + /** + * @type {WorldStats} + */ + this.stats = { + limit: NaN, + internal: NaN, + external: NaN, + playing: NaN, + spectating: NaN, + name: null, + gamemode: null, + loadTime: NaN, + uptime: NaN + }; + + this.setBorder({x: this.settings.worldMapX, y: this.settings.worldMapY, w: this.settings.worldMapW, h: this.settings.worldMapH}); + } + + get settings() { + return this.handle.settings; + } + + get nextCellId() { + return this._nextCellId >= 4294967296 ? (this._nextCellId = 1) : this._nextCellId++; + } + + afterCreation() { + for (let i = 0; i < this.settings.worldPlayerBotsPerWorld; i++) { + new PlayerBot(this); + } + } + + destroy() { + while (this.players.length > 0) { + this.removePlayer(this.players[0]); + } + + while (this.cells.length > 0) { + this.removeCell(this.cells[0]); + } + } + + /** + * @param {Rect} range + */ + setBorder(range) { + this.border.x = range.x; + this.border.y = range.y; + this.border.w = range.w; + this.border.h = range.h; + + if (this.finder !== null) { + this.finder.destroy(); + } + + this.finder = new QuadTree( + this.border, + this.settings.worldFinderMaxLevel, + this.settings.worldFinderMaxItems + ); + + for (let i = 0, l = this.cells.length; i < l; i++) { + const cell = this.cells[i]; + + if (cell.type === 0) { + continue; + } + + this.finder.insert(cell); + + if (!fullyIntersects(this.border, cell.range)) { + this.removeCell(cell); + } + } + } + + /** @param {Cell} cell */ + addCell(cell) { + cell.exists = true; + + cell.range = { + x: cell.x, + y: cell.y, + w: cell.size, + h: cell.size + }; + + this.cells.push(cell); + this.finder.insert(cell); + cell.onSpawned(); + this.handle.gamemode.onNewCell(cell); + } + + /** @param {Cell} cell */ + setCellAsBoosting(cell) { + if (cell.isBoosting) { + return false; + } + + cell.isBoosting = true; + this.boostingCells.push(cell); + + return true; + } + + /** @param {Cell} cell */ + setCellAsNotBoosting(cell) { + if (!cell.isBoosting) { + return false; + } + + cell.isBoosting = false; + this.boostingCells.splice(this.boostingCells.indexOf(cell), 1); + + return true; + } + + /** @param {Cell} cell */ + updateCell(cell) { + cell.range.x = cell.x; + cell.range.y = cell.y; + cell.range.w = cell.size; + cell.range.h = cell.size; + this.finder.update(cell); + } + + /** @param {Cell} cell */ + removeCell(cell) { + this.handle.gamemode.onCellRemove(cell); + cell.onRemoved(); + this.finder.remove(cell); + delete cell.range; + this.setCellAsNotBoosting(cell); + this.cells.splice(this.cells.indexOf(cell), 1); + cell.exists = false; + } + + /** @param {Player} player */ + addPlayer(player) { + this.players.push(player); + player.world = this; + player.hasWorld = true; + this.worldChat.add(player.router); + this.handle.gamemode.onPlayerJoinWorld(player, this); + player.router.onWorldSet(); + this.handle.logger.debug(`player ${player.id} has been added to world ${this.id}`); + + if (!player.router.isExternal) { + return; + } + + for (let i = 0; i < this.settings.worldMinionsPerPlayer; i++) { + new Minion(player.router); + } + } + + /** @param {Player} player */ + removePlayer(player) { + this.players.splice(this.players.indexOf(player), 1); + this.handle.gamemode.onPlayerLeaveWorld(player, this); + player.world = null; + player.hasWorld = false; + this.worldChat.remove(player.router); + + while (player.ownedCells.length > 0) { + this.removeCell(player.ownedCells[0]); + } + + player.router.onWorldReset(); + this.handle.logger.debug(`player ${player.id} has been removed from world ${this.id}`); + } + + /** + * @param {number} cellSize + * @returns {Point} + */ + getRandomPos(cellSize) { + return { + x: this.border.x - this.border.w + cellSize + Math.random() * (2 * this.border.w - cellSize), + y: this.border.y - this.border.h + cellSize + Math.random() * (2 * this.border.h - cellSize), + }; + } + + /** + * @param {Rect} range + */ + isSafeSpawnPos(range) { + return !this.finder.containsAny(range, /** @param {Cell} other */(item) => item.avoidWhenSpawning); + } + + /** + * @param {number} cellSize + * @returns {Point} + */ + getSafeSpawnPos(cellSize) { + let tries = this.settings.worldSafeSpawnTries; + + while (--tries >= 0) { + const pos = this.getRandomPos(cellSize); + + if (this.isSafeSpawnPos({x: pos.x, y: pos.y, w: cellSize, h: cellSize})) { + return pos; + } + } + + return this.getRandomPos(cellSize); + } + + /** + * @param {number} cellSize + * @returns {{ color: number, pos: Point }} + */ + getPlayerSpawn(cellSize) { + if (this.settings.worldSafeSpawnFromEjectedChance > Math.random() && this.ejectedCells.length > 0) { + let tries = this.settings.worldSafeSpawnTries; + + while (--tries >= 0) { + const cell = this.ejectedCells[~~(Math.random() * this.ejectedCells.length)]; + + if (this.isSafeSpawnPos({x: cell.x, y: cell.y, w: cellSize, h: cellSize})) { + this.removeCell(cell); + + return {color: cell.color, pos: {x: cell.x, y: cell.y}}; + } + } + } + + return {color: null, pos: this.getSafeSpawnPos(cellSize)}; + } + + /** + * @param {Player} player + * @param {Point} pos + * @param {number} size + */ + spawnPlayer(player, pos, size) { + const playerCell = new PlayerCell(player, pos.x, pos.y, size); + this.addCell(playerCell); + player.updateState(0); + } + + update() { + this.frozen ? this.frozenUpdate() : this.liveUpdate(); + } + + frozenUpdate() { + for (let i = 0, l = this.players.length; i < l; i++) { + const router = this.players[i].router; + router.splitAttempts = 0; + router.ejectAttempts = 0; + + if (router.isPressingQ) { + if (!router.hasProcessedQ) { + router.onQPress(); + } + + router.hasProcessedQ = true; + } else { + router.hasProcessedQ = false; + } + + router.requestingSpectate = false; + router.spawningName = null; + } + } + + liveUpdate() { + this.handle.gamemode.onWorldTick(this); + + /** @type {Cell[]} */ + const eat = []; + /** @type {Cell[]} */ + const rigid = []; + /** @type {number} */ + let i; + /** @type {number} */ + let l; + + for (i = 0, l = this.cells.length; i < l; i++) { + this.cells[i].onTick(); + } + + while (this.pelletCount < this.settings.pelletCount) { + const pos = this.getSafeSpawnPos(this.settings.pelletMinSize); + this.addCell(new Pellet(this, this, pos.x, pos.y)); + } + + while (this.virusCount < this.settings.virusMinCount) { + const pos = this.getSafeSpawnPos(this.settings.virusSize); + this.addCell(new Virus(this, pos.x, pos.y)); + } + + while (this.mothercellCount < this.settings.mothercellCount) { + const pos = this.getSafeSpawnPos(this.settings.mothercellSize); + this.addCell(new Mothercell(this, pos.x, pos.y)); + } + + for (i = 0, l = this.boostingCells.length; i < l;) { + if (!this.boostCell(this.boostingCells[i])) { + l--; + } else { + i++; + } + } + + for (i = 0; i < l; i++) { + const cell = this.boostingCells[i]; + + if (cell.type !== 2 && cell.type !== 3) { + continue; + } + + this.finder.search(cell.range, (other) => { + if (cell.id === other.id) { + return; + } + + switch (cell.getEatResult(other)) { + case 1: + rigid.push(cell, other); + break; + case 2: + eat.push(cell, other); + break; + case 3: + eat.push(other, cell); + break; + } + }); + } + + for (i = 0, l = this.playerCells.length; i < l; i++) { + const cell = this.playerCells[i]; + this.movePlayerCell(cell); + this.decayPlayerCell(cell); + this.autosplitPlayerCell(cell); + this.bounceCell(cell); + this.updateCell(cell); + } + + for (i = 0, l = this.playerCells.length; i < l; i++) { + const cell = this.playerCells[i]; + + this.finder.search(cell.range, (other) => { + if (cell.id === other.id) { + return; + } + + switch (cell.getEatResult(other)) { + case 1: + rigid.push(cell, other); + break; + case 2: + eat.push(cell, other); + break; + case 3: + eat.push(other, cell); + break; + } + }); + } + + for (i = 0, l = rigid.length; i < l;) { + this.resolveRigidCheck(rigid[i++], rigid[i++]); + } + + for (i = 0, l = eat.length; i < l;) { + this.resolveEatCheck(eat[i++], eat[i++]); + } + + this.largestPlayer = null; + + for (i = 0, l = this.players.length; i < l; i++) { + const player = this.players[i]; + + if (!isNaN(player.score) && (this.largestPlayer === null || player.score > this.largestPlayer.score)) { + this.largestPlayer = player; + } + } + + for (i = 0, l = this.players.length; i < l; i++) { + const player = this.players[i]; + player.checkExistence(); + + if (!player.exists) { + i--; + l--; + continue; + } + + if (player.state === 1 && this.largestPlayer == null) { + player.updateState(2); + } + + const router = player.router; + + for (let j = 0, k = this.settings.playerSplitCap; j < k && router.splitAttempts > 0; j++) { + router.attemptSplit(); + router.splitAttempts--; + } + + const nextEjectTick = this.handle.tick - this.settings.playerEjectDelay; + + if (router.ejectAttempts > 0 && nextEjectTick >= router.ejectTick) { + router.attemptEject(); + router.ejectAttempts = 0; + router.ejectTick = this.handle.tick; + } + + if (router.isPressingQ) { + if (!router.hasProcessedQ) { + router.onQPress(); + } + + router.hasProcessedQ = true; + } else { + router.hasProcessedQ = false; + } + + if (router.requestingSpectate) { + router.onSpectateRequest(); + router.requestingSpectate = false; + } + + if (router.spawningName !== null) { + router.onSpawnRequest(); + router.spawningName = null; + } + + player.updateViewArea(); + } + + this.compileStatistics(); + this.handle.gamemode.compileLeaderboard(this); + + if (this.stats.external <= 0 && Object.keys(this.handle.worlds).length > this.settings.worldMinCount) { + this.handle.removeWorld(this.id); + } + } + + /** + * @param {Cell} a + * @param {Cell} b + */ + resolveRigidCheck(a, b) { + let dx = b.x - a.x; + let dy = b.y - a.y; + let d = Math.sqrt(dx * dx + dy * dy); + const m = a.size + b.size - d; + + if (m <= 0) { + return; + } + + if (d === 0) { + d = 1, dx = 1, dy = 0; + } else { + dx /= d, dy /= d; + } + + const M = a.squareSize + b.squareSize; + const aM = b.squareSize / M; + const bM = a.squareSize / M; + a.x -= dx * m * aM; + a.y -= dy * m * aM; + b.x += dx * m * bM; + b.y += dy * m * bM; + this.bounceCell(a); + this.bounceCell(b); + this.updateCell(a); + this.updateCell(b); + } + + /** + * @param {Cell} a + * @param {Cell} b + */ + resolveEatCheck(a, b) { + if (!a.exists || !b.exists) { + return; + } + + const dx = b.x - a.x; + const dy = b.y - a.y; + const d = Math.sqrt(dx * dx + dy * dy); + + if (d > a.size - b.size / this.settings.worldEatOverlapDiv) { + return; + } + + if (!this.handle.gamemode.canEat(a, b)) { + return; + } + + a.whenAte(b); + b.whenEatenBy(a); + this.removeCell(b); + this.updateCell(a); + } + + /** + * @param {Cell} cell + */ + boostCell(cell) { + const d = cell.boost.d / 9 * this.handle.stepMult; + cell.x += cell.boost.dx * d; + cell.y += cell.boost.dy * d; + this.bounceCell(cell, true); + this.updateCell(cell); + + if ((cell.boost.d -= d) >= 1) { + return true; + } + + this.setCellAsNotBoosting(cell); + + return false; + } + + /** + * @param {Cell} cell + * @param {boolean=} bounce + */ + bounceCell(cell, bounce) { + const r = cell.size / 2; + const b = this.border; + + if (cell.x <= b.x - b.w + r) { + cell.x = b.x - b.w + r; + + if (bounce) { + cell.boost.dx = -cell.boost.dx; + } + } + + if (cell.x >= b.x + b.w - r) { + cell.x = b.x + b.w - r; + + if (bounce) { + cell.boost.dx = -cell.boost.dx; + } + } + + if (cell.y <= b.y - b.h + r) { + cell.y = b.y - b.h + r; + + if (bounce) { + cell.boost.dy = -cell.boost.dy; + } + } + + if (cell.y >= b.y + b.h - r) { + cell.y = b.y + b.h - r; + + if (bounce) { + cell.boost.dy = -cell.boost.dy; + } + } + } + + /** + * @param {Virus} virus + */ + splitVirus(virus) { + const newVirus = new Virus(this, virus.x, virus.y); + newVirus.boost.dx = Math.sin(virus.splitAngle); + newVirus.boost.dy = Math.cos(virus.splitAngle); + newVirus.boost.d = this.settings.virusSplitBoost; + this.addCell(newVirus); + this.setCellAsBoosting(newVirus); + } + + /** + * @param {PlayerCell} cell + */ + movePlayerCell(cell) { + const router = cell.owner.router; + + if (router.disconnected) { + return; + } + + let dx = router.mouseX - cell.x; + let dy = router.mouseY - cell.y; + + const d = Math.sqrt(dx * dx + dy * dy); + + if (d < 1) { + return; + } + + dx /= d; + dy /= d; + const m = Math.min(cell.moveSpeed, d) * this.handle.stepMult; + cell.x += dx * m; + cell.y += dy * m; + } + + /** + * @param {PlayerCell} cell + */ + decayPlayerCell(cell) { + const newSize = cell.size - cell.size * this.handle.gamemode.getDecayMult(cell) / 50 * this.handle.stepMult; + cell.size = Math.max(newSize, this.settings.playerMinSize); + } + + /** + * @param {PlayerCell} cell + * @param {number} size + * @param {Boost} boost + */ + launchPlayerCell(cell, size, boost) { + cell.squareSize -= size * size; + const x = cell.x + this.settings.playerSplitDistance * boost.dx; + const y = cell.y + this.settings.playerSplitDistance * boost.dy; + const newCell = new PlayerCell(cell.owner, x, y, size); + newCell.boost.dx = boost.dx; + newCell.boost.dy = boost.dy; + newCell.boost.d = boost.d; + this.addCell(newCell); + this.setCellAsBoosting(newCell); + } + + /** + * @param {PlayerCell} cell + */ + autosplitPlayerCell(cell) { + const minSplit = this.settings.playerMaxSize * this.settings.playerMaxSize; + const cellsLeft = 1 + this.settings.playerMaxCells - cell.owner.ownedCells.length; + const overflow = Math.ceil(cell.squareSize / minSplit); + + if (overflow === 1 || cellsLeft <= 0) { + return; + } + + const splitTimes = Math.min(overflow, cellsLeft); + const splitSize = Math.min(Math.sqrt(cell.squareSize / splitTimes), this.settings.playerMaxSize); + + for (let i = 1; i < splitTimes; i++) { + const angle = Math.random() * 2 * Math.PI; + + this.launchPlayerCell(cell, splitSize, { + dx: Math.sin(angle), + dy: Math.cos(angle), + d: this.settings.playerSplitBoost + }); + } + + cell.size = splitSize; + } + + /** + * @param {Player} player + */ + splitPlayer(player) { + const router = player.router; + const l = player.ownedCells.length; + + for (let i = 0; i < l; i++) { + if (player.ownedCells.length >= this.settings.playerMaxCells) { + break; + } + + const cell = player.ownedCells[i]; + + if (cell.size < this.settings.playerMinSplitSize) { + continue; + } + + let dx = router.mouseX - cell.x; + let dy = router.mouseY - cell.y; + let d = Math.sqrt(dx * dx + dy * dy); + + if (d < 1) { + dx = 1, dy = 0, d = 1; + } else { + dx /= d, dy /= d; + } + + this.launchPlayerCell(cell, cell.size / this.settings.playerSplitSizeDiv, { + dx: dx, + dy: dy, + d: this.settings.playerSplitBoost + }); + } + } + + /** + * @param {Player} player + */ + ejectFromPlayer(player) { + const dispersion = this.settings.ejectDispersion; + const loss = this.settings.ejectingLoss * this.settings.ejectingLoss; + const router = player.router; + const l = player.ownedCells.length; + + for (let i = 0; i < l; i++) { + const cell = player.ownedCells[i]; + + if (cell.size < this.settings.playerMinEjectSize) { + continue; + } + + let dx = router.mouseX - cell.x; + let dy = router.mouseY - cell.y; + let d = Math.sqrt(dx * dx + dy * dy); + + if (d < 1) { + dx = 1, dy = 0, d = 1; + } else { + dx /= d, dy /= d; + } + + const sx = cell.x + dx * cell.size; + const sy = cell.y + dy * cell.size; + const newCell = new EjectedCell(this, player, sx, sy, cell.color); + const a = Math.atan2(dx, dy) - dispersion + Math.random() * 2 * dispersion; + newCell.boost.dx = Math.sin(a); + newCell.boost.dy = Math.cos(a); + newCell.boost.d = this.settings.ejectedCellBoost; + this.addCell(newCell); + this.setCellAsBoosting(newCell); + cell.squareSize -= loss; + this.updateCell(cell); + } + } + + /** + * @param {PlayerCell} cell + */ + popPlayerCell(cell) { + const splits = this.distributeCellMass(cell); + + for (let i = 0, l = splits.length; i < l; i++) { + const angle = Math.random() * 2 * Math.PI; + + this.launchPlayerCell(cell, Math.sqrt(splits[i] * 100), { + dx: Math.sin(angle), + dy: Math.cos(angle), + d: this.settings.playerSplitBoost + }); + } + } + + /** + * @param {PlayerCell} cell + * @returns {number[]} + */ + distributeCellMass(cell) { + const player = cell.owner; + let cellsLeft = this.settings.playerMaxCells - player.ownedCells.length; + + if (cellsLeft <= 0) { + return []; + } + + let splitMin = this.settings.playerMinSplitSize; + splitMin = splitMin * splitMin / 100; + const cellMass = cell.mass; + + if (this.settings.virusMonotonePops) { + const amount = Math.min(Math.floor(cellMass / splitMin), cellsLeft); + const perPiece = cellMass / (amount + 1); + + return new Array(amount).fill(perPiece); + } + + if (cellMass / cellsLeft < splitMin) { + let amount = 2, perPiece = NaN; + + while ((perPiece = cellMass / (amount + 1)) >= splitMin && amount * 2 <= cellsLeft) { + amount *= 2; + } + + return new Array(amount).fill(perPiece); + } + + const splits = []; + + let nextMass = cellMass / 2; + let massLeft = cellMass / 2; + + while (cellsLeft > 0) { + if (nextMass / cellsLeft < splitMin) { + break; + } + + while (nextMass >= massLeft && cellsLeft > 1) { + nextMass /= 2; + } + + splits.push(nextMass); + massLeft -= nextMass; + cellsLeft--; + } + + nextMass = massLeft / cellsLeft; + + return splits.concat(new Array(cellsLeft).fill(nextMass)); + } + + compileStatistics() { + let internal = 0, external = 0, playing = 0, spectating = 0; + + for (let i = 0, l = this.players.length; i < l; i++) { + const player = this.players[i]; + + if (!player.router.isExternal) { + internal++; + continue; + } + + external++; + + if (player.state === 0) { + playing++; + } else if (player.state === 1 || player.state === 2) { + spectating++; + } + } + + this.stats.limit = this.settings.listenerMaxConnections - this.handle.listener.connections.length + external; + this.stats.internal = internal; + this.stats.external = external; + this.stats.playing = playing; + this.stats.spectating = spectating; + this.stats.name = this.settings.serverName; + this.stats.gamemode = this.handle.gamemode.name; + this.stats.loadTime = this.handle.averageTickTime / this.handle.stepMult; + this.stats.uptime = Math.floor((Date.now() - this.handle.startTime.getTime()) / 1000); + } } module.exports = World; const Cell = require("../cells/Cell"); const Player = require("./Player"); -const ServerHandle = require("../ServerHandle"); +const ServerHandle = require("../ServerHandle"); \ No newline at end of file