From 4dec6dc3237985c9a579a2c634476ac7d35d1b6c Mon Sep 17 00:00:00 2001 From: Peanutzy Leguminoso Date: Tue, 16 Jun 2026 15:05:03 +0530 Subject: [PATCH 1/6] changed appropriate events to restart afking --- src/index.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index fab0e865..6205611a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -257,14 +257,32 @@ Events.on(EventType.ServerLoadEvent, (e) => { // Keeps track of any action performed on a tile for use in tilelog. -Events.on(EventType.BlockBuildBeginEvent, addToTileHistory); -Events.on(EventType.BuildRotateEvent, addToTileHistory); -Events.on(EventType.ConfigEvent, addToTileHistory); -Events.on(EventType.PickupEvent, addToTileHistory); -Events.on(EventType.PayloadDropEvent, addToTileHistory); +Events.on(EventType.BlockBuildBeginEvent, (e) => { + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); +Events.on(EventType.BuildRotateEvent, (e) => { + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); +Events.on(EventType.ConfigEvent, (e) => { + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); +Events.on(EventType.PickupEvent, (e) => { + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); +Events.on(EventType.PayloadDropEvent, (e) => { + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); Events.on(EventType.UnitDestroyEvent, addToTileHistory); Events.on(EventType.BlockDestroyEvent, addToTileHistory); -Events.on(EventType.UnitControlEvent, addToTileHistory); +Events.on(EventType.UnitControlEvent, (e) => { + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); Events.on(EventType.TapEvent, handleTapEvent); From 2634585d89228b4212df50e5c1080575a876c1a3 Mon Sep 17 00:00:00 2001 From: Peanutzy Leguminoso Date: Mon, 1 Jun 2026 15:36:20 +0700 Subject: [PATCH 2/6] remove old afk check --- src/players.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/players.ts b/src/players.ts index 59015b1d..c05118bb 100644 --- a/src/players.ts +++ b/src/players.ts @@ -564,13 +564,7 @@ export class FishPlayer { static updateAFKCheck(){ //TODO better AFK check this.forEachPlayer((fishP, mp) => { - if(fishP.lastMousePosition[0] != mp.mouseX || fishP.lastMousePosition[1] != mp.mouseY){ - fishP.lastActive = Date.now(); - } fishP.lastMousePosition = [mp.mouseX, mp.mouseY]; - if(fishP.lastUnitPosition[0] != mp.x || fishP.lastUnitPosition[1] != mp.y){ - fishP.lastActive = Date.now(); - } fishP.lastUnitPosition = [mp.x, mp.y]; fishP.updateName(); }); From 6a172ff60ea9e504be35a0fef9db8017253c41c9 Mon Sep 17 00:00:00 2001 From: Peanutzy Leguminoso Date: Mon, 1 Jun 2026 16:06:47 +0700 Subject: [PATCH 3/6] cleaner code for handling resetting afk and tilehistory --- build/scripts/index.js | 12 ++++++------ build/scripts/players.js | 6 ------ build/scripts/utils.js | 6 ++++++ src/index.ts | 32 +++++++------------------------- src/utils.ts | 5 +++++ 5 files changed, 24 insertions(+), 37 deletions(-) diff --git a/build/scripts/index.js b/build/scripts/index.js index ac14dce2..3d9b4aa0 100644 --- a/build/scripts/index.js +++ b/build/scripts/index.js @@ -261,14 +261,14 @@ Events.on(EventType.ServerLoadEvent, function (e) { Log.info("fish-commands: initialized in @ms (incl previous)", Time.elapsed()); }); // Keeps track of any action performed on a tile for use in tilelog. -Events.on(EventType.BlockBuildBeginEvent, utils_1.addToTileHistory); -Events.on(EventType.BuildRotateEvent, utils_1.addToTileHistory); -Events.on(EventType.ConfigEvent, utils_1.addToTileHistory); -Events.on(EventType.PickupEvent, utils_1.addToTileHistory); -Events.on(EventType.PayloadDropEvent, utils_1.addToTileHistory); +Events.on(EventType.BlockBuildBeginEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.BuildRotateEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.ConfigEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.PickupEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.PayloadDropEvent, utils_1.tilelogAndResetAfk); Events.on(EventType.UnitDestroyEvent, utils_1.addToTileHistory); Events.on(EventType.BlockDestroyEvent, utils_1.addToTileHistory); -Events.on(EventType.UnitControlEvent, utils_1.addToTileHistory); +Events.on(EventType.UnitControlEvent, utils_1.tilelogAndResetAfk); Events.on(EventType.TapEvent, commands_1.handleTapEvent); Events.on(EventType.GameOverEvent, function (e) { var e_1, _a; diff --git a/build/scripts/players.js b/build/scripts/players.js index 8c3bd77b..826e1c46 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -681,13 +681,7 @@ var FishPlayer = /** @class */ (function () { FishPlayer.updateAFKCheck = function () { //TODO better AFK check this.forEachPlayer(function (fishP, mp) { - if (fishP.lastMousePosition[0] != mp.mouseX || fishP.lastMousePosition[1] != mp.mouseY) { - fishP.lastActive = Date.now(); - } fishP.lastMousePosition = [mp.mouseX, mp.mouseY]; - if (fishP.lastUnitPosition[0] != mp.x || fishP.lastUnitPosition[1] != mp.y) { - fishP.lastActive = Date.now(); - } fishP.lastUnitPosition = [mp.x, mp.y]; fishP.updateName(); }); diff --git a/build/scripts/utils.js b/build/scripts/utils.js index 8b0ccd6f..ec0ee659 100644 --- a/build/scripts/utils.js +++ b/build/scripts/utils.js @@ -82,6 +82,7 @@ exports.outputMessage = outputMessage; exports.outputConsole = outputConsole; exports.updateBans = updateBans; exports.processChat = processChat; +exports.tilelogAndResetAfk = tilelogAndResetAfk; exports.getIPRange = getIPRange; exports.getHash = getHash; exports.match = match; @@ -923,6 +924,11 @@ exports.addToTileHistory = logErrors("Error while saving a tilelog entry", funct }, 1); }); }); }); +function tilelogAndResetAfk(e) { + (0, exports.addToTileHistory)(e); + players_1.FishPlayer.get(e.unit.player).lastActive = Date.now(); +} +; function getIPRange(input, error) { if (globals_1.ipRangeCIDRPattern.test(input)) { var _a = __read(input.split("/"), 2), ip = _a[0], maskLength = _a[1]; diff --git a/src/index.ts b/src/index.ts index 6205611a..8f8fe5f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import { PartialMapRun } from "/maps"; import { loadPacketHandlers } from "/packetHandlers"; import { FishPlayer } from "/players"; import * as timers from "/timers"; -import { addToTileHistory, fishCommandsRootDirPath, formatTimeRelative, matchFilter, processChat, restartNow, serverRestartLoop } from "/utils"; +import { addToTileHistory, fishCommandsRootDirPath, formatTimeRelative, matchFilter, processChat, restartNow, serverRestartLoop, tilelogAndResetAfk } from "/utils"; Events.on(EventType.ConnectionEvent, (e) => { @@ -257,32 +257,14 @@ Events.on(EventType.ServerLoadEvent, (e) => { // Keeps track of any action performed on a tile for use in tilelog. -Events.on(EventType.BlockBuildBeginEvent, (e) => { - addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); -}); -Events.on(EventType.BuildRotateEvent, (e) => { - addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); -}); -Events.on(EventType.ConfigEvent, (e) => { - addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); -}); -Events.on(EventType.PickupEvent, (e) => { - addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); -}); -Events.on(EventType.PayloadDropEvent, (e) => { - addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); -}); +Events.on(EventType.BlockBuildBeginEvent, tilelogAndResetAfk) +Events.on(EventType.BuildRotateEvent, tilelogAndResetAfk); +Events.on(EventType.ConfigEvent, tilelogAndResetAfk); +Events.on(EventType.PickupEvent, tilelogAndResetAfk); +Events.on(EventType.PayloadDropEvent, tilelogAndResetAfk); Events.on(EventType.UnitDestroyEvent, addToTileHistory); Events.on(EventType.BlockDestroyEvent, addToTileHistory); -Events.on(EventType.UnitControlEvent, (e) => { - addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); -}); +Events.on(EventType.UnitControlEvent, tilelogAndResetAfk); Events.on(EventType.TapEvent, handleTapEvent); diff --git a/src/utils.ts b/src/utils.ts index f3741d69..a556970f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -789,6 +789,11 @@ export const addToTileHistory = logErrors("Error while saving a tilelog entry", }); +export function tilelogAndResetAfk(e: { unit: Unit }){ + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}; + export function getIPRange(input:string, error?:(message:string) => never):string | null { if(ipRangeCIDRPattern.test(input)){ const [ip, maskLength] = input.split("/"); From 42abb7bdb52b859e4dbd26ace514321f9be5063e Mon Sep 17 00:00:00 2001 From: Peanutzy Leguminoso Date: Tue, 16 Jun 2026 18:26:32 +0700 Subject: [PATCH 4/6] optional chaining for properties. --- src/commands/LICENSE | 2 ++ src/utils.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 src/commands/LICENSE diff --git a/src/commands/LICENSE b/src/commands/LICENSE new file mode 100644 index 00000000..5eeef4ba --- /dev/null +++ b/src/commands/LICENSE @@ -0,0 +1,2 @@ + +Copyright © BalaM314, 2026. All Rights Reserved. diff --git a/src/utils.ts b/src/utils.ts index a556970f..e2b5972e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -791,7 +791,7 @@ export const addToTileHistory = logErrors("Error while saving a tilelog entry", export function tilelogAndResetAfk(e: { unit: Unit }){ addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); + FishPlayer.get(e.unit?.player).lastActive = Date.now(); }; export function getIPRange(input:string, error?:(message:string) => never):string | null { From 7f16d330c2689f89fe275d260374736326c71cd7 Mon Sep 17 00:00:00 2001 From: Peanutzy Leguminoso Date: Wed, 17 Jun 2026 15:39:52 +0700 Subject: [PATCH 5/6] make e the event any. --- build/scripts/utils.js | 2322 ++++++++++++++++++++-------------------- src/utils.ts | 6 +- 2 files changed, 1163 insertions(+), 1165 deletions(-) diff --git a/build/scripts/utils.js b/build/scripts/utils.js index dedd8188..68646df0 100644 --- a/build/scripts/utils.js +++ b/build/scripts/utils.js @@ -1,1162 +1,1160 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains many utility functions that need access to any values from other files. -For functions that don't need values from other files, see funcs.ts. -*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.addToTileHistory = exports.foolifyChat = exports.getMap = exports.getUnitType = exports.getItem = void 0; -exports.memoizeChatFilter = memoizeChatFilter; -exports.formatTime = formatTime; -exports.formatTimeShort = formatTimeShort; -exports.formatModeName = formatModeName; -exports.formatTimestampFull = formatTimestampFull; -exports.formatTimestamp = formatTimestamp; -exports.formatTimestampShort = formatTimestampShort; -exports.formatTimeRelative = formatTimeRelative; -exports.getColor = getColor; -exports.nearbyEnemyTile = nearbyEnemyTile; -exports.getTeam = getTeam; -exports.matchFilter = matchFilter; -exports.removeFoosChars = removeFoosChars; -exports.cleanText = cleanText; -exports.isImpersonator = isImpersonator; -exports.logAction = logAction; -exports.parseTimeString = parseTimeString; -exports.serverRestartLoop = serverRestartLoop; -exports.restartNow = restartNow; -exports.isBuildable = isBuildable; -exports.isMapValidForGamemode = isMapValidForGamemode; -exports.getBlock = getBlock; -exports.teleportPlayer = teleportPlayer; -exports.logErrors = logErrors; -exports.definitelyRealMemoryCorruption = definitelyRealMemoryCorruption; -exports.getEnemyTeam = getEnemyTeam; -exports.neutralGameover = neutralGameover; -exports.skipWaves = skipWaves; -exports.logHTrip = logHTrip; -exports.setType = setType; -exports.untilForever = untilForever; -exports.colorNumber = colorNumber; -exports.formatRatekeeper = formatRatekeeper; -exports.getAntiBotInfo = getAntiBotInfo; -exports.outputFail = outputFail; -exports.outputSuccess = outputSuccess; -exports.outputMessage = outputMessage; -exports.outputConsole = outputConsole; -exports.updateBans = updateBans; -exports.processChat = processChat; -exports.tilelogAndResetAfk = tilelogAndResetAfk; -exports.getIPRange = getIPRange; -exports.getHash = getHash; -exports.match = match; -exports.fishCommandsRootDirPath = fishCommandsRootDirPath; -exports.applyEffectMode = applyEffectMode; -exports.handleError = handleError; -exports.getStatuses = getStatuses; -var api = __importStar(require("/api")); -var config_1 = require("/config"); -var commands_1 = require("/frameworks/commands"); -var menus_1 = require("/frameworks/menus"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var players_1 = require("/players"); -function memoizeChatFilter(impl) { - var lastCleanedInput = null; - var lastOutput = null; - return function memoized(input) { - var cleanedInput = removeFoosChars(input); - if (cleanedInput === lastCleanedInput) - return lastOutput; - lastCleanedInput = cleanedInput; - return lastOutput = impl(input); - }; -} -function formatTime(time) { - if (globals_1.maxTime - (time + Date.now()) < 20000) - return "forever"; - if (isNaN(time)) - return "N/A"; - var months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); - var days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); - var hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); - var minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); - var seconds = Math.floor((time % (60 * 1000)) / (1000)); - return [ - months && "".concat(months, " month").concat(months != 1 ? "s" : ""), - days && "".concat(days, " day").concat(days != 1 ? "s" : ""), - hours && "".concat(hours, " hour").concat(hours != 1 ? "s" : ""), - minutes && "".concat(minutes, " minute").concat(minutes != 1 ? "s" : ""), - (seconds || time < 1000) && "".concat(seconds, " second").concat(seconds != 1 ? "s" : ""), - ].filter(Boolean).join(", "); -} -function formatTimeShort(time) { - if (globals_1.maxTime - (time + Date.now()) < 20000) - return "forever"; - if (isNaN(time)) - return "N/A"; - var months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); - var days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); - var hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); - var minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); - var seconds = Math.floor((time % (60 * 1000)) / (1000)); - return [ - months && "".concat(months, "mo"), - days && "".concat(days, "d"), - hours && "".concat(hours, "h"), - minutes && "".concat(minutes, "m"), - (seconds || time < 1000) && "".concat(seconds, "s"), - ].filter(Boolean).join(" "); -} -//TODO move this data to be right next to Mode -function formatModeName(name) { - return { - "attack": "Attack", - "survival": "Survival", - "hexed": "Hexed", - "pvp": "PVP", - "sandbox": "Sandbox", - "hardcore": "Hardcore", - "testsrv": "Testing Server", - "minigame": "Minigames", - }[name]; -} -function formatTimestampFull(time) { - var date = new Date(time); - return "".concat(date.toDateString(), ", ").concat(date.toTimeString()); -} -function formatTimestamp(time) { - return new Date(time).toLocaleString(); -} -function formatTimestampShort(time) { - var date = new Date(time); - return "".concat(date.getFullYear(), "-").concat(date.getMonth() + 1, "-").concat(date.getDate(), " ").concat(date.getHours(), ":").concat(date.getMinutes()); -} -function formatTimeRelative(time, raw) { - var difference = Math.abs(time - Date.now()); - if (difference < 1000) - return "just now"; - else if (time > Date.now()) - return (raw ? "" : "in ") + formatTime(difference); - else - return formatTime(difference) + (raw ? "" : " ago"); -} -/** Attempts to parse a Color from the input. */ -function getColor(input) { - try { - if (input.includes(',')) { - var formattedColor = input.split(','); - var col = { - r: Number(formattedColor[0]), - g: Number(formattedColor[1]), - b: Number(formattedColor[2]), - a: 255, - }; - return new Color(col.r, col.g, col.b, col.a); - } - else if (input.includes('#')) { - return Color.valueOf(input); - } - else if ((function (input) { return input in Color; })(input)) { - return Color[input]; - } - else { - return null; - } - } - catch (e) { - return null; - } -} -/** Searches for an enemy tile near a unit. */ -function nearbyEnemyTile(unit, dist) { - //because the indexer is buggy - if (dist > 10) - (0, funcs_1.crash)("nearbyEnemyTile(): dist (".concat(dist, ") is too high!")); - var x = Math.floor(unit.x / Vars.tilesize); - var y = Math.floor(unit.y / Vars.tilesize); - for (var i = -dist; i <= dist; i++) { - for (var j = -dist; j <= dist; j++) { - var build = Vars.world.build(x + i, y + j); - if (build && build.team != unit.team && build.team != Team.derelict) - return build; - } - } - return null; -} -/** Attempts to parse a Team from the input. */ -function getTeam(team) { - if (team in Team && Team[team] instanceof Team) - return Team[team]; - else if (Team.baseTeams.find(function (t) { return t.name.includes(team.toLowerCase()); })) - return Team.baseTeams.find(function (t) { return t.name.includes(team.toLowerCase()); }); - else if (!isNaN(Number(team))) - return "\"".concat(team, "\" is not a valid team string. Did you mean \"#").concat(team, "\"?"); - else if (!isNaN(Number(team.slice(1)))) { - var num = Number(team.slice(1)); - if (num <= 255 && num >= 0 && Number.isInteger(num)) - return Team.all[Number(team.slice(1))]; - else - return "Team ".concat(team, " is outside the valid range (integers 0-255)."); - } - return "\"".concat(team, "\" is not a valid team string."); -} -/** Attempts to parse an Item from the input. */ -exports.getItem = (0, funcs_1.searchFixed)(Vars.content.items().toArray(), [ - function (i, s) { return i.name == s; }, - function (i, s) { return i.name == s.toLowerCase(); }, - function (i, s) { return i.name.includes(s.toLowerCase()); }, - function (i, s) { return i.name.includes(s.toLowerCase().replace(" ", "-")); }, - function (i, s) { return i.emoji() == s; }, -]); -/** - * @param wordList "chat" is least strict, followed by "strict", and "name" is most strict. - * @returns a - */ -function matchFilter(input, wordList, aggressive) { - var e_1, _a, e_2, _b; - if (wordList === void 0) { wordList = "chat"; } - if (aggressive === void 0) { aggressive = false; } - var currentBannedWords = [ - wordList == "name" ? config_1.bannedWords.normal.filter(function (w) { return w[0] !== "uwu"; }) : config_1.bannedWords.normal, - (wordList == "strict" || wordList == "name") && config_1.bannedWords.strict, - wordList == "name" && config_1.bannedWords.names, - ].filter(Boolean).flat(); - if (aggressive) - currentBannedWords.push(["hitler", []]); - //Replace substitutions - var variations = [input, cleanText(input, false)]; - if (aggressive) - variations.push(cleanText(input, true)); - try { - for (var currentBannedWords_1 = __values(currentBannedWords), currentBannedWords_1_1 = currentBannedWords_1.next(); !currentBannedWords_1_1.done; currentBannedWords_1_1 = currentBannedWords_1.next()) { - var _c = __read(currentBannedWords_1_1.value, 2), banned = _c[0], whitelist = _c[1]; - var _loop_1 = function (text_1) { - if (banned instanceof RegExp ? banned.test(text_1) : text_1.includes(banned)) { - var modifiedText_1 = text_1; - whitelist.forEach(function (w) { return modifiedText_1 = modifiedText_1.replace(new RegExp(w, "g"), ""); }); //Replace whitelisted words with nothing - if (banned instanceof RegExp ? banned.test(modifiedText_1) : modifiedText_1.includes(banned)) //If the text still matches, fail - return { value: (banned === globals_1.uuidPattern ? "a Mindustry UUID" : - banned === globals_1.ipPattern || banned === globals_1.ipPortPattern ? "an IP address" : - //parsing regex with regex, massive hack - banned instanceof RegExp ? banned.source.replace(/\\b|\(\?|||>", "Name contains >|||> which is reserved for the server owner"], - "\uE817", "\uE82C", "\uE88E", "\uE813", - [/^[<\uE825].{1,3}[>\uE83A]/, "Name contains a prefix such as which is used for role prefixes"], - [function (replacedText) { return !isAdmin && config_1.adminNames.includes(replacedText.replace(/ /g, "")); }, "One of our admins uses this name"] - ]); - try { - for (var filters_1 = __values(filters), filters_1_1 = filters_1.next(); !filters_1_1.done; filters_1_1 = filters_1.next()) { - var _b = __read(filters_1_1.value, 2), check = _b[0], message = _b[1]; - if (check(replacedText)) - return message; - if (check(antiEvasionText)) - return message; - } - } - catch (e_3_1) { e_3 = { error: e_3_1 }; } - finally { - try { - if (filters_1_1 && !filters_1_1.done && (_a = filters_1.return)) _a.call(filters_1); - } - finally { if (e_3) throw e_3.error; } - } - return false; -} -function logAction(action, by, to, reason, duration) { - if (by === undefined) { //overload 1 - api.sendModerationMessage("".concat(action, "\n**Server:** ").concat(config_1.Gamemode.name())); - return; - } - if (to === undefined) { //overload 2 - api.sendModerationMessage("".concat((0, funcs_1.escapeTextDiscord)(Strings.stripColors(by.name)), " ").concat(action, "\n**Server:** ").concat(config_1.Gamemode.name())); - return; - } - if (to) { //overload 3 - var name = void 0, uuid = void 0, ip = void 0; - var actor = typeof by === "string" ? by : (0, funcs_1.escapeTextDiscord)(Strings.stripColors(by.name)); - if (to instanceof players_1.FishPlayer) { - name = (0, funcs_1.escapeTextDiscord)(to.name); - uuid = to.uuid; - ip = to.ip(); - } - else if (typeof to == "string") { - if (globals_1.uuidPattern.test(to)) { - name = "[".concat(to, "]"); - uuid = to; - ip = "[unknown]"; - } - else { - name = to; - uuid = "[unknown]"; - ip = "[unknown]"; - } - } - else { - name = (0, funcs_1.escapeTextDiscord)(to.lastName); - uuid = to.id; - ip = to.lastIP; - } - api.sendModerationMessage("".concat(actor, " ").concat(action, " ").concat(name, " ").concat(duration ? "for ".concat(formatTime(duration), " ") : "").concat(reason ? "with reason ".concat((0, funcs_1.escapeTextDiscord)(reason)) : "", "\n**Server:** ").concat(config_1.Gamemode.name(), "\n**uuid:** `").concat(uuid, "`\n**ip**: `").concat(ip, "`")); - return; - } -} -/** @returns the number of milliseconds. */ -function parseTimeString(str) { - var e_4, _a; - var formats = [ - [/(\d+)s/, 1], - [/(\d+)m/, 60], - [/(\d+)h/, 3600], - [/(\d+)d/, 86400], - [/(\d+)w/, 604800] - ].map(function (_a) { - var _b = __read(_a, 2), regex = _b[0], mult = _b[1]; - return [Pattern.compile(regex.source), mult]; - }); - if (str == "forever") - return (globals_1.maxTime - Date.now() - 10000); - try { - for (var formats_1 = __values(formats), formats_1_1 = formats_1.next(); !formats_1_1.done; formats_1_1 = formats_1.next()) { - var _b = __read(formats_1_1.value, 2), pattern = _b[0], mult = _b[1]; - //rhino regex doesn't work - var matcher = pattern.matcher(str); - if (matcher.matches()) { - var num = Number(matcher.group(1)); - if (!isNaN(num)) - return (num * mult) * 1000; - } - } - } - catch (e_4_1) { e_4 = { error: e_4_1 }; } - finally { - try { - if (formats_1_1 && !formats_1_1.done && (_a = formats_1.return)) _a.call(formats_1); - } - finally { if (e_4) throw e_4.error; } - } - return null; -} -/** - * Triggers the restart countdown. Execution always returns from this function. - * @param [fake=false] if set, server will not actually restart. - */ -function serverRestartLoop(sec, fake) { - if (fake === void 0) { fake = false; } - if (sec > 0) { - if (sec < 15 || sec % 5 == 0) - Call.sendMessage("[scarlet]Server restarting in: ".concat(sec)); - globals_1.fishState.restartLoopTask = Timer.schedule(function () { return serverRestartLoop(sec - 1); }, 1); - } - else if (!fake) { - restartNow(); - } -} -/** - * Actually restarts. Kicks all players. Execution always returns from this function. - * @param [removeSave=false] If set, save will be deleted instead of saved. Used to start a new game after the restart. - */ -function restartNow(removeSave) { - if (removeSave === void 0) { removeSave = false; } - Log.info("Restarting..."); - Vars.netServer.kickAll(Packets.KickReason.serverRestarting); - Vars.net.closeServer(); - Vars.state.set(GameState.State.menu); - var file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); - if (removeSave) { - Core.app.post(function () { - file.delete(); - Core.app.exit(); - }); - } - else { - Core.app.post(function () { - SaveIO.save(file); - Core.app.exit(); - }); - } -} -function isBuildable(block) { - return block == Blocks.powerVoid || (block.buildType != Blocks.air.buildType && !(block instanceof ConstructBlock)); -} -exports.getUnitType = (0, funcs_1.searchFixed)(function () { return Vars.content.units().select(function (u) { return !(u instanceof MissileUnitType || u.internal); }).toArray(); }, [ - function (u, q) { return u.name == q; }, - function (u, q) { return u.name.includes(q.toLowerCase()); }, -]); -/** The vanilla validation code doesn't work on servers */ -function isMapValidForGamemode(map) { - if (map.custom) - return true; //we assume that all custom maps are appropriate for the selected gamemode - var pvpMaps = ["Veins", "Glacier", "Passage"]; //Maps.pvpMaps - switch (Vars.state.rules.mode().name()) { - case "sandbox": - case "editor": return true; //sandbox can be played on any map - case "attack": - case "pvp": return pvpMaps.includes(map.name()); //technically the pvp maps are valid attack maps, since they have an (undefended) enemy core - case "survival": return !pvpMaps.includes(map.name()); - default: return false; //unreachable - } -} -exports.getMap = (0, funcs_1.searchFixed)(function () { return Vars.maps.all().select(isMapValidForGamemode).toArray(); }, [ - function (m, name) { return m.name().replace(/ /g, "_") === name; }, //exact match with spaces replaced - function (m, name) { return m.name().replace(/ /g, "_").toLowerCase() === name.toLowerCase(); }, //exact match with spaces replaced ignoring case - function (m, name) { return m.plainName().replace(/ /g, "_").toLowerCase() === name.toLowerCase(); }, //exact match with spaces replaced ignoring case and colors - function (m, name) { return m.plainName().toLowerCase().includes(name.toLowerCase()); }, //partial match ignoring case and colors - function (m, name) { return m.plainName().replace(/ /g, "_").toLowerCase().includes(name.toLowerCase()); }, //partial match with spaces replaced ignoring case and colors - function (m, name) { return m.plainName().replace(/ /g, "").toLowerCase().includes(name.toLowerCase()); }, //partial match with spaces removed ignoring case and colors - function (m, name) { return m.plainName().replace(/[^a-zA-Z]/gi, "").toLowerCase().includes(name.toLowerCase()); }, -], "recomputeOptions"); -//static cache -var buildableBlocks = null; -function getBlock(block, filter) { - buildableBlocks !== null && buildableBlocks !== void 0 ? buildableBlocks : (buildableBlocks = Vars.content.blocks().select(isBuildable)); - var check = { - buildable: function (b) { return isBuildable(b); }, - air: function (b) { return b == Blocks.air || isBuildable(b); }, - all: function (b) { return true; } - }[filter]; - var out; - if (block in Blocks && Blocks[block] instanceof Block && check(Blocks[block])) - return Blocks[block]; - else if ((out = Vars.content.blocks().find(function (t) { return t.name.includes(block.toLowerCase()) && check(t); }))) - return out; - else if ((out = Vars.content.blocks().find(function (t) { return t.name.replace(/-/g, "").includes(block.toLowerCase().replace(/ /g, "")) && check(t); }))) - return out; - else if (block.includes("airblast")) - return Blocks.blastDrill; - return "\"".concat(block, "\" is not a valid block."); -} -function teleportPlayer(player, to) { - Timer.schedule(function () { - var p = player.unit(); - var t = to.unit(); - if (p && t) { - p.set(t.x, t.y); - Call.setPosition(player.con, t.x, t.y); - Call.setCameraPosition(player.con, t.x, t.y); - } - }, 0, 0.016, 10); -} -function logErrors(message, func) { - return function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - try { - return func.apply(void 0, __spreadArray([], __read(args), false)); - } - catch (err) { - Log.err(message); - Log.err((0, funcs_1.parseError)(err)); - } - }; -} -function definitelyRealMemoryCorruption() { - Log.info("Triggering a prank: this will cause players to see two error messages claiming to be from a memory corruption, and cause a flickering amount of fissile matter and dormant cysts to be put in the core."); - players_1.FishPlayer.messageStaff("[gray]<[cyan]staff[gray]> [white]Activating memory corruption prank! (please don't ruin it by telling players what is happening, pretend you dont know)"); - api.sendModerationMessage("Activated memory corruption prank on server ".concat(Vars.state.rules.mode().name())); - var t1f = false; - var t2f = false; - globals_1.fishState.corruption_t1 = Timer.schedule(function () { - var _a; - t1f = !t1f; - (_a = Vars.state.rules.defaultTeam.items()) === null || _a === void 0 ? void 0 : _a.set(Items.dormantCyst, t1f ? 69 : 420); - }, 0, 0.4, 600); - globals_1.fishState.corruption_t2 = Timer.schedule(function () { - var _a; - t2f = !t2f; - (_a = Vars.state.rules.defaultTeam.items()) === null || _a === void 0 ? void 0 : _a.set(Items.fissileMatter, t2f ? 999 : 123); - }, 0, 1.5, 200); - var hexString = Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, "0"); - Call.sendMessage("[scarlet]Error: internal server error."); - Call.sendMessage("[scarlet]Error: memory corruption: mindustry.world.modules.ItemModule@".concat(hexString)); - globals_1.FishEvents.fire("memoryCorruption", []); -} -function getEnemyTeam() { - if (config_1.Gamemode.pvp()) - return Team.derelict; - else - return Vars.state.rules.waveTeam; -} -function neutralGameover() { - players_1.FishPlayer.ignoreGameover(function () { - Events.fire(new EventType.GameOverEvent(getEnemyTeam())); - }); -} -/** Please validate requestedWaves to ensure it is not huge */ -function skipWaves(requestedWaves, runIntermediateWaves) { - var winWave = Vars.state.rules.winWave; - if (winWave <= 0) - winWave = Infinity; - var wavesToSkip = Math.min(requestedWaves, winWave - Vars.state.wave); - if (runIntermediateWaves) { - for (var i = 0; i < wavesToSkip; i++) { - Vars.logic.skipWave(); - } - } - else { - Vars.state.wave += (wavesToSkip - 1); - Vars.logic.skipWave(); - } -} -function logHTrip(player, name, message) { - Log.warn("&yPlayer &b\"".concat(player.cleanedName, "\"&y (&b").concat(player.uuid, "&y/&b").concat(player.ip(), "&y) tripped &c").concat(name, "&y") + (message ? ": ".concat(message) : "")); - players_1.FishPlayer.messageStaff("[yellow]Player [blue]\"".concat(player.cleanedName, "\"[] tripped [cyan]").concat(name, "[]") + (message ? ": ".concat(message) : "")); - api.sendModerationMessage("Player `".concat(player.cleanedName, "` (`").concat(player.uuid, "`/`").concat(player.ip(), "`) tripped **").concat(name, "**").concat(message ? ": ".concat(message) : "", "\n**Server:** ").concat(config_1.Gamemode.name())); -} -function setType(input) { - //does not do any checking -} -function untilForever() { - return (globals_1.maxTime - Date.now() - 10000); -} -function colorNumber(number, getColor, side) { - if (side === void 0) { side = "client"; } - return getColor(number) + number.toString() + (side == "client" ? "[]" : "&fr"); -} -function formatRatekeeper(x) { - if (x.lastTime <= 1) - return "0"; - return "".concat(x.occurences, " / ").concat(formatTimeRelative(x.lastTime, true)); -} -function getAntiBotInfo(side) { - var color = side == "client" ? "[acid]" : "&ly"; - var True = side == "client" ? "[red]true[]" : "&lrtrue"; - var False = side == "client" ? "[green]false[]" : "&gfalse"; - return ("".concat(color, "Flag count: ").concat(formatRatekeeper(players_1.FishPlayer.autoflagRate), "\n").concat(color, "Autobanning flagged players: ").concat(players_1.FishPlayer.shouldWhackFlaggedPlayers() ? True : False, "\n").concat(color, "Kicking new players: ").concat(players_1.FishPlayer.shouldKickNewPlayers() ? True : False, "\n").concat(color, "Recent connect packets: ").concat(formatRatekeeper(players_1.FishPlayer.connectRate), "\n").concat(color, "Reason: ").concat(players_1.FishPlayer.lastAntibotReason)); -} -var failPrefix = "[scarlet]\u26A0 [yellow]"; -var successPrefix = "[#48e076]\uE800 "; -function outputFail(message, sender, ratelimit) { - var msg = failPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[yellow]") : message); - if (ratelimit) - sender.sendMessage(msg, ratelimit); - else - sender.sendMessage(msg); -} -function outputSuccess(message, sender) { - sender.sendMessage(successPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[#48e076]") : message)); -} -function outputMessage(message, sender) { - sender.sendMessage(((typeof message == "function" && "__partialFormatString" in message ? message(null) : message) + "").replace(/\t/g, " ".repeat(4))); -} -function outputConsole(message, channel) { - if (channel === void 0) { channel = Log.info; } - channel(typeof message == "function" && "__partialFormatString" in message ? message("") : message); -} -function updateBans(message) { - Groups.player.each(function (player) { - if (Vars.netServer.admins.isIDBanned(player.uuid())) { - player.con.kick(Packets.KickReason.banned); - if (message) - Call.sendMessage(message(player)); - } - }); -} -function processChat(player, message, effects) { - if (effects === void 0) { effects = false; } - var fishPlayer = players_1.FishPlayer.get(player); - var highlight = fishPlayer.highlight; - var filterTripText; - var suspicious = fishPlayer.suspicionLevel() == 3; - if ((!fishPlayer.hasPerm("bypassChatFilter") || fishPlayer.chatStrictness == "strict") - && (filterTripText = matchFilter(message, fishPlayer.chatStrictness, suspicious))) { - if (effects) { - if (suspicious && removeFoosChars(message).split(" ") - .map(function (w) { return w.replace(/[-_.^*,]/g, ""); }) - .some(function (w) { return config_1.bannedWords.autoWhack.includes(w); })) { - if (!fishPlayer.muted) { - logHTrip(fishPlayer, "bad words in chat", "message: `".concat(message, "`")); - fishPlayer.muted = true; - void fishPlayer.stop("automod", globals_1.maxTime, "Automatic stop due to suspicious activity", false); - } - } - Log.info("Censored message from player ".concat(player.name, ": \"").concat((0, funcs_1.escapeStringColorsServer)(message), "\"; contained \"").concat(filterTripText, "\"")); - players_1.FishPlayer.messageStaff("[yellow]Censored message from player ".concat(fishPlayer.cleanedName, ": \"").concat(message, "\" contained \"").concat(filterTripText, "\"")); - } - message = config_1.text.chatFilterReplacement.message(); - highlight !== null && highlight !== void 0 ? highlight : (highlight = config_1.text.chatFilterReplacement.highlight()); - } - if (message.startsWith("./")) - message = message.replace("./", "/"); - if (!fishPlayer.hasPerm("chat")) { - if (effects) { - players_1.FishPlayer.messageMuted(player.name, message); - Log.info("".concat(player.name, ": ").concat(message)); - } - return null; - } - return (highlight !== null && highlight !== void 0 ? highlight : "") + message; -} -var replacements = [ - //Serpulo units - ["dagger", "mace", "fortress", "scepter", "reign", "nova", "pulsar", "quasar", "vela", "corvus", "crawler", "atrax", "spiroct", "arkyid", "toxopid", "flare", "horizon", "zenith", "antumbra", "eclipse", "mono", "poly", "mega", "quad", "oct", "risso", "minke", "bryde", "sei", "omura", "retusa", "oxynoe", "cyerce", "aegires", "navanax", "fort", "toxo", "flarogus"], - //Erekir units - ["stell", "locus", "precept", "vanquish", "conquer", "merui", "cleroi", "anthicus", "tecta", "collaris", "elude", "avert", "obviate", "quell", "disrupt", "vanq", "crab", "anthi", "larry", "obvi"], - //Items, full form - ["copper", "lead", "metaglass", "graphite", "sand", "coal", "titanium", "thorium", "scrap", "silicon", "plastanium", "phase fabric", "surge alloy", "spore pod", "blast compound", "pyratite", "beryllium", "tungsten", "oxide", "carbide"], - //Items, short form - ["coppa", "meta", "graph", "tita", "titan", "thor", "scrap", "sili", "plast", "phase", "surge", "spore", "blast", "pyra", "beryl", "tung", "oxide", "carb"], - //Liquids - ["water", "slag", "oil", "cryo", "cryofluid"], - //Liquids/gases (erekir) - ["hydrogen", "ozone", "nitrogen", "cyanogen", "cyan", "nitro", "hydro", "arky", "arkycite", "neoplasm"], - //Gamemodes - ["attack", "sandbox", "pvp", "hexed", "survival"], - //teams - ["crux", "sharded", "malis", "neoplastic"], - //maps - ["rampant", "harbor war", "cave canal", "acheron", "wolframfestung", "avast", "fallen omura", "assault"], - //aquatic animals - ["fish", "shark", "whale", "dolphin", "salmon", "tuna", "squid", "jellyfish", "turtle"], - //antonym adjectives - ["fast", "slow"], ["big", "little"], ["hot", "cold"], ["hard", "easy", "difficult", "ez"], ["hello", "bye"], -].map(function (set) { return [set, new RegExp("\\b(?:".concat(set.join("|"), ")(e?s?(?:i?gone)?)\\b"), 'g')]; }); -var foolCounter = 0; -exports.foolifyChat = memoizeChatFilter(function foolifyChat(message) { - var e_5, _a; - var cleanedMessage = removeFoosChars(message); - setShuffle: { - if (foolCounter < 8) { - //Skip the next 5 messages no matter what - foolCounter++; - break setShuffle; - } - var replacedMessage = cleanedMessage; - var _loop_2 = function (set, regex) { - replacedMessage = replacedMessage.replace(regex, function (_, plural) { return (0, funcs_1.random)(set) + plural; }); - }; - try { - for (var replacements_1 = __values(replacements), replacements_1_1 = replacements_1.next(); !replacements_1_1.done; replacements_1_1 = replacements_1.next()) { - var _b = __read(replacements_1_1.value, 2), set = _b[0], regex = _b[1]; - _loop_2(set, regex); - } - } - catch (e_5_1) { e_5 = { error: e_5_1 }; } - finally { - try { - if (replacements_1_1 && !replacements_1_1.done && (_a = replacements_1.return)) _a.call(replacements_1); - } - finally { if (e_5) throw e_5.error; } - } - if (replacedMessage !== cleanedMessage) { - if (foolCounter < 11) { - //Skip the next 2 messages that would get altered - foolCounter++; - break setShuffle; - } - foolCounter = 0; - return replacedMessage; - } - else { - break setShuffle; - } - } - if (Math.random() < 0.01) { - return cleanedMessage.split("").reverse().join(""); - // eslint-disable-next-line no-dupe-else-if - } - else if (Math.random() < 0.01) { - return "[scarlet]I really hope everyone is having a fun time :} <3"; - } - else if (Math.random() < 0.005) { - return "[cyan]AMOGUS"; - } - else { - return message; - } -}); -exports.addToTileHistory = logErrors("Error while saving a tilelog entry", function (e) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3; - // eslint-disable-next-line prefer-const - var tile, uuid, action, type, time = Date.now(); - if (e instanceof EventType.BlockBuildBeginEvent) { - tile = e.tile; - uuid = (_e = (_c = (_b = (_a = e.unit) === null || _a === void 0 ? void 0 : _a.player) === null || _b === void 0 ? void 0 : _b.uuid()) !== null && _c !== void 0 ? _c : (_d = e.unit) === null || _d === void 0 ? void 0 : _d.type.name) !== null && _e !== void 0 ? _e : "unknown"; - if (e.breaking) { - action = "broke"; - type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.previous.name : "unknown"; - if (((_g = (_f = e.unit) === null || _f === void 0 ? void 0 : _f.player) === null || _g === void 0 ? void 0 : _g.uuid()) && ((_h = e.tile.build) === null || _h === void 0 ? void 0 : _h.team) != Team.derelict) { - var fishP = players_1.FishPlayer.get(e.unit.player); - //TODO move this code - fishP.tstats.blocksBroken++; - fishP.tstats.blockInteractionsThisMap++; - fishP.updateStats(function (stats) { return stats.blocksBroken++; }); - } - } - else { - action = "built"; - type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.current.name : "unknown"; - if ((_k = (_j = e.unit) === null || _j === void 0 ? void 0 : _j.player) === null || _k === void 0 ? void 0 : _k.uuid()) { - var fishP = players_1.FishPlayer.get(e.unit.player); - //TODO move this code - fishP.updateStats(function (stats) { return stats.blocksPlaced++; }); - fishP.tstats.blockInteractionsThisMap++; - } - } - } - else if (e instanceof EventType.ConfigEvent) { - tile = e.tile.tile; - uuid = (_m = (_l = e.player) === null || _l === void 0 ? void 0 : _l.uuid()) !== null && _m !== void 0 ? _m : "unknown"; - if (uuid != "unknown") { - var fishP = players_1.FishPlayer.getById(uuid); - if (fishP) - fishP.tstats.blockInteractionsThisMap++; - } - action = "configured"; - type = e.tile.block.name; - } - else if (e instanceof EventType.BuildRotateEvent) { - tile = e.build.tile; - uuid = (_s = (_q = (_p = (_o = e.unit) === null || _o === void 0 ? void 0 : _o.player) === null || _p === void 0 ? void 0 : _p.uuid()) !== null && _q !== void 0 ? _q : (_r = e.unit) === null || _r === void 0 ? void 0 : _r.type.name) !== null && _s !== void 0 ? _s : "unknown"; - if (uuid != "unknown") { - var fishP = players_1.FishPlayer.getById(uuid); - if (fishP) - fishP.tstats.blockInteractionsThisMap++; - } - action = "rotated"; - type = e.build.block.name; - } - else if (e instanceof EventType.UnitDestroyEvent) { - tile = e.unit.tileOn(); - if (!tile) - return; - if (!e.unit.type.playerControllable) - return; - uuid = e.unit.isPlayer() ? e.unit.getPlayer().uuid() : (_t = e.unit.lastCommanded) !== null && _t !== void 0 ? _t : "unknown"; - action = "killed"; - type = e.unit.type.name; - } - else if (e instanceof EventType.BlockDestroyEvent) { - if (config_1.Gamemode.attack() && ((_u = e.tile.build) === null || _u === void 0 ? void 0 : _u.team) != Vars.state.rules.defaultTeam) - return; //Don't log destruction of enemy blocks - tile = e.tile; - uuid = "[[something]"; - action = "killed"; - type = (_w = (_v = e.tile.block()) === null || _v === void 0 ? void 0 : _v.name) !== null && _w !== void 0 ? _w : "air"; - } - else if (e instanceof EventType.PayloadDropEvent) { - action = "pay-dropped"; - var controller = e.carrier.controller(); - uuid = (_z = (_y = (_x = e.carrier.player) === null || _x === void 0 ? void 0 : _x.uuid()) !== null && _y !== void 0 ? _y : (controller instanceof LogicAI && controller.controller ? - "".concat(e.carrier.type.name, " controlled by ").concat(controller.controller.block.name, " at ").concat(controller.controller.tileX(), ",").concat(controller.controller.tileY(), " last accessed by ").concat(e.carrier.getControllerName()) - : null)) !== null && _z !== void 0 ? _z : e.carrier.type.name; - if (e.build) { - tile = e.build.tile; - type = e.build.block.name; - } - else if (e.unit) { - tile = e.unit.tileOn(); - if (!tile) - return; - type = e.unit.type.name; - } - else - return; - } - else if (e instanceof EventType.PickupEvent) { - action = "picked up"; - if (e.carrier.isPlayer()) - return; //This event would have been handled by actionfilter - var controller = e.carrier.controller(); - if (!(controller instanceof LogicAI && controller.controller != null)) - return; - uuid = "".concat(e.carrier.type.name, " controlled by ").concat(controller.controller.block.name, " at ").concat(controller.controller.tileX(), ",").concat(controller.controller.tileY(), " last accessed by ").concat(e.carrier.getControllerName()); - if (e.build) { - tile = e.build.tile; - type = e.build.block.name; - } - else if (e.unit) { - tile = e.unit.tileOn(); - if (!tile) - return; - type = e.unit.type.name; - } - else - return; - } - else if (e instanceof EventType.UnitControlEvent) { - if (e.unit instanceof Packages.mindustry.gen.BlockUnitUnit) { - action = "controlled"; - tile = (_0 = e.unit) === null || _0 === void 0 ? void 0 : _0.tile().tile; - if (!tile) - return; - type = (_2 = (_1 = tile.block()) === null || _1 === void 0 ? void 0 : _1.name) !== null && _2 !== void 0 ? _2 : "air"; - uuid = e.player.uuid(); - } - else - return; - } - else if (e instanceof Object && "pos" in e && "uuid" in e && "action" in e && "type" in e) { - var pos = void 0; - (pos = e.pos, uuid = e.uuid, action = e.action, type = e.type); - tile = (_3 = Vars.world.tile(pos.split(",")[0], pos.split(",")[1])) !== null && _3 !== void 0 ? _3 : (0, funcs_1.crash)("Cannot log ".concat(action, " at ").concat(pos, ": Nonexistent tile")); - } - else - return; - if (tile == null) - return; - [tile, uuid, action, type, time]; - tile.getLinkedTiles(function (t) { - var pos = "".concat(t.x, ",").concat(t.y); - var existingData = globals_1.tileHistory[pos] ? funcs_1.StringIO.read(globals_1.tileHistory[pos], function (str) { return str.readArray(function (d) { return ({ - action: d.readString(2), - uuid: d.readString(3), - time: d.readNumber(16), - type: d.readString(2), - }); }, 1); }) : []; - existingData.push({ - action: action, - uuid: uuid, - time: time, - type: type - }); - existingData = existingData.slice(-9); - //Write - globals_1.tileHistory[t.x + ',' + t.y] = funcs_1.StringIO.write(existingData, function (str, data) { return str.writeArray(data, function (el) { - str.writeString(el.action, 2); - str.writeString(el.uuid, 3); - str.writeNumber(el.time, 16); - str.writeString(el.type, 2); - }, 1); }); - }); -}); -function tilelogAndResetAfk(e) { - (0, exports.addToTileHistory)(e); - players_1.FishPlayer.get(e.unit.player).lastActive = Date.now(); -} -; -function getIPRange(input, error) { - if (globals_1.ipRangeCIDRPattern.test(input)) { - var _a = __read(input.split("/"), 2), ip = _a[0], maskLength = _a[1]; - switch (maskLength) { - case "24": - return ip.split(".").slice(0, 3).join(".") + "."; - case "16": - return ip.split(".").slice(0, 2).join(".") + "."; - default: - error === null || error === void 0 ? void 0 : error("Mindustry does not currently support netmasks other than /16 and /24"); - return null; - } - } - else if (globals_1.ipRangeWildcardPattern.test(input)) { - //1.2.3.* - //1.2.* - var _b = __read(input.split("."), 4), a = _b[0], b = _b[1], c = _b[2], d = _b[3]; - if (c !== "*") - return "".concat(a, ".").concat(b, ".").concat(c, "."); - return "".concat(a, ".").concat(b, "."); - } - else - return null; -} -//this brings me physical pain -function getHash(file, algorithm) { - if (algorithm === void 0) { algorithm = "SHA-1"; } - try { - var header = "blob ".concat(file.length(), "\0"); - var fileSHAHeader = Packages.java.nio.charset.StandardCharsets.UTF_8.encode(header); - var contents = file.readBytes(); - var buffer = Packages.java.nio.ByteBuffer.allocate(fileSHAHeader.remaining() + contents.length); - buffer.put(fileSHAHeader); - buffer.put(contents); - buffer.flip(); - var digest = Packages.java.security.MessageDigest.getInstance(algorithm); - digest.update(buffer); - return digest.digest().map(function (byte) { - return (byte & 0xFF).toString(16).padStart(2, "0"); - }).join(""); - } - catch (e) { - Log.err("Cannot generate ".concat(algorithm, ", ").concat(String(e))); - return undefined; - } -} -function match(value, clauses, defaultValue) { - return Object.prototype.hasOwnProperty.call(clauses, value) ? clauses[value] : defaultValue; -} -/** @throws CommandError */ -function fishCommandsRootDirPath() { - var commandsDir = Vars.modDirectory.child("fish-commands"); - if (!commandsDir.exists()) - (0, commands_1.fail)("Fish commands directory at path ".concat(commandsDir.absolutePath(), " does not exist!")); - var fishCommandsRootDirPath = Paths.get(commandsDir.file().path); - if (Packages.java.nio.file.Files.isSymbolicLink(fishCommandsRootDirPath)) { - //fish-commands is linked to the build directory of somewhere else - //resolve and get the parent directory of the build directory - fishCommandsRootDirPath = fishCommandsRootDirPath.toRealPath().getParent(); - } - return fishCommandsRootDirPath; -} -/** Fails if "mode" is invalid. */ -function applyEffectMode(mode, unit, ticks) { - var e_6, _a; - var _b; - var modes = { - fast: [StatusEffects.fast], - fast2: [StatusEffects.fast, StatusEffects.overdrive, StatusEffects.overclock], - boss: [StatusEffects.boss], - health: [StatusEffects.boss, StatusEffects.shielded], - slow: [StatusEffects.slow], - slow2: [ - StatusEffects.slow, - StatusEffects.freezing, - StatusEffects.wet, - StatusEffects.muddy, - StatusEffects.sapped, - StatusEffects.sporeSlowed, - StatusEffects.electrified, - StatusEffects.tarred, - ], - freeze: [StatusEffects.unmoving], - disarm: [StatusEffects.disarmed], - invincible: [StatusEffects.invincible], - boost: [ - StatusEffects.fast, - StatusEffects.overdrive, - StatusEffects.overclock, - StatusEffects.boss, - StatusEffects.shielded, - ], - damage: [ - StatusEffects.burning, - StatusEffects.freezing, - StatusEffects.wet, - StatusEffects.muddy, - StatusEffects.melting, - StatusEffects.sapped, - StatusEffects.tarred, - StatusEffects.shocked, - StatusEffects.blasted, - StatusEffects.corroded, - StatusEffects.sporeSlowed, - StatusEffects.electrified, - StatusEffects.fast, - ], - clear: function (unit) { - unit.clearStatuses(); - unit.maxHealth = unit.type.health; - }, - paper: function (unit) { - unit.health = 1; - unit.maxHealth = 1; - unit.apply(StatusEffects.disarmed, Number.MAX_VALUE / 2); - }, - heal: function (unit) { - unit.health = unit.maxHealth; - }, - overheal: function (unit) { - unit.maxHealth = unit.health = 1e15; - }, - shield: function (unit) { - unit.shield = 1e15; - } - }; - var effects = (_b = match(mode, modes, null)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Invalid mode. Supported modes: ".concat(Object.keys(modes).join(", "))); - if (typeof effects === "function") { - effects(unit); - } - else { - try { - for (var effects_1 = __values(effects), effects_1_1 = effects_1.next(); !effects_1_1.done; effects_1_1 = effects_1.next()) { - var effect = effects_1_1.value; - unit.apply(effect, ticks); - } - } - catch (e_6_1) { e_6 = { error: e_6_1 }; } - finally { - try { - if (effects_1_1 && !effects_1_1.done && (_a = effects_1.return)) _a.call(effects_1); - } - finally { if (e_6) throw e_6.error; } - } - } -} -function handleError(err, sender, outputFail, context) { - if (err instanceof commands_1.CommandError) { - //If the error is a command error, then just outputFail - outputFail(err.data, sender); - } - else if (err === menus_1.Cancel) { - //Menu cancelled, do nothing - return; - } - else { - sender.sendMessage("[scarlet]\u274C An error occurred while executing the command!"); - if (sender.hasPerm("seeErrorMessages")) - sender.sendMessage((0, funcs_1.parseError)(err)); - Log.err(context ? - "Unhandled error in command execution: ".concat(context) - : "Unhandled error in command execution."); - Log.err(err); - if (typeof err == "object" && err != null && "stack" in err) - Log.err(err.stack); - } -} -var sources = [ - Packages.mindustry.gen.UnitEntity, - Packages.mindustry.gen.MechUnit, - Packages.mindustry.gen.LegsUnit, - Packages.mindustry.gen.CrawlUnit, - Packages.mindustry.gen.UnitWaterMove, - Packages.mindustry.gen.BlockUnitUnit, - Packages.mindustry.gen.ElevationMoveUnit, - Packages.mindustry.gen.BuildingTetherPayloadUnit, - Packages.mindustry.gen.TimedKillUnit, - Packages.mindustry.gen.PayloadUnit, - Packages.mindustry.gen.TankUnit, -]; -function getStatuses(unit) { - var e_7, _a; - try { - for (var sources_1 = __values(sources), sources_1_1 = sources_1.next(); !sources_1_1.done; sources_1_1 = sources_1.next()) { - var clazz = sources_1_1.value; - if (unit instanceof clazz) - return ArcReflect.get(clazz, unit, "statuses"); - } - } - catch (e_7_1) { e_7 = { error: e_7_1 }; } - finally { - try { - if (sources_1_1 && !sources_1_1.done && (_a = sources_1.return)) _a.call(sources_1); - } - finally { if (e_7) throw e_7.error; } - } - return new Seq(); -} +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains many utility functions that need access to any values from other files. +For functions that don't need values from other files, see funcs.ts. +*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tilelogAndResetAfk = exports.addToTileHistory = exports.foolifyChat = exports.getMap = exports.getUnitType = exports.getItem = void 0; +exports.memoizeChatFilter = memoizeChatFilter; +exports.formatTime = formatTime; +exports.formatTimeShort = formatTimeShort; +exports.formatModeName = formatModeName; +exports.formatTimestampFull = formatTimestampFull; +exports.formatTimestamp = formatTimestamp; +exports.formatTimestampShort = formatTimestampShort; +exports.formatTimeRelative = formatTimeRelative; +exports.getColor = getColor; +exports.nearbyEnemyTile = nearbyEnemyTile; +exports.getTeam = getTeam; +exports.matchFilter = matchFilter; +exports.removeFoosChars = removeFoosChars; +exports.cleanText = cleanText; +exports.isImpersonator = isImpersonator; +exports.logAction = logAction; +exports.parseTimeString = parseTimeString; +exports.serverRestartLoop = serverRestartLoop; +exports.restartNow = restartNow; +exports.isBuildable = isBuildable; +exports.isMapValidForGamemode = isMapValidForGamemode; +exports.getBlock = getBlock; +exports.teleportPlayer = teleportPlayer; +exports.logErrors = logErrors; +exports.definitelyRealMemoryCorruption = definitelyRealMemoryCorruption; +exports.getEnemyTeam = getEnemyTeam; +exports.neutralGameover = neutralGameover; +exports.skipWaves = skipWaves; +exports.logHTrip = logHTrip; +exports.setType = setType; +exports.untilForever = untilForever; +exports.colorNumber = colorNumber; +exports.formatRatekeeper = formatRatekeeper; +exports.getAntiBotInfo = getAntiBotInfo; +exports.outputFail = outputFail; +exports.outputSuccess = outputSuccess; +exports.outputMessage = outputMessage; +exports.outputConsole = outputConsole; +exports.updateBans = updateBans; +exports.processChat = processChat; +exports.getIPRange = getIPRange; +exports.getHash = getHash; +exports.match = match; +exports.fishCommandsRootDirPath = fishCommandsRootDirPath; +exports.applyEffectMode = applyEffectMode; +exports.handleError = handleError; +exports.getStatuses = getStatuses; +var api = __importStar(require("/api")); +var config_1 = require("/config"); +var commands_1 = require("/frameworks/commands"); +var menus_1 = require("/frameworks/menus"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var players_1 = require("/players"); +function memoizeChatFilter(impl) { + var lastCleanedInput = null; + var lastOutput = null; + return function memoized(input) { + var cleanedInput = removeFoosChars(input); + if (cleanedInput === lastCleanedInput) + return lastOutput; + lastCleanedInput = cleanedInput; + return lastOutput = impl(input); + }; +} +function formatTime(time) { + if (globals_1.maxTime - (time + Date.now()) < 20000) + return "forever"; + if (isNaN(time)) + return "N/A"; + var months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); + var days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); + var hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + var minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); + var seconds = Math.floor((time % (60 * 1000)) / (1000)); + return [ + months && "".concat(months, " month").concat(months != 1 ? "s" : ""), + days && "".concat(days, " day").concat(days != 1 ? "s" : ""), + hours && "".concat(hours, " hour").concat(hours != 1 ? "s" : ""), + minutes && "".concat(minutes, " minute").concat(minutes != 1 ? "s" : ""), + (seconds || time < 1000) && "".concat(seconds, " second").concat(seconds != 1 ? "s" : ""), + ].filter(Boolean).join(", "); +} +function formatTimeShort(time) { + if (globals_1.maxTime - (time + Date.now()) < 20000) + return "forever"; + if (isNaN(time)) + return "N/A"; + var months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); + var days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); + var hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + var minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); + var seconds = Math.floor((time % (60 * 1000)) / (1000)); + return [ + months && "".concat(months, "mo"), + days && "".concat(days, "d"), + hours && "".concat(hours, "h"), + minutes && "".concat(minutes, "m"), + (seconds || time < 1000) && "".concat(seconds, "s"), + ].filter(Boolean).join(" "); +} +//TODO move this data to be right next to Mode +function formatModeName(name) { + return { + "attack": "Attack", + "survival": "Survival", + "hexed": "Hexed", + "pvp": "PVP", + "sandbox": "Sandbox", + "hardcore": "Hardcore", + "testsrv": "Testing Server", + "minigame": "Minigames", + }[name]; +} +function formatTimestampFull(time) { + var date = new Date(time); + return "".concat(date.toDateString(), ", ").concat(date.toTimeString()); +} +function formatTimestamp(time) { + return new Date(time).toLocaleString(); +} +function formatTimestampShort(time) { + var date = new Date(time); + return "".concat(date.getFullYear(), "-").concat(date.getMonth() + 1, "-").concat(date.getDate(), " ").concat(date.getHours(), ":").concat(date.getMinutes()); +} +function formatTimeRelative(time, raw) { + var difference = Math.abs(time - Date.now()); + if (difference < 1000) + return "just now"; + else if (time > Date.now()) + return (raw ? "" : "in ") + formatTime(difference); + else + return formatTime(difference) + (raw ? "" : " ago"); +} +/** Attempts to parse a Color from the input. */ +function getColor(input) { + try { + if (input.includes(',')) { + var formattedColor = input.split(','); + var col = { + r: Number(formattedColor[0]), + g: Number(formattedColor[1]), + b: Number(formattedColor[2]), + a: 255, + }; + return new Color(col.r, col.g, col.b, col.a); + } + else if (input.includes('#')) { + return Color.valueOf(input); + } + else if ((function (input) { return input in Color; })(input)) { + return Color[input]; + } + else { + return null; + } + } + catch (e) { + return null; + } +} +/** Searches for an enemy tile near a unit. */ +function nearbyEnemyTile(unit, dist) { + //because the indexer is buggy + if (dist > 10) + (0, funcs_1.crash)("nearbyEnemyTile(): dist (".concat(dist, ") is too high!")); + var x = Math.floor(unit.x / Vars.tilesize); + var y = Math.floor(unit.y / Vars.tilesize); + for (var i = -dist; i <= dist; i++) { + for (var j = -dist; j <= dist; j++) { + var build = Vars.world.build(x + i, y + j); + if (build && build.team != unit.team && build.team != Team.derelict) + return build; + } + } + return null; +} +/** Attempts to parse a Team from the input. */ +function getTeam(team) { + if (team in Team && Team[team] instanceof Team) + return Team[team]; + else if (Team.baseTeams.find(function (t) { return t.name.includes(team.toLowerCase()); })) + return Team.baseTeams.find(function (t) { return t.name.includes(team.toLowerCase()); }); + else if (!isNaN(Number(team))) + return "\"".concat(team, "\" is not a valid team string. Did you mean \"#").concat(team, "\"?"); + else if (!isNaN(Number(team.slice(1)))) { + var num = Number(team.slice(1)); + if (num <= 255 && num >= 0 && Number.isInteger(num)) + return Team.all[Number(team.slice(1))]; + else + return "Team ".concat(team, " is outside the valid range (integers 0-255)."); + } + return "\"".concat(team, "\" is not a valid team string."); +} +/** Attempts to parse an Item from the input. */ +exports.getItem = (0, funcs_1.searchFixed)(Vars.content.items().toArray(), [ + function (i, s) { return i.name == s; }, + function (i, s) { return i.name == s.toLowerCase(); }, + function (i, s) { return i.name.includes(s.toLowerCase()); }, + function (i, s) { return i.name.includes(s.toLowerCase().replace(" ", "-")); }, + function (i, s) { return i.emoji() == s; }, +]); +/** + * @param wordList "chat" is least strict, followed by "strict", and "name" is most strict. + * @returns a + */ +function matchFilter(input, wordList, aggressive) { + var e_1, _a, e_2, _b; + if (wordList === void 0) { wordList = "chat"; } + if (aggressive === void 0) { aggressive = false; } + var currentBannedWords = [ + wordList == "name" ? config_1.bannedWords.normal.filter(function (w) { return w[0] !== "uwu"; }) : config_1.bannedWords.normal, + (wordList == "strict" || wordList == "name") && config_1.bannedWords.strict, + wordList == "name" && config_1.bannedWords.names, + ].filter(Boolean).flat(); + if (aggressive) + currentBannedWords.push(["hitler", []]); + //Replace substitutions + var variations = [input, cleanText(input, false)]; + if (aggressive) + variations.push(cleanText(input, true)); + try { + for (var currentBannedWords_1 = __values(currentBannedWords), currentBannedWords_1_1 = currentBannedWords_1.next(); !currentBannedWords_1_1.done; currentBannedWords_1_1 = currentBannedWords_1.next()) { + var _c = __read(currentBannedWords_1_1.value, 2), banned = _c[0], whitelist = _c[1]; + var _loop_1 = function (text_1) { + if (banned instanceof RegExp ? banned.test(text_1) : text_1.includes(banned)) { + var modifiedText_1 = text_1; + whitelist.forEach(function (w) { return modifiedText_1 = modifiedText_1.replace(new RegExp(w, "g"), ""); }); //Replace whitelisted words with nothing + if (banned instanceof RegExp ? banned.test(modifiedText_1) : modifiedText_1.includes(banned)) //If the text still matches, fail + return { value: (banned === globals_1.uuidPattern ? "a Mindustry UUID" : + banned === globals_1.ipPattern || banned === globals_1.ipPortPattern ? "an IP address" : + //parsing regex with regex, massive hack + banned instanceof RegExp ? banned.source.replace(/\\b|\(\?|||>", "Name contains >|||> which is reserved for the server owner"], + "\uE817", "\uE82C", "\uE88E", "\uE813", + [/^[<\uE825].{1,3}[>\uE83A]/, "Name contains a prefix such as which is used for role prefixes"], + [function (replacedText) { return !isAdmin && config_1.adminNames.includes(replacedText.replace(/ /g, "")); }, "One of our admins uses this name"] + ]); + try { + for (var filters_1 = __values(filters), filters_1_1 = filters_1.next(); !filters_1_1.done; filters_1_1 = filters_1.next()) { + var _b = __read(filters_1_1.value, 2), check = _b[0], message = _b[1]; + if (check(replacedText)) + return message; + if (check(antiEvasionText)) + return message; + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (filters_1_1 && !filters_1_1.done && (_a = filters_1.return)) _a.call(filters_1); + } + finally { if (e_3) throw e_3.error; } + } + return false; +} +function logAction(action, by, to, reason, duration) { + if (by === undefined) { //overload 1 + api.sendModerationMessage("".concat(action, "\n**Server:** ").concat(config_1.Gamemode.name())); + return; + } + if (to === undefined) { //overload 2 + api.sendModerationMessage("".concat((0, funcs_1.escapeTextDiscord)(Strings.stripColors(by.name)), " ").concat(action, "\n**Server:** ").concat(config_1.Gamemode.name())); + return; + } + if (to) { //overload 3 + var name = void 0, uuid = void 0, ip = void 0; + var actor = typeof by === "string" ? by : (0, funcs_1.escapeTextDiscord)(Strings.stripColors(by.name)); + if (to instanceof players_1.FishPlayer) { + name = (0, funcs_1.escapeTextDiscord)(to.name); + uuid = to.uuid; + ip = to.ip(); + } + else if (typeof to == "string") { + if (globals_1.uuidPattern.test(to)) { + name = "[".concat(to, "]"); + uuid = to; + ip = "[unknown]"; + } + else { + name = to; + uuid = "[unknown]"; + ip = "[unknown]"; + } + } + else { + name = (0, funcs_1.escapeTextDiscord)(to.lastName); + uuid = to.id; + ip = to.lastIP; + } + api.sendModerationMessage("".concat(actor, " ").concat(action, " ").concat(name, " ").concat(duration ? "for ".concat(formatTime(duration), " ") : "").concat(reason ? "with reason ".concat((0, funcs_1.escapeTextDiscord)(reason)) : "", "\n**Server:** ").concat(config_1.Gamemode.name(), "\n**uuid:** `").concat(uuid, "`\n**ip**: `").concat(ip, "`")); + return; + } +} +/** @returns the number of milliseconds. */ +function parseTimeString(str) { + var e_4, _a; + var formats = [ + [/(\d+)s/, 1], + [/(\d+)m/, 60], + [/(\d+)h/, 3600], + [/(\d+)d/, 86400], + [/(\d+)w/, 604800] + ].map(function (_a) { + var _b = __read(_a, 2), regex = _b[0], mult = _b[1]; + return [Pattern.compile(regex.source), mult]; + }); + if (str == "forever") + return (globals_1.maxTime - Date.now() - 10000); + try { + for (var formats_1 = __values(formats), formats_1_1 = formats_1.next(); !formats_1_1.done; formats_1_1 = formats_1.next()) { + var _b = __read(formats_1_1.value, 2), pattern = _b[0], mult = _b[1]; + //rhino regex doesn't work + var matcher = pattern.matcher(str); + if (matcher.matches()) { + var num = Number(matcher.group(1)); + if (!isNaN(num)) + return (num * mult) * 1000; + } + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (formats_1_1 && !formats_1_1.done && (_a = formats_1.return)) _a.call(formats_1); + } + finally { if (e_4) throw e_4.error; } + } + return null; +} +/** + * Triggers the restart countdown. Execution always returns from this function. + * @param [fake=false] if set, server will not actually restart. + */ +function serverRestartLoop(sec, fake) { + if (fake === void 0) { fake = false; } + if (sec > 0) { + if (sec < 15 || sec % 5 == 0) + Call.sendMessage("[scarlet]Server restarting in: ".concat(sec)); + globals_1.fishState.restartLoopTask = Timer.schedule(function () { return serverRestartLoop(sec - 1); }, 1); + } + else if (!fake) { + restartNow(); + } +} +/** + * Actually restarts. Kicks all players. Execution always returns from this function. + * @param [removeSave=false] If set, save will be deleted instead of saved. Used to start a new game after the restart. + */ +function restartNow(removeSave) { + if (removeSave === void 0) { removeSave = false; } + Log.info("Restarting..."); + Vars.netServer.kickAll(Packets.KickReason.serverRestarting); + Vars.net.closeServer(); + Vars.state.set(GameState.State.menu); + var file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); + if (removeSave) { + Core.app.post(function () { + file.delete(); + Core.app.exit(); + }); + } + else { + Core.app.post(function () { + SaveIO.save(file); + Core.app.exit(); + }); + } +} +function isBuildable(block) { + return block == Blocks.powerVoid || (block.buildType != Blocks.air.buildType && !(block instanceof ConstructBlock)); +} +exports.getUnitType = (0, funcs_1.searchFixed)(function () { return Vars.content.units().select(function (u) { return !(u instanceof MissileUnitType || u.internal); }).toArray(); }, [ + function (u, q) { return u.name == q; }, + function (u, q) { return u.name.includes(q.toLowerCase()); }, +]); +/** The vanilla validation code doesn't work on servers */ +function isMapValidForGamemode(map) { + if (map.custom) + return true; //we assume that all custom maps are appropriate for the selected gamemode + var pvpMaps = ["Veins", "Glacier", "Passage"]; //Maps.pvpMaps + switch (Vars.state.rules.mode().name()) { + case "sandbox": + case "editor": return true; //sandbox can be played on any map + case "attack": + case "pvp": return pvpMaps.includes(map.name()); //technically the pvp maps are valid attack maps, since they have an (undefended) enemy core + case "survival": return !pvpMaps.includes(map.name()); + default: return false; //unreachable + } +} +exports.getMap = (0, funcs_1.searchFixed)(function () { return Vars.maps.all().select(isMapValidForGamemode).toArray(); }, [ + function (m, name) { return m.name().replace(/ /g, "_") === name; }, //exact match with spaces replaced + function (m, name) { return m.name().replace(/ /g, "_").toLowerCase() === name.toLowerCase(); }, //exact match with spaces replaced ignoring case + function (m, name) { return m.plainName().replace(/ /g, "_").toLowerCase() === name.toLowerCase(); }, //exact match with spaces replaced ignoring case and colors + function (m, name) { return m.plainName().toLowerCase().includes(name.toLowerCase()); }, //partial match ignoring case and colors + function (m, name) { return m.plainName().replace(/ /g, "_").toLowerCase().includes(name.toLowerCase()); }, //partial match with spaces replaced ignoring case and colors + function (m, name) { return m.plainName().replace(/ /g, "").toLowerCase().includes(name.toLowerCase()); }, //partial match with spaces removed ignoring case and colors + function (m, name) { return m.plainName().replace(/[^a-zA-Z]/gi, "").toLowerCase().includes(name.toLowerCase()); }, +], "recomputeOptions"); +//static cache +var buildableBlocks = null; +function getBlock(block, filter) { + buildableBlocks !== null && buildableBlocks !== void 0 ? buildableBlocks : (buildableBlocks = Vars.content.blocks().select(isBuildable)); + var check = { + buildable: function (b) { return isBuildable(b); }, + air: function (b) { return b == Blocks.air || isBuildable(b); }, + all: function (b) { return true; } + }[filter]; + var out; + if (block in Blocks && Blocks[block] instanceof Block && check(Blocks[block])) + return Blocks[block]; + else if ((out = Vars.content.blocks().find(function (t) { return t.name.includes(block.toLowerCase()) && check(t); }))) + return out; + else if ((out = Vars.content.blocks().find(function (t) { return t.name.replace(/-/g, "").includes(block.toLowerCase().replace(/ /g, "")) && check(t); }))) + return out; + else if (block.includes("airblast")) + return Blocks.blastDrill; + return "\"".concat(block, "\" is not a valid block."); +} +function teleportPlayer(player, to) { + Timer.schedule(function () { + var p = player.unit(); + var t = to.unit(); + if (p && t) { + p.set(t.x, t.y); + Call.setPosition(player.con, t.x, t.y); + Call.setCameraPosition(player.con, t.x, t.y); + } + }, 0, 0.016, 10); +} +function logErrors(message, func) { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + try { + return func.apply(void 0, __spreadArray([], __read(args), false)); + } + catch (err) { + Log.err(message); + Log.err((0, funcs_1.parseError)(err)); + } + }; +} +function definitelyRealMemoryCorruption() { + Log.info("Triggering a prank: this will cause players to see two error messages claiming to be from a memory corruption, and cause a flickering amount of fissile matter and dormant cysts to be put in the core."); + players_1.FishPlayer.messageStaff("[gray]<[cyan]staff[gray]> [white]Activating memory corruption prank! (please don't ruin it by telling players what is happening, pretend you dont know)"); + api.sendModerationMessage("Activated memory corruption prank on server ".concat(Vars.state.rules.mode().name())); + var t1f = false; + var t2f = false; + globals_1.fishState.corruption_t1 = Timer.schedule(function () { + var _a; + t1f = !t1f; + (_a = Vars.state.rules.defaultTeam.items()) === null || _a === void 0 ? void 0 : _a.set(Items.dormantCyst, t1f ? 69 : 420); + }, 0, 0.4, 600); + globals_1.fishState.corruption_t2 = Timer.schedule(function () { + var _a; + t2f = !t2f; + (_a = Vars.state.rules.defaultTeam.items()) === null || _a === void 0 ? void 0 : _a.set(Items.fissileMatter, t2f ? 999 : 123); + }, 0, 1.5, 200); + var hexString = Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, "0"); + Call.sendMessage("[scarlet]Error: internal server error."); + Call.sendMessage("[scarlet]Error: memory corruption: mindustry.world.modules.ItemModule@".concat(hexString)); + globals_1.FishEvents.fire("memoryCorruption", []); +} +function getEnemyTeam() { + if (config_1.Gamemode.pvp()) + return Team.derelict; + else + return Vars.state.rules.waveTeam; +} +function neutralGameover() { + players_1.FishPlayer.ignoreGameover(function () { + Events.fire(new EventType.GameOverEvent(getEnemyTeam())); + }); +} +/** Please validate requestedWaves to ensure it is not huge */ +function skipWaves(requestedWaves, runIntermediateWaves) { + var winWave = Vars.state.rules.winWave; + if (winWave <= 0) + winWave = Infinity; + var wavesToSkip = Math.min(requestedWaves, winWave - Vars.state.wave); + if (runIntermediateWaves) { + for (var i = 0; i < wavesToSkip; i++) { + Vars.logic.skipWave(); + } + } + else { + Vars.state.wave += (wavesToSkip - 1); + Vars.logic.skipWave(); + } +} +function logHTrip(player, name, message) { + Log.warn("&yPlayer &b\"".concat(player.cleanedName, "\"&y (&b").concat(player.uuid, "&y/&b").concat(player.ip(), "&y) tripped &c").concat(name, "&y") + (message ? ": ".concat(message) : "")); + players_1.FishPlayer.messageStaff("[yellow]Player [blue]\"".concat(player.cleanedName, "\"[] tripped [cyan]").concat(name, "[]") + (message ? ": ".concat(message) : "")); + api.sendModerationMessage("Player `".concat(player.cleanedName, "` (`").concat(player.uuid, "`/`").concat(player.ip(), "`) tripped **").concat(name, "**").concat(message ? ": ".concat(message) : "", "\n**Server:** ").concat(config_1.Gamemode.name())); +} +function setType(input) { + //does not do any checking +} +function untilForever() { + return (globals_1.maxTime - Date.now() - 10000); +} +function colorNumber(number, getColor, side) { + if (side === void 0) { side = "client"; } + return getColor(number) + number.toString() + (side == "client" ? "[]" : "&fr"); +} +function formatRatekeeper(x) { + if (x.lastTime <= 1) + return "0"; + return "".concat(x.occurences, " / ").concat(formatTimeRelative(x.lastTime, true)); +} +function getAntiBotInfo(side) { + var color = side == "client" ? "[acid]" : "&ly"; + var True = side == "client" ? "[red]true[]" : "&lrtrue"; + var False = side == "client" ? "[green]false[]" : "&gfalse"; + return ("".concat(color, "Flag count: ").concat(formatRatekeeper(players_1.FishPlayer.autoflagRate), "\n").concat(color, "Autobanning flagged players: ").concat(players_1.FishPlayer.shouldWhackFlaggedPlayers() ? True : False, "\n").concat(color, "Kicking new players: ").concat(players_1.FishPlayer.shouldKickNewPlayers() ? True : False, "\n").concat(color, "Recent connect packets: ").concat(formatRatekeeper(players_1.FishPlayer.connectRate), "\n").concat(color, "Reason: ").concat(players_1.FishPlayer.lastAntibotReason)); +} +var failPrefix = "[scarlet]\u26A0 [yellow]"; +var successPrefix = "[#48e076]\uE800 "; +function outputFail(message, sender, ratelimit) { + var msg = failPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[yellow]") : message); + if (ratelimit) + sender.sendMessage(msg, ratelimit); + else + sender.sendMessage(msg); +} +function outputSuccess(message, sender) { + sender.sendMessage(successPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[#48e076]") : message)); +} +function outputMessage(message, sender) { + sender.sendMessage(((typeof message == "function" && "__partialFormatString" in message ? message(null) : message) + "").replace(/\t/g, " ".repeat(4))); +} +function outputConsole(message, channel) { + if (channel === void 0) { channel = Log.info; } + channel(typeof message == "function" && "__partialFormatString" in message ? message("") : message); +} +function updateBans(message) { + Groups.player.each(function (player) { + if (Vars.netServer.admins.isIDBanned(player.uuid())) { + player.con.kick(Packets.KickReason.banned); + if (message) + Call.sendMessage(message(player)); + } + }); +} +function processChat(player, message, effects) { + if (effects === void 0) { effects = false; } + var fishPlayer = players_1.FishPlayer.get(player); + var highlight = fishPlayer.highlight; + var filterTripText; + var suspicious = fishPlayer.suspicionLevel() == 3; + if ((!fishPlayer.hasPerm("bypassChatFilter") || fishPlayer.chatStrictness == "strict") + && (filterTripText = matchFilter(message, fishPlayer.chatStrictness, suspicious))) { + if (effects) { + if (suspicious && removeFoosChars(message).split(" ") + .map(function (w) { return w.replace(/[-_.^*,]/g, ""); }) + .some(function (w) { return config_1.bannedWords.autoWhack.includes(w); })) { + if (!fishPlayer.muted) { + logHTrip(fishPlayer, "bad words in chat", "message: `".concat(message, "`")); + fishPlayer.muted = true; + void fishPlayer.stop("automod", globals_1.maxTime, "Automatic stop due to suspicious activity", false); + } + } + Log.info("Censored message from player ".concat(player.name, ": \"").concat((0, funcs_1.escapeStringColorsServer)(message), "\"; contained \"").concat(filterTripText, "\"")); + players_1.FishPlayer.messageStaff("[yellow]Censored message from player ".concat(fishPlayer.cleanedName, ": \"").concat(message, "\" contained \"").concat(filterTripText, "\"")); + } + message = config_1.text.chatFilterReplacement.message(); + highlight !== null && highlight !== void 0 ? highlight : (highlight = config_1.text.chatFilterReplacement.highlight()); + } + if (message.startsWith("./")) + message = message.replace("./", "/"); + if (!fishPlayer.hasPerm("chat")) { + if (effects) { + players_1.FishPlayer.messageMuted(player.name, message); + Log.info("".concat(player.name, ": ").concat(message)); + } + return null; + } + return (highlight !== null && highlight !== void 0 ? highlight : "") + message; +} +var replacements = [ + //Serpulo units + ["dagger", "mace", "fortress", "scepter", "reign", "nova", "pulsar", "quasar", "vela", "corvus", "crawler", "atrax", "spiroct", "arkyid", "toxopid", "flare", "horizon", "zenith", "antumbra", "eclipse", "mono", "poly", "mega", "quad", "oct", "risso", "minke", "bryde", "sei", "omura", "retusa", "oxynoe", "cyerce", "aegires", "navanax", "fort", "toxo", "flarogus"], + //Erekir units + ["stell", "locus", "precept", "vanquish", "conquer", "merui", "cleroi", "anthicus", "tecta", "collaris", "elude", "avert", "obviate", "quell", "disrupt", "vanq", "crab", "anthi", "larry", "obvi"], + //Items, full form + ["copper", "lead", "metaglass", "graphite", "sand", "coal", "titanium", "thorium", "scrap", "silicon", "plastanium", "phase fabric", "surge alloy", "spore pod", "blast compound", "pyratite", "beryllium", "tungsten", "oxide", "carbide"], + //Items, short form + ["coppa", "meta", "graph", "tita", "titan", "thor", "scrap", "sili", "plast", "phase", "surge", "spore", "blast", "pyra", "beryl", "tung", "oxide", "carb"], + //Liquids + ["water", "slag", "oil", "cryo", "cryofluid"], + //Liquids/gases (erekir) + ["hydrogen", "ozone", "nitrogen", "cyanogen", "cyan", "nitro", "hydro", "arky", "arkycite", "neoplasm"], + //Gamemodes + ["attack", "sandbox", "pvp", "hexed", "survival"], + //teams + ["crux", "sharded", "malis", "neoplastic"], + //maps + ["rampant", "harbor war", "cave canal", "acheron", "wolframfestung", "avast", "fallen omura", "assault"], + //aquatic animals + ["fish", "shark", "whale", "dolphin", "salmon", "tuna", "squid", "jellyfish", "turtle"], + //antonym adjectives + ["fast", "slow"], ["big", "little"], ["hot", "cold"], ["hard", "easy", "difficult", "ez"], ["hello", "bye"], +].map(function (set) { return [set, new RegExp("\\b(?:".concat(set.join("|"), ")(e?s?(?:i?gone)?)\\b"), 'g')]; }); +var foolCounter = 0; +exports.foolifyChat = memoizeChatFilter(function foolifyChat(message) { + var e_5, _a; + var cleanedMessage = removeFoosChars(message); + setShuffle: { + if (foolCounter < 8) { + //Skip the next 5 messages no matter what + foolCounter++; + break setShuffle; + } + var replacedMessage = cleanedMessage; + var _loop_2 = function (set, regex) { + replacedMessage = replacedMessage.replace(regex, function (_, plural) { return (0, funcs_1.random)(set) + plural; }); + }; + try { + for (var replacements_1 = __values(replacements), replacements_1_1 = replacements_1.next(); !replacements_1_1.done; replacements_1_1 = replacements_1.next()) { + var _b = __read(replacements_1_1.value, 2), set = _b[0], regex = _b[1]; + _loop_2(set, regex); + } + } + catch (e_5_1) { e_5 = { error: e_5_1 }; } + finally { + try { + if (replacements_1_1 && !replacements_1_1.done && (_a = replacements_1.return)) _a.call(replacements_1); + } + finally { if (e_5) throw e_5.error; } + } + if (replacedMessage !== cleanedMessage) { + if (foolCounter < 11) { + //Skip the next 2 messages that would get altered + foolCounter++; + break setShuffle; + } + foolCounter = 0; + return replacedMessage; + } + else { + break setShuffle; + } + } + if (Math.random() < 0.01) { + return cleanedMessage.split("").reverse().join(""); + // eslint-disable-next-line no-dupe-else-if + } + else if (Math.random() < 0.01) { + return "[scarlet]I really hope everyone is having a fun time :} <3"; + } + else if (Math.random() < 0.005) { + return "[cyan]AMOGUS"; + } + else { + return message; + } +}); +exports.addToTileHistory = logErrors("Error while saving a tilelog entry", function (e) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3; + // eslint-disable-next-line prefer-const + var tile, uuid, action, type, time = Date.now(); + if (e instanceof EventType.BlockBuildBeginEvent) { + tile = e.tile; + uuid = (_e = (_c = (_b = (_a = e.unit) === null || _a === void 0 ? void 0 : _a.player) === null || _b === void 0 ? void 0 : _b.uuid()) !== null && _c !== void 0 ? _c : (_d = e.unit) === null || _d === void 0 ? void 0 : _d.type.name) !== null && _e !== void 0 ? _e : "unknown"; + if (e.breaking) { + action = "broke"; + type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.previous.name : "unknown"; + if (((_g = (_f = e.unit) === null || _f === void 0 ? void 0 : _f.player) === null || _g === void 0 ? void 0 : _g.uuid()) && ((_h = e.tile.build) === null || _h === void 0 ? void 0 : _h.team) != Team.derelict) { + var fishP = players_1.FishPlayer.get(e.unit.player); + //TODO move this code + fishP.tstats.blocksBroken++; + fishP.tstats.blockInteractionsThisMap++; + fishP.updateStats(function (stats) { return stats.blocksBroken++; }); + } + } + else { + action = "built"; + type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.current.name : "unknown"; + if ((_k = (_j = e.unit) === null || _j === void 0 ? void 0 : _j.player) === null || _k === void 0 ? void 0 : _k.uuid()) { + var fishP = players_1.FishPlayer.get(e.unit.player); + //TODO move this code + fishP.updateStats(function (stats) { return stats.blocksPlaced++; }); + fishP.tstats.blockInteractionsThisMap++; + } + } + } + else if (e instanceof EventType.ConfigEvent) { + tile = e.tile.tile; + uuid = (_m = (_l = e.player) === null || _l === void 0 ? void 0 : _l.uuid()) !== null && _m !== void 0 ? _m : "unknown"; + if (uuid != "unknown") { + var fishP = players_1.FishPlayer.getById(uuid); + if (fishP) + fishP.tstats.blockInteractionsThisMap++; + } + action = "configured"; + type = e.tile.block.name; + } + else if (e instanceof EventType.BuildRotateEvent) { + tile = e.build.tile; + uuid = (_s = (_q = (_p = (_o = e.unit) === null || _o === void 0 ? void 0 : _o.player) === null || _p === void 0 ? void 0 : _p.uuid()) !== null && _q !== void 0 ? _q : (_r = e.unit) === null || _r === void 0 ? void 0 : _r.type.name) !== null && _s !== void 0 ? _s : "unknown"; + if (uuid != "unknown") { + var fishP = players_1.FishPlayer.getById(uuid); + if (fishP) + fishP.tstats.blockInteractionsThisMap++; + } + action = "rotated"; + type = e.build.block.name; + } + else if (e instanceof EventType.UnitDestroyEvent) { + tile = e.unit.tileOn(); + if (!tile) + return; + if (!e.unit.type.playerControllable) + return; + uuid = e.unit.isPlayer() ? e.unit.getPlayer().uuid() : (_t = e.unit.lastCommanded) !== null && _t !== void 0 ? _t : "unknown"; + action = "killed"; + type = e.unit.type.name; + } + else if (e instanceof EventType.BlockDestroyEvent) { + if (config_1.Gamemode.attack() && ((_u = e.tile.build) === null || _u === void 0 ? void 0 : _u.team) != Vars.state.rules.defaultTeam) + return; //Don't log destruction of enemy blocks + tile = e.tile; + uuid = "[[something]"; + action = "killed"; + type = (_w = (_v = e.tile.block()) === null || _v === void 0 ? void 0 : _v.name) !== null && _w !== void 0 ? _w : "air"; + } + else if (e instanceof EventType.PayloadDropEvent) { + action = "pay-dropped"; + var controller = e.carrier.controller(); + uuid = (_z = (_y = (_x = e.carrier.player) === null || _x === void 0 ? void 0 : _x.uuid()) !== null && _y !== void 0 ? _y : (controller instanceof LogicAI && controller.controller ? + "".concat(e.carrier.type.name, " controlled by ").concat(controller.controller.block.name, " at ").concat(controller.controller.tileX(), ",").concat(controller.controller.tileY(), " last accessed by ").concat(e.carrier.getControllerName()) + : null)) !== null && _z !== void 0 ? _z : e.carrier.type.name; + if (e.build) { + tile = e.build.tile; + type = e.build.block.name; + } + else if (e.unit) { + tile = e.unit.tileOn(); + if (!tile) + return; + type = e.unit.type.name; + } + else + return; + } + else if (e instanceof EventType.PickupEvent) { + action = "picked up"; + if (e.carrier.isPlayer()) + return; //This event would have been handled by actionfilter + var controller = e.carrier.controller(); + if (!(controller instanceof LogicAI && controller.controller != null)) + return; + uuid = "".concat(e.carrier.type.name, " controlled by ").concat(controller.controller.block.name, " at ").concat(controller.controller.tileX(), ",").concat(controller.controller.tileY(), " last accessed by ").concat(e.carrier.getControllerName()); + if (e.build) { + tile = e.build.tile; + type = e.build.block.name; + } + else if (e.unit) { + tile = e.unit.tileOn(); + if (!tile) + return; + type = e.unit.type.name; + } + else + return; + } + else if (e instanceof EventType.UnitControlEvent) { + if (e.unit instanceof Packages.mindustry.gen.BlockUnitUnit) { + action = "controlled"; + tile = (_0 = e.unit) === null || _0 === void 0 ? void 0 : _0.tile().tile; + if (!tile) + return; + type = (_2 = (_1 = tile.block()) === null || _1 === void 0 ? void 0 : _1.name) !== null && _2 !== void 0 ? _2 : "air"; + uuid = e.player.uuid(); + } + else + return; + } + else if (e instanceof Object && "pos" in e && "uuid" in e && "action" in e && "type" in e) { + var pos = void 0; + (pos = e.pos, uuid = e.uuid, action = e.action, type = e.type); + tile = (_3 = Vars.world.tile(pos.split(",")[0], pos.split(",")[1])) !== null && _3 !== void 0 ? _3 : (0, funcs_1.crash)("Cannot log ".concat(action, " at ").concat(pos, ": Nonexistent tile")); + } + else + return; + if (tile == null) + return; + [tile, uuid, action, type, time]; + tile.getLinkedTiles(function (t) { + var pos = "".concat(t.x, ",").concat(t.y); + var existingData = globals_1.tileHistory[pos] ? funcs_1.StringIO.read(globals_1.tileHistory[pos], function (str) { return str.readArray(function (d) { return ({ + action: d.readString(2), + uuid: d.readString(3), + time: d.readNumber(16), + type: d.readString(2), + }); }, 1); }) : []; + existingData.push({ + action: action, + uuid: uuid, + time: time, + type: type + }); + existingData = existingData.slice(-9); + //Write + globals_1.tileHistory[t.x + ',' + t.y] = funcs_1.StringIO.write(existingData, function (str, data) { return str.writeArray(data, function (el) { + str.writeString(el.action, 2); + str.writeString(el.uuid, 3); + str.writeNumber(el.time, 16); + str.writeString(el.type, 2); + }, 1); }); + }); +}); +exports.tilelogAndResetAfk = logErrors("Error while saving a tilelog entry and resetting afk", function (e) { + (0, exports.addToTileHistory)(e); + players_1.FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); +function getIPRange(input, error) { + if (globals_1.ipRangeCIDRPattern.test(input)) { + var _a = __read(input.split("/"), 2), ip = _a[0], maskLength = _a[1]; + switch (maskLength) { + case "24": + return ip.split(".").slice(0, 3).join(".") + "."; + case "16": + return ip.split(".").slice(0, 2).join(".") + "."; + default: + error === null || error === void 0 ? void 0 : error("Mindustry does not currently support netmasks other than /16 and /24"); + return null; + } + } + else if (globals_1.ipRangeWildcardPattern.test(input)) { + //1.2.3.* + //1.2.* + var _b = __read(input.split("."), 4), a = _b[0], b = _b[1], c = _b[2], d = _b[3]; + if (c !== "*") + return "".concat(a, ".").concat(b, ".").concat(c, "."); + return "".concat(a, ".").concat(b, "."); + } + else + return null; +} +//this brings me physical pain +function getHash(file, algorithm) { + if (algorithm === void 0) { algorithm = "SHA-1"; } + try { + var header = "blob ".concat(file.length(), "\0"); + var fileSHAHeader = Packages.java.nio.charset.StandardCharsets.UTF_8.encode(header); + var contents = file.readBytes(); + var buffer = Packages.java.nio.ByteBuffer.allocate(fileSHAHeader.remaining() + contents.length); + buffer.put(fileSHAHeader); + buffer.put(contents); + buffer.flip(); + var digest = Packages.java.security.MessageDigest.getInstance(algorithm); + digest.update(buffer); + return digest.digest().map(function (byte) { + return (byte & 0xFF).toString(16).padStart(2, "0"); + }).join(""); + } + catch (e) { + Log.err("Cannot generate ".concat(algorithm, ", ").concat(String(e))); + return undefined; + } +} +function match(value, clauses, defaultValue) { + return Object.prototype.hasOwnProperty.call(clauses, value) ? clauses[value] : defaultValue; +} +/** @throws CommandError */ +function fishCommandsRootDirPath() { + var commandsDir = Vars.modDirectory.child("fish-commands"); + if (!commandsDir.exists()) + (0, commands_1.fail)("Fish commands directory at path ".concat(commandsDir.absolutePath(), " does not exist!")); + var fishCommandsRootDirPath = Paths.get(commandsDir.file().path); + if (Packages.java.nio.file.Files.isSymbolicLink(fishCommandsRootDirPath)) { + //fish-commands is linked to the build directory of somewhere else + //resolve and get the parent directory of the build directory + fishCommandsRootDirPath = fishCommandsRootDirPath.toRealPath().getParent(); + } + return fishCommandsRootDirPath; +} +/** Fails if "mode" is invalid. */ +function applyEffectMode(mode, unit, ticks) { + var e_6, _a; + var _b; + var modes = { + fast: [StatusEffects.fast], + fast2: [StatusEffects.fast, StatusEffects.overdrive, StatusEffects.overclock], + boss: [StatusEffects.boss], + health: [StatusEffects.boss, StatusEffects.shielded], + slow: [StatusEffects.slow], + slow2: [ + StatusEffects.slow, + StatusEffects.freezing, + StatusEffects.wet, + StatusEffects.muddy, + StatusEffects.sapped, + StatusEffects.sporeSlowed, + StatusEffects.electrified, + StatusEffects.tarred, + ], + freeze: [StatusEffects.unmoving], + disarm: [StatusEffects.disarmed], + invincible: [StatusEffects.invincible], + boost: [ + StatusEffects.fast, + StatusEffects.overdrive, + StatusEffects.overclock, + StatusEffects.boss, + StatusEffects.shielded, + ], + damage: [ + StatusEffects.burning, + StatusEffects.freezing, + StatusEffects.wet, + StatusEffects.muddy, + StatusEffects.melting, + StatusEffects.sapped, + StatusEffects.tarred, + StatusEffects.shocked, + StatusEffects.blasted, + StatusEffects.corroded, + StatusEffects.sporeSlowed, + StatusEffects.electrified, + StatusEffects.fast, + ], + clear: function (unit) { + unit.clearStatuses(); + unit.maxHealth = unit.type.health; + }, + paper: function (unit) { + unit.health = 1; + unit.maxHealth = 1; + unit.apply(StatusEffects.disarmed, Number.MAX_VALUE / 2); + }, + heal: function (unit) { + unit.health = unit.maxHealth; + }, + overheal: function (unit) { + unit.maxHealth = unit.health = 1e15; + }, + shield: function (unit) { + unit.shield = 1e15; + } + }; + var effects = (_b = match(mode, modes, null)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Invalid mode. Supported modes: ".concat(Object.keys(modes).join(", "))); + if (typeof effects === "function") { + effects(unit); + } + else { + try { + for (var effects_1 = __values(effects), effects_1_1 = effects_1.next(); !effects_1_1.done; effects_1_1 = effects_1.next()) { + var effect = effects_1_1.value; + unit.apply(effect, ticks); + } + } + catch (e_6_1) { e_6 = { error: e_6_1 }; } + finally { + try { + if (effects_1_1 && !effects_1_1.done && (_a = effects_1.return)) _a.call(effects_1); + } + finally { if (e_6) throw e_6.error; } + } + } +} +function handleError(err, sender, outputFail, context) { + if (err instanceof commands_1.CommandError) { + //If the error is a command error, then just outputFail + outputFail(err.data, sender); + } + else if (err === menus_1.Cancel) { + //Menu cancelled, do nothing + return; + } + else { + sender.sendMessage("[scarlet]\u274C An error occurred while executing the command!"); + if (sender.hasPerm("seeErrorMessages")) + sender.sendMessage((0, funcs_1.parseError)(err)); + Log.err(context ? + "Unhandled error in command execution: ".concat(context) + : "Unhandled error in command execution."); + Log.err(err); + if (typeof err == "object" && err != null && "stack" in err) + Log.err(err.stack); + } +} +var sources = [ + Packages.mindustry.gen.UnitEntity, + Packages.mindustry.gen.MechUnit, + Packages.mindustry.gen.LegsUnit, + Packages.mindustry.gen.CrawlUnit, + Packages.mindustry.gen.UnitWaterMove, + Packages.mindustry.gen.BlockUnitUnit, + Packages.mindustry.gen.ElevationMoveUnit, + Packages.mindustry.gen.BuildingTetherPayloadUnit, + Packages.mindustry.gen.TimedKillUnit, + Packages.mindustry.gen.PayloadUnit, + Packages.mindustry.gen.TankUnit, +]; +function getStatuses(unit) { + var e_7, _a; + try { + for (var sources_1 = __values(sources), sources_1_1 = sources_1.next(); !sources_1_1.done; sources_1_1 = sources_1.next()) { + var clazz = sources_1_1.value; + if (unit instanceof clazz) + return ArcReflect.get(clazz, unit, "statuses"); + } + } + catch (e_7_1) { e_7 = { error: e_7_1 }; } + finally { + try { + if (sources_1_1 && !sources_1_1.done && (_a = sources_1.return)) _a.call(sources_1); + } + finally { if (e_7) throw e_7.error; } + } + return new Seq(); +} diff --git a/src/utils.ts b/src/utils.ts index e2b5972e..a5958f63 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -789,10 +789,10 @@ export const addToTileHistory = logErrors("Error while saving a tilelog entry", }); -export function tilelogAndResetAfk(e: { unit: Unit }){ +export const tilelogAndResetAfk = logErrors("Error while saving a tilelog entry and resetting afk", (e:any) => { addToTileHistory(e); - FishPlayer.get(e.unit?.player).lastActive = Date.now(); -}; + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); export function getIPRange(input:string, error?:(message:string) => never):string | null { if(ipRangeCIDRPattern.test(input)){ From e40893678c94270f9a4f098c58e9c0331d25bd88 Mon Sep 17 00:00:00 2001 From: Peanutzy Leguminoso Date: Wed, 17 Jun 2026 16:37:39 +0700 Subject: [PATCH 6/6] i think i should convert only utils.ts --- build/scripts/achievements.js | 1650 +++---- build/scripts/api.js | 428 +- build/scripts/commands/aggregate.js | 108 +- build/scripts/commands/console.js | 2258 +++++----- build/scripts/commands/general.js | 2868 ++++++------ build/scripts/commands/member.js | 274 +- build/scripts/commands/staff.js | 3168 ++++++------- build/scripts/config.js | 716 +-- build/scripts/files.js | 192 +- build/scripts/fjsContext.js | 202 +- build/scripts/frameworks/commands.js | 44 +- build/scripts/frameworks/commands/commands.js | 1534 +++---- build/scripts/frameworks/commands/errors.js | 40 +- .../scripts/frameworks/commands/formatting.js | 366 +- build/scripts/frameworks/commands/perm.js | 190 +- .../frameworks/commands/requirements.js | 184 +- build/scripts/frameworks/commands/types.js | 58 +- build/scripts/frameworks/io.js | 776 ++-- build/scripts/frameworks/menus.js | 764 ++-- build/scripts/funcs.js | 948 ++-- build/scripts/globals.js | 68 +- build/scripts/index.js | 682 +-- build/scripts/maps.js | 722 +-- build/scripts/metrics.js | 152 +- build/scripts/mindustryTypes.js | 24 +- build/scripts/packetHandlers.js | 562 +-- build/scripts/players.js | 3974 ++++++++--------- build/scripts/promise.js | 272 +- build/scripts/ranks.js | 192 +- build/scripts/timers.js | 328 +- build/scripts/types.js | 12 +- build/scripts/votes.js | 436 +- src/README.md | 6 +- src/achievements.ts | 1350 +++--- src/api.ts | 404 +- src/client.ts | 16 +- src/config.ts | 760 ++-- src/files.ts | 230 +- src/fjsContext.ts | 218 +- src/funcs.ts | 764 ++-- src/globals.ts | 110 +- src/index.ts | 594 +-- src/main.js | 152 +- src/maps.ts | 604 +-- src/metrics.ts | 134 +- src/mindustryTypes.ts | 1940 ++++---- src/packetHandlers.ts | 678 +-- src/players.ts | 3376 +++++++------- src/promise.ts | 290 +- src/ranks.ts | 202 +- src/rhino-env.d.ts | 14 +- src/timers.ts | 218 +- src/types.ts | 238 +- src/utils.ts | 1936 ++++---- src/votes.ts | 296 +- 55 files changed, 19361 insertions(+), 19361 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index a67f9bff..074d2db7 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -1,825 +1,825 @@ -"use strict"; -var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { - if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } - return cooked; -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Achievements = exports.Achievement = void 0; -var config_1 = require("/config"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var players_1 = require("/players"); -var ranks_1 = require("/ranks"); -var utils_1 = require("/utils"); -//scrap doesn't count -var serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; -var erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; -var usefulItems10k = { - serpulo: serpuloItems.map(function (i) { return new ItemStack(i, 10000); }), - erekir: erekirItems.map(function (i) { return new ItemStack(i, 10000); }), - sun: __spreadArray(__spreadArray([], __read(serpuloItems), false), __read(erekirItems), false).map(function (i) { return new ItemStack(i, 10000); }), -}; -var allItems1k = Vars.content.items().select(function (i) { return !i.hidden; }).toArray().map(function (i) { return new ItemStack(i, 1000); }); -var mixtechItems = Items.serpuloItems.copy(); -Items.erekirItems.each(function (i) { return mixtechItems.add(i); }); -var Achievement = /** @class */ (function () { - function Achievement(icon, name, description, options) { - var _a; - if (options === void 0) { options = {}; } - this.name = name; - this.notify = "player"; - this.hidden = false; - this.disabled = false; - if (Array.isArray(icon)) { - this.icon = (icon[0].startsWith("[") ? icon[0] : "[".concat(icon[0], "]")) + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); - } - else if (typeof icon == "number") { - this.icon = String.fromCharCode(icon); - } - else { - this.icon = icon; - } - if (Array.isArray(description)) { - _a = __read(description, 2), this.description = _a[0], this.extendedDescription = _a[1]; - } - else - this.description = description; - this.nid = Achievement._id++; - Object.assign(this, options); - if (options.modes) { - var _b = __read(options.modes), type = _b[0], modes_1 = _b.slice(1); - if (type == "only") { - this.allowedModes = modes_1; - this.modesText = modes_1.join(", "); - } - else { - this.allowedModes = config_1.GamemodeNames.filter(function (m) { return !modes_1.includes(m); }); - this.modesText = "all except ".concat(modes_1.join(", ")); - } - } - else { - this.allowedModes = config_1.GamemodeNames; - this.modesText = "all"; - } - if (!this.disabled) { - Achievement.all.push(this); - if (this.checkPlayerFrequent || this.checkFrequent) - Achievement.checkFrequent.push(this); - if (this.checkPlayerInfrequent || this.checkInfrequent) - Achievement.checkInfrequent.push(this); - if (this.checkPlayerJoin) - Achievement.checkJoin.push(this); - if (this.checkPlayerGameover || this.checkGameover) - Achievement.checkGameover.push(this); - } - } - Achievement.prototype.message = function () { - return config_1.FColor.achievement(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Achievement granted!\n[accent]", "[white]: ", ""], ["Achievement granted!\\n[accent]", "[white]: ", ""])), this.name, this.description); - }; - Achievement.prototype.messageToEveryone = function (player) { - return config_1.FColor.achievement(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Player ", " has completed the achievement \"", "\"."], ["Player ", " has completed the achievement \"", "\"."])), player.prefixedName, this.name); - }; - Achievement.prototype.allowedInMode = function () { - return this.allowedModes.includes(config_1.Gamemode.name()); - }; - Achievement.prototype.grantToAllOnline = function (team) { - var _this = this; - players_1.FishPlayer.forEachPlayer(function (p) { - if (!_this.has(p) && (!team || p.team() == team)) { - if (_this.notify != "nobody") - p.sendMessage(_this.message()); - _this.setObtained(p); - } - }); - }; - /** Do not call this in a loop on an achievement set to notify everyone. */ - Achievement.prototype.grantTo = function (player, allowRepeatMessage) { - if (allowRepeatMessage === void 0) { allowRepeatMessage = true; } - var has = this.has(player); - if (!has || allowRepeatMessage) { - if (this.notify == "everyone") - Call.sendMessage(this.messageToEveryone(player)); - else if (this.notify == "player") - player.sendMessage(this.message()); - } - if (!has) - this.setObtained(player); - }; - Achievement.prototype.setObtained = function (player) { - //void player.updateSynced(fishP => fishP.achievements.set(this.nid)); - player.achievements.set(this.nid); - }; - Achievement.prototype.has = function (player) { - return player.achievements.get(this.nid); - }; - Achievement.all = []; - /** Checked every second. */ - Achievement.checkFrequent = []; - /** Checked every 10 seconds. Use for states that can be gained but not lost, such as "x wins". */ - Achievement.checkInfrequent = []; - Achievement.checkJoin = []; - Achievement.checkGameover = []; - Achievement._id = 0; - return Achievement; -}()); -exports.Achievement = Achievement; -Events.on(EventType.PlayerJoin, function (_a) { - var e_1, _b; - var _c; - var player = _a.player; - Time.mark(); - var _loop_1 = function (ach) { - if (ach.allowedInMode()) { - var fishP_1 = players_1.FishPlayer.get(player); - if (!ach.has(fishP_1) && ((_c = ach.checkPlayerJoin) === null || _c === void 0 ? void 0 : _c.call(ach, fishP_1))) { - if (fishP_1.dataSynced) - ach.grantTo(fishP_1); - else - Timer.schedule(function () { return ach.grantTo(fishP_1); }, 2); //2 seconds should be enough - } - } - }; - try { - for (var _d = __values(Achievement.checkJoin), _e = _d.next(); !_e.done; _e = _d.next()) { - var ach = _e.value; - _loop_1(ach); - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_e && !_e.done && (_b = _d.return)) _b.call(_d); - } - finally { if (e_1) throw e_1.error; } - } - Log.debug("ach join @", Time.elapsed()); -}); -globals_1.FishEvents.on("gameOver", function (_, winner) { - var e_2, _a; - var _b; - Time.mark(); - var _loop_2 = function (ach) { - if (ach.allowedInMode()) { - if ((_b = ach.checkGameover) === null || _b === void 0 ? void 0 : _b.call(ach, winner)) - ach.grantToAllOnline(); - else - players_1.FishPlayer.forEachPlayer(function (fishP) { - var _a; - if (!ach.has(fishP) && ((_a = ach.checkPlayerGameover) === null || _a === void 0 ? void 0 : _a.call(ach, fishP, winner))) { - ach.grantTo(fishP); - } - }); - } - }; - try { - for (var _c = __values(Achievement.checkGameover), _d = _c.next(); !_d.done; _d = _c.next()) { - var ach = _d.value; - _loop_2(ach); - } - } - catch (e_2_1) { e_2 = { error: e_2_1 }; } - finally { - try { - if (_d && !_d.done && (_a = _c.return)) _a.call(_c); - } - finally { if (e_2) throw e_2.error; } - } - Log.debug("ach gameover @", Time.elapsed()); -}); -Timer.schedule(function () { - var e_3, _a; - Time.mark(); - var _loop_3 = function (ach) { - if (ach.allowedInMode()) { - if (ach.checkFrequent) { - if (config_1.Gamemode.pvp()) { - Vars.state.teams.active.each(function (_a) { - var team = _a.team; - if (ach.checkFrequent(team)) - ach.grantToAllOnline(team); - }); - } - else { - if (ach.checkFrequent(Vars.state.rules.defaultTeam)) - ach.grantToAllOnline(); - } - } - else { - players_1.FishPlayer.forEachPlayer(function (fishP) { - var _a; - if (!ach.has(fishP) && ((_a = ach.checkPlayerFrequent) === null || _a === void 0 ? void 0 : _a.call(ach, fishP))) - ach.grantTo(fishP); - }); - } - } - }; - try { - for (var _b = __values(Achievement.checkFrequent), _c = _b.next(); !_c.done; _c = _b.next()) { - var ach = _c.value; - _loop_3(ach); - } - } - catch (e_3_1) { e_3 = { error: e_3_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_3) throw e_3.error; } - } - Log.debug("ach frequent @", Time.elapsed()); -}, 1, 1); -Timer.schedule(function () { - var e_4, _a; - Time.mark(); - var _loop_4 = function (ach) { - if (ach.allowedInMode()) { - if (ach.checkInfrequent) { - if (config_1.Gamemode.pvp()) { - Vars.state.teams.active.each(function (_a) { - var team = _a.team; - if (ach.checkInfrequent(team)) - ach.grantToAllOnline(team); - }); - } - else { - if (ach.checkInfrequent(Vars.state.rules.defaultTeam)) - ach.grantToAllOnline(); - } - } - else { - players_1.FishPlayer.forEachPlayer(function (fishP) { - var _a; - if (!ach.has(fishP) && ((_a = ach.checkPlayerInfrequent) === null || _a === void 0 ? void 0 : _a.call(ach, fishP))) - ach.grantTo(fishP); - }); - } - } - }; - try { - for (var _b = __values(Achievement.checkInfrequent), _c = _b.next(); !_c.done; _c = _b.next()) { - var ach = _c.value; - _loop_4(ach); - } - } - catch (e_4_1) { e_4 = { error: e_4_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_4) throw e_4.error; } - } - Log.debug("ach infrequent @", Time.elapsed()); -}, 10, 10); -exports.Achievements = { - // =========================== - // ╦ ╦ ╔═╗ ╦═╗ ╔╗╔ ╦ ╔╗╔ ╔═╗ ┬ - // ║║║ ╠═╣ ╠╦╝ ║║║ ║ ║║║ ║ ╦ │ - // ╚╩╝ ╩ ╩ ╩╚═ ╝╚╝ ╩ ╝╚╝ ╚═╝ o - // =========================== - // Do not change the order of any achievements. - // Do not remove any achievements: instead, set the "disabled" option to true. - // Reordering achievements will cause ID shifts. - //Joining based - welcome: new Achievement(["gold", Iconc.infoCircle], "Welcome", "Join the server.", { - checkPlayerJoin: function () { return true; }, - notify: "nobody" - }), - migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { - disabled: true - }), //TODO - frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { - checkPlayerJoin: function (p) { return p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > funcs_1.Duration.months(1)); } - }), - //Gamemode based - attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { - modes: ["only", "attack"], - checkPlayerGameover: function (player, winTeam) { - return Vars.state.rules.defaultTeam == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; - }, - }), - survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { - modes: ["only", "survival"], - checkPlayerInfrequent: function (player) { - return player.tstats.wavesSurvived >= 50; - }, - }), - pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { - modes: ["only", "pvp"], - checkPlayerGameover: function (player, winTeam) { - return player.team() == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; - }, - }), - sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { - modes: ["only", "sandbox"], - checkPlayerInfrequent: function (p) { return p.stats.timeInGame > funcs_1.Duration.hours(1); }, - }), - hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { - modes: ["only", "hexed"], - checkPlayerGameover: function (player) { - return player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; - }, - }), - minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { - modes: ["only", "minigame"], - checkPlayerGameover: function (player, winTeam) { - return player.team() == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; - }, - }), - //playtime based - playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { - checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(1); } - }), - playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { - checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(12); } - }), - playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { - checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(2); } - }), - playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { - checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(10); } - }), - //victories based - victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 1; } - }), - victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 5; } - }), - victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 30; } - }), - victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 100; }, - notify: "everyone" - }), - //games based - games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 10; } - }), - games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 40; } - }), - games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 100; } - }), - games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { - checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 200; }, - notify: "everyone" - }), - //messages based - messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { - checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 1; }, - notify: "nobody" - }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 100; } - }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", ["Send 500 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 500; } - }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", ["Send 2000 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 2000; } - }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", ["Send 5000 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 5000; }, - notify: "everyone" - }), - //blocks built based - builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { - checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced >= 1; }, - notify: "nobody" - }), - builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { - checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 200; } - }), - builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { - checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 1000; } - }), - builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { - checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 5000; }, - }), - //units - t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { - modes: ["not", "sandbox"], - checkPlayerFrequent: function (player) { - var _a; - return globals_1.unitsT5.includes((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type); - }, - }), - dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { - modes: ["not", "sandbox"], - disabled: true - }), //TODO - worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { - checkPlayerFrequent: function (player) { - var _a; - return ((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type) == UnitTypes.latum; - } - }), - //pvp - above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { - modes: ["only", "pvp"], - checkPlayerInfrequent: function (p) { return p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20; } - }), - head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { - modes: ["only", "pvp"], - disabled: true - }), //TODO - one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { - modes: ["only", "pvp"], - disabled: true - }), //TODO - //sandbox - underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { - modes: ["only", "sandbox"], - checkFrequent: function () { - var found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(function (_a) { - var graph = _a.graph; - //we don't need to actually check for power sources, just assume that ~1mil power is a source - if (graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && (graph.lastPowerProduced / Time.delta * 60) >= 999900) - found = true; - }); - return found; - } - }), - //easter eggs - memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { - notify: "nobody" - }), - run_js_without_perms: new Achievement(["yellow", Iconc.warning], "XKCD 838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { - notify: "everyone" - }), - script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { - notify: "nobody" - }), - hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { - hidden: true - }), - //items based - items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { - modes: ["not", "sandbox"], - checkPlayerFrequent: function (player) { - var _a; - if (!Vars.state.planet) - return false; - return ((_a = player.team().items()) === null || _a === void 0 ? void 0 : _a.has(usefulItems10k[Vars.state.planet.name])) || false; - }, - }), - fullVault: new Achievement(["green", Blocks.vault.emoji()], "Well Stocked", ["Fill a vault with every obtainable item.", "Requires mixtech."], { - modes: ["not", "sandbox"], - checkInfrequent: function (team) { - return Vars.indexer.getFlagged(team, BlockFlag.storage).contains(boolf(function (b) { return b.block == Blocks.vault && b.items.has(allItems1k) && b.linkedCore == null; })); - }, - }), - full_core: new Achievement(["green", Blocks.coreAcropolis.emoji()], "Multiblock Incinerator", "Completely fill the core with all obtainable items on a map with core incineration enabled.", { - modes: ["not", "sandbox"], - checkFrequent: function (team) { - var _a; - if (!Vars.state.planet) - return false; - var items; - switch (Vars.state.planet.name) { - case "serpulo": - items = Items.serpuloItems; - break; - case "erekir": - items = Items.erekirItems; - break; - case "sun": - items = mixtechItems; - break; - } - var capacity = (_a = team.core()) === null || _a === void 0 ? void 0 : _a.storageCapacity; - if (!capacity) - return false; - var module = team.items(); - return items.allMatch(function (i) { return module.has(i, capacity); }); - }, - }), - siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { - modes: ["not", "sandbox"] - }), - silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { - modes: ["not", "sandbox"], - checkFrequent: function (team) { return team.items().has(Items.silicon, 100000); } - }), - //other players based - alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { - notify: "nobody" - }), - join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { - checkPlayerJoin: function () { return Groups.player.size() > 20; }, - }), - meet_staff: new Achievement(["lime", Iconc.hammer], "Griefer Beware", "Meet a staff member in-game", { - checkPlayerJoin: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).ranksAtLeast("mod"); }); }, - }), - meet_fish: new Achievement(["blue", Iconc.admin], "The Big Fish", "Meet >|||>Fish himself in-game", { - checkPlayerJoin: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).ranksAtLeast("fish"); }); }, - hidden: true, - }), - server_speak: new Achievement(["pink", Iconc.host], "It Speaks!", "Hear the server talk in chat."), - see_marked_griefer: new Achievement(["red", Iconc.hammer], "Flying Tonk", "See a marked griefer in-game.", { - checkInfrequent: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).marked(); }); }, - }), - //maps based - beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { - notify: "everyone", - modes: ["not", "pvp"], - checkGameover: function (team) { return team == Vars.state.rules.defaultTeam && !Vars.state.map.custom; } - }), - //misc - power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { - modes: ["not", "sandbox"], - checkFrequent: function (team) { - var found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(function (_a) { - var _b; - var graph = _a.graph; - //we need to actually check for power sources - if ((graph.lastPowerProduced / Time.delta * 60) > 1e6 && - !graph.producers.contains(boolf(function (b) { return b.block == Blocks.powerSource; })) && - ((_b = graph.producers.firstOpt()) === null || _b === void 0 ? void 0 : _b.team) == team) - found = true; - }); - return found; - } - }), - pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { - modes: ["not", "sandbox"], - disabled: true - }), //TODO - core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 50 health, but survive.", { - modes: ["not", "sandbox"], - }), - enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 50 health, but survive.", { - modes: ["not", "sandbox"], - }), - verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { - checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "nobody" - }), - click_me: new Achievement(Iconc.bookOpen, "Clicked", "Run /achievementgrid and click this achievement."), - afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { - modes: ["not", "sandbox"], - checkPlayerGameover: function (player, winTeam) { - return player.team() == winTeam && player.tstats.blockInteractionsThisMap == 0; - }, - }), - status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { - checkPlayerFrequent: function (p) { - var unit = p.unit(); - if (!unit) - return false; - var statuses = (0, utils_1.getStatuses)(unit); - return statuses.size >= 5; - }, - modes: ["not", "sandbox"] - }), - drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { - notify: "everyone", - modes: ["not", "sandbox"] - }), - drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", "Drown a Mace in ".concat(Blocks.cryofluid.emoji(), " Cryofluid."), { - notify: "everyone", - modes: ["not", "sandbox"] - }), - max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { - checkPlayerFrequent: function (player) { - var _a, _b; - var tile = (_b = (_a = player.unit()) === null || _a === void 0 ? void 0 : _a.tile) === null || _b === void 0 ? void 0 : _b.call(_a); - if (!tile) - return false; - return tile.block == Blocks.duo && !tile.ammo.isEmpty() && tile.ammo.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; - }, - notify: "everyone", - modes: ["not", "sandbox"] - }), - foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { - notify: "everyone", - modes: ["not", "sandbox"] - }), - impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { - modes: ["not", "sandbox"], - notify: "everyone", - checkInfrequent: function (team) { - var found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(function (_a) { - var graph = _a.graph; - if (graph.producers.size >= 15 && graph.producers.count(function (b) { return b.block == Blocks.impactReactor && b.warmup > 0.99999; }) > 15 && graph.producers.first().team == team) - found = true; - }); - return found; - }, - }), - help_help: new Achievement(["brown", Iconc.info], "Help with help", "Run /help help", { - notify: "everyone" - }), - ohno: new Achievement(["scarlet", UnitTypes.alpha.emoji()], "Oh no", "Control an ohno unit."), - sniper_duel: new Achievement(["yellow", UnitTypes.omura.emoji()], "Sniper duel", "Kill a Foreshadow with an Omura from outside its range."), - around_the_world: new Achievement(Iconc.planet, "Around the World", "Fly your unit around the entire map without entering it, starting from the lower left.", { - notify: "everyone", - }), -}; -Object.entries(exports.Achievements).forEach(function (_a) { - var _b = __read(_a, 2), id = _b[0], a = _b[1]; - return a.sid = id; -}); -globals_1.FishEvents.on("commandUnauthorized", function (_, player, name) { - if ((name == "js" || name == "fjs") && !exports.Achievements.run_js_without_perms.has(player)) - exports.Achievements.run_js_without_perms.grantTo(player); -}); -Events.on(EventType.UnitDrownEvent, function (_a) { - var _b; - var unit = _a.unit; - if (!config_1.Gamemode.sandbox()) { - if (unit.type == UnitTypes.mace && ((_b = unit.tileOn()) === null || _b === void 0 ? void 0 : _b.floor()) == Blocks.cryofluid) - exports.Achievements.drown_mace_in_cryo.grantToAllOnline(); - else if (unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) { - if (config_1.Gamemode.pvp()) { - Vars.state.teams.active.map(function (t) { return t.team; }).select(function (t) { return t !== unit.team; }).each(function (t) { return exports.Achievements.drown_big_tank.grantToAllOnline(t); }); - } - else { - if (unit.team !== Vars.state.rules.defaultTeam) - exports.Achievements.drown_big_tank.grantToAllOnline(); - } - } - } -}); -Events.on(EventType.UnitBulletDestroyEvent, function (_a) { - var _b; - var unit = _a.unit, bullet = _a.bullet; - if (!config_1.Gamemode.sandbox() && unit.type == UnitTypes.dagger && ((_b = bullet.owner) === null || _b === void 0 ? void 0 : _b.block) == Blocks.foreshadow) { - var build = bullet.owner; - if (build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) - exports.Achievements.foreshadow_overkill.grantToAllOnline(build.team); - } -}); -Events.on(EventType.BuildingBulletDestroyEvent, function (_a) { - var _b; - var build = _a.build, bullet = _a.bullet; - if (!config_1.Gamemode.sandbox() && build.block == Blocks.foreshadow && ((_b = bullet.owner) === null || _b === void 0 ? void 0 : _b.type) == UnitTypes.omura) { - var unit = bullet.owner; - var player = unit.getPlayer(); - if (player && !unit.within(build, build.range() + unit.hitSize / 2)) - exports.Achievements.sniper_duel.grantTo(players_1.FishPlayer.get(player)); - } -}); -var siliconReached = Team.all.map(function (_) { return false; }); -Events.on(EventType.GameOverEvent, function () { return siliconReached = Team.all.map(function (_) { return false; }); }); -var isAlone = 0; -Timer.schedule(function () { - if (!Vars.state.gameOver && !config_1.Gamemode.sandbox()) { - Vars.state.teams.active.each(function (_a) { - var team = _a.team; - if (team.items().has(Items.silicon, 2000)) - siliconReached[team.id] = true; - else if (siliconReached[team.id] && team.items().get(Items.silicon) == 0) - exports.Achievements.siligone.grantToAllOnline(team); - }); - } - if (Groups.player.size() == 1) { - if (isAlone == 0) - isAlone = Date.now(); - else if (Date.now() > isAlone + funcs_1.Duration.minutes(2)) - exports.Achievements.alone.grantToAllOnline(); - } - else - isAlone = 0; -}, 2, 2); -var coreHealthTime = new Map(); -if (!config_1.Gamemode.sandbox()) - Timer.schedule(function () { - coreHealthTime.forEach(function (value, core) { - if (Date.now() > value) { - if (core.dead) { - coreHealthTime.delete(core); - } - else if (core.health > 50) { - //grant achievement - exports.Achievements.core_low_hp.grantToAllOnline(core.team); - players_1.FishPlayer.forEachPlayer(function (p) { - if (core.team != p.team() && !exports.Achievements.enemy_core_low_hp.has(p)) - exports.Achievements.enemy_core_low_hp.grantTo(p); - }); - coreHealthTime.delete(core); - } - } - }); - Vars.state.teams.active.flatMap(function (t) { return t.cores; }).each(function (core) { - if (core.health < 50 && !coreHealthTime.get(core)) - coreHealthTime.set(core, Date.now() + 12000); - }); - }, 1, 1); -var aroundTheWorld = {}; -Timer.schedule(function () { - var e_5, _a; - players_1.FishPlayer.forEachPlayer(function (p) { - var _a; - var _b; - var unit = p.unit(); - if (unit && unit.x < 0 && unit.y < 0) { - (_a = aroundTheWorld[_b = p.uuid]) !== null && _a !== void 0 ? _a : (aroundTheWorld[_b] = { player: p, unit: unit, side: "left" }); - } - }); - var _loop_5 = function (uuid, entry) { - if (!(function () { - if (entry.unit.dead) - return false; - var left = entry.unit.x < 0; - var bottom = entry.unit.y < 0; - var right = entry.unit.x > (Vars.world.width() - 1) * 8; - var top = entry.unit.y > (Vars.world.height() - 1) * 8; - switch (entry.side) { - case "left": - if (!left) - return false; - if (top) - entry.side = "top"; - break; - case "top": - if (!top) - return false; - if (right) - entry.side = "right"; - break; - case "right": - if (!right) - return false; - if (bottom) - entry.side = "bottom"; - break; - case "bottom": - if (!bottom) - return false; - if (left) { - exports.Achievements.around_the_world.grantTo(entry.player, true); - return false; - } - break; - } - return true; - })()) - delete aroundTheWorld[uuid]; - }; - try { - for (var _b = __values(Object.entries(aroundTheWorld)), _c = _b.next(); !_c.done; _c = _b.next()) { - var _d = __read(_c.value, 2), uuid = _d[0], entry = _d[1]; - _loop_5(uuid, entry); - } - } - catch (e_5_1) { e_5 = { error: e_5_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_5) throw e_5.error; } - } -}, 1, 0.5); -Events.on(EventType.GameOverEvent, function () { return coreHealthTime.clear(); }); -Events.on(EventType.WorldLoadEvent, function () { return coreHealthTime.clear(); }); -globals_1.FishEvents.on("scriptKiddie", function (_, p) { return Timer.schedule(function () { - if (!exports.Achievements.script_kiddie.has(p)) - exports.Achievements.script_kiddie.grantTo(p); -}, 2); }); -globals_1.FishEvents.on("memoryCorruption", function () { return exports.Achievements.memory_corruption.grantToAllOnline(); }); -globals_1.FishEvents.on("serverSays", function () { return exports.Achievements.server_speak.grantToAllOnline(); }); -var templateObject_1, templateObject_2; +"use strict"; +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Achievements = exports.Achievement = void 0; +var config_1 = require("/config"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var players_1 = require("/players"); +var ranks_1 = require("/ranks"); +var utils_1 = require("/utils"); +//scrap doesn't count +var serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; +var erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; +var usefulItems10k = { + serpulo: serpuloItems.map(function (i) { return new ItemStack(i, 10000); }), + erekir: erekirItems.map(function (i) { return new ItemStack(i, 10000); }), + sun: __spreadArray(__spreadArray([], __read(serpuloItems), false), __read(erekirItems), false).map(function (i) { return new ItemStack(i, 10000); }), +}; +var allItems1k = Vars.content.items().select(function (i) { return !i.hidden; }).toArray().map(function (i) { return new ItemStack(i, 1000); }); +var mixtechItems = Items.serpuloItems.copy(); +Items.erekirItems.each(function (i) { return mixtechItems.add(i); }); +var Achievement = /** @class */ (function () { + function Achievement(icon, name, description, options) { + var _a; + if (options === void 0) { options = {}; } + this.name = name; + this.notify = "player"; + this.hidden = false; + this.disabled = false; + if (Array.isArray(icon)) { + this.icon = (icon[0].startsWith("[") ? icon[0] : "[".concat(icon[0], "]")) + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); + } + else if (typeof icon == "number") { + this.icon = String.fromCharCode(icon); + } + else { + this.icon = icon; + } + if (Array.isArray(description)) { + _a = __read(description, 2), this.description = _a[0], this.extendedDescription = _a[1]; + } + else + this.description = description; + this.nid = Achievement._id++; + Object.assign(this, options); + if (options.modes) { + var _b = __read(options.modes), type = _b[0], modes_1 = _b.slice(1); + if (type == "only") { + this.allowedModes = modes_1; + this.modesText = modes_1.join(", "); + } + else { + this.allowedModes = config_1.GamemodeNames.filter(function (m) { return !modes_1.includes(m); }); + this.modesText = "all except ".concat(modes_1.join(", ")); + } + } + else { + this.allowedModes = config_1.GamemodeNames; + this.modesText = "all"; + } + if (!this.disabled) { + Achievement.all.push(this); + if (this.checkPlayerFrequent || this.checkFrequent) + Achievement.checkFrequent.push(this); + if (this.checkPlayerInfrequent || this.checkInfrequent) + Achievement.checkInfrequent.push(this); + if (this.checkPlayerJoin) + Achievement.checkJoin.push(this); + if (this.checkPlayerGameover || this.checkGameover) + Achievement.checkGameover.push(this); + } + } + Achievement.prototype.message = function () { + return config_1.FColor.achievement(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Achievement granted!\n[accent]", "[white]: ", ""], ["Achievement granted!\\n[accent]", "[white]: ", ""])), this.name, this.description); + }; + Achievement.prototype.messageToEveryone = function (player) { + return config_1.FColor.achievement(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Player ", " has completed the achievement \"", "\"."], ["Player ", " has completed the achievement \"", "\"."])), player.prefixedName, this.name); + }; + Achievement.prototype.allowedInMode = function () { + return this.allowedModes.includes(config_1.Gamemode.name()); + }; + Achievement.prototype.grantToAllOnline = function (team) { + var _this = this; + players_1.FishPlayer.forEachPlayer(function (p) { + if (!_this.has(p) && (!team || p.team() == team)) { + if (_this.notify != "nobody") + p.sendMessage(_this.message()); + _this.setObtained(p); + } + }); + }; + /** Do not call this in a loop on an achievement set to notify everyone. */ + Achievement.prototype.grantTo = function (player, allowRepeatMessage) { + if (allowRepeatMessage === void 0) { allowRepeatMessage = true; } + var has = this.has(player); + if (!has || allowRepeatMessage) { + if (this.notify == "everyone") + Call.sendMessage(this.messageToEveryone(player)); + else if (this.notify == "player") + player.sendMessage(this.message()); + } + if (!has) + this.setObtained(player); + }; + Achievement.prototype.setObtained = function (player) { + //void player.updateSynced(fishP => fishP.achievements.set(this.nid)); + player.achievements.set(this.nid); + }; + Achievement.prototype.has = function (player) { + return player.achievements.get(this.nid); + }; + Achievement.all = []; + /** Checked every second. */ + Achievement.checkFrequent = []; + /** Checked every 10 seconds. Use for states that can be gained but not lost, such as "x wins". */ + Achievement.checkInfrequent = []; + Achievement.checkJoin = []; + Achievement.checkGameover = []; + Achievement._id = 0; + return Achievement; +}()); +exports.Achievement = Achievement; +Events.on(EventType.PlayerJoin, function (_a) { + var e_1, _b; + var _c; + var player = _a.player; + Time.mark(); + var _loop_1 = function (ach) { + if (ach.allowedInMode()) { + var fishP_1 = players_1.FishPlayer.get(player); + if (!ach.has(fishP_1) && ((_c = ach.checkPlayerJoin) === null || _c === void 0 ? void 0 : _c.call(ach, fishP_1))) { + if (fishP_1.dataSynced) + ach.grantTo(fishP_1); + else + Timer.schedule(function () { return ach.grantTo(fishP_1); }, 2); //2 seconds should be enough + } + } + }; + try { + for (var _d = __values(Achievement.checkJoin), _e = _d.next(); !_e.done; _e = _d.next()) { + var ach = _e.value; + _loop_1(ach); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_e && !_e.done && (_b = _d.return)) _b.call(_d); + } + finally { if (e_1) throw e_1.error; } + } + Log.debug("ach join @", Time.elapsed()); +}); +globals_1.FishEvents.on("gameOver", function (_, winner) { + var e_2, _a; + var _b; + Time.mark(); + var _loop_2 = function (ach) { + if (ach.allowedInMode()) { + if ((_b = ach.checkGameover) === null || _b === void 0 ? void 0 : _b.call(ach, winner)) + ach.grantToAllOnline(); + else + players_1.FishPlayer.forEachPlayer(function (fishP) { + var _a; + if (!ach.has(fishP) && ((_a = ach.checkPlayerGameover) === null || _a === void 0 ? void 0 : _a.call(ach, fishP, winner))) { + ach.grantTo(fishP); + } + }); + } + }; + try { + for (var _c = __values(Achievement.checkGameover), _d = _c.next(); !_d.done; _d = _c.next()) { + var ach = _d.value; + _loop_2(ach); + } + } + catch (e_2_1) { e_2 = { error: e_2_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_2) throw e_2.error; } + } + Log.debug("ach gameover @", Time.elapsed()); +}); +Timer.schedule(function () { + var e_3, _a; + Time.mark(); + var _loop_3 = function (ach) { + if (ach.allowedInMode()) { + if (ach.checkFrequent) { + if (config_1.Gamemode.pvp()) { + Vars.state.teams.active.each(function (_a) { + var team = _a.team; + if (ach.checkFrequent(team)) + ach.grantToAllOnline(team); + }); + } + else { + if (ach.checkFrequent(Vars.state.rules.defaultTeam)) + ach.grantToAllOnline(); + } + } + else { + players_1.FishPlayer.forEachPlayer(function (fishP) { + var _a; + if (!ach.has(fishP) && ((_a = ach.checkPlayerFrequent) === null || _a === void 0 ? void 0 : _a.call(ach, fishP))) + ach.grantTo(fishP); + }); + } + } + }; + try { + for (var _b = __values(Achievement.checkFrequent), _c = _b.next(); !_c.done; _c = _b.next()) { + var ach = _c.value; + _loop_3(ach); + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_3) throw e_3.error; } + } + Log.debug("ach frequent @", Time.elapsed()); +}, 1, 1); +Timer.schedule(function () { + var e_4, _a; + Time.mark(); + var _loop_4 = function (ach) { + if (ach.allowedInMode()) { + if (ach.checkInfrequent) { + if (config_1.Gamemode.pvp()) { + Vars.state.teams.active.each(function (_a) { + var team = _a.team; + if (ach.checkInfrequent(team)) + ach.grantToAllOnline(team); + }); + } + else { + if (ach.checkInfrequent(Vars.state.rules.defaultTeam)) + ach.grantToAllOnline(); + } + } + else { + players_1.FishPlayer.forEachPlayer(function (fishP) { + var _a; + if (!ach.has(fishP) && ((_a = ach.checkPlayerInfrequent) === null || _a === void 0 ? void 0 : _a.call(ach, fishP))) + ach.grantTo(fishP); + }); + } + } + }; + try { + for (var _b = __values(Achievement.checkInfrequent), _c = _b.next(); !_c.done; _c = _b.next()) { + var ach = _c.value; + _loop_4(ach); + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_4) throw e_4.error; } + } + Log.debug("ach infrequent @", Time.elapsed()); +}, 10, 10); +exports.Achievements = { + // =========================== + // ╦ ╦ ╔═╗ ╦═╗ ╔╗╔ ╦ ╔╗╔ ╔═╗ ┬ + // ║║║ ╠═╣ ╠╦╝ ║║║ ║ ║║║ ║ ╦ │ + // ╚╩╝ ╩ ╩ ╩╚═ ╝╚╝ ╩ ╝╚╝ ╚═╝ o + // =========================== + // Do not change the order of any achievements. + // Do not remove any achievements: instead, set the "disabled" option to true. + // Reordering achievements will cause ID shifts. + //Joining based + welcome: new Achievement(["gold", Iconc.infoCircle], "Welcome", "Join the server.", { + checkPlayerJoin: function () { return true; }, + notify: "nobody" + }), + migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { + disabled: true + }), //TODO + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { + checkPlayerJoin: function (p) { return p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > funcs_1.Duration.months(1)); } + }), + //Gamemode based + attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { + modes: ["only", "attack"], + checkPlayerGameover: function (player, winTeam) { + return Vars.state.rules.defaultTeam == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), + survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { + modes: ["only", "survival"], + checkPlayerInfrequent: function (player) { + return player.tstats.wavesSurvived >= 50; + }, + }), + pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { + modes: ["only", "pvp"], + checkPlayerGameover: function (player, winTeam) { + return player.team() == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), + sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { + modes: ["only", "sandbox"], + checkPlayerInfrequent: function (p) { return p.stats.timeInGame > funcs_1.Duration.hours(1); }, + }), + hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { + modes: ["only", "hexed"], + checkPlayerGameover: function (player) { + return player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), + minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { + modes: ["only", "minigame"], + checkPlayerGameover: function (player, winTeam) { + return player.team() == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), + //playtime based + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(1); } + }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(12); } + }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(2); } + }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(10); } + }), + //victories based + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 1; } + }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 5; } + }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 30; } + }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 100; }, + notify: "everyone" + }), + //games based + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 10; } + }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 40; } + }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 100; } + }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 200; }, + notify: "everyone" + }), + //messages based + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 1; }, + notify: "nobody" + }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 100; } + }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", ["Send 500 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 500; } + }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", ["Send 2000 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 2000; } + }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", ["Send 5000 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 5000; }, + notify: "everyone" + }), + //blocks built based + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced >= 1; }, + notify: "nobody" + }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 200; } + }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 1000; } + }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 5000; }, + }), + //units + t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { + modes: ["not", "sandbox"], + checkPlayerFrequent: function (player) { + var _a; + return globals_1.unitsT5.includes((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type); + }, + }), + dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { + modes: ["not", "sandbox"], + disabled: true + }), //TODO + worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { + checkPlayerFrequent: function (player) { + var _a; + return ((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type) == UnitTypes.latum; + } + }), + //pvp + above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { + modes: ["only", "pvp"], + checkPlayerInfrequent: function (p) { return p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20; } + }), + head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { + modes: ["only", "pvp"], + disabled: true + }), //TODO + one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { + modes: ["only", "pvp"], + disabled: true + }), //TODO + //sandbox + underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { + modes: ["only", "sandbox"], + checkFrequent: function () { + var found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(function (_a) { + var graph = _a.graph; + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if (graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && (graph.lastPowerProduced / Time.delta * 60) >= 999900) + found = true; + }); + return found; + } + }), + //easter eggs + memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { + notify: "nobody" + }), + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "XKCD 838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { + notify: "everyone" + }), + script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { + notify: "nobody" + }), + hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { + hidden: true + }), + //items based + items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { + modes: ["not", "sandbox"], + checkPlayerFrequent: function (player) { + var _a; + if (!Vars.state.planet) + return false; + return ((_a = player.team().items()) === null || _a === void 0 ? void 0 : _a.has(usefulItems10k[Vars.state.planet.name])) || false; + }, + }), + fullVault: new Achievement(["green", Blocks.vault.emoji()], "Well Stocked", ["Fill a vault with every obtainable item.", "Requires mixtech."], { + modes: ["not", "sandbox"], + checkInfrequent: function (team) { + return Vars.indexer.getFlagged(team, BlockFlag.storage).contains(boolf(function (b) { return b.block == Blocks.vault && b.items.has(allItems1k) && b.linkedCore == null; })); + }, + }), + full_core: new Achievement(["green", Blocks.coreAcropolis.emoji()], "Multiblock Incinerator", "Completely fill the core with all obtainable items on a map with core incineration enabled.", { + modes: ["not", "sandbox"], + checkFrequent: function (team) { + var _a; + if (!Vars.state.planet) + return false; + var items; + switch (Vars.state.planet.name) { + case "serpulo": + items = Items.serpuloItems; + break; + case "erekir": + items = Items.erekirItems; + break; + case "sun": + items = mixtechItems; + break; + } + var capacity = (_a = team.core()) === null || _a === void 0 ? void 0 : _a.storageCapacity; + if (!capacity) + return false; + var module = team.items(); + return items.allMatch(function (i) { return module.has(i, capacity); }); + }, + }), + siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { + modes: ["not", "sandbox"] + }), + silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { + modes: ["not", "sandbox"], + checkFrequent: function (team) { return team.items().has(Items.silicon, 100000); } + }), + //other players based + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { + notify: "nobody" + }), + join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { + checkPlayerJoin: function () { return Groups.player.size() > 20; }, + }), + meet_staff: new Achievement(["lime", Iconc.hammer], "Griefer Beware", "Meet a staff member in-game", { + checkPlayerJoin: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).ranksAtLeast("mod"); }); }, + }), + meet_fish: new Achievement(["blue", Iconc.admin], "The Big Fish", "Meet >|||>Fish himself in-game", { + checkPlayerJoin: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).ranksAtLeast("fish"); }); }, + hidden: true, + }), + server_speak: new Achievement(["pink", Iconc.host], "It Speaks!", "Hear the server talk in chat."), + see_marked_griefer: new Achievement(["red", Iconc.hammer], "Flying Tonk", "See a marked griefer in-game.", { + checkInfrequent: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).marked(); }); }, + }), + //maps based + beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { + notify: "everyone", + modes: ["not", "pvp"], + checkGameover: function (team) { return team == Vars.state.rules.defaultTeam && !Vars.state.map.custom; } + }), + //misc + power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { + modes: ["not", "sandbox"], + checkFrequent: function (team) { + var found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(function (_a) { + var _b; + var graph = _a.graph; + //we need to actually check for power sources + if ((graph.lastPowerProduced / Time.delta * 60) > 1e6 && + !graph.producers.contains(boolf(function (b) { return b.block == Blocks.powerSource; })) && + ((_b = graph.producers.firstOpt()) === null || _b === void 0 ? void 0 : _b.team) == team) + found = true; + }); + return found; + } + }), + pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { + modes: ["not", "sandbox"], + disabled: true + }), //TODO + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 50 health, but survive.", { + modes: ["not", "sandbox"], + }), + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 50 health, but survive.", { + modes: ["not", "sandbox"], + }), + verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { + checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "nobody" + }), + click_me: new Achievement(Iconc.bookOpen, "Clicked", "Run /achievementgrid and click this achievement."), + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { + modes: ["not", "sandbox"], + checkPlayerGameover: function (player, winTeam) { + return player.team() == winTeam && player.tstats.blockInteractionsThisMap == 0; + }, + }), + status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { + checkPlayerFrequent: function (p) { + var unit = p.unit(); + if (!unit) + return false; + var statuses = (0, utils_1.getStatuses)(unit); + return statuses.size >= 5; + }, + modes: ["not", "sandbox"] + }), + drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { + notify: "everyone", + modes: ["not", "sandbox"] + }), + drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", "Drown a Mace in ".concat(Blocks.cryofluid.emoji(), " Cryofluid."), { + notify: "everyone", + modes: ["not", "sandbox"] + }), + max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { + checkPlayerFrequent: function (player) { + var _a, _b; + var tile = (_b = (_a = player.unit()) === null || _a === void 0 ? void 0 : _a.tile) === null || _b === void 0 ? void 0 : _b.call(_a); + if (!tile) + return false; + return tile.block == Blocks.duo && !tile.ammo.isEmpty() && tile.ammo.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; + }, + notify: "everyone", + modes: ["not", "sandbox"] + }), + foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { + notify: "everyone", + modes: ["not", "sandbox"] + }), + impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { + modes: ["not", "sandbox"], + notify: "everyone", + checkInfrequent: function (team) { + var found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(function (_a) { + var graph = _a.graph; + if (graph.producers.size >= 15 && graph.producers.count(function (b) { return b.block == Blocks.impactReactor && b.warmup > 0.99999; }) > 15 && graph.producers.first().team == team) + found = true; + }); + return found; + }, + }), + help_help: new Achievement(["brown", Iconc.info], "Help with help", "Run /help help", { + notify: "everyone" + }), + ohno: new Achievement(["scarlet", UnitTypes.alpha.emoji()], "Oh no", "Control an ohno unit."), + sniper_duel: new Achievement(["yellow", UnitTypes.omura.emoji()], "Sniper duel", "Kill a Foreshadow with an Omura from outside its range."), + around_the_world: new Achievement(Iconc.planet, "Around the World", "Fly your unit around the entire map without entering it, starting from the lower left.", { + notify: "everyone", + }), +}; +Object.entries(exports.Achievements).forEach(function (_a) { + var _b = __read(_a, 2), id = _b[0], a = _b[1]; + return a.sid = id; +}); +globals_1.FishEvents.on("commandUnauthorized", function (_, player, name) { + if ((name == "js" || name == "fjs") && !exports.Achievements.run_js_without_perms.has(player)) + exports.Achievements.run_js_without_perms.grantTo(player); +}); +Events.on(EventType.UnitDrownEvent, function (_a) { + var _b; + var unit = _a.unit; + if (!config_1.Gamemode.sandbox()) { + if (unit.type == UnitTypes.mace && ((_b = unit.tileOn()) === null || _b === void 0 ? void 0 : _b.floor()) == Blocks.cryofluid) + exports.Achievements.drown_mace_in_cryo.grantToAllOnline(); + else if (unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) { + if (config_1.Gamemode.pvp()) { + Vars.state.teams.active.map(function (t) { return t.team; }).select(function (t) { return t !== unit.team; }).each(function (t) { return exports.Achievements.drown_big_tank.grantToAllOnline(t); }); + } + else { + if (unit.team !== Vars.state.rules.defaultTeam) + exports.Achievements.drown_big_tank.grantToAllOnline(); + } + } + } +}); +Events.on(EventType.UnitBulletDestroyEvent, function (_a) { + var _b; + var unit = _a.unit, bullet = _a.bullet; + if (!config_1.Gamemode.sandbox() && unit.type == UnitTypes.dagger && ((_b = bullet.owner) === null || _b === void 0 ? void 0 : _b.block) == Blocks.foreshadow) { + var build = bullet.owner; + if (build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) + exports.Achievements.foreshadow_overkill.grantToAllOnline(build.team); + } +}); +Events.on(EventType.BuildingBulletDestroyEvent, function (_a) { + var _b; + var build = _a.build, bullet = _a.bullet; + if (!config_1.Gamemode.sandbox() && build.block == Blocks.foreshadow && ((_b = bullet.owner) === null || _b === void 0 ? void 0 : _b.type) == UnitTypes.omura) { + var unit = bullet.owner; + var player = unit.getPlayer(); + if (player && !unit.within(build, build.range() + unit.hitSize / 2)) + exports.Achievements.sniper_duel.grantTo(players_1.FishPlayer.get(player)); + } +}); +var siliconReached = Team.all.map(function (_) { return false; }); +Events.on(EventType.GameOverEvent, function () { return siliconReached = Team.all.map(function (_) { return false; }); }); +var isAlone = 0; +Timer.schedule(function () { + if (!Vars.state.gameOver && !config_1.Gamemode.sandbox()) { + Vars.state.teams.active.each(function (_a) { + var team = _a.team; + if (team.items().has(Items.silicon, 2000)) + siliconReached[team.id] = true; + else if (siliconReached[team.id] && team.items().get(Items.silicon) == 0) + exports.Achievements.siligone.grantToAllOnline(team); + }); + } + if (Groups.player.size() == 1) { + if (isAlone == 0) + isAlone = Date.now(); + else if (Date.now() > isAlone + funcs_1.Duration.minutes(2)) + exports.Achievements.alone.grantToAllOnline(); + } + else + isAlone = 0; +}, 2, 2); +var coreHealthTime = new Map(); +if (!config_1.Gamemode.sandbox()) + Timer.schedule(function () { + coreHealthTime.forEach(function (value, core) { + if (Date.now() > value) { + if (core.dead) { + coreHealthTime.delete(core); + } + else if (core.health > 50) { + //grant achievement + exports.Achievements.core_low_hp.grantToAllOnline(core.team); + players_1.FishPlayer.forEachPlayer(function (p) { + if (core.team != p.team() && !exports.Achievements.enemy_core_low_hp.has(p)) + exports.Achievements.enemy_core_low_hp.grantTo(p); + }); + coreHealthTime.delete(core); + } + } + }); + Vars.state.teams.active.flatMap(function (t) { return t.cores; }).each(function (core) { + if (core.health < 50 && !coreHealthTime.get(core)) + coreHealthTime.set(core, Date.now() + 12000); + }); + }, 1, 1); +var aroundTheWorld = {}; +Timer.schedule(function () { + var e_5, _a; + players_1.FishPlayer.forEachPlayer(function (p) { + var _a; + var _b; + var unit = p.unit(); + if (unit && unit.x < 0 && unit.y < 0) { + (_a = aroundTheWorld[_b = p.uuid]) !== null && _a !== void 0 ? _a : (aroundTheWorld[_b] = { player: p, unit: unit, side: "left" }); + } + }); + var _loop_5 = function (uuid, entry) { + if (!(function () { + if (entry.unit.dead) + return false; + var left = entry.unit.x < 0; + var bottom = entry.unit.y < 0; + var right = entry.unit.x > (Vars.world.width() - 1) * 8; + var top = entry.unit.y > (Vars.world.height() - 1) * 8; + switch (entry.side) { + case "left": + if (!left) + return false; + if (top) + entry.side = "top"; + break; + case "top": + if (!top) + return false; + if (right) + entry.side = "right"; + break; + case "right": + if (!right) + return false; + if (bottom) + entry.side = "bottom"; + break; + case "bottom": + if (!bottom) + return false; + if (left) { + exports.Achievements.around_the_world.grantTo(entry.player, true); + return false; + } + break; + } + return true; + })()) + delete aroundTheWorld[uuid]; + }; + try { + for (var _b = __values(Object.entries(aroundTheWorld)), _c = _b.next(); !_c.done; _c = _b.next()) { + var _d = __read(_c.value, 2), uuid = _d[0], entry = _d[1]; + _loop_5(uuid, entry); + } + } + catch (e_5_1) { e_5 = { error: e_5_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_5) throw e_5.error; } + } +}, 1, 0.5); +Events.on(EventType.GameOverEvent, function () { return coreHealthTime.clear(); }); +Events.on(EventType.WorldLoadEvent, function () { return coreHealthTime.clear(); }); +globals_1.FishEvents.on("scriptKiddie", function (_, p) { return Timer.schedule(function () { + if (!exports.Achievements.script_kiddie.has(p)) + exports.Achievements.script_kiddie.grantTo(p); +}, 2); }); +globals_1.FishEvents.on("memoryCorruption", function () { return exports.Achievements.memory_corruption.grantToAllOnline(); }); +globals_1.FishEvents.on("serverSays", function () { return exports.Achievements.server_speak.grantToAllOnline(); }); +var templateObject_1, templateObject_2; diff --git a/build/scripts/api.js b/build/scripts/api.js index 8196294e..e479452f 100644 --- a/build/scripts/api.js +++ b/build/scripts/api.js @@ -1,214 +1,214 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains wrappers over the API calls to the backend server. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.isVpn = isVpn; -exports.isVpnCached = isVpnCached; -exports.sendModerationMessage = sendModerationMessage; -exports.getStaffMessages = getStaffMessages; -exports.sendStaffMessage = sendStaffMessage; -exports.ban = ban; -exports.unban = unban; -exports.getBanned = getBanned; -exports.getFishPlayerData = getFishPlayerData; -exports.setFishPlayerData = setFishPlayerData; -var config_1 = require("/config"); -var players_1 = require("/players"); -var promise_1 = require("/promise"); -var cachedIps = {}; -/** Make an API request to see if an IP is likely VPN. */ -function isVpn(ip, callback, callbackError) { - if (ip in cachedIps) - return callback(cachedIps[ip]); - Http.get("http://ip-api.com/json/".concat(ip, "?fields=proxy,hosting"), function (res) { - var data = res.getResultAsString(); - var json = JSON.parse(data); - var isVpn = json.proxy || json.hosting; - cachedIps[ip] = isVpn; - players_1.FishPlayer.stats.numIpsChecked++; - if (isVpn) - players_1.FishPlayer.stats.numIpsFlagged++; - callback(isVpn); - }, callbackError !== null && callbackError !== void 0 ? callbackError : (function (err) { - Log.err("[API] Network error when trying to call api.isVpn()"); - players_1.FishPlayer.stats.numIpsErrored++; - callback(false); - })); -} -function isVpnCached(ip) { - return cachedIps[ip]; -} -/** Send text to the moderation logs channel in Discord. */ -function sendModerationMessage(message) { - if (config_1.Mode.noBackend) { - Log.info("Sent moderation log message: ".concat(message)); - return; - } - var req = Http.post("http://".concat(config_1.backendIP, "/api/mod-dump"), JSON.stringify({ message: message })).header('Content-Type', 'application/json').header('Accept', '*/*'); - req.timeout = 10000; - req.error(function () { return Log.err("[API] Network error when trying to call api.sendModerationMessage()"); }); - req.submit(function (response) { - //Log.info(response.getResultAsString()); - }); -} -/** Get staff messages from discord. */ -function getStaffMessages(callback) { - if (config_1.Mode.noBackend) - return; - var req = Http.post("http://".concat(config_1.backendIP, "/api/getStaffMessages"), JSON.stringify({ server: config_1.Gamemode.name() })) - .header('Content-Type', 'application/json').header('Accept', '*/*'); - req.timeout = 10000; - req.error(function () { return Log.err("[API] Network error when trying to call api.getStaffMessages()"); }); - req.submit(function (response) { - var temp = response.getResultAsString(); - if (!temp.length) - Log.err("[API] Network error(empty response) when trying to call api.getStaffMessages()"); - else - callback(JSON.parse(temp).messages); - }); -} -/** Send staff messages from server. */ -function sendStaffMessage(message, playerName, isStaff, callback) { - if (config_1.Mode.noBackend) - return; - var req = Http.post("http://".concat(config_1.backendIP, "/api/sendStaffMessage"), - // need to send both name variants so one can be sent to the other servers with color and discord can use the clean one - JSON.stringify({ message: message, playerName: playerName, cleanedName: Strings.stripColors(playerName), server: config_1.Gamemode.name(), isStaff: isStaff })).header('Content-Type', 'application/json').header('Accept', '*/*'); - req.timeout = 10000; - req.error(function () { - Log.err("[API] Network error when trying to call api.sendStaffMessage()"); - callback === null || callback === void 0 ? void 0 : callback(false); - }); - req.submit(function (response) { - var temp = response.getResultAsString(); - if (!temp.length) - Log.err("[API] Network error(empty response) when trying to call api.sendStaffMessage()"); - else - callback === null || callback === void 0 ? void 0 : callback(JSON.parse(temp).data); - }); -} -/** Bans the provided ip and/or uuid. */ -function ban(data, callback) { - if (callback === void 0) { callback = function () { }; } - if (config_1.Mode.noBackend) - return; - var req = Http.post("http://".concat(config_1.backendIP, "/api/ban"), JSON.stringify(data)) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(function () { return Log.err("[API] Network error when trying to call api.ban(".concat(data.ip, ", ").concat(data.uuid, ")")); }); - req.submit(function (response) { - var str = response.getResultAsString(); - if (!str.length) - return Log.err("[API] Network error(empty response) when trying to call api.ban()"); - callback(JSON.parse(str).data); - }); -} -/** Unbans the provided ip and/or uuid. */ -function unban(data, callback) { - if (callback === void 0) { callback = function () { }; } - if (config_1.Mode.noBackend) - return; - var req = Http.post("http://".concat(config_1.backendIP, "/api/unban"), JSON.stringify(data)) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(function () { return Log.err("[API] Network error when trying to call api.ban({".concat(data.ip, ", ").concat(data.uuid, "})")); }); - req.submit(function (response) { - var str = response.getResultAsString(); - if (!str.length) - return Log.err("[API] Network error(empty response) when trying to call api.unban()"); - var parsedData = JSON.parse(str); - callback(parsedData.status, parsedData.error); - }); -} -/** Gets if either the provided uuid or ip is banned. */ -function getBanned(data, callback) { - if (config_1.Mode.noBackend) { - Log.info("[API] Attempted to getBanned(".concat(data.uuid, "/").concat(data.ip, "), assuming false due to local debug")); - callback(false); - return; - } - //TODO cache 4s - var req = Http.post("http://".concat(config_1.backendIP, "/api/checkIsBanned"), JSON.stringify(data)) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(function () { return Log.err("[API] Network error when trying to call api.getBanned()"); }); - req.submit(function (response) { - var str = response.getResultAsString(); - if (!str.length) - return Log.err("[API] Network error(empty response) when trying to call api.getBanned()"); - callback(JSON.parse(str).data); - }); -} -/** - * Fetches fish player data from the backend. - **/ -function getFishPlayerData(uuid) { - var _a = promise_1.Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; - function fail(err) { - Log.err("[API] Network error when trying to call api.getFishPlayerData()"); - if (err) - Log.err(err); - reject(err); - } - if (config_1.Mode.noBackend) { - reject("local debug mode"); - return promise; - } - var req = Http.post("http://".concat(config_1.backendIP, "/api/fish-player"), JSON.stringify({ - id: uuid, - gamemode: config_1.Gamemode.name(), - })) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(fail); - req.submit(function (response) { - var data = response.getResultAsString(); - if (data) { - var result = JSON.parse(data); - if (!result || typeof result != "object") - fail("Invalid fish player data"); - resolve(result); - } - else { - resolve(null); - } - }); - return promise; -} -/** Pushes fish player data to the backend. */ -function setFishPlayerData(data, repeats, ignoreActivelySyncedFields) { - var _a = promise_1.Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; - if (config_1.Mode.noBackend) { - resolve(); - return promise; - } - var req = Http.post("http://".concat(config_1.backendIP, "/api/fish-player/set"), JSON.stringify({ - player: data, - gamemode: config_1.Gamemode.name(), - ignoreActivelySyncedFields: ignoreActivelySyncedFields, - })) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(function (err) { - var _a, _b; - Log.err("[API] Network error when trying to call api.setFishPlayerData(), repeats=".concat(repeats)); - Log.err(err); - if (err === null || err === void 0 ? void 0 : err.response) - Log.err(err.response.getResultAsString()); - if (repeats > 0 && !(((_a = err.status) === null || _a === void 0 ? void 0 : _a.code) >= 400 && ((_b = err.status) === null || _b === void 0 ? void 0 : _b.code) <= 499)) - setFishPlayerData(data, repeats - 1, ignoreActivelySyncedFields).then(resolve).catch(reject); - else - reject(err); - }); - req.submit(function (response) { - resolve(); - }); - return promise; -} +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains wrappers over the API calls to the backend server. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isVpn = isVpn; +exports.isVpnCached = isVpnCached; +exports.sendModerationMessage = sendModerationMessage; +exports.getStaffMessages = getStaffMessages; +exports.sendStaffMessage = sendStaffMessage; +exports.ban = ban; +exports.unban = unban; +exports.getBanned = getBanned; +exports.getFishPlayerData = getFishPlayerData; +exports.setFishPlayerData = setFishPlayerData; +var config_1 = require("/config"); +var players_1 = require("/players"); +var promise_1 = require("/promise"); +var cachedIps = {}; +/** Make an API request to see if an IP is likely VPN. */ +function isVpn(ip, callback, callbackError) { + if (ip in cachedIps) + return callback(cachedIps[ip]); + Http.get("http://ip-api.com/json/".concat(ip, "?fields=proxy,hosting"), function (res) { + var data = res.getResultAsString(); + var json = JSON.parse(data); + var isVpn = json.proxy || json.hosting; + cachedIps[ip] = isVpn; + players_1.FishPlayer.stats.numIpsChecked++; + if (isVpn) + players_1.FishPlayer.stats.numIpsFlagged++; + callback(isVpn); + }, callbackError !== null && callbackError !== void 0 ? callbackError : (function (err) { + Log.err("[API] Network error when trying to call api.isVpn()"); + players_1.FishPlayer.stats.numIpsErrored++; + callback(false); + })); +} +function isVpnCached(ip) { + return cachedIps[ip]; +} +/** Send text to the moderation logs channel in Discord. */ +function sendModerationMessage(message) { + if (config_1.Mode.noBackend) { + Log.info("Sent moderation log message: ".concat(message)); + return; + } + var req = Http.post("http://".concat(config_1.backendIP, "/api/mod-dump"), JSON.stringify({ message: message })).header('Content-Type', 'application/json').header('Accept', '*/*'); + req.timeout = 10000; + req.error(function () { return Log.err("[API] Network error when trying to call api.sendModerationMessage()"); }); + req.submit(function (response) { + //Log.info(response.getResultAsString()); + }); +} +/** Get staff messages from discord. */ +function getStaffMessages(callback) { + if (config_1.Mode.noBackend) + return; + var req = Http.post("http://".concat(config_1.backendIP, "/api/getStaffMessages"), JSON.stringify({ server: config_1.Gamemode.name() })) + .header('Content-Type', 'application/json').header('Accept', '*/*'); + req.timeout = 10000; + req.error(function () { return Log.err("[API] Network error when trying to call api.getStaffMessages()"); }); + req.submit(function (response) { + var temp = response.getResultAsString(); + if (!temp.length) + Log.err("[API] Network error(empty response) when trying to call api.getStaffMessages()"); + else + callback(JSON.parse(temp).messages); + }); +} +/** Send staff messages from server. */ +function sendStaffMessage(message, playerName, isStaff, callback) { + if (config_1.Mode.noBackend) + return; + var req = Http.post("http://".concat(config_1.backendIP, "/api/sendStaffMessage"), + // need to send both name variants so one can be sent to the other servers with color and discord can use the clean one + JSON.stringify({ message: message, playerName: playerName, cleanedName: Strings.stripColors(playerName), server: config_1.Gamemode.name(), isStaff: isStaff })).header('Content-Type', 'application/json').header('Accept', '*/*'); + req.timeout = 10000; + req.error(function () { + Log.err("[API] Network error when trying to call api.sendStaffMessage()"); + callback === null || callback === void 0 ? void 0 : callback(false); + }); + req.submit(function (response) { + var temp = response.getResultAsString(); + if (!temp.length) + Log.err("[API] Network error(empty response) when trying to call api.sendStaffMessage()"); + else + callback === null || callback === void 0 ? void 0 : callback(JSON.parse(temp).data); + }); +} +/** Bans the provided ip and/or uuid. */ +function ban(data, callback) { + if (callback === void 0) { callback = function () { }; } + if (config_1.Mode.noBackend) + return; + var req = Http.post("http://".concat(config_1.backendIP, "/api/ban"), JSON.stringify(data)) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(function () { return Log.err("[API] Network error when trying to call api.ban(".concat(data.ip, ", ").concat(data.uuid, ")")); }); + req.submit(function (response) { + var str = response.getResultAsString(); + if (!str.length) + return Log.err("[API] Network error(empty response) when trying to call api.ban()"); + callback(JSON.parse(str).data); + }); +} +/** Unbans the provided ip and/or uuid. */ +function unban(data, callback) { + if (callback === void 0) { callback = function () { }; } + if (config_1.Mode.noBackend) + return; + var req = Http.post("http://".concat(config_1.backendIP, "/api/unban"), JSON.stringify(data)) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(function () { return Log.err("[API] Network error when trying to call api.ban({".concat(data.ip, ", ").concat(data.uuid, "})")); }); + req.submit(function (response) { + var str = response.getResultAsString(); + if (!str.length) + return Log.err("[API] Network error(empty response) when trying to call api.unban()"); + var parsedData = JSON.parse(str); + callback(parsedData.status, parsedData.error); + }); +} +/** Gets if either the provided uuid or ip is banned. */ +function getBanned(data, callback) { + if (config_1.Mode.noBackend) { + Log.info("[API] Attempted to getBanned(".concat(data.uuid, "/").concat(data.ip, "), assuming false due to local debug")); + callback(false); + return; + } + //TODO cache 4s + var req = Http.post("http://".concat(config_1.backendIP, "/api/checkIsBanned"), JSON.stringify(data)) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(function () { return Log.err("[API] Network error when trying to call api.getBanned()"); }); + req.submit(function (response) { + var str = response.getResultAsString(); + if (!str.length) + return Log.err("[API] Network error(empty response) when trying to call api.getBanned()"); + callback(JSON.parse(str).data); + }); +} +/** + * Fetches fish player data from the backend. + **/ +function getFishPlayerData(uuid) { + var _a = promise_1.Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; + function fail(err) { + Log.err("[API] Network error when trying to call api.getFishPlayerData()"); + if (err) + Log.err(err); + reject(err); + } + if (config_1.Mode.noBackend) { + reject("local debug mode"); + return promise; + } + var req = Http.post("http://".concat(config_1.backendIP, "/api/fish-player"), JSON.stringify({ + id: uuid, + gamemode: config_1.Gamemode.name(), + })) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(fail); + req.submit(function (response) { + var data = response.getResultAsString(); + if (data) { + var result = JSON.parse(data); + if (!result || typeof result != "object") + fail("Invalid fish player data"); + resolve(result); + } + else { + resolve(null); + } + }); + return promise; +} +/** Pushes fish player data to the backend. */ +function setFishPlayerData(data, repeats, ignoreActivelySyncedFields) { + var _a = promise_1.Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; + if (config_1.Mode.noBackend) { + resolve(); + return promise; + } + var req = Http.post("http://".concat(config_1.backendIP, "/api/fish-player/set"), JSON.stringify({ + player: data, + gamemode: config_1.Gamemode.name(), + ignoreActivelySyncedFields: ignoreActivelySyncedFields, + })) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(function (err) { + var _a, _b; + Log.err("[API] Network error when trying to call api.setFishPlayerData(), repeats=".concat(repeats)); + Log.err(err); + if (err === null || err === void 0 ? void 0 : err.response) + Log.err(err.response.getResultAsString()); + if (repeats > 0 && !(((_a = err.status) === null || _a === void 0 ? void 0 : _a.code) >= 400 && ((_b = err.status) === null || _b === void 0 ? void 0 : _b.code) <= 499)) + setFishPlayerData(data, repeats - 1, ignoreActivelySyncedFields).then(resolve).catch(reject); + else + reject(err); + }); + req.submit(function (response) { + resolve(); + }); + return promise; +} diff --git a/build/scripts/commands/aggregate.js b/build/scripts/commands/aggregate.js index c8d305d9..ed0e4481 100644 --- a/build/scripts/commands/aggregate.js +++ b/build/scripts/commands/aggregate.js @@ -1,54 +1,54 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.registerAll = registerAll; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file combines all the commands defined in various files into one function. -*/ -var console_1 = require("/commands/console"); -var general_1 = require("/commands/general"); -var member_1 = require("/commands/member"); -var staff_1 = require("/commands/staff"); -var commands = __importStar(require("/frameworks/commands")); -var packetHandlers_1 = require("/packetHandlers"); -function registerAll(clientHandler, serverHandler) { - commands.register(staff_1.commands, clientHandler, serverHandler); - commands.register(general_1.commands, clientHandler, serverHandler); - commands.register(member_1.commands, clientHandler, serverHandler); - commands.register(packetHandlers_1.commands, clientHandler, serverHandler); - commands.registerConsole(console_1.commands, serverHandler); - commands.initialize(); -} +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerAll = registerAll; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file combines all the commands defined in various files into one function. +*/ +var console_1 = require("/commands/console"); +var general_1 = require("/commands/general"); +var member_1 = require("/commands/member"); +var staff_1 = require("/commands/staff"); +var commands = __importStar(require("/frameworks/commands")); +var packetHandlers_1 = require("/packetHandlers"); +function registerAll(clientHandler, serverHandler) { + commands.register(staff_1.commands, clientHandler, serverHandler); + commands.register(general_1.commands, clientHandler, serverHandler); + commands.register(member_1.commands, clientHandler, serverHandler); + commands.register(packetHandlers_1.commands, clientHandler, serverHandler); + commands.registerConsole(console_1.commands, serverHandler); + commands.initialize(); +} diff --git a/build/scripts/commands/console.js b/build/scripts/commands/console.js index ba906eca..63703986 100644 --- a/build/scripts/commands/console.js +++ b/build/scripts/commands/console.js @@ -1,1129 +1,1129 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains all the console commands, which can be run through the server console. -*/ -var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { - if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } - return cooked; -}; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.commands = void 0; -var api = __importStar(require("/api")); -var config_1 = require("/config"); -var files_1 = require("/files"); -var fjsContext = __importStar(require("/fjsContext")); -var commands_1 = require("/frameworks/commands"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var players_1 = require("/players"); -var ranks_1 = require("/ranks"); -var utils_1 = require("/utils"); -exports.commands = (0, commands_1.consoleCommandList)({ - setrank: { - args: ["player:player", "rank:rank"], - description: "Set a player's rank.", - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (args.rank == ranks_1.Rank.pi && !config_1.Mode.localDebug) - (0, commands_1.fail)(f(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Rank ", " is immutable."], ["Rank ", " is immutable."])), args.rank)); - if (args.player.immutable() && !config_1.Mode.localDebug) - (0, commands_1.fail)(f(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Player ", " is immutable."], ["Player ", " is immutable."])), args.player)); - return [4 /*yield*/, args.player.setRank(args.rank)]; - case 1: - _c.sent(); - (0, utils_1.logAction)("set rank to ".concat(args.rank.name, " for"), "console", args.player); - outputSuccess(f(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Set rank of player ", " to ", ""], ["Set rank of player ", " to ", ""])), args.player, args.rank)); - return [2 /*return*/]; - } - }); - }); - } - }, - admin: { - args: ["nothing:string?"], - description: "Use the setrank command instead.", - handler: function () { - (0, commands_1.fail)("Use the \"setrank\" command instead. Hint: \"setrank player admin\""); - } - }, - setflag: { - args: ["player:player", "flag:roleflag", "value:boolean"], - description: "Set a player's role flags.", - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: return [4 /*yield*/, args.player.setFlag(args.flag, args.value)]; - case 1: - _c.sent(); - (0, utils_1.logAction)("set roleflag ".concat(args.flag.name, " to ").concat(args.value, " for"), "console", args.player); - outputSuccess(f(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Set role flag ", " of player ", " to ", ""], ["Set role flag ", " of player ", " to ", ""])), args.flag, args.player, args.value)); - return [2 /*return*/]; - } - }); - }); - } - }, - savePlayers: { - args: [], - description: "Runs FishPlayer.save()", - handler: function (_a) { - var outputSuccess = _a.outputSuccess; - players_1.FishPlayer.saveAll(); - outputSuccess("Successfully wrote fish player data."); - } - }, - info: { - args: ["player:string"], - description: "Find player info(s). Displays all names and ips of a player.", - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - function display(infoList) { - var e_2, _a; - var outputString = [""]; - var _loop_1 = function (playerInfo, fishP) { - var flagsText = [ - (fishP === null || fishP === void 0 ? void 0 : fishP.marked()) && (globals_1.maxTime - fishP.unmarkTime < 20000 ? - "&lris marked forever&fr" - : "&lris marked&fr until ".concat((0, utils_1.formatTimeRelative)(fishP.unmarkTime))), - (fishP === null || fishP === void 0 ? void 0 : fishP.muted) && "&lris muted&fr", - (fishP === null || fishP === void 0 ? void 0 : fishP.hasFlag("member")) && "&lmis member&fr", - (fishP === null || fishP === void 0 ? void 0 : fishP.autoflagged) && "&lris autoflagged&fr", - playerInfo.banned && "&bris UUID banned&fr", - ].filter(Boolean).join(", "); - var lastJoinedColor = (fishP === null || fishP === void 0 ? void 0 : fishP.lastJoined) && fishP.lastJoined !== -1 ? (function () { - var timeSinceLastJoin = (Date.now() - fishP.lastJoined) / 1000; - if (timeSinceLastJoin < 3600) - return "&br"; - if (timeSinceLastJoin < 24 * 3600) - return "&by"; - if (timeSinceLastJoin < 7 * 24 * 3600) - return "&lw"; - return "&lk"; - })() : "&fr"; - outputString.push([ - "".concat(lastJoinedColor, "Trace info for player &fr&y").concat(playerInfo.id, "&fr").concat(lastJoinedColor, " / &c\"").concat((0, funcs_1.escapeStringColorsServer)(Strings.stripColors(playerInfo.lastName)), "\" &lk(").concat((0, funcs_1.escapeStringColorsServer)(playerInfo.lastName), ")&fr"), - playerInfo.names.size > 1 && "all names used: ".concat(playerInfo.names.map(funcs_1.escapeStringColorsServer).map(function (n) { return "&c\"".concat(n, "\"&fr"); }).items.join(', ')), - "all IPs used: ".concat(playerInfo.ips.map(function (n) { return (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr'; }).items.join(", ")), - "joined &c".concat(playerInfo.timesJoined, "&fr times, kicked &c").concat(playerInfo.timesKicked, "&fr times"), - fishP && fishP.lastJoined !== -1 && "Last joined: ".concat((0, utils_1.formatTimeRelative)(fishP.lastJoined)), - fishP && fishP.firstJoined !== -1 && (0, utils_1.formatTimeRelative)(fishP.firstJoined), - fishP && "USID: &c".concat(fishP.usid, "&fr"), - fishP && fishP.rank !== ranks_1.Rank.player && "Rank: &c".concat(fishP.rank.name, "&fr"), - flagsText, - ].filter(Boolean).map(function (l, i) { return i == 0 ? l : '\t' + l; }).join("\n")); - }; - try { - for (var infoList_1 = __values(infoList), infoList_1_1 = infoList_1.next(); !infoList_1_1.done; infoList_1_1 = infoList_1.next()) { - var _b = __read(infoList_1_1.value, 2), playerInfo = _b[0], fishP = _b[1]; - _loop_1(playerInfo, fishP); - } - } - catch (e_2_1) { e_2 = { error: e_2_1 }; } - finally { - try { - if (infoList_1_1 && !infoList_1_1.done && (_a = infoList_1.return)) _a.call(infoList_1); - } - finally { if (e_2) throw e_2.error; } - } - output(outputString.join("\n")); - } - var infoList, playersToFetch, _c, _d, batch, e_1_1, err_1; - var e_1, _e; - var _this = this; - var args = _b.args, output = _b.output, admins = _b.admins; - return __generator(this, function (_f) { - switch (_f.label) { - case 0: - infoList = admins.findByName(args.player) - .toSeq().toArray() - .map(function (p) { return [p, players_1.FishPlayer.getById(p.id)]; }); - if (infoList.length == 0) - (0, commands_1.fail)("No players found."); - playersToFetch = infoList.filter(function (_a) { - var _b = __read(_a, 2), a = _b[0], b = _b[1]; - return !b; - }).map(function (_a) { - var _b = __read(_a, 2), a = _b[0], b = _b[1]; - return a; - }); - if (!(playersToFetch.length == 0)) return [3 /*break*/, 1]; - display(infoList); - return [3 /*break*/, 13]; - case 1: - //Attempt to fetch data - //If there are too many players, give up - if (playersToFetch.length > 50) - display(infoList); - output("Fetching data..."); - _f.label = 2; - case 2: - _f.trys.push([2, 11, , 12]); - _f.label = 3; - case 3: - _f.trys.push([3, 8, 9, 10]); - _c = __values((0, funcs_1.to2DArray)(playersToFetch, 10)), _d = _c.next(); - _f.label = 4; - case 4: - if (!!_d.done) return [3 /*break*/, 7]; - batch = _d.value; - return [4 /*yield*/, Promise.all(batch.map(function (info) { return __awaiter(_this, void 0, void 0, function () { - var data, fishP; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, api.getFishPlayerData(info.id)]; - case 1: - data = _a.sent(); - if (data) { - fishP = players_1.FishPlayer.createFromInfo(info); - fishP.updateData(data); - players_1.FishPlayer.cachedPlayers[info.id] = fishP; - } - return [2 /*return*/]; - } - }); - }); }))]; - case 5: - _f.sent(); - _f.label = 6; - case 6: - _d = _c.next(); - return [3 /*break*/, 4]; - case 7: return [3 /*break*/, 10]; - case 8: - e_1_1 = _f.sent(); - e_1 = { error: e_1_1 }; - return [3 /*break*/, 10]; - case 9: - try { - if (_d && !_d.done && (_e = _c.return)) _e.call(_c); - } - finally { if (e_1) throw e_1.error; } - return [7 /*endfinally*/]; - case 10: return [3 /*break*/, 12]; - case 11: - err_1 = _f.sent(); - Log.err(err_1); - return [3 /*break*/, 12]; - case 12: - infoList = admins.findByName(args.player) - .toSeq().toArray() - .map(function (p) { return [p, players_1.FishPlayer.getById(p.id)]; }); - display(infoList); - _f.label = 13; - case 13: return [2 /*return*/]; - } - }); - }); - } - }, - infoonline: { - args: ["player:string"], - description: "Display information about an online player.", - handler: function (_a) { - var e_3, _b; - var args = _a.args, output = _a.output, admins = _a.admins; - var infoList = args.player == "*" ? players_1.FishPlayer.getAllOnline() : players_1.FishPlayer.getAllByName(args.player, false); - if (infoList.length == 0) - (0, commands_1.fail)("Nobody with that name could be found."); - var outputString = [""]; - var _loop_2 = function (player) { - var playerInfo = admins.getInfo(player.uuid); - outputString.push("Info for player &c\"".concat(player.cleanedName, "\" &lk(").concat(player.name, ")&fr\n\tUUID: &c\"").concat(playerInfo.id, "\"&fr\n\tUSID: &c").concat(player.usid ? "\"".concat(player.usid, "\"") : "unknown", "&fr\n\tall names used: ").concat(playerInfo.names.map(function (n) { return "&c\"".concat(n, "\"&fr"); }).items.join(', '), "\n\tall IPs used: ").concat(playerInfo.ips.map(function (n) { return (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr'; }).items.join(", "), "\n\tjoined &c").concat(playerInfo.timesJoined, "&fr times, kicked &c").concat(playerInfo.timesKicked, "&fr times\n\trank: &c").concat(player.rank.name, "&fr").concat((player.marked() ? ", &lris marked&fr" : "") + (player.muted ? ", &lris muted&fr" : "") + (player.hasFlag("member") ? ", &lmis member&fr" : "") + (player.autoflagged ? ", &lris autoflagged&fr" : ""))); - }; - try { - for (var infoList_2 = __values(infoList), infoList_2_1 = infoList_2.next(); !infoList_2_1.done; infoList_2_1 = infoList_2.next()) { - var player = infoList_2_1.value; - _loop_2(player); - } - } - catch (e_3_1) { e_3 = { error: e_3_1 }; } - finally { - try { - if (infoList_2_1 && !infoList_2_1.done && (_b = infoList_2.return)) _b.call(infoList_2); - } - finally { if (e_3) throw e_3.error; } - } - output(outputString.join("\n")); - } - }, - unblacklist: { - args: ["ip:string"], - description: "Unblacklists an ip from the DOS blacklist.", - handler: function (_a) { - var args = _a.args, output = _a.output, admins = _a.admins; - if (args.ip === '*') { - var size = admins.dosBlacklist.size; - if (size == 0) - (0, commands_1.fail)('DOS blacklist is already empty.'); - admins.dosBlacklist.clear(); - output("Cleared ".concat(size, " IPs from the DOS blacklist.")); - } - else { - if (admins.dosBlacklist.remove(args.ip)) - output("Removed ".concat(args.ip, " from the DOS blacklist.")); - else - (0, commands_1.fail)("IP address ".concat(args.ip, " is not DOS blacklisted.")); - } - } - }, - blacklist: { - args: ["verbose:boolean?"], - description: "Allows you to view the DOS blacklist.", - handler: function (_a) { - var args = _a.args, output = _a.output, admins = _a.admins; - var blacklist = admins.dosBlacklist; - if (blacklist.isEmpty()) - (0, commands_1.fail)("The blacklist is empty"); - if (args.verbose) { - var outputString_1 = ["DOS Blacklist:"]; - blacklist.each(function (ip) { - var info = admins.findByIP(ip); - if (info) { - outputString_1.push("IP: &c".concat(ip, "&fr UUID: &c\"").concat(info.id, "\"&fr Last name used: &c\"").concat(info.plainLastName(), "\"&fr")); - } - }); - output(outputString_1.join("\n")); - output("".concat(blacklist.size, " blacklisted IPs")); - } - else { - output(blacklist.toString()); - output("".concat(blacklist.size, " blacklisted IPs")); - } - } - }, - whack: { - args: ["target:string"], - description: "Whacks (ipbans) a player.", - handler: function (_a) { - var args = _a.args, output = _a.output, outputFail = _a.outputFail, admins = _a.admins; - var range; - if (globals_1.ipPattern.test(args.target)) { - //target is an ip - api.ban({ ip: args.target }); - var info = admins.findByIP(args.target); - if (info) - (0, utils_1.logAction)("whacked", "console", info); - else - (0, utils_1.logAction)("console ip-whacked ".concat(args.target)); - if (admins.isIPBanned(args.target)) { - output("IP &c\"".concat(args.target, "\"&fr is already banned. Ban was synced to other servers.")); - } - else { - admins.banPlayerIP(args.target); - output("&lrIP &c\"".concat(args.target, "\"&lr was banned. Ban was synced to other servers.")); - } - } - else if ((range = (0, utils_1.getIPRange)(args.target)) != null) { - if (admins.subnetBans.contains(boolf(function (ip) { return ip.replace(/\.$/, "") == range; }))) { - output("Subnet &c\"".concat(range, "\"&fr is already banned.")); - } - else { - admins.subnetBans.add(range); - output("&lrIP range &c\"".concat(range, "\"&lr was banned. Subnet bans are not synced.")); - } - } - else if (globals_1.uuidPattern.test(args.target)) { - var info = admins.getInfoOptional(args.target); - if (info) - (0, utils_1.logAction)("whacked", "console", info); - else - (0, utils_1.logAction)("console ip-whacked ".concat(args.target)); - if (admins.isIDBanned(args.target)) { - api.ban({ uuid: args.target }); - output("UUID &c\"".concat(args.target, "\"&fr is already banned. Ban was synced to other servers.")); - } - else { - admins.banPlayerID(args.target); - if (info) { - admins.banPlayerIP(info.lastIP); - api.ban({ uuid: args.target, ip: info.lastIP }); - output("&lrUUID &c\"".concat(args.target, "\" &lrwas banned. IP &c\"").concat(info.lastIP, "\"&lr was banned. Ban was synced to other servers.")); - } - else { - api.ban({ uuid: args.target }); - output("&lrUUID &c\"".concat(args.target, "\" &lrwas banned. Ban was synced to other servers. Warning: no stored info for this UUID, player may not exist. Unable to determine IP.")); - } - } - } - else { - var player = players_1.FishPlayer.getOneMindustryPlayerByName(args.target); - if (player === "none") { - outputFail("Could not find a player name matching &c\"".concat(args.target, "\"")); - } - else if (player === "multiple") { - outputFail("Name &c\"".concat(args.target, "\"&fr could refer to more than one player.")); - } - else { - if (player.admin) - (0, commands_1.fail)("Player &c\"".concat(player.name, "\"&fr is an admin, you probably don't want to ban them.")); - var ip = player.ip(); - var uuid = player.uuid(); - admins.banPlayerID(uuid); - admins.banPlayerIP(ip); - (0, utils_1.logAction)("console whacked ".concat(Strings.stripColors(player.name), " (`").concat(uuid, "`/`").concat(ip, "`)")); - api.ban({ uuid: uuid, ip: ip }); - output("&lrIP &c\"".concat(ip, "\"&lr was banned. UUID &c\"").concat(uuid, "\"&lr was banned. Ban was synced to other servers.")); - } - } - (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked."); }); - } - }, - unwhack: { - args: ["target:string"], - description: "Unbans a player.", - handler: function (_a) { - var args = _a.args, output = _a.output, admins = _a.admins; - var range; - if (globals_1.ipPattern.test(args.target)) { - //target is an ip - if (players_1.FishPlayer.removePunishedIP(args.target)) { - output("Removed IP &c\"".concat(args.target, "\"&fr from the anti-evasion list.")); - } - if (admins.kickedIPs.remove(args.target)) { - output("Removed temporary kick for IP &c\"".concat(args.target, "\"&fr.")); - } - output("Checking ban status..."); - api.getBanned({ ip: args.target }, function (banned) { - if (banned) { - api.unban({ ip: args.target }); - (0, utils_1.logAction)("console unbanned ip `".concat(args.target, "`")); - output("IP &c\"".concat(args.target, "\"&fr has been globally unbanned.")); - } - else { - output("IP &c\"".concat(args.target, "\"&fr is not globally banned.")); - } - if (admins.isIPBanned(args.target)) { - admins.unbanPlayerIP(args.target); - output("IP &c\"".concat(args.target, "\"&fr has been locally unbanned.")); - } - else { - output("IP &c\"".concat(args.target, "\"&fr was not locally banned.")); - } - var size = admins.subnetBans.size; - admins.subnetBans.removeAll(function (r) { return args.target.startsWith(r); }); - if (admins.subnetBans.size < size) { - output("Unbanned IP ranges affecting this IP."); - } - }); - } - else if ((range = (0, utils_1.getIPRange)(args.target)) != null) { - if (admins.subnetBans.remove(function (b) { return b.replace(/\.$/, ".") == range.replace(/\.$/, "."); })) { - output("IP range &c\"".concat(range, "\"&fr was unbanned.")); - } - else { - output("IP range &c\"".concat(range, "\"&fr was not banned.")); - } - } - else if (globals_1.uuidPattern.test(args.target)) { - if (players_1.FishPlayer.removePunishedUUID(args.target)) { - output("Removed UUID &c\"".concat(args.target, "\"&fr from the anti-evasion list.")); - } - output("Checking ban status..."); - var info_1 = admins.findByIP(args.target); - api.getBanned({ uuid: args.target }, function (banned) { - if (banned) { - api.unban({ uuid: args.target }); - (0, utils_1.logAction)("console unbanned uuid `".concat(args.target, "`")); - output("UUID &c\"".concat(args.target, "\"&fr has been globally unbanned.")); - } - else { - output("UUID &c\"".concat(args.target, "\"&fr is not globally banned.")); - } - if (admins.isIDBanned(args.target)) { - admins.unbanPlayerID(args.target); - output("UUID &c\"".concat(args.target, "\"&fr has been locally unbanned.")); - } - else { - output("UUID &c\"".concat(args.target, "\"&fr was not locally banned.")); - } - if (info_1) { - if (info_1.lastKicked > 0) { - info_1.lastKicked = 0; - output("Removed temporary kick for UUID &c\"".concat(args.target, "\"&fr.")); - } - output("You may also want to consider unbanning the IP \"".concat(info_1.lastIP, "\".")); - } - }); - } - else { - (0, commands_1.fail)("Cannot unban by name; please use the info or search commands to find the IP and UUID of the player you are looking for."); - } - } - }, - ban: { - args: ["any:string"], - description: "Please use the whack command instead.", - handler: function () { - (0, commands_1.fail)("Use the whack command instead."); - } - }, - unban: { - args: ["any:string"], - description: "Please use the unwhack command instead.", - handler: function () { - (0, commands_1.fail)("Use the unwhack command instead."); - } - }, - "subnet-ban": { - args: ["any:string?", "anyb:string?"], - description: "Please use the whack and unwhack commands instead.", - handler: function (_a) { - var args = _a.args, output = _a.output, admins = _a.admins; - if (args.any) - (0, commands_1.fail)("Use the whack and unwhack commands instead."); - output("List of all subnet bans:"); - output(admins.subnetBans.toString("\n")); - } - }, - joinbell: { - args: ["on:boolean?"], - description: "Toggles the join bell function.", - handler: function (_a) { - var _b = _a.args.on, on = _b === void 0 ? !globals_1.fishState.joinBell : _b; - globals_1.fishState.joinBell = on; - if (globals_1.fishState.joinBell) { - Log.info("Enabled sound on new player join. Run \"joinbell\" again to turn it off."); - } - else { - Log.info("Disabled sound on new player join."); - } - } - }, - loadfishplayerdata: { - args: ["areyousure:boolean", "fishplayerdata:string"], - description: "Overwrites current fish player data.", - handler: function (_a) { - var args = _a.args, output = _a.output; - if (args.areyousure) { - var before = Object.keys(players_1.FishPlayer.cachedPlayers).length; - players_1.FishPlayer.loadAll(args.fishplayerdata); - output("Loaded fish player data. before:".concat(before, ", after:").concat(Object.keys(players_1.FishPlayer.cachedPlayers).length)); - } - } - }, - resetauth: { - args: ["player:string"], - description: "Removes the USID of the player provided, use this if they are getting kicked with the message \"Authorization failure!\". Specify \"last\" to use the last player that got kicked.", - handler: function (_a) { - var _b, _c; - var args = _a.args, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, admins = _a.admins; - var player = args.player == "last" ? ((_b = players_1.FishPlayer.lastAuthKicked) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Nobody has been kicked for authorization failure since the last restart.")) : - (_c = players_1.FishPlayer.getById(args.player)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(admins.getInfoOptional(args.player) - ? "Player ".concat(args.player, " has joined the server, but their info was not cached, most likely because they have no rank, so there is no stored USID.") - : "Unknown player ".concat(args.player)); - if (player.ranksAtLeast("admin")) - (0, commands_1.fail)("Please use the setusid command instead."); - var oldusid = player.usid; - player.usid = null; - api.setFishPlayerData(player.getData(), 1, true).then(function () { - outputSuccess("Removed the usid of player ".concat(player.name, "/").concat(player.uuid, " (was ").concat(oldusid, ")")); - }).catch(function (err) { - Log.err(err); - outputFail("Failed to remove the usid, please try running the command again."); - }); - } - }, - setusid: { - args: ["uuid:string", "usid:string"], - description: "Sets the USID of a player.", - handler: function (_a) { - var _b; - var args = _a.args, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, f = _a.f; - if (args.usid.length !== 12) - (0, commands_1.fail)("Invalid USID: should be 12 characters ending with an equal sign"); - var player = (_b = players_1.FishPlayer.lastAuthKicked) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("No authorization failures have occurred since the last restart."); - var oldusid = player.usid; - player.usid = args.usid; - api.setFishPlayerData(player.getData(), 1, true).then(function () { - outputSuccess("Set the usid of player ".concat(player.name, "/").concat(player.uuid, " to ").concat(args.usid, " (was ").concat(oldusid, ")")); - }).catch(function (err) { - Log.err(err); - outputFail("Failed to remove the usid, please try running the command again."); - }); - } - }, - update: { - args: ["branch:string?"], - description: "Updates the plugin.", - handler: function (_a) { - var args = _a.args, output = _a.output, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; - if (config_1.Mode.localDebug) - (0, commands_1.fail)("Cannot update in local debug mode."); - output("Updating..."); - var path = (0, utils_1.fishCommandsRootDirPath)().toString(); - Threads.thread(function () { - var _a, _b; - try { - var initialVersion = OS.exec("git", "-C", path, "rev-parse", "HEAD"); - var gitFetch = new ProcessBuilder("git", "-C", path, "fetch", "origin") - .redirectErrorStream(true) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .start(); - gitFetch.waitFor(); - if (gitFetch.exitValue() == 0) { - outputSuccess("Fetched data, updating files..."); - } - else { - outputFail("Update failed!"); - return; - } - var newVersion = OS.exec("git", "-C", path, "rev-parse", "origin/".concat((_a = args.branch) !== null && _a !== void 0 ? _a : "master")); - if (initialVersion == newVersion) { - outputSuccess("Already up to date."); - return; - } - var gitCheckout = new ProcessBuilder("git", "-C", path, "checkout", "-q", "-f", "origin/".concat((_b = args.branch) !== null && _b !== void 0 ? _b : "master")) - .redirectErrorStream(true) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .start(); - gitCheckout.waitFor(); - if (gitCheckout.exitValue() == 0) { - outputSuccess("Updated successfully from ".concat(initialVersion, " to ").concat(newVersion, ". Restart to apply changes.")); - } - else { - outputFail("Update failed!"); - return; - } - } - catch (err) { - Log.err(err); - outputFail("Update failed!"); - } - }); - } - }, - restart: { - args: ["time:number?"], - description: "Restarts the server.", - handler: function (_a) { - var _b; - var time = _a.args.time; - (_b = globals_1.fishState.restartLoopTask) === null || _b === void 0 ? void 0 : _b.cancel(); - var timeInferred = time == undefined; - if (Groups.player.isEmpty()) { - if (time == undefined) { - Log.info("Restarting immediately as no players are online."); - time !== null && time !== void 0 ? time : (time = 0); - } - } - else if (config_1.Gamemode.pvp()) { - time !== null && time !== void 0 ? time : (time = -1); - } - else { - time !== null && time !== void 0 ? time : (time = 60); - } - if (time == -1) { - Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart queued. The server will restart after the current match is over.[]\n[accent]---[[[coral]+++[]]---"); - if (config_1.Gamemode.pvp() && timeInferred) - Log.info("PVP: restart will occur at the end of the current game. Specify a time to override, but &rthat would interrupt the current pvp match, and players would lose their teams.&fr"); - else - Log.info("Restarting once the current game ends."); - globals_1.fishState.restartQueued = true; - } - else { - if (time < 0 || time > 100) - (0, commands_1.fail)("Invalid time: out of valid range."); - (0, utils_1.serverRestartLoop)(time); - if (time == 0) - Log.info("Restarting now."); - else - Log.info("Restarting in ".concat(time, " second").concat(time == 1 ? "" : "s", ".")); - if (config_1.Gamemode.pvp()) { - Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 20 seconds.[]\n[accent]---[[[coral]+++[]]---"); - } - else { - Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 20 seconds, and all progress will be saved.[]\n[accent]---[[[coral]+++[]]---"); - } - } - } - }, - restartcancel: { - args: [], - description: "Cancels a planned server restart.", - handler: function (_a) { - var _b; - var outputSuccess = _a.outputSuccess; - var task = (_b = globals_1.fishState.restartLoopTask) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("No restart scheduled."); - Call.sendMessage("[scarlet]Aborting..."); - task.cancel(); - Call.sendMessage("[scarlet]Server restart canceled."); - outputSuccess("Canceled restart."); - } - }, - rename: { - args: ["player:player", "newname:string"], - description: "Changes the name of a player.", - handler: function (_a) { - var args = _a.args, f = _a.f, outputSuccess = _a.outputSuccess; - if (args.player.hasPerm("blockTrolling")) - (0, commands_1.fail)(f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Operation aborted: Player ", " is insufficiently trollable."], ["Operation aborted: Player ", " is insufficiently trollable."])), args.player)); - var oldName = args.player.name; - args.player.player.name = args.player.prefixedName = args.newname; - args.player.shouldUpdateName = false; - outputSuccess("Renamed ".concat(oldName, " to ").concat(args.newname, ".")); - } - }, - fjs: { - args: ["js:string"], - description: "Executes arbitrary javascript code, but has access to fish-commands's variables.", - handler: function (_a) { - var args = _a.args; - fjsContext.runJS(args.js); - } - }, - checkmem: { - args: [], - description: "Checks memory usage of various objects.", - handler: function (_a) { - var output = _a.output; - output("Memory usage:\nTotal: ".concat(Math.round(Core.app.getJavaHeap() / (Math.pow(2, 10))), " KB\nNumber of cached fish players: ").concat(Object.keys(players_1.FishPlayer.cachedPlayers).length, " (stored locally: ").concat(Object.values(players_1.FishPlayer.cachedPlayers).filter(function (p) { return p.shouldCache(); }).length, ")\nFish player data string length: ").concat(players_1.FishPlayer.getFishPlayersString.length, " (").concat(Core.settings.getInt("fish-subkeys"), " subkeys)\nLength of tilelog entries: ").concat(Math.round(Object.values(globals_1.tileHistory).reduce(function (acc, a) { return acc + a.length; }, 0) / (Math.pow(2, 10))), " KB")); - } - }, - stopplayer: { - args: ['player:player', "time:time?", "message:string?"], - description: 'Stops a player.', - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var previousTime, time; - var _c, _d, _e; - var args = _b.args, f = _b.f, outputSuccess = _b.outputSuccess; - return __generator(this, function (_f) { - switch (_f.label) { - case 0: - if (!args.player.marked()) return [3 /*break*/, 2]; - //overload: overwrite stoptime - if (!args.time) - (0, commands_1.fail)(f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Player ", " is already marked."], ["Player ", " is already marked."])), args.player)); - previousTime = (0, utils_1.formatTime)(args.player.unmarkTime - Date.now()); - return [4 /*yield*/, args.player.updateStopTime(args.time)]; - case 1: - _f.sent(); - outputSuccess(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Player ", "'s stop time has been updated to ", " (was ", ")."], ["Player ", "'s stop time has been updated to ", " (was ", ")."])), args.player, (0, utils_1.formatTime)(args.time), previousTime)); - return [2 /*return*/]; - case 2: - time = (_c = args.time) !== null && _c !== void 0 ? _c : funcs_1.Duration.days(7); - if (time + Date.now() > globals_1.maxTime) - (0, commands_1.fail)("Error: time too high."); - return [4 /*yield*/, args.player.stop("console", time, (_d = args.message) !== null && _d !== void 0 ? _d : undefined)]; - case 3: - _f.sent(); - (0, utils_1.logAction)('stopped', "console", args.player, (_e = args.message) !== null && _e !== void 0 ? _e : undefined, time); - Call.sendMessage("[scarlet]Player \"".concat(args.player.prefixedName, "[scarlet]\" has been marked for ").concat((0, utils_1.formatTime)(time)).concat(args.message ? " with reason: [white]".concat(args.message, "[]") : "", ".")); - return [2 /*return*/]; - } - }); - }); - } - }, - stopoffline: { - args: ["uuid:uuid", "time:time?"], - description: "Stops a player by uuid.", - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var stopTime, info, fishP; - var _c = _b.args, uuid = _c.uuid, time = _c.time, outputSuccess = _b.outputSuccess, admins = _b.admins; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - stopTime = time !== null && time !== void 0 ? time : (globals_1.maxTime - Date.now() - 10000); - info = admins.getInfoOptional(uuid); - if (info == null) - (0, commands_1.fail)("Unknown player ".concat(uuid)); - fishP = players_1.FishPlayer.getFromInfo(info); - return [4 /*yield*/, fishP.stop("console", stopTime)]; - case 1: - _d.sent(); - (0, utils_1.logAction)('stopped', "console", info, undefined, stopTime); - outputSuccess("Player \"".concat(info.lastName, "\" was marked for ").concat((0, utils_1.formatTime)(stopTime), ".")); - return [2 /*return*/]; - } - }); - }); - } - }, - clearfire: { - args: [], - description: "Clears all the fires.", - handler: function (_a) { - var output = _a.output, outputSuccess = _a.outputSuccess; - output("Removing fires..."); - var totalRemoved = 0; - Call.sendMessage("[scarlet][[Fire Department]:[yellow] Fires were reported. Trucks are en-route. Removing all fires shortly."); - Timer.schedule(function () { - totalRemoved += Groups.fire.size(); - Groups.fire.each(function (f) { return f.remove(); }); - Groups.fire.clear(); - }, 2, 0.1, 40); - Timer.schedule(function () { - outputSuccess("Removed ".concat(totalRemoved, " fires.")); - Call.sendMessage("[scarlet][[Fire Department]:[yellow] We've extinguished ".concat(totalRemoved, " fires.")); - }, 6.1); - } - }, - status: { - args: [], - description: "Displays server status.", - handler: function (_a) { - var output = _a.output; - if (Vars.state.isMenu()) - (0, commands_1.fail)("Status: Server closed."); - var uptime = Packages.java.lang.management.ManagementFactory.getRuntimeMXBean().getUptime(); - var numStaff = 0; - players_1.FishPlayer.forEachPlayer(function (p) { - if (p.ranksAtLeast("mod")) - numStaff++; - }); - var uptimeColor = uptime < funcs_1.Duration.days(2) ? "" : - uptime < funcs_1.Duration.days(5) ? "&ly" : - uptime < funcs_1.Duration.days(9) ? "&y" : - "&br"; - output("\nStatus:\nPlaying on map &fi".concat(Vars.state.map.plainName(), "&fr for ").concat((0, utils_1.formatTime)(1000 * Vars.state.tick / 60), "\n").concat(Vars.state.rules.waves ? "Wave &c".concat(Vars.state.wave, "&fr, &c").concat(Math.ceil(Vars.state.wavetime / 60), "&fr seconds until next wave.\n") : "", "&c").concat(Groups.unit.size(), "&fr units, &c").concat(Vars.state.enemies, "&fr enemies, &c").concat(Groups.build.size(), "&fr buildings\nTPS: ").concat((0, utils_1.colorNumber)(Core.graphics.getFramesPerSecond(), function (f) { return f > 58 ? "&g" : f > 30 ? "&y" : f > 10 ? "&r" : "&br&w"; }, "server"), ", Memory: &c").concat(Math.round(Core.app.getJavaHeap() / 1048576), "&fr MB\nServer uptime: ").concat(uptimeColor).concat((0, utils_1.formatTime)(uptime), "&fr (since ").concat((0, utils_1.formatTimestampFull)(Date.now() - uptime), ")\n").concat([ - globals_1.fishState.restartQueued ? "&by&lwRestart queued&fr" : "", - globals_1.fishState.restartLoopTask ? "&by&lwRestarting now&fr" : "", - players_1.FishPlayer.antiBotMode() ? "&br&wANTIBOT ACTIVE!&fr" + (0, utils_1.getAntiBotInfo)("server") : "", - ].filter(function (l) { return l.length > 0; }).join("\n"), "\n").concat((0, utils_1.colorNumber)(Groups.player.size(), function (n) { return n > 0 ? "&c" : "&lr"; }, "server"), " players online, ").concat((0, utils_1.colorNumber)(numStaff, function (n) { return n > 0 ? "&c" : "&lr"; }, "server"), " staff members.\n").concat(players_1.FishPlayer.mapPlayers(function (p) { - return "\t".concat(p.rank.shortPrefix, " &c").concat(p.uuid, "&fr &c").concat(p.name, "&fr"); - }).join("\n") || "&lrNo players connected.&fr", "\n")); - } - }, - tmux: { - args: ["attach:string"], - description: "Oopsie", - handler: function () { - (0, commands_1.fail)("You are already in the Mindustry server console. Please regain situational awareness before running any further commands."); - } - }, - BEGIN: { - args: ["transaction:string"], - description: "Oopsie", - handler: function (_a) { - var args = _a.args; - if (args.transaction == "TRANSACTION") - (0, commands_1.fail)("Not possible :( please download and run locally, and make a backup"); - else - (0, commands_1.fail)("Command not found. Did you mean \"BEGIN TRANSACTION\"?"); - } - }, - prune: { - args: ["confirm:boolean?"], - description: "Prunes fish player data", - handler: function (_a) { - var args = _a.args, admins = _a.admins, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; - var playersToPrune = Object.values(players_1.FishPlayer.cachedPlayers) - .filter(function (player) { - if (player.hasData()) - return false; - var data = admins.getInfoOptional(player.uuid); - return (!data || - data.timesJoined == 1 || - (data.timesJoined < 10 && - (Date.now() - player.lastJoined) > funcs_1.Duration.months(1))); - }); - if (args.confirm) { - outputSuccess("Creating backup..."); - var backupScript = Core.settings.getDataDirectory().child("backup.sh"); - if (!backupScript.exists()) - (0, commands_1.fail)("./backup.sh does not exist! aborting"); - var backupProcess_1 = new ProcessBuilder(backupScript.absolutePath()) - .directory(Core.settings.getDataDirectory().file()) - .redirectErrorStream(true) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .start(); - Threads.daemon(function () { - backupProcess_1.waitFor(); - if (backupProcess_1.exitValue() == 0) { - outputSuccess("Successfully created a backup."); - Core.app.post(function () { - playersToPrune.forEach(function (u) { delete players_1.FishPlayer.cachedPlayers[u.uuid]; }); - outputSuccess("Pruned ".concat(playersToPrune.length, " players.")); - }); - } - else { - outputFail("Backup failed!"); - } - }); - } - else { - outputSuccess("Pruning would remove fish data for ".concat(playersToPrune.length, " players with no data and (1 join or inactive with <10 joins). (Mindustry data will remain.)\nRun \"prune y\" to prune data.")); - } - } - }, - backup: { - args: [], - description: "Creates a backup of the settings.bin file.", - handler: function (_a) { - var output = _a.output, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess; - output("Creating backup..."); - var backupScript = Core.settings.getDataDirectory().child("backup.sh"); - if (!backupScript.exists()) - (0, commands_1.fail)("./backup.sh does not exist! aborting"); - var backupProcess = new ProcessBuilder(backupScript.absolutePath()) - .directory(Core.settings.getDataDirectory().file()) - .redirectErrorStream(true) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .start(); - Threads.daemon(function () { - backupProcess.waitFor(); - if (backupProcess.exitValue() == 0) - outputSuccess("Successfully created a backup."); - else - outputFail("Backup failed!"); - }); - } - }, - updateMaps: { - args: [], - description: 'Attempt to fetch and update all map files', - handler: function (_a) { - var output = _a.output, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; - output("Updating maps... (this may take a while)"); - (0, files_1.updateMaps)() - .then(function (changed) { return outputSuccess(changed ? "Maps were updated." : "Map update completed, already up to date."); }) - .catch(function (message) { return outputFail("Map update failed: ".concat(String(message))); }); - }, - }, - switchall: { - args: ["server:string"], - description: "Forces all currently online players to another server.", - handler: function (_a) { - var _b; - var args = _a.args; - if (globals_1.ipPortPattern.test(args.server)) { - Groups.player.each(function (target) { - //direct connect - Call.connect.apply(Call, __spreadArray([target.con], __read(args.server.split(":")), false)); - }); - } - else { - var server_1 = (_b = config_1.FishServer.byName(args.server)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Unknown server ".concat(args.server, ". Valid options: ").concat(config_1.FishServer.all.map(function (s) { return s.name; }).join(", "))); - Groups.player.each(function (target) { - Call.connect(target.con, server_1.ip, server_1.port); - }); - } - } - }, - mute: { - args: ['player:player'], - description: 'Stops a player from chatting.', - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (args.player.muted) - (0, commands_1.fail)(f(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Player ", " is already muted."], ["Player ", " is already muted."])), args.player)); - return [4 /*yield*/, args.player.mute("console")]; - case 1: - _c.sent(); - (0, utils_1.logAction)('muted', "console", args.player); - outputSuccess(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Muted player ", "."], ["Muted player ", "."])), args.player)); - return [2 /*return*/]; - } - }); - }); - } - }, - unmute: { - args: ['player:player'], - description: 'Unmutes a player', - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!args.player.muted && args.player.autoflagged) - (0, commands_1.fail)(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."], ["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."])), args.player)); - if (!args.player.muted) - (0, commands_1.fail)(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Player ", " is not muted."], ["Player ", " is not muted."])), args.player)); - return [4 /*yield*/, args.player.unmute("console")]; - case 1: - _c.sent(); - (0, utils_1.logAction)('unmuted', "console", args.player); - outputSuccess(f(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Unmuted player ", "."], ["Unmuted player ", "."])), args.player)); - return [2 /*return*/]; - } - }); - }); - } - }, - free: { - args: ['player:player'], - description: 'Frees a player.', - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, outputSuccess = _b.outputSuccess, outputFail = _b.outputFail, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!args.player.marked()) return [3 /*break*/, 2]; - return [4 /*yield*/, args.player.free("console")]; - case 1: - _c.sent(); - (0, utils_1.logAction)('freed', "console", args.player); - outputSuccess(f(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Player ", " has been unmarked."], ["Player ", " has been unmarked."])), args.player)); - return [3 /*break*/, 3]; - case 2: - if (args.player.autoflagged) { - args.player.autoflagged = false; - if (args.player.connected()) { - args.player.sendMessage("[yellow]You have been unflagged."); - args.player.updateName(); - args.player.forceRespawn(); - } - outputSuccess(f(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Player ", " has been unflagged."], ["Player ", " has been unflagged."])), args.player)); - } - else { - outputFail(f(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Player ", " is not marked or autoflagged."], ["Player ", " is not marked or autoflagged."])), args.player)); - } - _c.label = 3; - case 3: return [2 /*return*/]; - } - }); - }); - } - }, - say: { - args: ["message:string"], - description: "Sends a message to the in-game chat.", - handler: function (_a) { - var message = _a.args.message; - Call.sendMessage("[scarlet][[Server]:[] ".concat(message)); - Log.info("&fi&lcServer: &fr&lw".concat(message)); - globals_1.FishEvents.fire("serverSays", []); - } - }, - whitelist: { - args: ["_:string?"], - description: "Disabled to prevent accidental lag", - handler: function () { - (0, commands_1.fail)("This command has been disabled to prevent lag. Fish servers do not use a whitelist."); - } - }, - loglevel: { - args: ["duration:time?"], - description: "Sets log level to debug", - handler: function (_a) { - var _b = _a.args.duration, duration = _b === void 0 ? funcs_1.Duration.minutes(5) : _b, outputSuccess = _a.outputSuccess; - Log.level = Log.LogLevel.debug; - Timer.schedule(function () { - Log.level = Log.LogLevel.info; - }, duration / 1000); - outputSuccess("Set log level to debug for ".concat((0, utils_1.formatTime)(duration))); - } - } -}); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains all the console commands, which can be run through the server console. +*/ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.commands = void 0; +var api = __importStar(require("/api")); +var config_1 = require("/config"); +var files_1 = require("/files"); +var fjsContext = __importStar(require("/fjsContext")); +var commands_1 = require("/frameworks/commands"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var players_1 = require("/players"); +var ranks_1 = require("/ranks"); +var utils_1 = require("/utils"); +exports.commands = (0, commands_1.consoleCommandList)({ + setrank: { + args: ["player:player", "rank:rank"], + description: "Set a player's rank.", + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (args.rank == ranks_1.Rank.pi && !config_1.Mode.localDebug) + (0, commands_1.fail)(f(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Rank ", " is immutable."], ["Rank ", " is immutable."])), args.rank)); + if (args.player.immutable() && !config_1.Mode.localDebug) + (0, commands_1.fail)(f(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Player ", " is immutable."], ["Player ", " is immutable."])), args.player)); + return [4 /*yield*/, args.player.setRank(args.rank)]; + case 1: + _c.sent(); + (0, utils_1.logAction)("set rank to ".concat(args.rank.name, " for"), "console", args.player); + outputSuccess(f(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Set rank of player ", " to ", ""], ["Set rank of player ", " to ", ""])), args.player, args.rank)); + return [2 /*return*/]; + } + }); + }); + } + }, + admin: { + args: ["nothing:string?"], + description: "Use the setrank command instead.", + handler: function () { + (0, commands_1.fail)("Use the \"setrank\" command instead. Hint: \"setrank player admin\""); + } + }, + setflag: { + args: ["player:player", "flag:roleflag", "value:boolean"], + description: "Set a player's role flags.", + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, args.player.setFlag(args.flag, args.value)]; + case 1: + _c.sent(); + (0, utils_1.logAction)("set roleflag ".concat(args.flag.name, " to ").concat(args.value, " for"), "console", args.player); + outputSuccess(f(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Set role flag ", " of player ", " to ", ""], ["Set role flag ", " of player ", " to ", ""])), args.flag, args.player, args.value)); + return [2 /*return*/]; + } + }); + }); + } + }, + savePlayers: { + args: [], + description: "Runs FishPlayer.save()", + handler: function (_a) { + var outputSuccess = _a.outputSuccess; + players_1.FishPlayer.saveAll(); + outputSuccess("Successfully wrote fish player data."); + } + }, + info: { + args: ["player:string"], + description: "Find player info(s). Displays all names and ips of a player.", + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + function display(infoList) { + var e_2, _a; + var outputString = [""]; + var _loop_1 = function (playerInfo, fishP) { + var flagsText = [ + (fishP === null || fishP === void 0 ? void 0 : fishP.marked()) && (globals_1.maxTime - fishP.unmarkTime < 20000 ? + "&lris marked forever&fr" + : "&lris marked&fr until ".concat((0, utils_1.formatTimeRelative)(fishP.unmarkTime))), + (fishP === null || fishP === void 0 ? void 0 : fishP.muted) && "&lris muted&fr", + (fishP === null || fishP === void 0 ? void 0 : fishP.hasFlag("member")) && "&lmis member&fr", + (fishP === null || fishP === void 0 ? void 0 : fishP.autoflagged) && "&lris autoflagged&fr", + playerInfo.banned && "&bris UUID banned&fr", + ].filter(Boolean).join(", "); + var lastJoinedColor = (fishP === null || fishP === void 0 ? void 0 : fishP.lastJoined) && fishP.lastJoined !== -1 ? (function () { + var timeSinceLastJoin = (Date.now() - fishP.lastJoined) / 1000; + if (timeSinceLastJoin < 3600) + return "&br"; + if (timeSinceLastJoin < 24 * 3600) + return "&by"; + if (timeSinceLastJoin < 7 * 24 * 3600) + return "&lw"; + return "&lk"; + })() : "&fr"; + outputString.push([ + "".concat(lastJoinedColor, "Trace info for player &fr&y").concat(playerInfo.id, "&fr").concat(lastJoinedColor, " / &c\"").concat((0, funcs_1.escapeStringColorsServer)(Strings.stripColors(playerInfo.lastName)), "\" &lk(").concat((0, funcs_1.escapeStringColorsServer)(playerInfo.lastName), ")&fr"), + playerInfo.names.size > 1 && "all names used: ".concat(playerInfo.names.map(funcs_1.escapeStringColorsServer).map(function (n) { return "&c\"".concat(n, "\"&fr"); }).items.join(', ')), + "all IPs used: ".concat(playerInfo.ips.map(function (n) { return (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr'; }).items.join(", ")), + "joined &c".concat(playerInfo.timesJoined, "&fr times, kicked &c").concat(playerInfo.timesKicked, "&fr times"), + fishP && fishP.lastJoined !== -1 && "Last joined: ".concat((0, utils_1.formatTimeRelative)(fishP.lastJoined)), + fishP && fishP.firstJoined !== -1 && (0, utils_1.formatTimeRelative)(fishP.firstJoined), + fishP && "USID: &c".concat(fishP.usid, "&fr"), + fishP && fishP.rank !== ranks_1.Rank.player && "Rank: &c".concat(fishP.rank.name, "&fr"), + flagsText, + ].filter(Boolean).map(function (l, i) { return i == 0 ? l : '\t' + l; }).join("\n")); + }; + try { + for (var infoList_1 = __values(infoList), infoList_1_1 = infoList_1.next(); !infoList_1_1.done; infoList_1_1 = infoList_1.next()) { + var _b = __read(infoList_1_1.value, 2), playerInfo = _b[0], fishP = _b[1]; + _loop_1(playerInfo, fishP); + } + } + catch (e_2_1) { e_2 = { error: e_2_1 }; } + finally { + try { + if (infoList_1_1 && !infoList_1_1.done && (_a = infoList_1.return)) _a.call(infoList_1); + } + finally { if (e_2) throw e_2.error; } + } + output(outputString.join("\n")); + } + var infoList, playersToFetch, _c, _d, batch, e_1_1, err_1; + var e_1, _e; + var _this = this; + var args = _b.args, output = _b.output, admins = _b.admins; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + infoList = admins.findByName(args.player) + .toSeq().toArray() + .map(function (p) { return [p, players_1.FishPlayer.getById(p.id)]; }); + if (infoList.length == 0) + (0, commands_1.fail)("No players found."); + playersToFetch = infoList.filter(function (_a) { + var _b = __read(_a, 2), a = _b[0], b = _b[1]; + return !b; + }).map(function (_a) { + var _b = __read(_a, 2), a = _b[0], b = _b[1]; + return a; + }); + if (!(playersToFetch.length == 0)) return [3 /*break*/, 1]; + display(infoList); + return [3 /*break*/, 13]; + case 1: + //Attempt to fetch data + //If there are too many players, give up + if (playersToFetch.length > 50) + display(infoList); + output("Fetching data..."); + _f.label = 2; + case 2: + _f.trys.push([2, 11, , 12]); + _f.label = 3; + case 3: + _f.trys.push([3, 8, 9, 10]); + _c = __values((0, funcs_1.to2DArray)(playersToFetch, 10)), _d = _c.next(); + _f.label = 4; + case 4: + if (!!_d.done) return [3 /*break*/, 7]; + batch = _d.value; + return [4 /*yield*/, Promise.all(batch.map(function (info) { return __awaiter(_this, void 0, void 0, function () { + var data, fishP; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, api.getFishPlayerData(info.id)]; + case 1: + data = _a.sent(); + if (data) { + fishP = players_1.FishPlayer.createFromInfo(info); + fishP.updateData(data); + players_1.FishPlayer.cachedPlayers[info.id] = fishP; + } + return [2 /*return*/]; + } + }); + }); }))]; + case 5: + _f.sent(); + _f.label = 6; + case 6: + _d = _c.next(); + return [3 /*break*/, 4]; + case 7: return [3 /*break*/, 10]; + case 8: + e_1_1 = _f.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 10]; + case 9: + try { + if (_d && !_d.done && (_e = _c.return)) _e.call(_c); + } + finally { if (e_1) throw e_1.error; } + return [7 /*endfinally*/]; + case 10: return [3 /*break*/, 12]; + case 11: + err_1 = _f.sent(); + Log.err(err_1); + return [3 /*break*/, 12]; + case 12: + infoList = admins.findByName(args.player) + .toSeq().toArray() + .map(function (p) { return [p, players_1.FishPlayer.getById(p.id)]; }); + display(infoList); + _f.label = 13; + case 13: return [2 /*return*/]; + } + }); + }); + } + }, + infoonline: { + args: ["player:string"], + description: "Display information about an online player.", + handler: function (_a) { + var e_3, _b; + var args = _a.args, output = _a.output, admins = _a.admins; + var infoList = args.player == "*" ? players_1.FishPlayer.getAllOnline() : players_1.FishPlayer.getAllByName(args.player, false); + if (infoList.length == 0) + (0, commands_1.fail)("Nobody with that name could be found."); + var outputString = [""]; + var _loop_2 = function (player) { + var playerInfo = admins.getInfo(player.uuid); + outputString.push("Info for player &c\"".concat(player.cleanedName, "\" &lk(").concat(player.name, ")&fr\n\tUUID: &c\"").concat(playerInfo.id, "\"&fr\n\tUSID: &c").concat(player.usid ? "\"".concat(player.usid, "\"") : "unknown", "&fr\n\tall names used: ").concat(playerInfo.names.map(function (n) { return "&c\"".concat(n, "\"&fr"); }).items.join(', '), "\n\tall IPs used: ").concat(playerInfo.ips.map(function (n) { return (n == playerInfo.lastIP ? '&c' : '&w') + n + '&fr'; }).items.join(", "), "\n\tjoined &c").concat(playerInfo.timesJoined, "&fr times, kicked &c").concat(playerInfo.timesKicked, "&fr times\n\trank: &c").concat(player.rank.name, "&fr").concat((player.marked() ? ", &lris marked&fr" : "") + (player.muted ? ", &lris muted&fr" : "") + (player.hasFlag("member") ? ", &lmis member&fr" : "") + (player.autoflagged ? ", &lris autoflagged&fr" : ""))); + }; + try { + for (var infoList_2 = __values(infoList), infoList_2_1 = infoList_2.next(); !infoList_2_1.done; infoList_2_1 = infoList_2.next()) { + var player = infoList_2_1.value; + _loop_2(player); + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (infoList_2_1 && !infoList_2_1.done && (_b = infoList_2.return)) _b.call(infoList_2); + } + finally { if (e_3) throw e_3.error; } + } + output(outputString.join("\n")); + } + }, + unblacklist: { + args: ["ip:string"], + description: "Unblacklists an ip from the DOS blacklist.", + handler: function (_a) { + var args = _a.args, output = _a.output, admins = _a.admins; + if (args.ip === '*') { + var size = admins.dosBlacklist.size; + if (size == 0) + (0, commands_1.fail)('DOS blacklist is already empty.'); + admins.dosBlacklist.clear(); + output("Cleared ".concat(size, " IPs from the DOS blacklist.")); + } + else { + if (admins.dosBlacklist.remove(args.ip)) + output("Removed ".concat(args.ip, " from the DOS blacklist.")); + else + (0, commands_1.fail)("IP address ".concat(args.ip, " is not DOS blacklisted.")); + } + } + }, + blacklist: { + args: ["verbose:boolean?"], + description: "Allows you to view the DOS blacklist.", + handler: function (_a) { + var args = _a.args, output = _a.output, admins = _a.admins; + var blacklist = admins.dosBlacklist; + if (blacklist.isEmpty()) + (0, commands_1.fail)("The blacklist is empty"); + if (args.verbose) { + var outputString_1 = ["DOS Blacklist:"]; + blacklist.each(function (ip) { + var info = admins.findByIP(ip); + if (info) { + outputString_1.push("IP: &c".concat(ip, "&fr UUID: &c\"").concat(info.id, "\"&fr Last name used: &c\"").concat(info.plainLastName(), "\"&fr")); + } + }); + output(outputString_1.join("\n")); + output("".concat(blacklist.size, " blacklisted IPs")); + } + else { + output(blacklist.toString()); + output("".concat(blacklist.size, " blacklisted IPs")); + } + } + }, + whack: { + args: ["target:string"], + description: "Whacks (ipbans) a player.", + handler: function (_a) { + var args = _a.args, output = _a.output, outputFail = _a.outputFail, admins = _a.admins; + var range; + if (globals_1.ipPattern.test(args.target)) { + //target is an ip + api.ban({ ip: args.target }); + var info = admins.findByIP(args.target); + if (info) + (0, utils_1.logAction)("whacked", "console", info); + else + (0, utils_1.logAction)("console ip-whacked ".concat(args.target)); + if (admins.isIPBanned(args.target)) { + output("IP &c\"".concat(args.target, "\"&fr is already banned. Ban was synced to other servers.")); + } + else { + admins.banPlayerIP(args.target); + output("&lrIP &c\"".concat(args.target, "\"&lr was banned. Ban was synced to other servers.")); + } + } + else if ((range = (0, utils_1.getIPRange)(args.target)) != null) { + if (admins.subnetBans.contains(boolf(function (ip) { return ip.replace(/\.$/, "") == range; }))) { + output("Subnet &c\"".concat(range, "\"&fr is already banned.")); + } + else { + admins.subnetBans.add(range); + output("&lrIP range &c\"".concat(range, "\"&lr was banned. Subnet bans are not synced.")); + } + } + else if (globals_1.uuidPattern.test(args.target)) { + var info = admins.getInfoOptional(args.target); + if (info) + (0, utils_1.logAction)("whacked", "console", info); + else + (0, utils_1.logAction)("console ip-whacked ".concat(args.target)); + if (admins.isIDBanned(args.target)) { + api.ban({ uuid: args.target }); + output("UUID &c\"".concat(args.target, "\"&fr is already banned. Ban was synced to other servers.")); + } + else { + admins.banPlayerID(args.target); + if (info) { + admins.banPlayerIP(info.lastIP); + api.ban({ uuid: args.target, ip: info.lastIP }); + output("&lrUUID &c\"".concat(args.target, "\" &lrwas banned. IP &c\"").concat(info.lastIP, "\"&lr was banned. Ban was synced to other servers.")); + } + else { + api.ban({ uuid: args.target }); + output("&lrUUID &c\"".concat(args.target, "\" &lrwas banned. Ban was synced to other servers. Warning: no stored info for this UUID, player may not exist. Unable to determine IP.")); + } + } + } + else { + var player = players_1.FishPlayer.getOneMindustryPlayerByName(args.target); + if (player === "none") { + outputFail("Could not find a player name matching &c\"".concat(args.target, "\"")); + } + else if (player === "multiple") { + outputFail("Name &c\"".concat(args.target, "\"&fr could refer to more than one player.")); + } + else { + if (player.admin) + (0, commands_1.fail)("Player &c\"".concat(player.name, "\"&fr is an admin, you probably don't want to ban them.")); + var ip = player.ip(); + var uuid = player.uuid(); + admins.banPlayerID(uuid); + admins.banPlayerIP(ip); + (0, utils_1.logAction)("console whacked ".concat(Strings.stripColors(player.name), " (`").concat(uuid, "`/`").concat(ip, "`)")); + api.ban({ uuid: uuid, ip: ip }); + output("&lrIP &c\"".concat(ip, "\"&lr was banned. UUID &c\"").concat(uuid, "\"&lr was banned. Ban was synced to other servers.")); + } + } + (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked."); }); + } + }, + unwhack: { + args: ["target:string"], + description: "Unbans a player.", + handler: function (_a) { + var args = _a.args, output = _a.output, admins = _a.admins; + var range; + if (globals_1.ipPattern.test(args.target)) { + //target is an ip + if (players_1.FishPlayer.removePunishedIP(args.target)) { + output("Removed IP &c\"".concat(args.target, "\"&fr from the anti-evasion list.")); + } + if (admins.kickedIPs.remove(args.target)) { + output("Removed temporary kick for IP &c\"".concat(args.target, "\"&fr.")); + } + output("Checking ban status..."); + api.getBanned({ ip: args.target }, function (banned) { + if (banned) { + api.unban({ ip: args.target }); + (0, utils_1.logAction)("console unbanned ip `".concat(args.target, "`")); + output("IP &c\"".concat(args.target, "\"&fr has been globally unbanned.")); + } + else { + output("IP &c\"".concat(args.target, "\"&fr is not globally banned.")); + } + if (admins.isIPBanned(args.target)) { + admins.unbanPlayerIP(args.target); + output("IP &c\"".concat(args.target, "\"&fr has been locally unbanned.")); + } + else { + output("IP &c\"".concat(args.target, "\"&fr was not locally banned.")); + } + var size = admins.subnetBans.size; + admins.subnetBans.removeAll(function (r) { return args.target.startsWith(r); }); + if (admins.subnetBans.size < size) { + output("Unbanned IP ranges affecting this IP."); + } + }); + } + else if ((range = (0, utils_1.getIPRange)(args.target)) != null) { + if (admins.subnetBans.remove(function (b) { return b.replace(/\.$/, ".") == range.replace(/\.$/, "."); })) { + output("IP range &c\"".concat(range, "\"&fr was unbanned.")); + } + else { + output("IP range &c\"".concat(range, "\"&fr was not banned.")); + } + } + else if (globals_1.uuidPattern.test(args.target)) { + if (players_1.FishPlayer.removePunishedUUID(args.target)) { + output("Removed UUID &c\"".concat(args.target, "\"&fr from the anti-evasion list.")); + } + output("Checking ban status..."); + var info_1 = admins.findByIP(args.target); + api.getBanned({ uuid: args.target }, function (banned) { + if (banned) { + api.unban({ uuid: args.target }); + (0, utils_1.logAction)("console unbanned uuid `".concat(args.target, "`")); + output("UUID &c\"".concat(args.target, "\"&fr has been globally unbanned.")); + } + else { + output("UUID &c\"".concat(args.target, "\"&fr is not globally banned.")); + } + if (admins.isIDBanned(args.target)) { + admins.unbanPlayerID(args.target); + output("UUID &c\"".concat(args.target, "\"&fr has been locally unbanned.")); + } + else { + output("UUID &c\"".concat(args.target, "\"&fr was not locally banned.")); + } + if (info_1) { + if (info_1.lastKicked > 0) { + info_1.lastKicked = 0; + output("Removed temporary kick for UUID &c\"".concat(args.target, "\"&fr.")); + } + output("You may also want to consider unbanning the IP \"".concat(info_1.lastIP, "\".")); + } + }); + } + else { + (0, commands_1.fail)("Cannot unban by name; please use the info or search commands to find the IP and UUID of the player you are looking for."); + } + } + }, + ban: { + args: ["any:string"], + description: "Please use the whack command instead.", + handler: function () { + (0, commands_1.fail)("Use the whack command instead."); + } + }, + unban: { + args: ["any:string"], + description: "Please use the unwhack command instead.", + handler: function () { + (0, commands_1.fail)("Use the unwhack command instead."); + } + }, + "subnet-ban": { + args: ["any:string?", "anyb:string?"], + description: "Please use the whack and unwhack commands instead.", + handler: function (_a) { + var args = _a.args, output = _a.output, admins = _a.admins; + if (args.any) + (0, commands_1.fail)("Use the whack and unwhack commands instead."); + output("List of all subnet bans:"); + output(admins.subnetBans.toString("\n")); + } + }, + joinbell: { + args: ["on:boolean?"], + description: "Toggles the join bell function.", + handler: function (_a) { + var _b = _a.args.on, on = _b === void 0 ? !globals_1.fishState.joinBell : _b; + globals_1.fishState.joinBell = on; + if (globals_1.fishState.joinBell) { + Log.info("Enabled sound on new player join. Run \"joinbell\" again to turn it off."); + } + else { + Log.info("Disabled sound on new player join."); + } + } + }, + loadfishplayerdata: { + args: ["areyousure:boolean", "fishplayerdata:string"], + description: "Overwrites current fish player data.", + handler: function (_a) { + var args = _a.args, output = _a.output; + if (args.areyousure) { + var before = Object.keys(players_1.FishPlayer.cachedPlayers).length; + players_1.FishPlayer.loadAll(args.fishplayerdata); + output("Loaded fish player data. before:".concat(before, ", after:").concat(Object.keys(players_1.FishPlayer.cachedPlayers).length)); + } + } + }, + resetauth: { + args: ["player:string"], + description: "Removes the USID of the player provided, use this if they are getting kicked with the message \"Authorization failure!\". Specify \"last\" to use the last player that got kicked.", + handler: function (_a) { + var _b, _c; + var args = _a.args, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, admins = _a.admins; + var player = args.player == "last" ? ((_b = players_1.FishPlayer.lastAuthKicked) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Nobody has been kicked for authorization failure since the last restart.")) : + (_c = players_1.FishPlayer.getById(args.player)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(admins.getInfoOptional(args.player) + ? "Player ".concat(args.player, " has joined the server, but their info was not cached, most likely because they have no rank, so there is no stored USID.") + : "Unknown player ".concat(args.player)); + if (player.ranksAtLeast("admin")) + (0, commands_1.fail)("Please use the setusid command instead."); + var oldusid = player.usid; + player.usid = null; + api.setFishPlayerData(player.getData(), 1, true).then(function () { + outputSuccess("Removed the usid of player ".concat(player.name, "/").concat(player.uuid, " (was ").concat(oldusid, ")")); + }).catch(function (err) { + Log.err(err); + outputFail("Failed to remove the usid, please try running the command again."); + }); + } + }, + setusid: { + args: ["uuid:string", "usid:string"], + description: "Sets the USID of a player.", + handler: function (_a) { + var _b; + var args = _a.args, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, f = _a.f; + if (args.usid.length !== 12) + (0, commands_1.fail)("Invalid USID: should be 12 characters ending with an equal sign"); + var player = (_b = players_1.FishPlayer.lastAuthKicked) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("No authorization failures have occurred since the last restart."); + var oldusid = player.usid; + player.usid = args.usid; + api.setFishPlayerData(player.getData(), 1, true).then(function () { + outputSuccess("Set the usid of player ".concat(player.name, "/").concat(player.uuid, " to ").concat(args.usid, " (was ").concat(oldusid, ")")); + }).catch(function (err) { + Log.err(err); + outputFail("Failed to remove the usid, please try running the command again."); + }); + } + }, + update: { + args: ["branch:string?"], + description: "Updates the plugin.", + handler: function (_a) { + var args = _a.args, output = _a.output, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; + if (config_1.Mode.localDebug) + (0, commands_1.fail)("Cannot update in local debug mode."); + output("Updating..."); + var path = (0, utils_1.fishCommandsRootDirPath)().toString(); + Threads.thread(function () { + var _a, _b; + try { + var initialVersion = OS.exec("git", "-C", path, "rev-parse", "HEAD"); + var gitFetch = new ProcessBuilder("git", "-C", path, "fetch", "origin") + .redirectErrorStream(true) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start(); + gitFetch.waitFor(); + if (gitFetch.exitValue() == 0) { + outputSuccess("Fetched data, updating files..."); + } + else { + outputFail("Update failed!"); + return; + } + var newVersion = OS.exec("git", "-C", path, "rev-parse", "origin/".concat((_a = args.branch) !== null && _a !== void 0 ? _a : "master")); + if (initialVersion == newVersion) { + outputSuccess("Already up to date."); + return; + } + var gitCheckout = new ProcessBuilder("git", "-C", path, "checkout", "-q", "-f", "origin/".concat((_b = args.branch) !== null && _b !== void 0 ? _b : "master")) + .redirectErrorStream(true) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start(); + gitCheckout.waitFor(); + if (gitCheckout.exitValue() == 0) { + outputSuccess("Updated successfully from ".concat(initialVersion, " to ").concat(newVersion, ". Restart to apply changes.")); + } + else { + outputFail("Update failed!"); + return; + } + } + catch (err) { + Log.err(err); + outputFail("Update failed!"); + } + }); + } + }, + restart: { + args: ["time:number?"], + description: "Restarts the server.", + handler: function (_a) { + var _b; + var time = _a.args.time; + (_b = globals_1.fishState.restartLoopTask) === null || _b === void 0 ? void 0 : _b.cancel(); + var timeInferred = time == undefined; + if (Groups.player.isEmpty()) { + if (time == undefined) { + Log.info("Restarting immediately as no players are online."); + time !== null && time !== void 0 ? time : (time = 0); + } + } + else if (config_1.Gamemode.pvp()) { + time !== null && time !== void 0 ? time : (time = -1); + } + else { + time !== null && time !== void 0 ? time : (time = 60); + } + if (time == -1) { + Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart queued. The server will restart after the current match is over.[]\n[accent]---[[[coral]+++[]]---"); + if (config_1.Gamemode.pvp() && timeInferred) + Log.info("PVP: restart will occur at the end of the current game. Specify a time to override, but &rthat would interrupt the current pvp match, and players would lose their teams.&fr"); + else + Log.info("Restarting once the current game ends."); + globals_1.fishState.restartQueued = true; + } + else { + if (time < 0 || time > 100) + (0, commands_1.fail)("Invalid time: out of valid range."); + (0, utils_1.serverRestartLoop)(time); + if (time == 0) + Log.info("Restarting now."); + else + Log.info("Restarting in ".concat(time, " second").concat(time == 1 ? "" : "s", ".")); + if (config_1.Gamemode.pvp()) { + Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 20 seconds.[]\n[accent]---[[[coral]+++[]]---"); + } + else { + Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 20 seconds, and all progress will be saved.[]\n[accent]---[[[coral]+++[]]---"); + } + } + } + }, + restartcancel: { + args: [], + description: "Cancels a planned server restart.", + handler: function (_a) { + var _b; + var outputSuccess = _a.outputSuccess; + var task = (_b = globals_1.fishState.restartLoopTask) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("No restart scheduled."); + Call.sendMessage("[scarlet]Aborting..."); + task.cancel(); + Call.sendMessage("[scarlet]Server restart canceled."); + outputSuccess("Canceled restart."); + } + }, + rename: { + args: ["player:player", "newname:string"], + description: "Changes the name of a player.", + handler: function (_a) { + var args = _a.args, f = _a.f, outputSuccess = _a.outputSuccess; + if (args.player.hasPerm("blockTrolling")) + (0, commands_1.fail)(f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Operation aborted: Player ", " is insufficiently trollable."], ["Operation aborted: Player ", " is insufficiently trollable."])), args.player)); + var oldName = args.player.name; + args.player.player.name = args.player.prefixedName = args.newname; + args.player.shouldUpdateName = false; + outputSuccess("Renamed ".concat(oldName, " to ").concat(args.newname, ".")); + } + }, + fjs: { + args: ["js:string"], + description: "Executes arbitrary javascript code, but has access to fish-commands's variables.", + handler: function (_a) { + var args = _a.args; + fjsContext.runJS(args.js); + } + }, + checkmem: { + args: [], + description: "Checks memory usage of various objects.", + handler: function (_a) { + var output = _a.output; + output("Memory usage:\nTotal: ".concat(Math.round(Core.app.getJavaHeap() / (Math.pow(2, 10))), " KB\nNumber of cached fish players: ").concat(Object.keys(players_1.FishPlayer.cachedPlayers).length, " (stored locally: ").concat(Object.values(players_1.FishPlayer.cachedPlayers).filter(function (p) { return p.shouldCache(); }).length, ")\nFish player data string length: ").concat(players_1.FishPlayer.getFishPlayersString.length, " (").concat(Core.settings.getInt("fish-subkeys"), " subkeys)\nLength of tilelog entries: ").concat(Math.round(Object.values(globals_1.tileHistory).reduce(function (acc, a) { return acc + a.length; }, 0) / (Math.pow(2, 10))), " KB")); + } + }, + stopplayer: { + args: ['player:player', "time:time?", "message:string?"], + description: 'Stops a player.', + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var previousTime, time; + var _c, _d, _e; + var args = _b.args, f = _b.f, outputSuccess = _b.outputSuccess; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + if (!args.player.marked()) return [3 /*break*/, 2]; + //overload: overwrite stoptime + if (!args.time) + (0, commands_1.fail)(f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Player ", " is already marked."], ["Player ", " is already marked."])), args.player)); + previousTime = (0, utils_1.formatTime)(args.player.unmarkTime - Date.now()); + return [4 /*yield*/, args.player.updateStopTime(args.time)]; + case 1: + _f.sent(); + outputSuccess(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Player ", "'s stop time has been updated to ", " (was ", ")."], ["Player ", "'s stop time has been updated to ", " (was ", ")."])), args.player, (0, utils_1.formatTime)(args.time), previousTime)); + return [2 /*return*/]; + case 2: + time = (_c = args.time) !== null && _c !== void 0 ? _c : funcs_1.Duration.days(7); + if (time + Date.now() > globals_1.maxTime) + (0, commands_1.fail)("Error: time too high."); + return [4 /*yield*/, args.player.stop("console", time, (_d = args.message) !== null && _d !== void 0 ? _d : undefined)]; + case 3: + _f.sent(); + (0, utils_1.logAction)('stopped', "console", args.player, (_e = args.message) !== null && _e !== void 0 ? _e : undefined, time); + Call.sendMessage("[scarlet]Player \"".concat(args.player.prefixedName, "[scarlet]\" has been marked for ").concat((0, utils_1.formatTime)(time)).concat(args.message ? " with reason: [white]".concat(args.message, "[]") : "", ".")); + return [2 /*return*/]; + } + }); + }); + } + }, + stopoffline: { + args: ["uuid:uuid", "time:time?"], + description: "Stops a player by uuid.", + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var stopTime, info, fishP; + var _c = _b.args, uuid = _c.uuid, time = _c.time, outputSuccess = _b.outputSuccess, admins = _b.admins; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + stopTime = time !== null && time !== void 0 ? time : (globals_1.maxTime - Date.now() - 10000); + info = admins.getInfoOptional(uuid); + if (info == null) + (0, commands_1.fail)("Unknown player ".concat(uuid)); + fishP = players_1.FishPlayer.getFromInfo(info); + return [4 /*yield*/, fishP.stop("console", stopTime)]; + case 1: + _d.sent(); + (0, utils_1.logAction)('stopped', "console", info, undefined, stopTime); + outputSuccess("Player \"".concat(info.lastName, "\" was marked for ").concat((0, utils_1.formatTime)(stopTime), ".")); + return [2 /*return*/]; + } + }); + }); + } + }, + clearfire: { + args: [], + description: "Clears all the fires.", + handler: function (_a) { + var output = _a.output, outputSuccess = _a.outputSuccess; + output("Removing fires..."); + var totalRemoved = 0; + Call.sendMessage("[scarlet][[Fire Department]:[yellow] Fires were reported. Trucks are en-route. Removing all fires shortly."); + Timer.schedule(function () { + totalRemoved += Groups.fire.size(); + Groups.fire.each(function (f) { return f.remove(); }); + Groups.fire.clear(); + }, 2, 0.1, 40); + Timer.schedule(function () { + outputSuccess("Removed ".concat(totalRemoved, " fires.")); + Call.sendMessage("[scarlet][[Fire Department]:[yellow] We've extinguished ".concat(totalRemoved, " fires.")); + }, 6.1); + } + }, + status: { + args: [], + description: "Displays server status.", + handler: function (_a) { + var output = _a.output; + if (Vars.state.isMenu()) + (0, commands_1.fail)("Status: Server closed."); + var uptime = Packages.java.lang.management.ManagementFactory.getRuntimeMXBean().getUptime(); + var numStaff = 0; + players_1.FishPlayer.forEachPlayer(function (p) { + if (p.ranksAtLeast("mod")) + numStaff++; + }); + var uptimeColor = uptime < funcs_1.Duration.days(2) ? "" : + uptime < funcs_1.Duration.days(5) ? "&ly" : + uptime < funcs_1.Duration.days(9) ? "&y" : + "&br"; + output("\nStatus:\nPlaying on map &fi".concat(Vars.state.map.plainName(), "&fr for ").concat((0, utils_1.formatTime)(1000 * Vars.state.tick / 60), "\n").concat(Vars.state.rules.waves ? "Wave &c".concat(Vars.state.wave, "&fr, &c").concat(Math.ceil(Vars.state.wavetime / 60), "&fr seconds until next wave.\n") : "", "&c").concat(Groups.unit.size(), "&fr units, &c").concat(Vars.state.enemies, "&fr enemies, &c").concat(Groups.build.size(), "&fr buildings\nTPS: ").concat((0, utils_1.colorNumber)(Core.graphics.getFramesPerSecond(), function (f) { return f > 58 ? "&g" : f > 30 ? "&y" : f > 10 ? "&r" : "&br&w"; }, "server"), ", Memory: &c").concat(Math.round(Core.app.getJavaHeap() / 1048576), "&fr MB\nServer uptime: ").concat(uptimeColor).concat((0, utils_1.formatTime)(uptime), "&fr (since ").concat((0, utils_1.formatTimestampFull)(Date.now() - uptime), ")\n").concat([ + globals_1.fishState.restartQueued ? "&by&lwRestart queued&fr" : "", + globals_1.fishState.restartLoopTask ? "&by&lwRestarting now&fr" : "", + players_1.FishPlayer.antiBotMode() ? "&br&wANTIBOT ACTIVE!&fr" + (0, utils_1.getAntiBotInfo)("server") : "", + ].filter(function (l) { return l.length > 0; }).join("\n"), "\n").concat((0, utils_1.colorNumber)(Groups.player.size(), function (n) { return n > 0 ? "&c" : "&lr"; }, "server"), " players online, ").concat((0, utils_1.colorNumber)(numStaff, function (n) { return n > 0 ? "&c" : "&lr"; }, "server"), " staff members.\n").concat(players_1.FishPlayer.mapPlayers(function (p) { + return "\t".concat(p.rank.shortPrefix, " &c").concat(p.uuid, "&fr &c").concat(p.name, "&fr"); + }).join("\n") || "&lrNo players connected.&fr", "\n")); + } + }, + tmux: { + args: ["attach:string"], + description: "Oopsie", + handler: function () { + (0, commands_1.fail)("You are already in the Mindustry server console. Please regain situational awareness before running any further commands."); + } + }, + BEGIN: { + args: ["transaction:string"], + description: "Oopsie", + handler: function (_a) { + var args = _a.args; + if (args.transaction == "TRANSACTION") + (0, commands_1.fail)("Not possible :( please download and run locally, and make a backup"); + else + (0, commands_1.fail)("Command not found. Did you mean \"BEGIN TRANSACTION\"?"); + } + }, + prune: { + args: ["confirm:boolean?"], + description: "Prunes fish player data", + handler: function (_a) { + var args = _a.args, admins = _a.admins, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; + var playersToPrune = Object.values(players_1.FishPlayer.cachedPlayers) + .filter(function (player) { + if (player.hasData()) + return false; + var data = admins.getInfoOptional(player.uuid); + return (!data || + data.timesJoined == 1 || + (data.timesJoined < 10 && + (Date.now() - player.lastJoined) > funcs_1.Duration.months(1))); + }); + if (args.confirm) { + outputSuccess("Creating backup..."); + var backupScript = Core.settings.getDataDirectory().child("backup.sh"); + if (!backupScript.exists()) + (0, commands_1.fail)("./backup.sh does not exist! aborting"); + var backupProcess_1 = new ProcessBuilder(backupScript.absolutePath()) + .directory(Core.settings.getDataDirectory().file()) + .redirectErrorStream(true) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start(); + Threads.daemon(function () { + backupProcess_1.waitFor(); + if (backupProcess_1.exitValue() == 0) { + outputSuccess("Successfully created a backup."); + Core.app.post(function () { + playersToPrune.forEach(function (u) { delete players_1.FishPlayer.cachedPlayers[u.uuid]; }); + outputSuccess("Pruned ".concat(playersToPrune.length, " players.")); + }); + } + else { + outputFail("Backup failed!"); + } + }); + } + else { + outputSuccess("Pruning would remove fish data for ".concat(playersToPrune.length, " players with no data and (1 join or inactive with <10 joins). (Mindustry data will remain.)\nRun \"prune y\" to prune data.")); + } + } + }, + backup: { + args: [], + description: "Creates a backup of the settings.bin file.", + handler: function (_a) { + var output = _a.output, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess; + output("Creating backup..."); + var backupScript = Core.settings.getDataDirectory().child("backup.sh"); + if (!backupScript.exists()) + (0, commands_1.fail)("./backup.sh does not exist! aborting"); + var backupProcess = new ProcessBuilder(backupScript.absolutePath()) + .directory(Core.settings.getDataDirectory().file()) + .redirectErrorStream(true) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .start(); + Threads.daemon(function () { + backupProcess.waitFor(); + if (backupProcess.exitValue() == 0) + outputSuccess("Successfully created a backup."); + else + outputFail("Backup failed!"); + }); + } + }, + updateMaps: { + args: [], + description: 'Attempt to fetch and update all map files', + handler: function (_a) { + var output = _a.output, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; + output("Updating maps... (this may take a while)"); + (0, files_1.updateMaps)() + .then(function (changed) { return outputSuccess(changed ? "Maps were updated." : "Map update completed, already up to date."); }) + .catch(function (message) { return outputFail("Map update failed: ".concat(String(message))); }); + }, + }, + switchall: { + args: ["server:string"], + description: "Forces all currently online players to another server.", + handler: function (_a) { + var _b; + var args = _a.args; + if (globals_1.ipPortPattern.test(args.server)) { + Groups.player.each(function (target) { + //direct connect + Call.connect.apply(Call, __spreadArray([target.con], __read(args.server.split(":")), false)); + }); + } + else { + var server_1 = (_b = config_1.FishServer.byName(args.server)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Unknown server ".concat(args.server, ". Valid options: ").concat(config_1.FishServer.all.map(function (s) { return s.name; }).join(", "))); + Groups.player.each(function (target) { + Call.connect(target.con, server_1.ip, server_1.port); + }); + } + } + }, + mute: { + args: ['player:player'], + description: 'Stops a player from chatting.', + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (args.player.muted) + (0, commands_1.fail)(f(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Player ", " is already muted."], ["Player ", " is already muted."])), args.player)); + return [4 /*yield*/, args.player.mute("console")]; + case 1: + _c.sent(); + (0, utils_1.logAction)('muted', "console", args.player); + outputSuccess(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Muted player ", "."], ["Muted player ", "."])), args.player)); + return [2 /*return*/]; + } + }); + }); + } + }, + unmute: { + args: ['player:player'], + description: 'Unmutes a player', + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!args.player.muted && args.player.autoflagged) + (0, commands_1.fail)(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."], ["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."])), args.player)); + if (!args.player.muted) + (0, commands_1.fail)(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Player ", " is not muted."], ["Player ", " is not muted."])), args.player)); + return [4 /*yield*/, args.player.unmute("console")]; + case 1: + _c.sent(); + (0, utils_1.logAction)('unmuted', "console", args.player); + outputSuccess(f(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Unmuted player ", "."], ["Unmuted player ", "."])), args.player)); + return [2 /*return*/]; + } + }); + }); + } + }, + free: { + args: ['player:player'], + description: 'Frees a player.', + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, outputSuccess = _b.outputSuccess, outputFail = _b.outputFail, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!args.player.marked()) return [3 /*break*/, 2]; + return [4 /*yield*/, args.player.free("console")]; + case 1: + _c.sent(); + (0, utils_1.logAction)('freed', "console", args.player); + outputSuccess(f(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Player ", " has been unmarked."], ["Player ", " has been unmarked."])), args.player)); + return [3 /*break*/, 3]; + case 2: + if (args.player.autoflagged) { + args.player.autoflagged = false; + if (args.player.connected()) { + args.player.sendMessage("[yellow]You have been unflagged."); + args.player.updateName(); + args.player.forceRespawn(); + } + outputSuccess(f(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Player ", " has been unflagged."], ["Player ", " has been unflagged."])), args.player)); + } + else { + outputFail(f(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Player ", " is not marked or autoflagged."], ["Player ", " is not marked or autoflagged."])), args.player)); + } + _c.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); + } + }, + say: { + args: ["message:string"], + description: "Sends a message to the in-game chat.", + handler: function (_a) { + var message = _a.args.message; + Call.sendMessage("[scarlet][[Server]:[] ".concat(message)); + Log.info("&fi&lcServer: &fr&lw".concat(message)); + globals_1.FishEvents.fire("serverSays", []); + } + }, + whitelist: { + args: ["_:string?"], + description: "Disabled to prevent accidental lag", + handler: function () { + (0, commands_1.fail)("This command has been disabled to prevent lag. Fish servers do not use a whitelist."); + } + }, + loglevel: { + args: ["duration:time?"], + description: "Sets log level to debug", + handler: function (_a) { + var _b = _a.args.duration, duration = _b === void 0 ? funcs_1.Duration.minutes(5) : _b, outputSuccess = _a.outputSuccess; + Log.level = Log.LogLevel.debug; + Timer.schedule(function () { + Log.level = Log.LogLevel.info; + }, duration / 1000); + outputSuccess("Set log level to debug for ".concat((0, utils_1.formatTime)(duration))); + } + } +}); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15; diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index 8bda3c4b..a32ea5b5 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1,1434 +1,1434 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains most in-game chat commands that can be run by untrusted players. -*/ -var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { - if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } - return cooked; -}; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var _a; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.commands = void 0; -var achievements_1 = require("/achievements"); -var api = __importStar(require("/api")); -var config_1 = require("/config"); -var commands_1 = require("/frameworks/commands"); -var menus_1 = require("/frameworks/menus"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var maps_1 = require("/maps"); -var players_1 = require("/players"); -var ranks_1 = require("/ranks"); -var utils_1 = require("/utils"); -var votes_1 = require("/votes"); -exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { - args: [], - description: 'Prints information about the plugin.', - perm: commands_1.Perm.none, - handler: function (_a) { - var _b, _c; - var output = _a.output; - output("[accent][cyan]fish-commands[] is the monolithic plugin used for the Fish servers' features.\n[accent]==========\n[accent]Source code available at: [cyan]https://github.com/Fish-Community/fish-commands/\n[accent]Current plugin version: [cyan]".concat((_c = (_b = globals_1.fishPlugin.version) === null || _b === void 0 ? void 0 : _b.slice(0, 8)) !== null && _c !== void 0 ? _c : "[scarlet]null[]", "[]")); - } - }, unpause: (0, commands_1.command)({ - args: [], - description: 'Unpauses the game.', - perm: commands_1.Perm.trusted, - requirements: [commands_1.Req.mode('pvp')], - init: function () { - var data = { unpaused: false }; - Events.on(EventType.PlayEvent, function () { - if (data.unpaused) { - data.unpaused = false; - Vars.state.rules.pvpAutoPause = true; - } - }); - return data; - }, - handler: function (_a) { - var data = _a.data, outputSuccess = _a.outputSuccess; - Vars.state.rules.pvpAutoPause = false; - data.unpaused = true; - Core.app.post(function () { return Vars.state.set(GameState.State.playing); }); - outputSuccess("Unpaused."); - }, - }), tp: { - args: ['player:player'], - description: 'Teleport to another player.', - perm: commands_1.Perm.play, - requirements: [commands_1.Req.modeNot("pvp")], - handler: function (_a) { - var _b, _c, _d; - var args = _a.args, sender = _a.sender; - if (!((_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.spawnedByCore)) - (0, commands_1.fail)("Can only teleport while in a core unit."); - if (sender.team() !== args.player.team()) - (0, commands_1.fail)("Cannot teleport to players on another team."); - if ((_d = (_c = sender.unit()).hasPayload) === null || _d === void 0 ? void 0 : _d.call(_c)) - (0, commands_1.fail)("Cannot teleport to players while holding a payload."); - (0, utils_1.teleportPlayer)(sender.player, args.player.player); - }, - }, clean: (0, commands_1.command)({ - args: [], - description: 'Removes all boulders from the map.', - perm: commands_1.Perm.play, - requirements: [], - data: { lastRanMapStartTime: (_a = maps_1.PartialMapRun.current) === null || _a === void 0 ? void 0 : _a.startTime }, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var array, removed, i, t; - var sender = _b.sender, outputSuccess = _b.outputSuccess, data = _b.data; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!maps_1.PartialMapRun.current) - (0, commands_1.fail)("This game is already over."); - if (data.lastRanMapStartTime == maps_1.PartialMapRun.current.startTime) - (0, commands_1.fail)("This command was already run on this map."); - data.lastRanMapStartTime = maps_1.PartialMapRun.current.startTime; - Timer.schedule(function () { return Call.sound(sender.con, Sounds.rockBreak, 1, 1, 0); }, 0, 0.05, 10); - array = ArcReflect.get(Vars.world.tiles, "array"); - removed = 0; - i = 0; - _c.label = 1; - case 1: - if (!(i < array.length)) return [3 /*break*/, 4]; - t = array[i]; - if (!(t.breakable() && t.block() instanceof Prop)) return [3 /*break*/, 3]; - t.removeNet(); - removed++; - if (!(removed % 500 == 0)) return [3 /*break*/, 3]; - return [4 /*yield*/, (0, funcs_1.delay)(100)]; - case 2: - _c.sent(); - _c.label = 3; - case 3: - i++; - return [3 /*break*/, 1]; - case 4: - outputSuccess("Cleared the map of boulders."); - return [2 /*return*/]; - } - }); - }); - } - }), die: { - args: [], - description: 'Kills your unit.', - perm: commands_1.Perm.mod.exceptModes({ - sandbox: commands_1.Perm.play - }, "You do not have permission to die."), - handler: function (_a) { - var _b; - var sender = _a.sender; - (_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.kill(); - }, - }, discord: { - args: [], - description: 'Takes you to our discord.', - perm: commands_1.Perm.none, - handler: function (_a) { - var sender = _a.sender; - Call.openURI(sender.con, config_1.text.discordURL); - }, - }, tilelog: (0, commands_1.command)({ - args: ['persist:boolean?', 'showUUID:boolean?'], - description: 'Checks the history of a tile.', - perm: commands_1.Perm.none, - data: { showUUID: true }, - handler: function (_a) { - var args = _a.args, output = _a.output, outputSuccess = _a.outputSuccess, currentTapMode = _a.currentTapMode, handleTaps = _a.handleTaps, sender = _a.sender, data = _a.data; - var changed = args.showUUID !== undefined && args.showUUID != data.showUUID; - if (args.showUUID !== undefined) { - if (!sender.hasPerm("viewUUIDs")) - (0, commands_1.fail)("You do not have permission to show UUIDs."); - data.showUUID = args.showUUID; - } - if (args.persist && currentTapMode !== "on") { - outputSuccess("Tilelog mode enabled. Click tiles to check their recent history. Run /tilelog to disable."); - handleTaps("on"); - } - else if (args.persist && changed) { - outputSuccess("".concat(data.showUUID ? "Now showing UUIDs." : "No longer showing UUIDs.", " Click tiles to check their recent history. Run /tilelog to disable.")); - handleTaps("on"); - } - else if (currentTapMode == "off" || changed) { - handleTaps("once"); - output("Click on a tile to check its recent history..."); - } - else { - handleTaps("off"); - outputSuccess("Tilelog disabled."); - } - }, - tapped: function (_a) { - var _b; - var tile = _a.tile, x = _a.x, y = _a.y, output = _a.output, sender = _a.sender, admins = _a.admins, data = _a.data; - var historyData = (_b = globals_1.tileHistory["".concat(x, ",").concat(y)]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("There is no recorded history for the selected tile (".concat(tile.x, ", ").concat(tile.y, ").")); - var history = funcs_1.StringIO.read(historyData, function (str) { return str.readArray(function (d) { return ({ - action: d.readString(2), - uuid: d.readString(3), - time: d.readNumber(16), - type: d.readString(2), - }); }, 1); }); - output("[yellow]Tile history for tile (".concat(tile.x, ", ").concat(tile.y, "):\n") + history.map(function (e) { - var _a, _b; - return globals_1.uuidPattern.test(e.uuid) - ? (sender.hasPerm("viewUUIDs") && data.showUUID - ? "[yellow]".concat((_a = admins.getInfoOptional(e.uuid)) === null || _a === void 0 ? void 0 : _a.plainLastName(), "[lightgray](").concat(e.uuid, ")[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)) - : "[yellow]".concat((_b = admins.getInfoOptional(e.uuid)) === null || _b === void 0 ? void 0 : _b.plainLastName(), " ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time))) - : "[yellow]".concat(e.uuid, "[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); - }).join('\n')); - } - }), aoelog: (0, commands_1.command)(function () { - var allowedActions = [ - "built", "broke", "rotated", "killed", "configured", "pay-dropped", "picked up", "controlled" - ]; - var cachedPointMap = Object.create(null); - return { - args: ['persist:boolean?', 'amount:number?', 'action:string?'], - description: 'Checks the history of all tiles in the selected region. Can be filtered by action.', - perm: commands_1.Perm.none, - handler: function (_a) { - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, currentTapMode = _a.currentTapMode, handleTaps = _a.handleTaps; - if (currentTapMode === "off" || args.action || args.amount) { - if (args.action && !allowedActions.includes(args.action)) - (0, commands_1.fail)("Invalid action. Allowed actions: ".concat(allowedActions.join(", "))); - if (args.amount && args.amount > 100) - (0, commands_1.fail)("Limit cannot be greater than 100."); - cachedPointMap[sender.uuid] = undefined; - handleTaps("on"); - outputSuccess("Aoelog mode enabled. To see the recent history of all tiles in a rectangular region, tap opposite corners of the rectangle. Run /aoelog with no arguments to disable."); - } - else { - handleTaps("off"); - outputSuccess("Aoelog disabled."); - } - }, - tapped: function (_a) { - var x = _a.x, y = _a.y, output = _a.output, outputFail = _a.outputFail, sender = _a.sender, admins = _a.admins, handleTaps = _a.handleTaps, args = _a.args; - function handleArea(p1, p2) { - var minX = Math.min(p1[0], p2[0]); - var maxX = Math.max(p1[0], p2[0]); - var minY = Math.min(p1[1], p2[1]); - var maxY = Math.max(p1[1], p2[1]); - var limitTiles = 0; - var amount = args.amount != null ? Math.floor(Math.abs(args.amount)) : 10; - outer: for (var i = minX; i <= maxX; i++) { - for (var j = minY; j <= maxY; j++) { - var tileData = globals_1.tileHistory["".concat(i, ",").concat(j)]; - if (!tileData) - continue; - var history = funcs_1.StringIO.read(globals_1.tileHistory["".concat(i, ",").concat(j)], function (str) { return str.readArray(function (d) { - var _a, _b, _c; - return ({ - action: (_a = d.readString(2)) !== null && _a !== void 0 ? _a : "??", - uuid: (_b = d.readString(3)) !== null && _b !== void 0 ? _b : "??", - time: d.readNumber(16), - type: (_c = d.readString(2)) !== null && _c !== void 0 ? _c : "??", - }); - }, 1); }); - if (args.action) - history = history.filter(function (e) { return e.action === args.action; }); - if (history.length == 0) - continue; - output("[yellow]Tile history for tile (".concat(i, ", ").concat(j, "):\n") + history.map(function (e) { - var _a, _b; - if (globals_1.uuidPattern.test(e.uuid)) { - if (sender.hasPerm("viewUUIDs")) - return "[yellow]".concat((_a = admins.getInfoOptional(e.uuid)) === null || _a === void 0 ? void 0 : _a.plainLastName(), "[lightgray](").concat(e.uuid, ")[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); - else - return "[yellow]".concat((_b = admins.getInfoOptional(e.uuid)) === null || _b === void 0 ? void 0 : _b.plainLastName(), " ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); - } - else - return "[yellow]".concat(e.uuid, "[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); - }).join('\n')); - limitTiles++; - if (limitTiles === amount) - break outer; - } - } - if (limitTiles == 0) { - if (args.action) - outputFail("There is no recorded history for the selected region matching the provided filters."); - else - outputFail("There is no recorded history for the selected region."); - } - if (limitTiles == amount) - output("Displaying first ".concat(limitTiles, " entries. To show other entries, increase the limit or select a smaller area.")); - } - var p1 = cachedPointMap[sender.uuid]; - if (!p1) { - cachedPointMap[sender.uuid] = [x, y]; - output("1st point set at (".concat(x, ",").concat(y, ")")); - } - else { - var p2 = [x, y]; - output("2nd point set at (".concat(x, ", ").concat(y, ")")); - var width = Math.abs(p1[0] - p2[0]); - var height = Math.abs(p1[1] - p2[1]); - if (width > 50 || height > 50) - (0, commands_1.fail)("Selection too large: width/height cannot be more than 50."); - handleArea(p1, p2); - cachedPointMap[sender.uuid] = undefined; - if (!args.persist) - handleTaps("off"); - } - }, - }; - }), afk: { - args: [], - description: 'Toggles your afk status.', - perm: commands_1.Perm.none, - handler: function (_a) { - var sender = _a.sender, outputSuccess = _a.outputSuccess; - sender.manualAfk = !sender.manualAfk; - sender.updateName(); - if (sender.manualAfk) - outputSuccess("You are now marked as AFK."); - else - outputSuccess("You are no longer marked as AFK."); - }, - }, vanish: { - args: ['target:player?'], - description: "Toggles visibility of your rank and flags.", - perm: commands_1.Perm.vanish, - handler: function (_a) { - var sender = _a.sender, _b = _a.args.target, target = _b === void 0 ? sender : _b, outputSuccess = _a.outputSuccess; - if (sender.stelled()) - (0, commands_1.fail)("Marked players may not hide flags."); - if (sender.muted) - (0, commands_1.fail)("Muted players may not hide flags."); - if (sender != target && target.hasPerm("blockTrolling")) - (0, commands_1.fail)("Target is insufficentlly trollable."); - if (sender != target && !sender.ranksAtLeast("mod")) - (0, commands_1.fail)("You do not have permission to vanish other players."); - target.showRankPrefix = !target.showRankPrefix; - outputSuccess("".concat(target == sender ? "Your" : "".concat(target.name, "'s"), " rank prefix is now ").concat(target.showRankPrefix ? "visible" : "hidden", ".")); - }, - }, tileid: { - args: [], - description: 'Checks id of a tile.', - perm: commands_1.Perm.none, - handler: function (_a) { - var output = _a.output, handleTaps = _a.handleTaps; - handleTaps("once"); - output("Click a tile to see its id..."); - }, - tapped: function (_a) { - var output = _a.output, f = _a.f, tile = _a.tile; - output(f(templateObject_1 || (templateObject_1 = __makeTemplateObject(["ID is ", ""], ["ID is ", ""])), tile.block().id)); - } - } }, Object.fromEntries(config_1.FishServer.all.map(function (server) { return [ - server.name, - { - args: [], - description: "Switches to the ".concat(server.name, " server."), - perm: server.requiredPerm ? commands_1.Perm.getByName(server.requiredPerm) : commands_1.Perm.none, - isHidden: true, - handler: function (_a) { - var sender = _a.sender, lastUsedSuccessfullySender = _a.lastUsedSuccessfullySender; - if (Date.now() - lastUsedSuccessfullySender > funcs_1.Duration.minutes(1)) - players_1.FishPlayer.messageAllWithPerm(server.requiredPerm, "".concat(sender.name, "[magenta] has gone to the ").concat(server.name, " server. Use [cyan]/").concat(server.name, " [magenta]to join them!")); - Call.connect(sender.con, server.ip, server.port); - }, - }, -]; }))), { switch: { - args: ["server:string", "target:player?"], - description: "Switches to another server.", - perm: commands_1.Perm.play, - handler: function (_a) { - var _b, _c; - var args = _a.args, sender = _a.sender, f = _a.f, lastUsedSuccessfullySender = _a.lastUsedSuccessfullySender; - if (args.target != null && args.target != sender && !sender.canModerate(args.target, true, "admin", true)) - (0, commands_1.fail)(f(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You do not have permission to switch player ", "."], ["You do not have permission to switch player ", "."])), args.target)); - var target = (_b = args.target) !== null && _b !== void 0 ? _b : sender; - if (globals_1.ipPortPattern.test(args.server) && sender.hasPerm("admin")) { - //direct connect - Call.connect.apply(Call, __spreadArray([target.con], __read(args.server.split(":")), false)); - } - else { - var unknownServerMessage = "Unknown server ".concat(args.server, ". Valid options: ").concat(config_1.FishServer.all.filter(function (s) { return !s.requiredPerm || sender.hasPerm(s.requiredPerm); }).map(function (s) { return s.name; }).join(", ")); - var server = (_c = config_1.FishServer.byName(args.server)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(unknownServerMessage); - //Pretend the server doesn't exist - if (server.requiredPerm && !sender.hasPerm(server.requiredPerm)) - (0, commands_1.fail)(unknownServerMessage); - if (target == sender && Date.now() - lastUsedSuccessfullySender > funcs_1.Duration.minutes(1)) - players_1.FishPlayer.messageAllWithPerm(server.requiredPerm, "".concat(sender.name, "[magenta] has gone to the ").concat(server.name, " server. Use [cyan]/").concat(server.name, " [magenta]to join them!")); - Call.connect(target.con, server.ip, server.port); - } - } - }, s: { - args: ['message:string'], - description: "Sends a message to staff only.", - perm: commands_1.Perm.chat, - handler: function (_a) { - var sender = _a.sender, args = _a.args, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, lastUsedSender = _a.lastUsedSender; - if (!sender.hasPerm("mod")) { - if (Date.now() - lastUsedSender < 4000) - (0, commands_1.fail)("This command was used recently and is on cooldown. [orange]Misuse of this command may result in a mute."); - } - api.sendStaffMessage(args.message, sender.name, sender.hasPerm("mod"), function (sent) { - if (!sender.hasPerm("mod")) { - if (sent) { - outputSuccess("Message sent to [orange]all online staff."); - } - else { - var wasReceived = players_1.FishPlayer.messageStaff(sender.prefixedName, args.message); - if (wasReceived) - outputSuccess("Message sent to staff."); - else - outputFail("No staff were online to receive your message."); - } - } - }); - }, - }, - /** - * This command is mostly for mobile (or players without foos). - * - * Since the player's unit follows the camera and we are moving the - * camera, we need to keep setting the players real position to the - * spot the command was made. This is pretty buggy but otherwise the - * player will be up the target player's butt - */ - watch: (0, commands_1.command)({ - args: ['player:player?'], - description: "Watch/unwatch a player.", - perm: commands_1.Perm.none, - data: new Set, - handler: function (_a) { - var _b; - var args = _a.args, data = _a.data, sender = _a.sender, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; - if (data.has(sender.uuid)) { - outputSuccess("No longer watching a player."); - data.delete(sender.uuid); - } - else if (args.player) { - data.add(sender.uuid); - var senderUnit_1 = (_b = sender.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("You do not have a unit."); - var stayX_1 = senderUnit_1.x; - var stayY_1 = senderUnit_1.y; - var target_1 = args.player.player; - (function watch() { - var _a, _b; - var unit = target_1.unit(); - if (data.has(sender.uuid) && unit) { - // Self.X+(172.5-Self.X)/10 - Call.setCameraPosition(sender.con, unit.x, unit.y); - if (senderUnit_1) - (_b = (_a = sender.unit()) === null || _a === void 0 ? void 0 : _a.set) === null || _b === void 0 ? void 0 : _b.call(_a, stayX_1, stayY_1); - Timer.schedule(function () { return watch(); }, 0.1, 0.1, 0); - } - else { - Call.setCameraPosition(sender.con, stayX_1, stayY_1); - } - })(); - } - else { - outputFail("No player to unwatch."); - } - }, - }), spectate: (0, commands_1.command)(function () { - //TODO revise code - /** Mapping between player and original team */ - var spectators = new Map(); - function spectate(target) { - spectators.set(target, target.team()); - target.forceRespawn(); - target.setTeam(Team.derelict); - target.forceRespawn(); - } - function resume(target) { - if (spectators.get(target) == null) - return; // this state is possible for a person who left not in spectate - target.setTeam(spectators.get(target)); - spectators.delete(target); - target.forceRespawn(); - } - Events.on(EventType.GameOverEvent, function () { return spectators.clear(); }); - Events.on(EventType.PlayerLeave, function (_a) { - var player = _a.player; - return resume(players_1.FishPlayer.get(player)); - }); - return { - args: ["target:player?"], - description: "Toggles spectator mode in PVP games.", - perm: commands_1.Perm.play, - requirements: [commands_1.Req.gameRunning], - handler: function (_a) { - var sender = _a.sender, _b = _a.args.target, target = _b === void 0 ? sender : _b, outputSuccess = _a.outputSuccess, f = _a.f; - if (!config_1.Gamemode.pvp() && !sender.hasPerm("mod")) - (0, commands_1.fail)("You do not have permission to spectate on a non-pvp server."); - if (target !== sender && target.hasPerm("blockTrolling")) - (0, commands_1.fail)("Target player is insufficiently trollable."); - if (target !== sender && !sender.ranksAtLeast("admin")) - (0, commands_1.fail)("You do not have permission to force other players to spectate."); - if (spectators.has(target)) { - resume(target); - outputSuccess(target == sender - ? f(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Rejoining game as team ", "."], ["Rejoining game as team ", "."])), target.team()) : f(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Forced ", " out of spectator mode."], ["Forced ", " out of spectator mode."])), target)); - } - else { - spectate(target); - outputSuccess(target == sender - ? f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Now spectating. Run /spectate again to resume gameplay."], ["Now spectating. Run /spectate again to resume gameplay."]))) : f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Forced ", " into spectator mode."], ["Forced ", " into spectator mode."])), target)); - } - } - }; - }), help: { - args: ['name:string?'], - description: 'Displays a list of all commands.', - perm: commands_1.Perm.none, - handler: function (_a) { - var _b; - var args = _a.args, output = _a.output, sender = _a.sender, allCommands = _a.allCommands; - var formatCommand = function (name, color) { - return new funcs_1.StringBuilder() - .add("".concat(color, "/").concat(name)) - .chunk("[white]".concat(allCommands[name].args.map(commands_1.formatArg).join(' '))) - .chunk("[lightgray]- ".concat(allCommands[name].description)).str; - }; - var formatList = function (commandList, color) { return commandList.map(function (c) { return formatCommand(c, color); }).join('\n'); }; - if (args.name && isNaN(parseInt(args.name)) && !['mod', 'admin', 'member'].includes(args.name)) { - //name is not a number or a category, therefore it is probably a command name - if (args.name in allCommands && (!allCommands[args.name].isHidden || allCommands[args.name].perm.check(sender))) { - if (args.name == "help") - achievements_1.Achievements.help_help.grantTo(sender, false); - output("Help for command ".concat(args.name, ":\n\t").concat(allCommands[args.name].description, "\n\tUsage: [sky]/").concat(args.name, " [white]").concat(allCommands[args.name].args.map(commands_1.formatArg).join(' '), "\n\tPermission required: ").concat(allCommands[args.name].perm.name)); - } - else - (0, commands_1.fail)("Command \"".concat(args.name, "\" does not exist.")); - } - else { - var commands_2 = { - player: [], - mod: [], - admin: [], - member: [], - }; - //TODO change this to category, not perm - Object.entries(allCommands).forEach(function (_a) { - var _b = __read(_a, 2), name = _b[0], data = _b[1]; - return (data.perm === commands_1.Perm.admin ? commands_2.admin : data.perm === commands_1.Perm.mod ? commands_2.mod : data.perm === commands_1.Perm.member ? commands_2.member : commands_2.player).push(name); - }); - var chunkedPlayerCommands = (0, funcs_1.to2DArray)(commands_2.player, 15); - switch (args.name) { - case 'admin': - output("".concat(commands_1.Perm.admin.color, "-- Admin commands --\n") + formatList(commands_2.admin, commands_1.Perm.admin.color)); - break; - case 'mod': - output("".concat(commands_1.Perm.mod.color, "-- Mod commands --\n") + formatList(commands_2.mod, commands_1.Perm.mod.color)); - break; - case 'member': - output("".concat(commands_1.Perm.member.color, "-- Member commands --\n") + formatList(commands_2.member, commands_1.Perm.member.color)); - break; - default: { - var pageNumber = args.name != undefined ? parseInt(args.name) : 1; - var page = (_b = chunkedPlayerCommands[pageNumber - 1]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("\"".concat(args.name, "\" is an invalid page number.")); - output("[sky]-- Commands page [lightgrey]".concat(pageNumber, "/").concat(chunkedPlayerCommands.length, "[sky] --\n") + formatList(page, '[sky]')); - } - } - } - }, - }, msg: { - args: ['player:player', 'message:string'], - description: 'Send a message to only one player.', - perm: commands_1.Perm.chat, - handler: function (_a) { - var args = _a.args, sender = _a.sender, output = _a.output, f = _a.f; - globals_1.recentWhispers[args.player.uuid] = sender.uuid; - args.player.sendMessage("".concat(sender.prefixedName, "[lightgray] whispered:[#BBBBBB] ").concat(args.message)); - output(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""], ["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""])), args.player, args.message)); - }, - }, r: { - args: ['message:string'], - description: 'Reply to the most recent message.', - perm: commands_1.Perm.chat, - handler: function (_a) { - var _b; - var args = _a.args, sender = _a.sender, output = _a.output, f = _a.f; - var recipient = players_1.FishPlayer.getById((_b = globals_1.recentWhispers[sender.uuid]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("It doesn't look like someone has messaged you recently. Try whispering to them with [white]\"/msg \"")); - if (!(recipient === null || recipient === void 0 ? void 0 : recipient.connected())) - (0, commands_1.fail)("The person who last messaged you doesn't seem to exist anymore. Try whispering to someone with [white]\"/msg \""); - globals_1.recentWhispers[globals_1.recentWhispers[sender.uuid]] = sender.uuid; - recipient.sendMessage("".concat(sender.name, "[lightgray] whispered:[#BBBBBB] ").concat(args.message)); - output(f(templateObject_8 || (templateObject_8 = __makeTemplateObject(["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""], ["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""])), recipient, args.message)); - }, - }, trail: { - args: ['type:string?', 'color:string?'], - description: 'Use command to see options and toggle trail on/off.', - perm: commands_1.Perm.none, - handler: function (_a) { - var args = _a.args, sender = _a.sender, output = _a.output, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess; - //overload 1: type not specified - if (!args.type) { - if (sender.trail != null) { - sender.trail = null; - outputSuccess("Trail turned off."); - } - else { - output("Available types:[yellow]\n1 - fluxVapor (flowing smoke, long lasting)\n2 - overclocked (diamonds)\n3 - overdriven (squares)\n4 - shieldBreak (smol)\n5 - upgradeCoreBloom (square, long lasting, only orange)\n6 - electrified (tiny spiratic diamonds, but only green)\n7 - unitDust (same as above but round, and can change colors)\n[white]Usage: [orange]/trail [lightgrey] [color/#hex/r,g,b]"); - } - return; - } - //overload 2: type specified - var trailTypes = { - "1": 'fluxVapor', - "2": 'overclocked', - "3": 'overdriven', - "4": 'shieldBreak', - "5": 'upgradeCoreBloom', - "6": 'electrified', - "7": 'unitDust', - }; - var selectedType = trailTypes[args.type]; - if (!selectedType) { - if (Object.values(trailTypes).includes(args.type)) - (0, commands_1.fail)("Please use the numeric id to refer to a trail type."); - else - (0, commands_1.fail)("\"".concat(args.type, "\" is not an available type.")); - } - var color = args.color ? (0, utils_1.getColor)(args.color) : Color.white; - if (color instanceof Color) { - sender.trail = { - type: selectedType, - color: color, - }; - } - else { - outputFail("[scarlet]Sorry, \"".concat(args.color, "\" is not a valid color.\n[yellow]Color can be in the following formats:\n[pink]pink [white]| [gray]#696969 [white]| 255,0,0.")); - } - }, - }, ohno: (0, commands_1.command)({ - args: [], - description: 'Spawns an ohno.', - perm: commands_1.Perm.play, - init: function () { - var Ohnos = { - enabled: true, - ohnos: new Array(), - makeOhno: function (team, x, y) { - var ohno = UnitTypes.atrax.create(team); - ohno.set(x, y); - ohno.type = UnitTypes.alpha; - ohno.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); - ohno.resetController(); //does this work? - ohno.add(); - this.ohnos.push(ohno); - return ohno; - }, - updateLength: function () { - this.ohnos = this.ohnos.filter(function (o) { return o && o.isAdded() && !o.dead; }); - }, - checkAchievement: function () { - var e_1, _a; - try { - for (var _b = __values(this.ohnos), _c = _b.next(); !_c.done; _c = _b.next()) { - var ohno = _c.value; - var player = ohno.getPlayer(); - if (player) - achievements_1.Achievements.ohno.grantTo(players_1.FishPlayer.get(player), false); - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_1) throw e_1.error; } - } - }, - killAll: function () { - this.ohnos.forEach(function (ohno) { var _a; return (_a = ohno === null || ohno === void 0 ? void 0 : ohno.kill) === null || _a === void 0 ? void 0 : _a.call(ohno); }); - this.ohnos = []; - }, - amount: function () { - return this.ohnos.length; - }, - }; - Events.on(EventType.GameOverEvent, function (e) { - Ohnos.killAll(); - }); - Timer.schedule(function () { return Ohnos.checkAchievement(); }, 1, 2); - return Ohnos; - }, - requirements: [ - commands_1.Req.gameRunning, commands_1.Req.modeNot("pvp"), - commands_1.Req.unitExists("You cannot spawn ohnos while dead.") - ], - handler: function (_a) { - var sender = _a.sender, Ohnos = _a.data; - if (!Ohnos.enabled) - (0, commands_1.fail)("Ohnos have been temporarily disabled."); - Ohnos.updateLength(); - if (Ohnos.ohnos.length >= (Groups.player.size() + 1) || - sender.team().data().countType(UnitTypes.alpha) >= Units.getCap(sender.team())) - (0, commands_1.fail)("Sorry, the max number of ohno units has been reached."); - if ((0, utils_1.nearbyEnemyTile)((sender.unit()), 6) != null) - (0, commands_1.fail)("Too close to an enemy building!"); - if (!UnitTypes.alpha.supportsEnv(Vars.state.rules.env)) - (0, commands_1.fail)("Ohnos cannot survive in this map."); - Ohnos.makeOhno(sender.team(), sender.player.x, sender.player.y); - }, - }), ranks: { - args: [], - description: 'Displays information about all ranks.', - perm: commands_1.Perm.none, - handler: function (_a) { - var output = _a.output; - output("List of ranks:\n" + - Object.values(ranks_1.Rank.ranks) - .map(function (rank) { return "".concat(rank.prefix, " ").concat(rank.color).concat((0, funcs_1.capitalizeText)(rank.name), "[]: ").concat(rank.color).concat(rank.description, "[]\n"); }) - .join("") + - "List of flags:\n" + - Object.values(ranks_1.RoleFlag.flags) - .map(function (flag) { return "".concat(flag.prefix, " ").concat(flag.color).concat((0, funcs_1.capitalizeText)(flag.name), "[]: ").concat(flag.color).concat(flag.description, "[]\n"); }) - .join("")); - }, - }, rules: { - args: ['player:player?'], - description: 'Displays the server rules.', - perm: commands_1.Perm.none, - handler: function (_a) { - var _b; - var args = _a.args, sender = _a.sender, output = _a.output, outputSuccess = _a.outputSuccess, f = _a.f; - var target = (_b = args.player) !== null && _b !== void 0 ? _b : sender; - if (target !== sender) { - if (!sender.hasPerm("warn")) - (0, commands_1.fail)("You do not have permission to show rules to other players."); - if (!sender.canModerate(target)) - commands_1.Req.cooldown(funcs_1.Duration.minutes(10)); - if (target.hasPerm("blockTrolling")) - (0, commands_1.fail)(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); - } - void menus_1.Menu.menu("Rules for [#0000ff]>|||> FISH [white]servers", config_1.rules.join("\n\n"), ["[green]I agree to abide by these rules[]", "No"], target, { onCancel: "null" }).then(function (option) { - if (option == "No") { - target.kick("You must agree to the rules to play on this server. Rejoin to agree to the rules.", 1); - outputSuccess('Player rejected the rules and was kicked.'); - } - else if (option == null) { - output('Player closed the menu.'); - } - else { - outputSuccess('Player acknowledged the rules.'); - } - }); - if (target !== sender) - outputSuccess(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Reminded ", " of the rules."], ["Reminded ", " of the rules."])), target)); - }, - }, void: { - args: ["player:player?"], - description: 'Warns other players about power voids.', - perm: commands_1.Perm.play, - requirements: function (_a) { - var args = _a.args; - return [ - commands_1.Req.mode("attack"), - args.player ? commands_1.Req.cooldown(20000) : commands_1.Req.cooldownGlobal(10000) - ]; - }, - handler: function (_a) { - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - if (args.player) { - if (!sender.hasPerm("trusted")) - (0, commands_1.fail)("You do not have permission to show popups to other players, please run /void with no arguments to send a chat message to everyone."); - if (args.player !== sender && args.player.hasPerm("blockTrolling")) - (0, commands_1.fail)("Target player is insufficiently trollable."); - void menus_1.Menu.menu("\uf83f [scarlet]WARNING[] \uf83f", "[white]Don't break the Power Void (\uF83F), it's a trap!\nPower voids disable anything they are connected to.\nIf you break it, [scarlet]you will get attacked[] by enemy units.\nPlease stop attacking and [lime]build defenses[] first!", ["I understand"], args.player, { onCancel: 'null' }).then(function () { return outputSuccess(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Player ", " acknowledged the warning."], ["Player ", " acknowledged the warning."])), args.player)); }); - (0, utils_1.logAction)("showed void warning", sender, args.player); - outputSuccess(f(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Warned ", " about power voids with a popup message."], ["Warned ", " about power voids with a popup message."])), args.player)); - } - else { - Call.sendMessage("[white]Don't break the Power Void (\uF83F), it's a trap!\nPower voids disable anything they are connected to. If you break it, [scarlet]you will get attacked[] by enemy units.\nPlease stop attacking and [lime]build defenses[] first!"); - } - }, - }, team: { - args: ['team:team', 'reason:string?'], - description: 'Changes your team.', - perm: commands_1.Perm.changeTeam, - handler: function (_a) { - var _b; - var sender = _a.sender, _c = _a.args, team = _c.team, reason = _c.reason, outputSuccess = _a.outputSuccess, f = _a.f; - if (config_1.Gamemode.sandbox() && globals_1.fishState.peacefulMode && !sender.hasPerm("admin")) - (0, commands_1.fail)("You do not have permission to change teams because peaceful mode is on."); - if (config_1.Gamemode.sandbox() && team === Vars.state.rules.waveTeam && !sender.hasPerm("admin")) - (0, commands_1.fail)("You do not have permission to change to the wave team on sandbox."); - if (!(config_1.Gamemode.sandbox() || config_1.Gamemode.testsrv()) && !sender.hasPerm("mod") && !reason) - (0, commands_1.fail)("Please specify a reason for changing teams."); - if (!sender.hasPerm("changeTeamExternal")) { - if (team.data().cores.size <= 0) - (0, commands_1.fail)("You do not have permission to change to a team with no cores."); - if (!sender.player.dead() && !((_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.spawnedByCore)) - sender.forceRespawn(); - } - if (!sender.hasPerm("mod")) - sender.changedTeam = true; - sender.setTeam(team); - outputSuccess(f(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Changed your team to ", "."], ["Changed your team to ", "."])), team)); - if (reason && !config_1.Gamemode.sandbox()) - (0, utils_1.logAction)("changed team to ".concat(team.name, " on ").concat((0, funcs_1.escapeTextDiscord)(Vars.state.map.plainName()), " with reason ").concat((0, funcs_1.escapeTextDiscord)(reason)), sender); - }, - }, teamp: { - args: ['team:team', 'target:player'], - description: 'Changes the team of a player.', - perm: commands_1.Perm.changeTeam, - handler: function (_a) { - var _b; - var sender = _a.sender, _c = _a.args, team = _c.team, target = _c.target, outputSuccess = _a.outputSuccess, f = _a.f; - if (!sender.canModerate(target, true, "mod", true)) - (0, commands_1.fail)(f(templateObject_14 || (templateObject_14 = __makeTemplateObject(["You do not have permission to change the team of ", ""], ["You do not have permission to change the team of ", ""])), target)); - if (config_1.Gamemode.sandbox() && globals_1.fishState.peacefulMode && !sender.hasPerm("admin")) - (0, commands_1.fail)("You do not have permission to change teams because peaceful mode is on."); - if (!sender.hasPerm("changeTeamExternal")) { - if (team.data().cores.size <= 0) - (0, commands_1.fail)("You do not have permission to change to a team with no cores."); - if (!target.player.dead() && !((_b = target.unit()) === null || _b === void 0 ? void 0 : _b.spawnedByCore)) - target.forceRespawn(); - } - target.setTeam(team); - outputSuccess(f(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Changed team of player ", " to ", "."], ["Changed team of player ", " to ", "."])), target, team)); - }, - }, rank: { - args: ['player:player'], - description: 'Displays the rank of a player.', - perm: commands_1.Perm.none, - handler: function (_a) { - var args = _a.args, output = _a.output, f = _a.f; - output(f(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Player ", "'s rank is ", "."], ["Player ", "'s rank is ", "."])), args.player, args.player.rank)); - }, - }, forcevnw: { - args: ["force:boolean?"], - description: 'Force skip to the next wave.', - perm: commands_1.Perm.admin, - handler: function (_a) { - var allCommands = _a.allCommands, sender = _a.sender, _b = _a.args.force, force = _b === void 0 ? true : _b; - if (allCommands.vnw.data.manager.session == null) { - if (force == false) - (0, commands_1.fail)("Cannot clear votes for VNW because no vote is currently ongoing."); - (0, utils_1.skipWaves)(1, true); - } - else { - if (force) - Call.sendMessage("VNW: [green]Vote was forced by admin [yellow]".concat(sender.name, "[green], skipping wave.")); - else - Call.sendMessage("VNW: [red]Votes cleared by admin [yellow]".concat(sender.name, "[red].")); - allCommands.vnw.data.manager.forceVote(force); - } - }, - }, vnw: (0, commands_1.command)({ - args: ["waves:number?"], - description: "Vote to start the next wave.", - perm: commands_1.Perm.play, - init: function () { return ({ - manager: new votes_1.VoteManager(funcs_1.Duration.minutes(1.5)) - .on("success", function (t) { return (0, utils_1.skipWaves)(t.session.data, true); }) - .on("vote passed", function () { return Call.sendMessage('VNW: [green]Vote passed, skipping to next wave.'); }) - .on("vote failed", function () { return Call.sendMessage('VNW: [red]Vote failed.'); }) - .on("player vote change", function (t, player) { return Call.sendMessage("VNW: ".concat(player.name, " [white] has voted on skipping [accent]").concat(t.session.data, "[white] wave(s). [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) - .on("player vote removed", function (t, player) { return Call.sendMessage("VNW: ".concat(player.name, " [white] has left. [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) - }); }, - requirements: [commands_1.Req.cooldown(3000), commands_1.Req.integerRange("waves", 1, 15), commands_1.Req.mode("survival", "testsrv"), commands_1.Req.gameRunning], - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var _c; - var sender = _b.sender, waves = _b.args.waves, manager = _b.data.manager; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - if (!!manager.session) return [3 /*break*/, 4]; - if (!(waves !== null && waves !== void 0)) return [3 /*break*/, 1]; - _c = waves; - return [3 /*break*/, 3]; - case 1: return [4 /*yield*/, menus_1.Menu.menu("Start a Next Wave Vote", "Select the amount of waves you would like to skip.", [1, 5, 10], sender, { - includeCancel: true, - optionStringifier: function (n) { return "".concat(n, " waves"); } - })]; - case 2: - _c = (waves = _d.sent()); - _d.label = 3; - case 3: - _c; - if (manager.session) { - //Someone else started a vote - if (manager.session.data != waves) - (0, commands_1.fail)("Someone else started a vote with a different number of waves to skip."); - else - manager.vote(sender, sender.voteWeight(), waves); - } - else { - manager.start(sender, sender.voteWeight(), waves); - } - return [3 /*break*/, 5]; - case 4: - manager.vote(sender, sender.voteWeight(), null); - _d.label = 5; - case 5: return [2 /*return*/]; - } - }); - }); - } - }), forcertv: { - args: ["force:boolean?"], - description: 'Force skip to the next map.', - perm: commands_1.Perm.admin, - handler: function (_a) { - var _b = _a.args.force, force = _b === void 0 ? true : _b, sender = _a.sender, allCommands = _a.allCommands; - if (allCommands.rtv.data.manager.session == null) { - if (force == false) - (0, commands_1.fail)("Cannot clear votes for RTV because no vote is currently ongoing."); - allCommands.rtv.data.manager.forceVote(true); - } - else { - if (force) - Call.sendMessage("RTV: [green]Vote was forced by admin [yellow]".concat(sender.name, "[green].")); - else - Call.sendMessage("RTV: [red]Votes cleared by admin [yellow]".concat(sender.name, "[red].")); - allCommands.rtv.data.manager.forceVote(force); - } - } - }, rtv: (0, commands_1.command)({ - args: [], - description: 'Rock the vote to change map.', - perm: commands_1.Perm.play, - init: function () { return ({ - manager: new votes_1.VoteManager(funcs_1.Duration.minutes(1.5), config_1.Gamemode.hexed() ? ["fractionOfVoters", 1] : undefined) //Require unanimity in Hexed, as it is often 1 v everyone - .on("success", function () { return (0, utils_1.neutralGameover)(); }) - .on("vote passed", function () { return Call.sendMessage("RTV: [green]Vote has passed, changing map."); }) - .on("vote failed", function () { return Call.sendMessage("RTV: [red]Vote failed."); }) - .on("player vote change", function (t, player, oldVote, newVote) { return Call.sendMessage("RTV: ".concat(player.name, "[white] ").concat(oldVote == newVote ? "still " : "", "wants to change the map. [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) - .on("player vote removed", function (t, player) { return Call.sendMessage("RTV: ".concat(player.name, "[white] has left the game. [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) - }); }, - requirements: [commands_1.Req.cooldown(3000), commands_1.Req.gameRunning], - handler: function (_a) { - var sender = _a.sender, manager = _a.data.manager; - manager.vote(sender, 1, 0); //No weighting for RTV except for removing AFK players - } - }), - // votekick: command({ - // args: ["target:player"], - // description: "Starts a vote to kick a player.", - // perm: Perm.play, - // data: new VoteManager( - // Duration.seconds(20), - // ["absolute", 3], - // (fishP, target) => fishP.team() == target.team() || fishP.hasPerm("voteOtherTeams") - // ), - // handler({args, sender, data: votekickmanager}){ - // if(votekickmanager.session) fail(`There is already a votekick in progress.`); - // votekickmanager.start(sender, 1, args.target); - // } - // }), - // vote: { - // args: ["vote:boolean"], - // description: "Use /votekick instead.", - // perm: Perm.play, - // handler({sender, args, allCommands}){ - // const votekickmanager = allCommands.votekick.data; - // votekickmanager.handleVote(sender, args ? 1 : -1); - // } - // }, - forcenextmap: { - args: ["map:mapOrRandom"], - description: 'Override the next map in queue.', - perm: commands_1.Perm.admin.exceptModes({ - testsrv: commands_1.Perm.play - }), - handler: function (_a) { - var allCommands = _a.allCommands, args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - Vars.maps.setNextMapOverride(args.map == "random" ? null : args.map); - if (allCommands.nextmap.data.voteEndTime() > -1) { - //Cancel /nextmap vote if it's ongoing - allCommands.nextmap.data.resetVotes(); - Call.sendMessage("[red]Admin ".concat(sender.name, "[red] has cancelled the vote. The next map will be ").concat(args.map == "random" ? "random" : "[yellow]".concat(args.map.name()), ".")); - } - else { - outputSuccess(f(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Forced the next map to be ", "."], ["Forced the next map to be ", "."])), args.map == "random" ? "random" : "\"".concat(args.map.name(), "\" by ").concat(args.map.author()))); - } - }, - }, maps: { - args: [], - description: 'Lists the available maps.', - perm: commands_1.Perm.none, - handler: function (_a) { - var output = _a.output; - output("[yellow]Use [white]/nextmap [lightgray] [yellow]to vote on a map.\n\n[blue]Available maps:\n_________________________\n".concat(Vars.maps.customMaps().toArray().map(function (map) { - return "[yellow]".concat(map.name()); - }).join("\n"))); - } - }, nextmap: (0, commands_1.command)(function () { - var random = { - name: function () { return "[lightgray]Random"; }, - plainName: function () { return "random"; } - }; - var votes = new Map(); - var lastVoteCount = 0; - var lastVoteTime = 0; - var voteEndTime = -1; - var voteDuration = funcs_1.Duration.minutes(1.5); - var task = null; - function resetVotes() { - votes.clear(); - voteEndTime = -1; - task === null || task === void 0 ? void 0 : task.cancel(); - } - function getMapData() { - return __spreadArray([], __read(votes.values()), false).reduce(function (acc, map) { return (acc.increment(map), acc); }, new ObjectIntMap()).entries().toArray(); - } - function showVotes() { - Call.sendMessage("[green]Current votes:\n------------------------------\n".concat(getMapData().map(function (_a) { - var map = _a.key, votes = _a.value; - return "[cyan]".concat(map.name(), "[yellow]: ").concat(votes); - }).toString("\n"))); - } - function startVote() { - voteEndTime = Date.now() + voteDuration; - task = Timer.schedule(endVote, voteDuration / 1000); - } - function endVote() { - if (voteEndTime == -1) - return; //aborted somehow - if (votes.size == 0) - return; //no votes? - if (votes.size + 2 <= lastVoteCount && (Date.now() - lastVoteTime) < funcs_1.Duration.minutes(10)) { - //If the number of votes is 2 less than the previous number of votes for a vote in the past 10 minutes, abor - Call.sendMessage("[cyan]Next Map Vote: [scarlet]Vote aborted because a previous vote had significantly higher turnout"); - resetVotes(); - return; - } - else { - lastVoteTime = Date.now(); - lastVoteCount = votes.size; - } - var mapData = getMapData(); - var highestVoteCount = mapData.max(floatf(function (e) { return e.value; })).value; - var highestVotedMaps = mapData.select(function (e) { return e.value == highestVoteCount; }); - var winner; - if (highestVotedMaps.size > 1) { - winner = highestVotedMaps.random().key; - Call.sendMessage("[green]There was a tie between the following maps:\n".concat(highestVotedMaps.map(function (_a) { - var map = _a.key, votes = _a.value; - return "[cyan]".concat(map.name(), "[yellow]: ").concat(votes); - }).toString("\n"), "\n[green]Picking random winner: [yellow]").concat(winner.name())); - } - else { - winner = highestVotedMaps.get(0).key; - Call.sendMessage("[green]Map voting complete! The next map will be [yellow]".concat(winner.name(), " [green]with [yellow]").concat(highestVoteCount, "[green] votes.")); - } - Vars.maps.setNextMapOverride(winner == random ? null : null); - resetVotes(); - } - Events.on(EventType.GameOverEvent, resetVotes); - Events.on(EventType.ServerLoadEvent, resetVotes); - return { - args: ['map:mapOrRandom'], - description: 'Allows you to vote for the next map. Use /maps to see all available maps.', - perm: commands_1.Perm.play, - data: { votes: votes, voteEndTime: function () { return voteEndTime; }, resetVotes: resetVotes, endVote: endVote }, - requirements: [commands_1.Req.cooldown(10000)], - handler: function (_a) { - var args = _a.args, sender = _a.sender; - var map = args.map === "random" ? random : args.map; - if (config_1.Gamemode.testsrv()) - (0, commands_1.fail)("Please use /forcenextmap instead."); - if (votes.get(sender)) - (0, commands_1.fail)("You have already voted."); - if (voteEndTime == -1) { - if ((Date.now() - lastVoteTime) < funcs_1.Duration.minutes(1)) - (0, commands_1.fail)("Please wait 1 minute before starting a new map vote."); - startVote(); - votes.set(sender, map); - Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] started a map vote, and voted for [yellow]").concat(map.name(), "[cyan]. Use [white]/nextmap ").concat(map.plainName(), "[] to add your vote, or run [white]/maps[] to see other available maps.")); - } - else { - votes.set(sender, map); - Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] voted for [yellow]").concat(map.name(), "[cyan]. Time left: [scarlet]").concat((0, utils_1.formatTimeRelative)(voteEndTime, true))); - showVotes(); - } - } - }; - }), surrender: (0, commands_1.command)(function () { - var prefix = "[orange]Surrender[white]: "; - var managers = Team.all.map(function (team) { - return new votes_1.VoteManager(funcs_1.Duration.minutes(1.5), ["fractionOfVoters", config_1.Gamemode.hexed() ? 1 : 3 / 4], function (p) { return p.team() == team; }) - .on("success", function () { return team.cores().copy().each(function (c) { return c.kill(); }); }) - .on("vote passed", function () { return Call.sendMessage(prefix + "Team ".concat(team.coloredName(), " has voted to forfeit this match.")); }) - .on("vote failed", function (t) { return t.messageEligibleVoters(prefix + "Team ".concat(team.coloredName(), " has chosen not to forfeit this match.")); }) - .on("player vote change", function (t, player, oldVote, newVote) { return t.messageEligibleVoters(prefix + "".concat(player.name, "[white] ").concat(oldVote == newVote ? "still " : "", "wants to forfeit this match. [orange]").concat(t.currentVotes(), "[white] votes, [orange]").concat(t.requiredVotes(), "[white] required.")); }) - .on("player vote removed", function (t, player) { return t.messageEligibleVoters(prefix + "Player ".concat(player.name, "[white] has left the game. [orange]").concat(t.currentVotes(), "[white] votes, [orange]").concat(t.requiredVotes(), "[white] required.")); }); - }); - globals_1.FishEvents.on("playerTeamChange", function (_, fishP, previous) { - managers[previous.id].unvote(fishP); - }); - return { - args: ["force:boolean?", "team:team?"], - description: "Vote to surrender to the enemy team.", - perm: commands_1.Perm.play, - requirements: [commands_1.Req.mode("pvp"), commands_1.Req.teamAlive], - data: { managers: managers }, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var t, manager; - var sender = _b.sender, _c = _b.args, force = _c.force, team = _c.team; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - t = sender.hasPerm("admin") && team ? team : sender.team(); - manager = managers[t.id]; - if (!(sender.hasPerm("admin") && force != undefined)) return [3 /*break*/, 4]; - if (!force) return [3 /*break*/, 2]; - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to force team ".concat(t.coloredName(), "[] to lose?"))]; - case 1: - _d.sent(); - manager.messageEligibleVoters(prefix + "Vote forced by admin ".concat(sender.name, "[white].")); - Call.sendMessage(prefix + "Team ".concat(t.coloredName(), " has voted to forfeit this match.")); - return [3 /*break*/, 3]; - case 2: - manager.messageEligibleVoters(prefix + "Votes cleared by admin ".concat(sender.name, "[white].")); - _d.label = 3; - case 3: - manager.forceVote(force); - return [2 /*return*/]; - case 4: - if (sender.ranksAtLeast("mod")) - commands_1.Req.cooldown(5000); - else - commands_1.Req.cooldown(20000); - manager.vote(sender, 1, 0); - return [2 /*return*/]; - } - }); - }); - }, - }; - }), stats: { - args: ["target:player", "global:boolean?"], - perm: commands_1.Perm.none, - description: "Views a player's stats.", - handler: function (_a) { - var _b = _a.args, target = _b.target, _c = _b.global, global = _c === void 0 ? false : _c, output = _a.output, f = _a.f; - var stats = global ? target.globalStats : target.stats; - output(f(templateObject_18 || (templateObject_18 = __makeTemplateObject(["[accent]Statistics for player ", " ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""], ["[accent]\\\nStatistics for player ", " ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""])), target, global ? "across all servers" : "on this server", stats.blocksBroken, stats.blocksPlaced, stats.chatMessagesSent, stats.gamesFinished, (0, utils_1.formatTime)(stats.timeInGame), stats.gamesWon / stats.gamesFinished)); - } - }, showworld: { - args: ["x:number?", "y:number?", "size:number?"], - perm: commands_1.Perm.none, - description: "Views the world as a 2D scrollable menu.", - requirements: [commands_1.Req.cooldown(4000), commands_1.Req.integerRange("size", 1, 20)], - handler: function (_a) { - var sender = _a.sender, _b = _a.args, _c = _b.size, size = _c === void 0 ? 7 : _c, x = _b.x, y = _b.y; - if (Vars.state.rules.fog) - (0, commands_1.fail)("This command is disabled when fog is enabled."); - var options = (0, funcs_1.to2DArray)(Reflect.get(Vars.world.tiles, "array").map(function (tile) { return ({ - text: tile.block().emoji(), - data: null, - }); }), Vars.world.width()).reverse(); - var height = Vars.world.height(); - void menus_1.Menu.scroll2D(sender, "The World", "Use the arrow keys to navigate around the world. Click a blank square to exit.", options, { - columns: size, - rows: size, - x: x ? x - Math.trunc(size / 2) : 0, - y: height - (y ? y + 1 + Math.trunc(size / 2) : size), - getCenterText: function (x, y) { return "".concat(x, ",").concat(height - y - size); } - }); - } - }, mapinfo: { - args: ["map:map?"], - perm: commands_1.Perm.none, - description: "Displays information about a map.", - handler: function (_a) { - var _b; - var output = _a.output, map = _a.args.map, f = _a.f, sender = _a.sender; - if (map) { - var fmap = (_b = maps_1.FMap.getCreate(map)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Map data is still being loaded, try again later."); - output(fmap.displayStats(f)); - } - else { - void menus_1.Menu.textPages(sender, Vars.maps.customMaps().map(function (m) { - return ["Map information", function () { var _a, _b; return (_b = (_a = maps_1.FMap.getCreate(m)) === null || _a === void 0 ? void 0 : _a.displayStats(f)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Map data is still being loaded, try again later."); }]; - }).toArray(), [], { - startPage: Vars.maps.customMaps().toArray().indexOf(Vars.state.map), - }); - } - } - }, gamemode: { - args: ["mode:string"], - perm: new commands_1.Perm("changeGamemode", "manager").exceptModes({ - testsrv: commands_1.Perm.play, - }), - description: "Sets the gamemode.", - requirements: [commands_1.Req.cooldownGlobal(10000)], - handler: function (_a) { - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, lastUsedSuccessfully = _a.lastUsedSuccessfully; - if (!sender.hasPerm('trusted')) - commands_1.Req.cooldownGlobal(30000)({ lastUsedSuccessfully: lastUsedSuccessfully }); - //Unpause - Vars.state.set(GameState.State.playing); - switch (args.mode) { - case "attack": - Vars.state.rules.attackMode = true; - Vars.state.rules.pvp = false; - Vars.state.rules.infiniteResources = false; - break; - case "survival": - Vars.state.rules.attackMode = false; - Vars.state.rules.waves = true; - Vars.state.rules.pvp = false; - Vars.state.rules.infiniteResources = false; - break; - case "pvp": - Vars.state.rules.attackMode = true; - Vars.state.rules.pvp = true; - Vars.state.rules.waves = false; - Vars.state.rules.infiniteResources = false; - break; - case "sandbox": - Vars.state.rules.attackMode = true; - Vars.state.rules.pvp = false; - Vars.state.rules.waves = false; - Vars.state.rules.infiniteResources = true; - break; - default: (0, commands_1.fail)("Invalid mode, valid modes are: attack, survival, pvp"); - } - var reloader = new WorldReloader(); - Reflect.set(reloader, "wasServer", true); - Reflect.set(reloader, "players", Groups.player.copy()); - Call.worldDataBegin(); - reloader.end(); - Call.sendMessage("[orange]Player ".concat(sender.cleanedName, " changed the gamemode to ").concat(args.mode)); - outputSuccess("Changed mode to ".concat(args.mode)); - } - }, mixunit: { - args: ["type:unittype", "base:unittype"], - description: "Spawns a unit that is made of two unit types mixed together.", - perm: commands_1.Perm.admin.exceptModes({ - sandbox: commands_1.Perm.play - }), - requirements: function (_a) { - var sender = _a.sender; - return [!sender.hasPerm("admin") && commands_1.Req.cooldown(1500), commands_1.Req.unitExists()].filter(Boolean); - }, - handler: function (_a) { - var args = _a.args, sender = _a.sender, f = _a.f, outputSuccess = _a.outputSuccess; - var _b = sender.unit(), team = _b.team, x = _b.x, y = _b.y; - var unit = args.base.create(team); - unit.type = args.type; - unit.maxHealth = args.type.health; //because half-dead units aren't fun - unit.set(x, y); - unit.add(); - outputSuccess(f(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Spawned a ", " that is partly a ", "."], ["Spawned a ", " that is partly a ", "."])), args.type, args.base)); - } - }, achievement: { - args: ["name:string?", "verbose:boolean?"], - description: "Displays information on a specific achievement.", - perm: commands_1.Perm.none, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var matching, achievement, _c; - var _d = _b.args, _e = _d.name, name = _e === void 0 ? "" : _e, _f = _d.verbose, verbose = _f === void 0 ? false : _f, sender = _b.sender, f = _b.f, output = _b.output; - return __generator(this, function (_g) { - switch (_g.label) { - case 0: - name = Strings.stripColors(name.toLowerCase()); - matching = achievements_1.Achievement.all.filter(function (a) { return Strings.stripColors(a.name).toLowerCase().includes(name); }); - if (matching.length == 0) - (0, commands_1.fail)(f(templateObject_20 || (templateObject_20 = __makeTemplateObject(["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."], ["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."])), name)); - if (!(matching.length > 2)) return [3 /*break*/, 2]; - return [4 /*yield*/, menus_1.Menu.pagedList(sender, "Achievement", "Select an achievement to view", matching, { - onCancel: "reject", - columns: 2, - optionStringifier: function (a) { return "".concat(a.icon, "[] ").concat(a.name); } - })]; - case 1: - _c = _g.sent(); - return [3 /*break*/, 3]; - case 2: - _c = matching[0]; - _g.label = 3; - case 3: - achievement = _c; - output(config_1.FColor.achievement(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Achievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "", "", ""], ["\\\nAchievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n", "\\\n", "\\\n"])), achievement.icon, achievement.name, achievement.description + (achievement.extendedDescription ? ("\n" + "[gray]".concat(achievement.extendedDescription)) : ""), achievement.modesText, f.boolGood(achievement.has(sender)), verbose ? "[gray]ID: (".concat(achievement.nid, ")").concat(achievement.sid, "\n") : "", verbose ? "[gray]Notifies: ".concat(achievement.notify, "\n") : "", achievement.hidden ? "This achievement is secret." : "")); - return [2 /*return*/]; - } - }); - }); - } - }, achievementlist: { - args: ["target:player?"], - description: "Shows all achievements in a paged menu.", - perm: commands_1.Perm.none, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var sender = _b.sender, _c = _b.args.target, target = _c === void 0 ? sender : _c, f = _b.f; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: return [4 /*yield*/, menus_1.Menu.textPages(sender, achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }) - .map(function (a) { return [ - "".concat(a.icon, "[] ").concat(a.name), - function () { return config_1.FColor.achievement(templateObject_22 || (templateObject_22 = __makeTemplateObject(["", "\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : ""); } - ]; }))]; - case 1: - _d.sent(); - return [2 /*return*/]; - } - }); - }); - } - }, achievementgrid: { - args: ["target:player?"], - description: "Shows all achievements in a 2D scrolling menu.", - perm: commands_1.Perm.none, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var visibleAchievements, options, numberAchievements, totalAchievements, x, y, a; - var _c; - var sender = _b.sender, _d = _b.args.target, target = _d === void 0 ? sender : _d, f = _b.f; - return __generator(this, function (_e) { - switch (_e.label) { - case 0: - visibleAchievements = achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }); - options = (0, funcs_1.to2DArray)(visibleAchievements, 7).map(function (row) { return row.map(function (a) { return ({ - data: a, - text: a.has(target) ? a.icon : "[gray]".concat(Strings.stripColors(a.icon)), - }); }); }); - numberAchievements = achievements_1.Achievement.all.filter(function (a) { return a.has(target); }).length; - totalAchievements = visibleAchievements.length; - x = 0, y = 0; - a = null; - _e.label = 1; - case 1: - if (!true) return [3 /*break*/, 3]; - return [4 /*yield*/, menus_1.Menu.scroll2D(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : - (target == sender ? "You have ".concat(numberAchievements, "/").concat(totalAchievements, " achievements.") - : config_1.FColor.achievement(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Player ", " has ", "/", " achievements."], ["Player ", " has ", "/", " achievements."])), target.prefixedName, numberAchievements, totalAchievements)) - + "\nClick an achievement icon to show more information.", options, { onCancel: "reject", columns: 5, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; - case 2: - //the loop will be aborted if the menu is cancelled (promise will reject) - _c = __read.apply(void 0, [_e.sent(), 3]), a = _c[0], x = _c[1], y = _c[2]; - if (a == achievements_1.Achievements.click_me && target == sender) - a.grantTo(sender); - return [3 /*break*/, 1]; - case 3: return [2 /*return*/]; - } - }); - }); - } - } })); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains most in-game chat commands that can be run by untrusted players. +*/ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.commands = void 0; +var achievements_1 = require("/achievements"); +var api = __importStar(require("/api")); +var config_1 = require("/config"); +var commands_1 = require("/frameworks/commands"); +var menus_1 = require("/frameworks/menus"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var maps_1 = require("/maps"); +var players_1 = require("/players"); +var ranks_1 = require("/ranks"); +var utils_1 = require("/utils"); +var votes_1 = require("/votes"); +exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { + args: [], + description: 'Prints information about the plugin.', + perm: commands_1.Perm.none, + handler: function (_a) { + var _b, _c; + var output = _a.output; + output("[accent][cyan]fish-commands[] is the monolithic plugin used for the Fish servers' features.\n[accent]==========\n[accent]Source code available at: [cyan]https://github.com/Fish-Community/fish-commands/\n[accent]Current plugin version: [cyan]".concat((_c = (_b = globals_1.fishPlugin.version) === null || _b === void 0 ? void 0 : _b.slice(0, 8)) !== null && _c !== void 0 ? _c : "[scarlet]null[]", "[]")); + } + }, unpause: (0, commands_1.command)({ + args: [], + description: 'Unpauses the game.', + perm: commands_1.Perm.trusted, + requirements: [commands_1.Req.mode('pvp')], + init: function () { + var data = { unpaused: false }; + Events.on(EventType.PlayEvent, function () { + if (data.unpaused) { + data.unpaused = false; + Vars.state.rules.pvpAutoPause = true; + } + }); + return data; + }, + handler: function (_a) { + var data = _a.data, outputSuccess = _a.outputSuccess; + Vars.state.rules.pvpAutoPause = false; + data.unpaused = true; + Core.app.post(function () { return Vars.state.set(GameState.State.playing); }); + outputSuccess("Unpaused."); + }, + }), tp: { + args: ['player:player'], + description: 'Teleport to another player.', + perm: commands_1.Perm.play, + requirements: [commands_1.Req.modeNot("pvp")], + handler: function (_a) { + var _b, _c, _d; + var args = _a.args, sender = _a.sender; + if (!((_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.spawnedByCore)) + (0, commands_1.fail)("Can only teleport while in a core unit."); + if (sender.team() !== args.player.team()) + (0, commands_1.fail)("Cannot teleport to players on another team."); + if ((_d = (_c = sender.unit()).hasPayload) === null || _d === void 0 ? void 0 : _d.call(_c)) + (0, commands_1.fail)("Cannot teleport to players while holding a payload."); + (0, utils_1.teleportPlayer)(sender.player, args.player.player); + }, + }, clean: (0, commands_1.command)({ + args: [], + description: 'Removes all boulders from the map.', + perm: commands_1.Perm.play, + requirements: [], + data: { lastRanMapStartTime: (_a = maps_1.PartialMapRun.current) === null || _a === void 0 ? void 0 : _a.startTime }, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var array, removed, i, t; + var sender = _b.sender, outputSuccess = _b.outputSuccess, data = _b.data; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!maps_1.PartialMapRun.current) + (0, commands_1.fail)("This game is already over."); + if (data.lastRanMapStartTime == maps_1.PartialMapRun.current.startTime) + (0, commands_1.fail)("This command was already run on this map."); + data.lastRanMapStartTime = maps_1.PartialMapRun.current.startTime; + Timer.schedule(function () { return Call.sound(sender.con, Sounds.rockBreak, 1, 1, 0); }, 0, 0.05, 10); + array = ArcReflect.get(Vars.world.tiles, "array"); + removed = 0; + i = 0; + _c.label = 1; + case 1: + if (!(i < array.length)) return [3 /*break*/, 4]; + t = array[i]; + if (!(t.breakable() && t.block() instanceof Prop)) return [3 /*break*/, 3]; + t.removeNet(); + removed++; + if (!(removed % 500 == 0)) return [3 /*break*/, 3]; + return [4 /*yield*/, (0, funcs_1.delay)(100)]; + case 2: + _c.sent(); + _c.label = 3; + case 3: + i++; + return [3 /*break*/, 1]; + case 4: + outputSuccess("Cleared the map of boulders."); + return [2 /*return*/]; + } + }); + }); + } + }), die: { + args: [], + description: 'Kills your unit.', + perm: commands_1.Perm.mod.exceptModes({ + sandbox: commands_1.Perm.play + }, "You do not have permission to die."), + handler: function (_a) { + var _b; + var sender = _a.sender; + (_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.kill(); + }, + }, discord: { + args: [], + description: 'Takes you to our discord.', + perm: commands_1.Perm.none, + handler: function (_a) { + var sender = _a.sender; + Call.openURI(sender.con, config_1.text.discordURL); + }, + }, tilelog: (0, commands_1.command)({ + args: ['persist:boolean?', 'showUUID:boolean?'], + description: 'Checks the history of a tile.', + perm: commands_1.Perm.none, + data: { showUUID: true }, + handler: function (_a) { + var args = _a.args, output = _a.output, outputSuccess = _a.outputSuccess, currentTapMode = _a.currentTapMode, handleTaps = _a.handleTaps, sender = _a.sender, data = _a.data; + var changed = args.showUUID !== undefined && args.showUUID != data.showUUID; + if (args.showUUID !== undefined) { + if (!sender.hasPerm("viewUUIDs")) + (0, commands_1.fail)("You do not have permission to show UUIDs."); + data.showUUID = args.showUUID; + } + if (args.persist && currentTapMode !== "on") { + outputSuccess("Tilelog mode enabled. Click tiles to check their recent history. Run /tilelog to disable."); + handleTaps("on"); + } + else if (args.persist && changed) { + outputSuccess("".concat(data.showUUID ? "Now showing UUIDs." : "No longer showing UUIDs.", " Click tiles to check their recent history. Run /tilelog to disable.")); + handleTaps("on"); + } + else if (currentTapMode == "off" || changed) { + handleTaps("once"); + output("Click on a tile to check its recent history..."); + } + else { + handleTaps("off"); + outputSuccess("Tilelog disabled."); + } + }, + tapped: function (_a) { + var _b; + var tile = _a.tile, x = _a.x, y = _a.y, output = _a.output, sender = _a.sender, admins = _a.admins, data = _a.data; + var historyData = (_b = globals_1.tileHistory["".concat(x, ",").concat(y)]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("There is no recorded history for the selected tile (".concat(tile.x, ", ").concat(tile.y, ").")); + var history = funcs_1.StringIO.read(historyData, function (str) { return str.readArray(function (d) { return ({ + action: d.readString(2), + uuid: d.readString(3), + time: d.readNumber(16), + type: d.readString(2), + }); }, 1); }); + output("[yellow]Tile history for tile (".concat(tile.x, ", ").concat(tile.y, "):\n") + history.map(function (e) { + var _a, _b; + return globals_1.uuidPattern.test(e.uuid) + ? (sender.hasPerm("viewUUIDs") && data.showUUID + ? "[yellow]".concat((_a = admins.getInfoOptional(e.uuid)) === null || _a === void 0 ? void 0 : _a.plainLastName(), "[lightgray](").concat(e.uuid, ")[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)) + : "[yellow]".concat((_b = admins.getInfoOptional(e.uuid)) === null || _b === void 0 ? void 0 : _b.plainLastName(), " ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time))) + : "[yellow]".concat(e.uuid, "[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); + }).join('\n')); + } + }), aoelog: (0, commands_1.command)(function () { + var allowedActions = [ + "built", "broke", "rotated", "killed", "configured", "pay-dropped", "picked up", "controlled" + ]; + var cachedPointMap = Object.create(null); + return { + args: ['persist:boolean?', 'amount:number?', 'action:string?'], + description: 'Checks the history of all tiles in the selected region. Can be filtered by action.', + perm: commands_1.Perm.none, + handler: function (_a) { + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, currentTapMode = _a.currentTapMode, handleTaps = _a.handleTaps; + if (currentTapMode === "off" || args.action || args.amount) { + if (args.action && !allowedActions.includes(args.action)) + (0, commands_1.fail)("Invalid action. Allowed actions: ".concat(allowedActions.join(", "))); + if (args.amount && args.amount > 100) + (0, commands_1.fail)("Limit cannot be greater than 100."); + cachedPointMap[sender.uuid] = undefined; + handleTaps("on"); + outputSuccess("Aoelog mode enabled. To see the recent history of all tiles in a rectangular region, tap opposite corners of the rectangle. Run /aoelog with no arguments to disable."); + } + else { + handleTaps("off"); + outputSuccess("Aoelog disabled."); + } + }, + tapped: function (_a) { + var x = _a.x, y = _a.y, output = _a.output, outputFail = _a.outputFail, sender = _a.sender, admins = _a.admins, handleTaps = _a.handleTaps, args = _a.args; + function handleArea(p1, p2) { + var minX = Math.min(p1[0], p2[0]); + var maxX = Math.max(p1[0], p2[0]); + var minY = Math.min(p1[1], p2[1]); + var maxY = Math.max(p1[1], p2[1]); + var limitTiles = 0; + var amount = args.amount != null ? Math.floor(Math.abs(args.amount)) : 10; + outer: for (var i = minX; i <= maxX; i++) { + for (var j = minY; j <= maxY; j++) { + var tileData = globals_1.tileHistory["".concat(i, ",").concat(j)]; + if (!tileData) + continue; + var history = funcs_1.StringIO.read(globals_1.tileHistory["".concat(i, ",").concat(j)], function (str) { return str.readArray(function (d) { + var _a, _b, _c; + return ({ + action: (_a = d.readString(2)) !== null && _a !== void 0 ? _a : "??", + uuid: (_b = d.readString(3)) !== null && _b !== void 0 ? _b : "??", + time: d.readNumber(16), + type: (_c = d.readString(2)) !== null && _c !== void 0 ? _c : "??", + }); + }, 1); }); + if (args.action) + history = history.filter(function (e) { return e.action === args.action; }); + if (history.length == 0) + continue; + output("[yellow]Tile history for tile (".concat(i, ", ").concat(j, "):\n") + history.map(function (e) { + var _a, _b; + if (globals_1.uuidPattern.test(e.uuid)) { + if (sender.hasPerm("viewUUIDs")) + return "[yellow]".concat((_a = admins.getInfoOptional(e.uuid)) === null || _a === void 0 ? void 0 : _a.plainLastName(), "[lightgray](").concat(e.uuid, ")[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); + else + return "[yellow]".concat((_b = admins.getInfoOptional(e.uuid)) === null || _b === void 0 ? void 0 : _b.plainLastName(), " ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); + } + else + return "[yellow]".concat(e.uuid, "[yellow] ").concat(e.action, " a [cyan]").concat(e.type, "[] ").concat((0, utils_1.formatTimeRelative)(e.time)); + }).join('\n')); + limitTiles++; + if (limitTiles === amount) + break outer; + } + } + if (limitTiles == 0) { + if (args.action) + outputFail("There is no recorded history for the selected region matching the provided filters."); + else + outputFail("There is no recorded history for the selected region."); + } + if (limitTiles == amount) + output("Displaying first ".concat(limitTiles, " entries. To show other entries, increase the limit or select a smaller area.")); + } + var p1 = cachedPointMap[sender.uuid]; + if (!p1) { + cachedPointMap[sender.uuid] = [x, y]; + output("1st point set at (".concat(x, ",").concat(y, ")")); + } + else { + var p2 = [x, y]; + output("2nd point set at (".concat(x, ", ").concat(y, ")")); + var width = Math.abs(p1[0] - p2[0]); + var height = Math.abs(p1[1] - p2[1]); + if (width > 50 || height > 50) + (0, commands_1.fail)("Selection too large: width/height cannot be more than 50."); + handleArea(p1, p2); + cachedPointMap[sender.uuid] = undefined; + if (!args.persist) + handleTaps("off"); + } + }, + }; + }), afk: { + args: [], + description: 'Toggles your afk status.', + perm: commands_1.Perm.none, + handler: function (_a) { + var sender = _a.sender, outputSuccess = _a.outputSuccess; + sender.manualAfk = !sender.manualAfk; + sender.updateName(); + if (sender.manualAfk) + outputSuccess("You are now marked as AFK."); + else + outputSuccess("You are no longer marked as AFK."); + }, + }, vanish: { + args: ['target:player?'], + description: "Toggles visibility of your rank and flags.", + perm: commands_1.Perm.vanish, + handler: function (_a) { + var sender = _a.sender, _b = _a.args.target, target = _b === void 0 ? sender : _b, outputSuccess = _a.outputSuccess; + if (sender.stelled()) + (0, commands_1.fail)("Marked players may not hide flags."); + if (sender.muted) + (0, commands_1.fail)("Muted players may not hide flags."); + if (sender != target && target.hasPerm("blockTrolling")) + (0, commands_1.fail)("Target is insufficentlly trollable."); + if (sender != target && !sender.ranksAtLeast("mod")) + (0, commands_1.fail)("You do not have permission to vanish other players."); + target.showRankPrefix = !target.showRankPrefix; + outputSuccess("".concat(target == sender ? "Your" : "".concat(target.name, "'s"), " rank prefix is now ").concat(target.showRankPrefix ? "visible" : "hidden", ".")); + }, + }, tileid: { + args: [], + description: 'Checks id of a tile.', + perm: commands_1.Perm.none, + handler: function (_a) { + var output = _a.output, handleTaps = _a.handleTaps; + handleTaps("once"); + output("Click a tile to see its id..."); + }, + tapped: function (_a) { + var output = _a.output, f = _a.f, tile = _a.tile; + output(f(templateObject_1 || (templateObject_1 = __makeTemplateObject(["ID is ", ""], ["ID is ", ""])), tile.block().id)); + } + } }, Object.fromEntries(config_1.FishServer.all.map(function (server) { return [ + server.name, + { + args: [], + description: "Switches to the ".concat(server.name, " server."), + perm: server.requiredPerm ? commands_1.Perm.getByName(server.requiredPerm) : commands_1.Perm.none, + isHidden: true, + handler: function (_a) { + var sender = _a.sender, lastUsedSuccessfullySender = _a.lastUsedSuccessfullySender; + if (Date.now() - lastUsedSuccessfullySender > funcs_1.Duration.minutes(1)) + players_1.FishPlayer.messageAllWithPerm(server.requiredPerm, "".concat(sender.name, "[magenta] has gone to the ").concat(server.name, " server. Use [cyan]/").concat(server.name, " [magenta]to join them!")); + Call.connect(sender.con, server.ip, server.port); + }, + }, +]; }))), { switch: { + args: ["server:string", "target:player?"], + description: "Switches to another server.", + perm: commands_1.Perm.play, + handler: function (_a) { + var _b, _c; + var args = _a.args, sender = _a.sender, f = _a.f, lastUsedSuccessfullySender = _a.lastUsedSuccessfullySender; + if (args.target != null && args.target != sender && !sender.canModerate(args.target, true, "admin", true)) + (0, commands_1.fail)(f(templateObject_2 || (templateObject_2 = __makeTemplateObject(["You do not have permission to switch player ", "."], ["You do not have permission to switch player ", "."])), args.target)); + var target = (_b = args.target) !== null && _b !== void 0 ? _b : sender; + if (globals_1.ipPortPattern.test(args.server) && sender.hasPerm("admin")) { + //direct connect + Call.connect.apply(Call, __spreadArray([target.con], __read(args.server.split(":")), false)); + } + else { + var unknownServerMessage = "Unknown server ".concat(args.server, ". Valid options: ").concat(config_1.FishServer.all.filter(function (s) { return !s.requiredPerm || sender.hasPerm(s.requiredPerm); }).map(function (s) { return s.name; }).join(", ")); + var server = (_c = config_1.FishServer.byName(args.server)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(unknownServerMessage); + //Pretend the server doesn't exist + if (server.requiredPerm && !sender.hasPerm(server.requiredPerm)) + (0, commands_1.fail)(unknownServerMessage); + if (target == sender && Date.now() - lastUsedSuccessfullySender > funcs_1.Duration.minutes(1)) + players_1.FishPlayer.messageAllWithPerm(server.requiredPerm, "".concat(sender.name, "[magenta] has gone to the ").concat(server.name, " server. Use [cyan]/").concat(server.name, " [magenta]to join them!")); + Call.connect(target.con, server.ip, server.port); + } + } + }, s: { + args: ['message:string'], + description: "Sends a message to staff only.", + perm: commands_1.Perm.chat, + handler: function (_a) { + var sender = _a.sender, args = _a.args, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, lastUsedSender = _a.lastUsedSender; + if (!sender.hasPerm("mod")) { + if (Date.now() - lastUsedSender < 4000) + (0, commands_1.fail)("This command was used recently and is on cooldown. [orange]Misuse of this command may result in a mute."); + } + api.sendStaffMessage(args.message, sender.name, sender.hasPerm("mod"), function (sent) { + if (!sender.hasPerm("mod")) { + if (sent) { + outputSuccess("Message sent to [orange]all online staff."); + } + else { + var wasReceived = players_1.FishPlayer.messageStaff(sender.prefixedName, args.message); + if (wasReceived) + outputSuccess("Message sent to staff."); + else + outputFail("No staff were online to receive your message."); + } + } + }); + }, + }, + /** + * This command is mostly for mobile (or players without foos). + * + * Since the player's unit follows the camera and we are moving the + * camera, we need to keep setting the players real position to the + * spot the command was made. This is pretty buggy but otherwise the + * player will be up the target player's butt + */ + watch: (0, commands_1.command)({ + args: ['player:player?'], + description: "Watch/unwatch a player.", + perm: commands_1.Perm.none, + data: new Set, + handler: function (_a) { + var _b; + var args = _a.args, data = _a.data, sender = _a.sender, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; + if (data.has(sender.uuid)) { + outputSuccess("No longer watching a player."); + data.delete(sender.uuid); + } + else if (args.player) { + data.add(sender.uuid); + var senderUnit_1 = (_b = sender.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("You do not have a unit."); + var stayX_1 = senderUnit_1.x; + var stayY_1 = senderUnit_1.y; + var target_1 = args.player.player; + (function watch() { + var _a, _b; + var unit = target_1.unit(); + if (data.has(sender.uuid) && unit) { + // Self.X+(172.5-Self.X)/10 + Call.setCameraPosition(sender.con, unit.x, unit.y); + if (senderUnit_1) + (_b = (_a = sender.unit()) === null || _a === void 0 ? void 0 : _a.set) === null || _b === void 0 ? void 0 : _b.call(_a, stayX_1, stayY_1); + Timer.schedule(function () { return watch(); }, 0.1, 0.1, 0); + } + else { + Call.setCameraPosition(sender.con, stayX_1, stayY_1); + } + })(); + } + else { + outputFail("No player to unwatch."); + } + }, + }), spectate: (0, commands_1.command)(function () { + //TODO revise code + /** Mapping between player and original team */ + var spectators = new Map(); + function spectate(target) { + spectators.set(target, target.team()); + target.forceRespawn(); + target.setTeam(Team.derelict); + target.forceRespawn(); + } + function resume(target) { + if (spectators.get(target) == null) + return; // this state is possible for a person who left not in spectate + target.setTeam(spectators.get(target)); + spectators.delete(target); + target.forceRespawn(); + } + Events.on(EventType.GameOverEvent, function () { return spectators.clear(); }); + Events.on(EventType.PlayerLeave, function (_a) { + var player = _a.player; + return resume(players_1.FishPlayer.get(player)); + }); + return { + args: ["target:player?"], + description: "Toggles spectator mode in PVP games.", + perm: commands_1.Perm.play, + requirements: [commands_1.Req.gameRunning], + handler: function (_a) { + var sender = _a.sender, _b = _a.args.target, target = _b === void 0 ? sender : _b, outputSuccess = _a.outputSuccess, f = _a.f; + if (!config_1.Gamemode.pvp() && !sender.hasPerm("mod")) + (0, commands_1.fail)("You do not have permission to spectate on a non-pvp server."); + if (target !== sender && target.hasPerm("blockTrolling")) + (0, commands_1.fail)("Target player is insufficiently trollable."); + if (target !== sender && !sender.ranksAtLeast("admin")) + (0, commands_1.fail)("You do not have permission to force other players to spectate."); + if (spectators.has(target)) { + resume(target); + outputSuccess(target == sender + ? f(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Rejoining game as team ", "."], ["Rejoining game as team ", "."])), target.team()) : f(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Forced ", " out of spectator mode."], ["Forced ", " out of spectator mode."])), target)); + } + else { + spectate(target); + outputSuccess(target == sender + ? f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Now spectating. Run /spectate again to resume gameplay."], ["Now spectating. Run /spectate again to resume gameplay."]))) : f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Forced ", " into spectator mode."], ["Forced ", " into spectator mode."])), target)); + } + } + }; + }), help: { + args: ['name:string?'], + description: 'Displays a list of all commands.', + perm: commands_1.Perm.none, + handler: function (_a) { + var _b; + var args = _a.args, output = _a.output, sender = _a.sender, allCommands = _a.allCommands; + var formatCommand = function (name, color) { + return new funcs_1.StringBuilder() + .add("".concat(color, "/").concat(name)) + .chunk("[white]".concat(allCommands[name].args.map(commands_1.formatArg).join(' '))) + .chunk("[lightgray]- ".concat(allCommands[name].description)).str; + }; + var formatList = function (commandList, color) { return commandList.map(function (c) { return formatCommand(c, color); }).join('\n'); }; + if (args.name && isNaN(parseInt(args.name)) && !['mod', 'admin', 'member'].includes(args.name)) { + //name is not a number or a category, therefore it is probably a command name + if (args.name in allCommands && (!allCommands[args.name].isHidden || allCommands[args.name].perm.check(sender))) { + if (args.name == "help") + achievements_1.Achievements.help_help.grantTo(sender, false); + output("Help for command ".concat(args.name, ":\n\t").concat(allCommands[args.name].description, "\n\tUsage: [sky]/").concat(args.name, " [white]").concat(allCommands[args.name].args.map(commands_1.formatArg).join(' '), "\n\tPermission required: ").concat(allCommands[args.name].perm.name)); + } + else + (0, commands_1.fail)("Command \"".concat(args.name, "\" does not exist.")); + } + else { + var commands_2 = { + player: [], + mod: [], + admin: [], + member: [], + }; + //TODO change this to category, not perm + Object.entries(allCommands).forEach(function (_a) { + var _b = __read(_a, 2), name = _b[0], data = _b[1]; + return (data.perm === commands_1.Perm.admin ? commands_2.admin : data.perm === commands_1.Perm.mod ? commands_2.mod : data.perm === commands_1.Perm.member ? commands_2.member : commands_2.player).push(name); + }); + var chunkedPlayerCommands = (0, funcs_1.to2DArray)(commands_2.player, 15); + switch (args.name) { + case 'admin': + output("".concat(commands_1.Perm.admin.color, "-- Admin commands --\n") + formatList(commands_2.admin, commands_1.Perm.admin.color)); + break; + case 'mod': + output("".concat(commands_1.Perm.mod.color, "-- Mod commands --\n") + formatList(commands_2.mod, commands_1.Perm.mod.color)); + break; + case 'member': + output("".concat(commands_1.Perm.member.color, "-- Member commands --\n") + formatList(commands_2.member, commands_1.Perm.member.color)); + break; + default: { + var pageNumber = args.name != undefined ? parseInt(args.name) : 1; + var page = (_b = chunkedPlayerCommands[pageNumber - 1]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("\"".concat(args.name, "\" is an invalid page number.")); + output("[sky]-- Commands page [lightgrey]".concat(pageNumber, "/").concat(chunkedPlayerCommands.length, "[sky] --\n") + formatList(page, '[sky]')); + } + } + } + }, + }, msg: { + args: ['player:player', 'message:string'], + description: 'Send a message to only one player.', + perm: commands_1.Perm.chat, + handler: function (_a) { + var args = _a.args, sender = _a.sender, output = _a.output, f = _a.f; + globals_1.recentWhispers[args.player.uuid] = sender.uuid; + args.player.sendMessage("".concat(sender.prefixedName, "[lightgray] whispered:[#BBBBBB] ").concat(args.message)); + output(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""], ["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""])), args.player, args.message)); + }, + }, r: { + args: ['message:string'], + description: 'Reply to the most recent message.', + perm: commands_1.Perm.chat, + handler: function (_a) { + var _b; + var args = _a.args, sender = _a.sender, output = _a.output, f = _a.f; + var recipient = players_1.FishPlayer.getById((_b = globals_1.recentWhispers[sender.uuid]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("It doesn't look like someone has messaged you recently. Try whispering to them with [white]\"/msg \"")); + if (!(recipient === null || recipient === void 0 ? void 0 : recipient.connected())) + (0, commands_1.fail)("The person who last messaged you doesn't seem to exist anymore. Try whispering to someone with [white]\"/msg \""); + globals_1.recentWhispers[globals_1.recentWhispers[sender.uuid]] = sender.uuid; + recipient.sendMessage("".concat(sender.name, "[lightgray] whispered:[#BBBBBB] ").concat(args.message)); + output(f(templateObject_8 || (templateObject_8 = __makeTemplateObject(["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""], ["[lightgray]Whispered to ", "[lightgray]:[#BBBBBB] ", ""])), recipient, args.message)); + }, + }, trail: { + args: ['type:string?', 'color:string?'], + description: 'Use command to see options and toggle trail on/off.', + perm: commands_1.Perm.none, + handler: function (_a) { + var args = _a.args, sender = _a.sender, output = _a.output, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess; + //overload 1: type not specified + if (!args.type) { + if (sender.trail != null) { + sender.trail = null; + outputSuccess("Trail turned off."); + } + else { + output("Available types:[yellow]\n1 - fluxVapor (flowing smoke, long lasting)\n2 - overclocked (diamonds)\n3 - overdriven (squares)\n4 - shieldBreak (smol)\n5 - upgradeCoreBloom (square, long lasting, only orange)\n6 - electrified (tiny spiratic diamonds, but only green)\n7 - unitDust (same as above but round, and can change colors)\n[white]Usage: [orange]/trail [lightgrey] [color/#hex/r,g,b]"); + } + return; + } + //overload 2: type specified + var trailTypes = { + "1": 'fluxVapor', + "2": 'overclocked', + "3": 'overdriven', + "4": 'shieldBreak', + "5": 'upgradeCoreBloom', + "6": 'electrified', + "7": 'unitDust', + }; + var selectedType = trailTypes[args.type]; + if (!selectedType) { + if (Object.values(trailTypes).includes(args.type)) + (0, commands_1.fail)("Please use the numeric id to refer to a trail type."); + else + (0, commands_1.fail)("\"".concat(args.type, "\" is not an available type.")); + } + var color = args.color ? (0, utils_1.getColor)(args.color) : Color.white; + if (color instanceof Color) { + sender.trail = { + type: selectedType, + color: color, + }; + } + else { + outputFail("[scarlet]Sorry, \"".concat(args.color, "\" is not a valid color.\n[yellow]Color can be in the following formats:\n[pink]pink [white]| [gray]#696969 [white]| 255,0,0.")); + } + }, + }, ohno: (0, commands_1.command)({ + args: [], + description: 'Spawns an ohno.', + perm: commands_1.Perm.play, + init: function () { + var Ohnos = { + enabled: true, + ohnos: new Array(), + makeOhno: function (team, x, y) { + var ohno = UnitTypes.atrax.create(team); + ohno.set(x, y); + ohno.type = UnitTypes.alpha; + ohno.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); + ohno.resetController(); //does this work? + ohno.add(); + this.ohnos.push(ohno); + return ohno; + }, + updateLength: function () { + this.ohnos = this.ohnos.filter(function (o) { return o && o.isAdded() && !o.dead; }); + }, + checkAchievement: function () { + var e_1, _a; + try { + for (var _b = __values(this.ohnos), _c = _b.next(); !_c.done; _c = _b.next()) { + var ohno = _c.value; + var player = ohno.getPlayer(); + if (player) + achievements_1.Achievements.ohno.grantTo(players_1.FishPlayer.get(player), false); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_1) throw e_1.error; } + } + }, + killAll: function () { + this.ohnos.forEach(function (ohno) { var _a; return (_a = ohno === null || ohno === void 0 ? void 0 : ohno.kill) === null || _a === void 0 ? void 0 : _a.call(ohno); }); + this.ohnos = []; + }, + amount: function () { + return this.ohnos.length; + }, + }; + Events.on(EventType.GameOverEvent, function (e) { + Ohnos.killAll(); + }); + Timer.schedule(function () { return Ohnos.checkAchievement(); }, 1, 2); + return Ohnos; + }, + requirements: [ + commands_1.Req.gameRunning, commands_1.Req.modeNot("pvp"), + commands_1.Req.unitExists("You cannot spawn ohnos while dead.") + ], + handler: function (_a) { + var sender = _a.sender, Ohnos = _a.data; + if (!Ohnos.enabled) + (0, commands_1.fail)("Ohnos have been temporarily disabled."); + Ohnos.updateLength(); + if (Ohnos.ohnos.length >= (Groups.player.size() + 1) || + sender.team().data().countType(UnitTypes.alpha) >= Units.getCap(sender.team())) + (0, commands_1.fail)("Sorry, the max number of ohno units has been reached."); + if ((0, utils_1.nearbyEnemyTile)((sender.unit()), 6) != null) + (0, commands_1.fail)("Too close to an enemy building!"); + if (!UnitTypes.alpha.supportsEnv(Vars.state.rules.env)) + (0, commands_1.fail)("Ohnos cannot survive in this map."); + Ohnos.makeOhno(sender.team(), sender.player.x, sender.player.y); + }, + }), ranks: { + args: [], + description: 'Displays information about all ranks.', + perm: commands_1.Perm.none, + handler: function (_a) { + var output = _a.output; + output("List of ranks:\n" + + Object.values(ranks_1.Rank.ranks) + .map(function (rank) { return "".concat(rank.prefix, " ").concat(rank.color).concat((0, funcs_1.capitalizeText)(rank.name), "[]: ").concat(rank.color).concat(rank.description, "[]\n"); }) + .join("") + + "List of flags:\n" + + Object.values(ranks_1.RoleFlag.flags) + .map(function (flag) { return "".concat(flag.prefix, " ").concat(flag.color).concat((0, funcs_1.capitalizeText)(flag.name), "[]: ").concat(flag.color).concat(flag.description, "[]\n"); }) + .join("")); + }, + }, rules: { + args: ['player:player?'], + description: 'Displays the server rules.', + perm: commands_1.Perm.none, + handler: function (_a) { + var _b; + var args = _a.args, sender = _a.sender, output = _a.output, outputSuccess = _a.outputSuccess, f = _a.f; + var target = (_b = args.player) !== null && _b !== void 0 ? _b : sender; + if (target !== sender) { + if (!sender.hasPerm("warn")) + (0, commands_1.fail)("You do not have permission to show rules to other players."); + if (!sender.canModerate(target)) + commands_1.Req.cooldown(funcs_1.Duration.minutes(10)); + if (target.hasPerm("blockTrolling")) + (0, commands_1.fail)(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); + } + void menus_1.Menu.menu("Rules for [#0000ff]>|||> FISH [white]servers", config_1.rules.join("\n\n"), ["[green]I agree to abide by these rules[]", "No"], target, { onCancel: "null" }).then(function (option) { + if (option == "No") { + target.kick("You must agree to the rules to play on this server. Rejoin to agree to the rules.", 1); + outputSuccess('Player rejected the rules and was kicked.'); + } + else if (option == null) { + output('Player closed the menu.'); + } + else { + outputSuccess('Player acknowledged the rules.'); + } + }); + if (target !== sender) + outputSuccess(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Reminded ", " of the rules."], ["Reminded ", " of the rules."])), target)); + }, + }, void: { + args: ["player:player?"], + description: 'Warns other players about power voids.', + perm: commands_1.Perm.play, + requirements: function (_a) { + var args = _a.args; + return [ + commands_1.Req.mode("attack"), + args.player ? commands_1.Req.cooldown(20000) : commands_1.Req.cooldownGlobal(10000) + ]; + }, + handler: function (_a) { + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + if (args.player) { + if (!sender.hasPerm("trusted")) + (0, commands_1.fail)("You do not have permission to show popups to other players, please run /void with no arguments to send a chat message to everyone."); + if (args.player !== sender && args.player.hasPerm("blockTrolling")) + (0, commands_1.fail)("Target player is insufficiently trollable."); + void menus_1.Menu.menu("\uf83f [scarlet]WARNING[] \uf83f", "[white]Don't break the Power Void (\uF83F), it's a trap!\nPower voids disable anything they are connected to.\nIf you break it, [scarlet]you will get attacked[] by enemy units.\nPlease stop attacking and [lime]build defenses[] first!", ["I understand"], args.player, { onCancel: 'null' }).then(function () { return outputSuccess(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Player ", " acknowledged the warning."], ["Player ", " acknowledged the warning."])), args.player)); }); + (0, utils_1.logAction)("showed void warning", sender, args.player); + outputSuccess(f(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Warned ", " about power voids with a popup message."], ["Warned ", " about power voids with a popup message."])), args.player)); + } + else { + Call.sendMessage("[white]Don't break the Power Void (\uF83F), it's a trap!\nPower voids disable anything they are connected to. If you break it, [scarlet]you will get attacked[] by enemy units.\nPlease stop attacking and [lime]build defenses[] first!"); + } + }, + }, team: { + args: ['team:team', 'reason:string?'], + description: 'Changes your team.', + perm: commands_1.Perm.changeTeam, + handler: function (_a) { + var _b; + var sender = _a.sender, _c = _a.args, team = _c.team, reason = _c.reason, outputSuccess = _a.outputSuccess, f = _a.f; + if (config_1.Gamemode.sandbox() && globals_1.fishState.peacefulMode && !sender.hasPerm("admin")) + (0, commands_1.fail)("You do not have permission to change teams because peaceful mode is on."); + if (config_1.Gamemode.sandbox() && team === Vars.state.rules.waveTeam && !sender.hasPerm("admin")) + (0, commands_1.fail)("You do not have permission to change to the wave team on sandbox."); + if (!(config_1.Gamemode.sandbox() || config_1.Gamemode.testsrv()) && !sender.hasPerm("mod") && !reason) + (0, commands_1.fail)("Please specify a reason for changing teams."); + if (!sender.hasPerm("changeTeamExternal")) { + if (team.data().cores.size <= 0) + (0, commands_1.fail)("You do not have permission to change to a team with no cores."); + if (!sender.player.dead() && !((_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.spawnedByCore)) + sender.forceRespawn(); + } + if (!sender.hasPerm("mod")) + sender.changedTeam = true; + sender.setTeam(team); + outputSuccess(f(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Changed your team to ", "."], ["Changed your team to ", "."])), team)); + if (reason && !config_1.Gamemode.sandbox()) + (0, utils_1.logAction)("changed team to ".concat(team.name, " on ").concat((0, funcs_1.escapeTextDiscord)(Vars.state.map.plainName()), " with reason ").concat((0, funcs_1.escapeTextDiscord)(reason)), sender); + }, + }, teamp: { + args: ['team:team', 'target:player'], + description: 'Changes the team of a player.', + perm: commands_1.Perm.changeTeam, + handler: function (_a) { + var _b; + var sender = _a.sender, _c = _a.args, team = _c.team, target = _c.target, outputSuccess = _a.outputSuccess, f = _a.f; + if (!sender.canModerate(target, true, "mod", true)) + (0, commands_1.fail)(f(templateObject_14 || (templateObject_14 = __makeTemplateObject(["You do not have permission to change the team of ", ""], ["You do not have permission to change the team of ", ""])), target)); + if (config_1.Gamemode.sandbox() && globals_1.fishState.peacefulMode && !sender.hasPerm("admin")) + (0, commands_1.fail)("You do not have permission to change teams because peaceful mode is on."); + if (!sender.hasPerm("changeTeamExternal")) { + if (team.data().cores.size <= 0) + (0, commands_1.fail)("You do not have permission to change to a team with no cores."); + if (!target.player.dead() && !((_b = target.unit()) === null || _b === void 0 ? void 0 : _b.spawnedByCore)) + target.forceRespawn(); + } + target.setTeam(team); + outputSuccess(f(templateObject_15 || (templateObject_15 = __makeTemplateObject(["Changed team of player ", " to ", "."], ["Changed team of player ", " to ", "."])), target, team)); + }, + }, rank: { + args: ['player:player'], + description: 'Displays the rank of a player.', + perm: commands_1.Perm.none, + handler: function (_a) { + var args = _a.args, output = _a.output, f = _a.f; + output(f(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Player ", "'s rank is ", "."], ["Player ", "'s rank is ", "."])), args.player, args.player.rank)); + }, + }, forcevnw: { + args: ["force:boolean?"], + description: 'Force skip to the next wave.', + perm: commands_1.Perm.admin, + handler: function (_a) { + var allCommands = _a.allCommands, sender = _a.sender, _b = _a.args.force, force = _b === void 0 ? true : _b; + if (allCommands.vnw.data.manager.session == null) { + if (force == false) + (0, commands_1.fail)("Cannot clear votes for VNW because no vote is currently ongoing."); + (0, utils_1.skipWaves)(1, true); + } + else { + if (force) + Call.sendMessage("VNW: [green]Vote was forced by admin [yellow]".concat(sender.name, "[green], skipping wave.")); + else + Call.sendMessage("VNW: [red]Votes cleared by admin [yellow]".concat(sender.name, "[red].")); + allCommands.vnw.data.manager.forceVote(force); + } + }, + }, vnw: (0, commands_1.command)({ + args: ["waves:number?"], + description: "Vote to start the next wave.", + perm: commands_1.Perm.play, + init: function () { return ({ + manager: new votes_1.VoteManager(funcs_1.Duration.minutes(1.5)) + .on("success", function (t) { return (0, utils_1.skipWaves)(t.session.data, true); }) + .on("vote passed", function () { return Call.sendMessage('VNW: [green]Vote passed, skipping to next wave.'); }) + .on("vote failed", function () { return Call.sendMessage('VNW: [red]Vote failed.'); }) + .on("player vote change", function (t, player) { return Call.sendMessage("VNW: ".concat(player.name, " [white] has voted on skipping [accent]").concat(t.session.data, "[white] wave(s). [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) + .on("player vote removed", function (t, player) { return Call.sendMessage("VNW: ".concat(player.name, " [white] has left. [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) + }); }, + requirements: [commands_1.Req.cooldown(3000), commands_1.Req.integerRange("waves", 1, 15), commands_1.Req.mode("survival", "testsrv"), commands_1.Req.gameRunning], + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var _c; + var sender = _b.sender, waves = _b.args.waves, manager = _b.data.manager; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + if (!!manager.session) return [3 /*break*/, 4]; + if (!(waves !== null && waves !== void 0)) return [3 /*break*/, 1]; + _c = waves; + return [3 /*break*/, 3]; + case 1: return [4 /*yield*/, menus_1.Menu.menu("Start a Next Wave Vote", "Select the amount of waves you would like to skip.", [1, 5, 10], sender, { + includeCancel: true, + optionStringifier: function (n) { return "".concat(n, " waves"); } + })]; + case 2: + _c = (waves = _d.sent()); + _d.label = 3; + case 3: + _c; + if (manager.session) { + //Someone else started a vote + if (manager.session.data != waves) + (0, commands_1.fail)("Someone else started a vote with a different number of waves to skip."); + else + manager.vote(sender, sender.voteWeight(), waves); + } + else { + manager.start(sender, sender.voteWeight(), waves); + } + return [3 /*break*/, 5]; + case 4: + manager.vote(sender, sender.voteWeight(), null); + _d.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); + } + }), forcertv: { + args: ["force:boolean?"], + description: 'Force skip to the next map.', + perm: commands_1.Perm.admin, + handler: function (_a) { + var _b = _a.args.force, force = _b === void 0 ? true : _b, sender = _a.sender, allCommands = _a.allCommands; + if (allCommands.rtv.data.manager.session == null) { + if (force == false) + (0, commands_1.fail)("Cannot clear votes for RTV because no vote is currently ongoing."); + allCommands.rtv.data.manager.forceVote(true); + } + else { + if (force) + Call.sendMessage("RTV: [green]Vote was forced by admin [yellow]".concat(sender.name, "[green].")); + else + Call.sendMessage("RTV: [red]Votes cleared by admin [yellow]".concat(sender.name, "[red].")); + allCommands.rtv.data.manager.forceVote(force); + } + } + }, rtv: (0, commands_1.command)({ + args: [], + description: 'Rock the vote to change map.', + perm: commands_1.Perm.play, + init: function () { return ({ + manager: new votes_1.VoteManager(funcs_1.Duration.minutes(1.5), config_1.Gamemode.hexed() ? ["fractionOfVoters", 1] : undefined) //Require unanimity in Hexed, as it is often 1 v everyone + .on("success", function () { return (0, utils_1.neutralGameover)(); }) + .on("vote passed", function () { return Call.sendMessage("RTV: [green]Vote has passed, changing map."); }) + .on("vote failed", function () { return Call.sendMessage("RTV: [red]Vote failed."); }) + .on("player vote change", function (t, player, oldVote, newVote) { return Call.sendMessage("RTV: ".concat(player.name, "[white] ").concat(oldVote == newVote ? "still " : "", "wants to change the map. [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) + .on("player vote removed", function (t, player) { return Call.sendMessage("RTV: ".concat(player.name, "[white] has left the game. [green]").concat(t.currentVotes(), "[white] votes, [green]").concat(t.requiredVotes(), "[white] required.")); }) + }); }, + requirements: [commands_1.Req.cooldown(3000), commands_1.Req.gameRunning], + handler: function (_a) { + var sender = _a.sender, manager = _a.data.manager; + manager.vote(sender, 1, 0); //No weighting for RTV except for removing AFK players + } + }), + // votekick: command({ + // args: ["target:player"], + // description: "Starts a vote to kick a player.", + // perm: Perm.play, + // data: new VoteManager( + // Duration.seconds(20), + // ["absolute", 3], + // (fishP, target) => fishP.team() == target.team() || fishP.hasPerm("voteOtherTeams") + // ), + // handler({args, sender, data: votekickmanager}){ + // if(votekickmanager.session) fail(`There is already a votekick in progress.`); + // votekickmanager.start(sender, 1, args.target); + // } + // }), + // vote: { + // args: ["vote:boolean"], + // description: "Use /votekick instead.", + // perm: Perm.play, + // handler({sender, args, allCommands}){ + // const votekickmanager = allCommands.votekick.data; + // votekickmanager.handleVote(sender, args ? 1 : -1); + // } + // }, + forcenextmap: { + args: ["map:mapOrRandom"], + description: 'Override the next map in queue.', + perm: commands_1.Perm.admin.exceptModes({ + testsrv: commands_1.Perm.play + }), + handler: function (_a) { + var allCommands = _a.allCommands, args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + Vars.maps.setNextMapOverride(args.map == "random" ? null : args.map); + if (allCommands.nextmap.data.voteEndTime() > -1) { + //Cancel /nextmap vote if it's ongoing + allCommands.nextmap.data.resetVotes(); + Call.sendMessage("[red]Admin ".concat(sender.name, "[red] has cancelled the vote. The next map will be ").concat(args.map == "random" ? "random" : "[yellow]".concat(args.map.name()), ".")); + } + else { + outputSuccess(f(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Forced the next map to be ", "."], ["Forced the next map to be ", "."])), args.map == "random" ? "random" : "\"".concat(args.map.name(), "\" by ").concat(args.map.author()))); + } + }, + }, maps: { + args: [], + description: 'Lists the available maps.', + perm: commands_1.Perm.none, + handler: function (_a) { + var output = _a.output; + output("[yellow]Use [white]/nextmap [lightgray] [yellow]to vote on a map.\n\n[blue]Available maps:\n_________________________\n".concat(Vars.maps.customMaps().toArray().map(function (map) { + return "[yellow]".concat(map.name()); + }).join("\n"))); + } + }, nextmap: (0, commands_1.command)(function () { + var random = { + name: function () { return "[lightgray]Random"; }, + plainName: function () { return "random"; } + }; + var votes = new Map(); + var lastVoteCount = 0; + var lastVoteTime = 0; + var voteEndTime = -1; + var voteDuration = funcs_1.Duration.minutes(1.5); + var task = null; + function resetVotes() { + votes.clear(); + voteEndTime = -1; + task === null || task === void 0 ? void 0 : task.cancel(); + } + function getMapData() { + return __spreadArray([], __read(votes.values()), false).reduce(function (acc, map) { return (acc.increment(map), acc); }, new ObjectIntMap()).entries().toArray(); + } + function showVotes() { + Call.sendMessage("[green]Current votes:\n------------------------------\n".concat(getMapData().map(function (_a) { + var map = _a.key, votes = _a.value; + return "[cyan]".concat(map.name(), "[yellow]: ").concat(votes); + }).toString("\n"))); + } + function startVote() { + voteEndTime = Date.now() + voteDuration; + task = Timer.schedule(endVote, voteDuration / 1000); + } + function endVote() { + if (voteEndTime == -1) + return; //aborted somehow + if (votes.size == 0) + return; //no votes? + if (votes.size + 2 <= lastVoteCount && (Date.now() - lastVoteTime) < funcs_1.Duration.minutes(10)) { + //If the number of votes is 2 less than the previous number of votes for a vote in the past 10 minutes, abor + Call.sendMessage("[cyan]Next Map Vote: [scarlet]Vote aborted because a previous vote had significantly higher turnout"); + resetVotes(); + return; + } + else { + lastVoteTime = Date.now(); + lastVoteCount = votes.size; + } + var mapData = getMapData(); + var highestVoteCount = mapData.max(floatf(function (e) { return e.value; })).value; + var highestVotedMaps = mapData.select(function (e) { return e.value == highestVoteCount; }); + var winner; + if (highestVotedMaps.size > 1) { + winner = highestVotedMaps.random().key; + Call.sendMessage("[green]There was a tie between the following maps:\n".concat(highestVotedMaps.map(function (_a) { + var map = _a.key, votes = _a.value; + return "[cyan]".concat(map.name(), "[yellow]: ").concat(votes); + }).toString("\n"), "\n[green]Picking random winner: [yellow]").concat(winner.name())); + } + else { + winner = highestVotedMaps.get(0).key; + Call.sendMessage("[green]Map voting complete! The next map will be [yellow]".concat(winner.name(), " [green]with [yellow]").concat(highestVoteCount, "[green] votes.")); + } + Vars.maps.setNextMapOverride(winner == random ? null : null); + resetVotes(); + } + Events.on(EventType.GameOverEvent, resetVotes); + Events.on(EventType.ServerLoadEvent, resetVotes); + return { + args: ['map:mapOrRandom'], + description: 'Allows you to vote for the next map. Use /maps to see all available maps.', + perm: commands_1.Perm.play, + data: { votes: votes, voteEndTime: function () { return voteEndTime; }, resetVotes: resetVotes, endVote: endVote }, + requirements: [commands_1.Req.cooldown(10000)], + handler: function (_a) { + var args = _a.args, sender = _a.sender; + var map = args.map === "random" ? random : args.map; + if (config_1.Gamemode.testsrv()) + (0, commands_1.fail)("Please use /forcenextmap instead."); + if (votes.get(sender)) + (0, commands_1.fail)("You have already voted."); + if (voteEndTime == -1) { + if ((Date.now() - lastVoteTime) < funcs_1.Duration.minutes(1)) + (0, commands_1.fail)("Please wait 1 minute before starting a new map vote."); + startVote(); + votes.set(sender, map); + Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] started a map vote, and voted for [yellow]").concat(map.name(), "[cyan]. Use [white]/nextmap ").concat(map.plainName(), "[] to add your vote, or run [white]/maps[] to see other available maps.")); + } + else { + votes.set(sender, map); + Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] voted for [yellow]").concat(map.name(), "[cyan]. Time left: [scarlet]").concat((0, utils_1.formatTimeRelative)(voteEndTime, true))); + showVotes(); + } + } + }; + }), surrender: (0, commands_1.command)(function () { + var prefix = "[orange]Surrender[white]: "; + var managers = Team.all.map(function (team) { + return new votes_1.VoteManager(funcs_1.Duration.minutes(1.5), ["fractionOfVoters", config_1.Gamemode.hexed() ? 1 : 3 / 4], function (p) { return p.team() == team; }) + .on("success", function () { return team.cores().copy().each(function (c) { return c.kill(); }); }) + .on("vote passed", function () { return Call.sendMessage(prefix + "Team ".concat(team.coloredName(), " has voted to forfeit this match.")); }) + .on("vote failed", function (t) { return t.messageEligibleVoters(prefix + "Team ".concat(team.coloredName(), " has chosen not to forfeit this match.")); }) + .on("player vote change", function (t, player, oldVote, newVote) { return t.messageEligibleVoters(prefix + "".concat(player.name, "[white] ").concat(oldVote == newVote ? "still " : "", "wants to forfeit this match. [orange]").concat(t.currentVotes(), "[white] votes, [orange]").concat(t.requiredVotes(), "[white] required.")); }) + .on("player vote removed", function (t, player) { return t.messageEligibleVoters(prefix + "Player ".concat(player.name, "[white] has left the game. [orange]").concat(t.currentVotes(), "[white] votes, [orange]").concat(t.requiredVotes(), "[white] required.")); }); + }); + globals_1.FishEvents.on("playerTeamChange", function (_, fishP, previous) { + managers[previous.id].unvote(fishP); + }); + return { + args: ["force:boolean?", "team:team?"], + description: "Vote to surrender to the enemy team.", + perm: commands_1.Perm.play, + requirements: [commands_1.Req.mode("pvp"), commands_1.Req.teamAlive], + data: { managers: managers }, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var t, manager; + var sender = _b.sender, _c = _b.args, force = _c.force, team = _c.team; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + t = sender.hasPerm("admin") && team ? team : sender.team(); + manager = managers[t.id]; + if (!(sender.hasPerm("admin") && force != undefined)) return [3 /*break*/, 4]; + if (!force) return [3 /*break*/, 2]; + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to force team ".concat(t.coloredName(), "[] to lose?"))]; + case 1: + _d.sent(); + manager.messageEligibleVoters(prefix + "Vote forced by admin ".concat(sender.name, "[white].")); + Call.sendMessage(prefix + "Team ".concat(t.coloredName(), " has voted to forfeit this match.")); + return [3 /*break*/, 3]; + case 2: + manager.messageEligibleVoters(prefix + "Votes cleared by admin ".concat(sender.name, "[white].")); + _d.label = 3; + case 3: + manager.forceVote(force); + return [2 /*return*/]; + case 4: + if (sender.ranksAtLeast("mod")) + commands_1.Req.cooldown(5000); + else + commands_1.Req.cooldown(20000); + manager.vote(sender, 1, 0); + return [2 /*return*/]; + } + }); + }); + }, + }; + }), stats: { + args: ["target:player", "global:boolean?"], + perm: commands_1.Perm.none, + description: "Views a player's stats.", + handler: function (_a) { + var _b = _a.args, target = _b.target, _c = _b.global, global = _c === void 0 ? false : _c, output = _a.output, f = _a.f; + var stats = global ? target.globalStats : target.stats; + output(f(templateObject_18 || (templateObject_18 = __makeTemplateObject(["[accent]Statistics for player ", " ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""], ["[accent]\\\nStatistics for player ", " ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""])), target, global ? "across all servers" : "on this server", stats.blocksBroken, stats.blocksPlaced, stats.chatMessagesSent, stats.gamesFinished, (0, utils_1.formatTime)(stats.timeInGame), stats.gamesWon / stats.gamesFinished)); + } + }, showworld: { + args: ["x:number?", "y:number?", "size:number?"], + perm: commands_1.Perm.none, + description: "Views the world as a 2D scrollable menu.", + requirements: [commands_1.Req.cooldown(4000), commands_1.Req.integerRange("size", 1, 20)], + handler: function (_a) { + var sender = _a.sender, _b = _a.args, _c = _b.size, size = _c === void 0 ? 7 : _c, x = _b.x, y = _b.y; + if (Vars.state.rules.fog) + (0, commands_1.fail)("This command is disabled when fog is enabled."); + var options = (0, funcs_1.to2DArray)(Reflect.get(Vars.world.tiles, "array").map(function (tile) { return ({ + text: tile.block().emoji(), + data: null, + }); }), Vars.world.width()).reverse(); + var height = Vars.world.height(); + void menus_1.Menu.scroll2D(sender, "The World", "Use the arrow keys to navigate around the world. Click a blank square to exit.", options, { + columns: size, + rows: size, + x: x ? x - Math.trunc(size / 2) : 0, + y: height - (y ? y + 1 + Math.trunc(size / 2) : size), + getCenterText: function (x, y) { return "".concat(x, ",").concat(height - y - size); } + }); + } + }, mapinfo: { + args: ["map:map?"], + perm: commands_1.Perm.none, + description: "Displays information about a map.", + handler: function (_a) { + var _b; + var output = _a.output, map = _a.args.map, f = _a.f, sender = _a.sender; + if (map) { + var fmap = (_b = maps_1.FMap.getCreate(map)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Map data is still being loaded, try again later."); + output(fmap.displayStats(f)); + } + else { + void menus_1.Menu.textPages(sender, Vars.maps.customMaps().map(function (m) { + return ["Map information", function () { var _a, _b; return (_b = (_a = maps_1.FMap.getCreate(m)) === null || _a === void 0 ? void 0 : _a.displayStats(f)) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Map data is still being loaded, try again later."); }]; + }).toArray(), [], { + startPage: Vars.maps.customMaps().toArray().indexOf(Vars.state.map), + }); + } + } + }, gamemode: { + args: ["mode:string"], + perm: new commands_1.Perm("changeGamemode", "manager").exceptModes({ + testsrv: commands_1.Perm.play, + }), + description: "Sets the gamemode.", + requirements: [commands_1.Req.cooldownGlobal(10000)], + handler: function (_a) { + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, lastUsedSuccessfully = _a.lastUsedSuccessfully; + if (!sender.hasPerm('trusted')) + commands_1.Req.cooldownGlobal(30000)({ lastUsedSuccessfully: lastUsedSuccessfully }); + //Unpause + Vars.state.set(GameState.State.playing); + switch (args.mode) { + case "attack": + Vars.state.rules.attackMode = true; + Vars.state.rules.pvp = false; + Vars.state.rules.infiniteResources = false; + break; + case "survival": + Vars.state.rules.attackMode = false; + Vars.state.rules.waves = true; + Vars.state.rules.pvp = false; + Vars.state.rules.infiniteResources = false; + break; + case "pvp": + Vars.state.rules.attackMode = true; + Vars.state.rules.pvp = true; + Vars.state.rules.waves = false; + Vars.state.rules.infiniteResources = false; + break; + case "sandbox": + Vars.state.rules.attackMode = true; + Vars.state.rules.pvp = false; + Vars.state.rules.waves = false; + Vars.state.rules.infiniteResources = true; + break; + default: (0, commands_1.fail)("Invalid mode, valid modes are: attack, survival, pvp"); + } + var reloader = new WorldReloader(); + Reflect.set(reloader, "wasServer", true); + Reflect.set(reloader, "players", Groups.player.copy()); + Call.worldDataBegin(); + reloader.end(); + Call.sendMessage("[orange]Player ".concat(sender.cleanedName, " changed the gamemode to ").concat(args.mode)); + outputSuccess("Changed mode to ".concat(args.mode)); + } + }, mixunit: { + args: ["type:unittype", "base:unittype"], + description: "Spawns a unit that is made of two unit types mixed together.", + perm: commands_1.Perm.admin.exceptModes({ + sandbox: commands_1.Perm.play + }), + requirements: function (_a) { + var sender = _a.sender; + return [!sender.hasPerm("admin") && commands_1.Req.cooldown(1500), commands_1.Req.unitExists()].filter(Boolean); + }, + handler: function (_a) { + var args = _a.args, sender = _a.sender, f = _a.f, outputSuccess = _a.outputSuccess; + var _b = sender.unit(), team = _b.team, x = _b.x, y = _b.y; + var unit = args.base.create(team); + unit.type = args.type; + unit.maxHealth = args.type.health; //because half-dead units aren't fun + unit.set(x, y); + unit.add(); + outputSuccess(f(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Spawned a ", " that is partly a ", "."], ["Spawned a ", " that is partly a ", "."])), args.type, args.base)); + } + }, achievement: { + args: ["name:string?", "verbose:boolean?"], + description: "Displays information on a specific achievement.", + perm: commands_1.Perm.none, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var matching, achievement, _c; + var _d = _b.args, _e = _d.name, name = _e === void 0 ? "" : _e, _f = _d.verbose, verbose = _f === void 0 ? false : _f, sender = _b.sender, f = _b.f, output = _b.output; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + name = Strings.stripColors(name.toLowerCase()); + matching = achievements_1.Achievement.all.filter(function (a) { return Strings.stripColors(a.name).toLowerCase().includes(name); }); + if (matching.length == 0) + (0, commands_1.fail)(f(templateObject_20 || (templateObject_20 = __makeTemplateObject(["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."], ["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."])), name)); + if (!(matching.length > 2)) return [3 /*break*/, 2]; + return [4 /*yield*/, menus_1.Menu.pagedList(sender, "Achievement", "Select an achievement to view", matching, { + onCancel: "reject", + columns: 2, + optionStringifier: function (a) { return "".concat(a.icon, "[] ").concat(a.name); } + })]; + case 1: + _c = _g.sent(); + return [3 /*break*/, 3]; + case 2: + _c = matching[0]; + _g.label = 3; + case 3: + achievement = _c; + output(config_1.FColor.achievement(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Achievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "", "", ""], ["\\\nAchievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n", "\\\n", "\\\n"])), achievement.icon, achievement.name, achievement.description + (achievement.extendedDescription ? ("\n" + "[gray]".concat(achievement.extendedDescription)) : ""), achievement.modesText, f.boolGood(achievement.has(sender)), verbose ? "[gray]ID: (".concat(achievement.nid, ")").concat(achievement.sid, "\n") : "", verbose ? "[gray]Notifies: ".concat(achievement.notify, "\n") : "", achievement.hidden ? "This achievement is secret." : "")); + return [2 /*return*/]; + } + }); + }); + } + }, achievementlist: { + args: ["target:player?"], + description: "Shows all achievements in a paged menu.", + perm: commands_1.Perm.none, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var sender = _b.sender, _c = _b.args.target, target = _c === void 0 ? sender : _c, f = _b.f; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: return [4 /*yield*/, menus_1.Menu.textPages(sender, achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }) + .map(function (a) { return [ + "".concat(a.icon, "[] ").concat(a.name), + function () { return config_1.FColor.achievement(templateObject_22 || (templateObject_22 = __makeTemplateObject(["", "\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : ""); } + ]; }))]; + case 1: + _d.sent(); + return [2 /*return*/]; + } + }); + }); + } + }, achievementgrid: { + args: ["target:player?"], + description: "Shows all achievements in a 2D scrolling menu.", + perm: commands_1.Perm.none, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var visibleAchievements, options, numberAchievements, totalAchievements, x, y, a; + var _c; + var sender = _b.sender, _d = _b.args.target, target = _d === void 0 ? sender : _d, f = _b.f; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + visibleAchievements = achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }); + options = (0, funcs_1.to2DArray)(visibleAchievements, 7).map(function (row) { return row.map(function (a) { return ({ + data: a, + text: a.has(target) ? a.icon : "[gray]".concat(Strings.stripColors(a.icon)), + }); }); }); + numberAchievements = achievements_1.Achievement.all.filter(function (a) { return a.has(target); }).length; + totalAchievements = visibleAchievements.length; + x = 0, y = 0; + a = null; + _e.label = 1; + case 1: + if (!true) return [3 /*break*/, 3]; + return [4 /*yield*/, menus_1.Menu.scroll2D(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : + (target == sender ? "You have ".concat(numberAchievements, "/").concat(totalAchievements, " achievements.") + : config_1.FColor.achievement(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Player ", " has ", "/", " achievements."], ["Player ", " has ", "/", " achievements."])), target.prefixedName, numberAchievements, totalAchievements)) + + "\nClick an achievement icon to show more information.", options, { onCancel: "reject", columns: 5, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; + case 2: + //the loop will be aborted if the menu is cancelled (promise will reject) + _c = __read.apply(void 0, [_e.sent(), 3]), a = _c[0], x = _c[1], y = _c[2]; + if (a == achievements_1.Achievements.click_me && target == sender) + a.grantTo(sender); + return [3 /*break*/, 1]; + case 3: return [2 /*return*/]; + } + }); + }); + } + } })); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24; diff --git a/build/scripts/commands/member.js b/build/scripts/commands/member.js index b432bb30..90115b4d 100644 --- a/build/scripts/commands/member.js +++ b/build/scripts/commands/member.js @@ -1,137 +1,137 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains member commands, which are fun cosmetics for donators. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.commands = void 0; -var commands_1 = require("/frameworks/commands"); -exports.commands = (0, commands_1.commandList)({ - pet: (0, commands_1.command)({ - args: ["name:string?"], - description: 'Spawns a cool pet with a displayed name that follows you around.', - perm: commands_1.Perm.member, - data: {}, - handler: function (_a) { - var _b, _c; - var args = _a.args, sender = _a.sender, data = _a.data, outputSuccess = _a.outputSuccess; - if (!args.name) { - var pet_1 = data[sender.uuid]; - if (pet_1) { - pet_1.kill(); - delete data[sender.uuid]; - outputSuccess("Your pet has been removed."); - return; - } - } - if (sender.muted || !args.name) - args.name = "".concat(sender.name, "[white]'s pet"); - if (args.name.length > 500) - (0, commands_1.fail)("Name cannot be more than 500 characters."); - if (Strings.stripColors(args.name).length > 70) - (0, commands_1.fail)("Name cannot be more than 70 characters, not including color tags."); - (_b = data[sender.uuid]) === null || _b === void 0 ? void 0 : _b.kill(); - var unit = (_c = sender.unit()) !== null && _c !== void 0 ? _c : (0, commands_1.fail)("You do not have a unit for the pet to follow."); - var pet = UnitTypes.merui.spawn(sender.team(), unit.x, unit.y); - pet.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); - data[sender.uuid] = pet; - Call.infoPopup('[#7FD7FD7f]\uE81B', 5, Align.topRight, 180, 0, 0, 10); - outputSuccess("Spawned a pet."); - var petName = args.name; - var vec = new Vec2(0, 0); - (function controlUnit() { - try { - var unit_1 = sender.unit(); - var currentPet = data[sender.uuid]; - if (pet != currentPet) - return; - if (currentPet.dead) { - delete data[sender.uuid]; - return; - } - if (!sender.connected()) { - currentPet === null || currentPet === void 0 ? void 0 : currentPet.kill(); - return; - } - if (unit_1 && currentPet) { - var distX = unit_1.x - currentPet.x; - var distY = unit_1.y - currentPet.y; - vec.set(distX, distY); - if (vec.len() > 50) { - currentPet.approach(vec); - } - if (vec.len() > 20 * 8) { - currentPet.apply(StatusEffects.fast, 60); - } - Call.label(petName, 0.07, currentPet.x, currentPet.y + 5); - //Pets share the sender's trail - if (sender.trail) { - Call.effect(Fx[sender.trail.type], currentPet.x, currentPet.y, 0, sender.trail.color); - } - } - return Timer.schedule(controlUnit, 0.05); - } - catch (err) { - Log.err(err); - } - })(); - } - }), - highlight: { - args: ['color:string?'], - description: 'Makes your chat text colored by default.', - perm: commands_1.Perm.member, - handler: function (_a) { - var args = _a.args, sender = _a.sender, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess; - if (args.color == null || args.color.length == 0) { - if (sender.highlight != null) { - sender.highlight = null; - outputSuccess("Cleared your highlight."); - } - else { - outputFail("No highlight to clear."); - } - } - else if (Strings.stripColors(args.color) == "") { - sender.highlight = args.color; - outputSuccess("Set highlight to ".concat(args.color.replace("[", "").replace("]", ""), ".")); - } - else if (Strings.stripColors("[".concat(args.color, "]")) == "") { - sender.highlight = "[".concat(args.color, "]"); - outputSuccess("Set highlight to ".concat(args.color, ".")); - } - else { - outputFail("[yellow]\"".concat(args.color, "[yellow]\" was not a valid color!")); - } - } - }, - rainbow: { - args: ["speed:number?"], - description: 'Make your name change colors.', - perm: commands_1.Perm.member, - requirements: [commands_1.Req.integerRange("speed", 0, 10)], - handler: function (_a) { - var _b; - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess; - var colors = ['[red]', '[orange]', '[yellow]', '[acid]', '[blue]', '[purple]']; - function rainbowLoop(index, fishP) { - Timer.schedule(function () { - if (!(fishP.rainbow && fishP.player && fishP.connected())) - return; - fishP.player.name = colors[index % colors.length] + Strings.stripColors(fishP.player.name); - rainbowLoop(index + 1, fishP); - }, args.speed / 5); - } - if (!args.speed) { - sender.rainbow = null; - sender.updateName(); - outputSuccess("Turned off rainbow."); - } - else { - (_b = sender.rainbow) !== null && _b !== void 0 ? _b : (sender.rainbow = { speed: args.speed }); - rainbowLoop(0, sender); - outputSuccess("Activated rainbow name mode with speed ".concat(args.speed)); - } - } - } -}); +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains member commands, which are fun cosmetics for donators. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.commands = void 0; +var commands_1 = require("/frameworks/commands"); +exports.commands = (0, commands_1.commandList)({ + pet: (0, commands_1.command)({ + args: ["name:string?"], + description: 'Spawns a cool pet with a displayed name that follows you around.', + perm: commands_1.Perm.member, + data: {}, + handler: function (_a) { + var _b, _c; + var args = _a.args, sender = _a.sender, data = _a.data, outputSuccess = _a.outputSuccess; + if (!args.name) { + var pet_1 = data[sender.uuid]; + if (pet_1) { + pet_1.kill(); + delete data[sender.uuid]; + outputSuccess("Your pet has been removed."); + return; + } + } + if (sender.muted || !args.name) + args.name = "".concat(sender.name, "[white]'s pet"); + if (args.name.length > 500) + (0, commands_1.fail)("Name cannot be more than 500 characters."); + if (Strings.stripColors(args.name).length > 70) + (0, commands_1.fail)("Name cannot be more than 70 characters, not including color tags."); + (_b = data[sender.uuid]) === null || _b === void 0 ? void 0 : _b.kill(); + var unit = (_c = sender.unit()) !== null && _c !== void 0 ? _c : (0, commands_1.fail)("You do not have a unit for the pet to follow."); + var pet = UnitTypes.merui.spawn(sender.team(), unit.x, unit.y); + pet.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); + data[sender.uuid] = pet; + Call.infoPopup('[#7FD7FD7f]\uE81B', 5, Align.topRight, 180, 0, 0, 10); + outputSuccess("Spawned a pet."); + var petName = args.name; + var vec = new Vec2(0, 0); + (function controlUnit() { + try { + var unit_1 = sender.unit(); + var currentPet = data[sender.uuid]; + if (pet != currentPet) + return; + if (currentPet.dead) { + delete data[sender.uuid]; + return; + } + if (!sender.connected()) { + currentPet === null || currentPet === void 0 ? void 0 : currentPet.kill(); + return; + } + if (unit_1 && currentPet) { + var distX = unit_1.x - currentPet.x; + var distY = unit_1.y - currentPet.y; + vec.set(distX, distY); + if (vec.len() > 50) { + currentPet.approach(vec); + } + if (vec.len() > 20 * 8) { + currentPet.apply(StatusEffects.fast, 60); + } + Call.label(petName, 0.07, currentPet.x, currentPet.y + 5); + //Pets share the sender's trail + if (sender.trail) { + Call.effect(Fx[sender.trail.type], currentPet.x, currentPet.y, 0, sender.trail.color); + } + } + return Timer.schedule(controlUnit, 0.05); + } + catch (err) { + Log.err(err); + } + })(); + } + }), + highlight: { + args: ['color:string?'], + description: 'Makes your chat text colored by default.', + perm: commands_1.Perm.member, + handler: function (_a) { + var args = _a.args, sender = _a.sender, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess; + if (args.color == null || args.color.length == 0) { + if (sender.highlight != null) { + sender.highlight = null; + outputSuccess("Cleared your highlight."); + } + else { + outputFail("No highlight to clear."); + } + } + else if (Strings.stripColors(args.color) == "") { + sender.highlight = args.color; + outputSuccess("Set highlight to ".concat(args.color.replace("[", "").replace("]", ""), ".")); + } + else if (Strings.stripColors("[".concat(args.color, "]")) == "") { + sender.highlight = "[".concat(args.color, "]"); + outputSuccess("Set highlight to ".concat(args.color, ".")); + } + else { + outputFail("[yellow]\"".concat(args.color, "[yellow]\" was not a valid color!")); + } + } + }, + rainbow: { + args: ["speed:number?"], + description: 'Make your name change colors.', + perm: commands_1.Perm.member, + requirements: [commands_1.Req.integerRange("speed", 0, 10)], + handler: function (_a) { + var _b; + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess; + var colors = ['[red]', '[orange]', '[yellow]', '[acid]', '[blue]', '[purple]']; + function rainbowLoop(index, fishP) { + Timer.schedule(function () { + if (!(fishP.rainbow && fishP.player && fishP.connected())) + return; + fishP.player.name = colors[index % colors.length] + Strings.stripColors(fishP.player.name); + rainbowLoop(index + 1, fishP); + }, args.speed / 5); + } + if (!args.speed) { + sender.rainbow = null; + sender.updateName(); + outputSuccess("Turned off rainbow."); + } + else { + (_b = sender.rainbow) !== null && _b !== void 0 ? _b : (sender.rainbow = { speed: args.speed }); + rainbowLoop(0, sender); + outputSuccess("Activated rainbow name mode with speed ".concat(args.speed)); + } + } + } +}); diff --git a/build/scripts/commands/staff.js b/build/scripts/commands/staff.js index 7a5bb59d..4d65abef 100644 --- a/build/scripts/commands/staff.js +++ b/build/scripts/commands/staff.js @@ -1,1584 +1,1584 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the in-game chat commands that can be run by trusted staff. -*/ -var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { - if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } - return cooked; -}; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.commands = void 0; -var api = __importStar(require("/api")); -var config_1 = require("/config"); -var files_1 = require("/files"); -var fjsContext = __importStar(require("/fjsContext")); -var commands_1 = require("/frameworks/commands"); -var menus_1 = require("/frameworks/menus"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var maps_1 = require("/maps"); -var players_1 = require("/players"); -var ranks_1 = require("/ranks"); -var utils_1 = require("/utils"); -exports.commands = (0, commands_1.commandList)({ - warn: { - args: ['player:player', 'message:string?'], - description: 'Sends the player a warning (menu popup).', - perm: commands_1.Perm.warn, - requirements: [commands_1.Req.cooldown(3000)], - handler: function (_a) { - var _b; - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - if (args.player.hasPerm("blockTrolling")) - (0, commands_1.fail)(f(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); - var message = (_b = args.message) !== null && _b !== void 0 ? _b : "You have been warned. I suggest you stop what you're doing"; - void menus_1.Menu.menu('Warning', message, ["[green]Accept"], args.player, { onCancel: 'null' }) - .then(function () { return outputSuccess('Player acknowledged the warning.'); }); - (0, utils_1.logAction)('warned', sender, args.player, message); - outputSuccess(f(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Warned player ", " for \"", "\""], ["Warned player ", " for \"", "\""])), args.player, message)); - } - }, - mute: { - args: ['player:player'], - description: 'Stops a player from chatting.', - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("player")], - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (args.player.muted) - (0, commands_1.fail)(f(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Player ", " is already muted."], ["Player ", " is already muted."])), args.player)); - return [4 /*yield*/, args.player.mute(sender)]; - case 1: - _c.sent(); - (0, utils_1.logAction)('muted', sender, args.player); - outputSuccess(f(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Muted player ", "."], ["Muted player ", "."])), args.player)); - return [2 /*return*/]; - } - }); - }); - } - }, - unmute: { - args: ['player:player'], - description: 'Unmutes a player', - perm: commands_1.Perm.mod, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!args.player.muted && args.player.autoflagged) - (0, commands_1.fail)(f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."], ["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."])), args.player)); - if (!args.player.muted) - (0, commands_1.fail)(f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Player ", " is not muted."], ["Player ", " is not muted."])), args.player)); - return [4 /*yield*/, args.player.unmute(sender)]; - case 1: - _c.sent(); - (0, utils_1.logAction)('unmuted', sender, args.player); - outputSuccess(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Unmuted player ", "."], ["Unmuted player ", "."])), args.player)); - return [2 /*return*/]; - } - }); - }); - } - }, - kick: { - args: ["player:player", "duration:time?", "reason:string?"], - description: 'Kick a player with optional reason.', - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("player")], - handler: function (_a) { - var _b, _c, _d; - var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f, sender = _a.sender; - if (!sender.hasPerm("admin") && args.duration && args.duration > funcs_1.Duration.hours(6)) - (0, commands_1.fail)("Maximum kick duration is 6 hours."); - var reason = (_b = args.reason) !== null && _b !== void 0 ? _b : "A staff member did not like your actions."; - var duration = (_c = args.duration) !== null && _c !== void 0 ? _c : 60000; - args.player.kick(reason, duration); - (0, utils_1.logAction)("kicked", sender, args.player, (_d = args.reason) !== null && _d !== void 0 ? _d : undefined, duration); - if (duration > 60000) - args.player.setPunishedIP(config_1.stopAntiEvadeTime); - outputSuccess(f(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Kicked player ", " for ", " with reason \"", "\""], ["Kicked player ", " for ", " with reason \"", "\""])), args.player, (0, utils_1.formatTime)(duration), reason)); - } - }, - pardon: { - args: ["player:offlinePlayer"], - description: 'Pardons a votekicked player.', - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("player")], - handler: function (_a) { - var player = _a.args.player, admins = _a.admins, outputSuccess = _a.outputSuccess, f = _a.f; - var info = admins.getInfo(player.uuid); - if (Time.millis() > info.lastKicked && !admins.kickedIPs.containsKey(info.lastIP)) - (0, commands_1.fail)("That player is not kicked."); - info.lastKicked = 0; - admins.kickedIPs.remove(info.lastIP); - outputSuccess(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Pardoned player ", "."], ["Pardoned player ", "."])), player)); - } - }, - stop: { - args: ['player:player', "time:time?", "message:string?"], - description: 'Stops a player.', - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("player", true)], - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var previousTime, time; - var _c, _d, _e, _f; - var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_g) { - switch (_g.label) { - case 0: - if (!args.player.marked()) return [3 /*break*/, 2]; - //overload: overwrite stoptime - if (!args.time) - (0, commands_1.fail)(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Player ", " is already marked."], ["Player ", " is already marked."])), args.player)); - previousTime = (0, utils_1.formatTimeRelative)(args.player.unmarkTime, true); - return [4 /*yield*/, args.player.updateStopTime(args.time)]; - case 1: - _g.sent(); - outputSuccess(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Player ", "'s stop time has been updated to ", " (was ", ")."], ["Player ", "'s stop time has been updated to ", " (was ", ")."])), args.player, (0, utils_1.formatTime)(args.time), previousTime)); - (0, utils_1.logAction)("updated stop time of", sender, args.player, (_c = args.message) !== null && _c !== void 0 ? _c : undefined, args.time); - return [3 /*break*/, 4]; - case 2: - time = (_d = args.time) !== null && _d !== void 0 ? _d : (0, utils_1.untilForever)(); - if (time + Date.now() > globals_1.maxTime) - (0, commands_1.fail)("Error: time too high."); - return [4 /*yield*/, args.player.stop(sender, time, (_e = args.message) !== null && _e !== void 0 ? _e : undefined)]; - case 3: - _g.sent(); - (0, utils_1.logAction)('stopped', sender, args.player, (_f = args.message) !== null && _f !== void 0 ? _f : undefined, time); - //TODO outputGlobal() - Call.sendMessage("[orange]Player \"".concat(args.player.prefixedName, "[orange]\" has been marked for ").concat((0, utils_1.formatTime)(time)).concat(args.message ? " with reason: [white]".concat(args.message, "[]") : "", ".")); - _g.label = 4; - case 4: return [2 /*return*/]; - } - }); - }); - } - }, - free: { - args: ['player:player'], - description: 'Frees a player.', - perm: commands_1.Perm.mod, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, outputFail = _b.outputFail, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!args.player.marked()) return [3 /*break*/, 2]; - return [4 /*yield*/, args.player.free(sender)]; - case 1: - _c.sent(); - (0, utils_1.logAction)('freed', sender, args.player); - outputSuccess(f(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Player ", " has been unmarked."], ["Player ", " has been unmarked."])), args.player)); - return [3 /*break*/, 3]; - case 2: - if (args.player.autoflagged) { - args.player.autoflagged = false; - args.player.sendMessage("[yellow]You have been freed! Enjoy!"); - args.player.updateName(); - args.player.forceRespawn(); - outputSuccess(f(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Player ", " has been unflagged."], ["Player ", " has been unflagged."])), args.player)); - } - else { - outputFail(f(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Player ", " is not marked or autoflagged."], ["Player ", " is not marked or autoflagged."])), args.player)); - } - _c.label = 3; - case 3: return [2 /*return*/]; - } - }); - }); - } - }, - setrank: { - args: ["player:player", "rank:rank"], - description: "Set a player's rank.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("player")], - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var _c = _b.args, rank = _c.rank, player = _c.player, outputSuccess = _b.outputSuccess, f = _b.f, sender = _b.sender; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - if (rank.level >= sender.rank.level) - (0, commands_1.fail)(f(templateObject_15 || (templateObject_15 = __makeTemplateObject(["You do not have permission to promote players to rank ", ", because your current rank is ", ""], ["You do not have permission to promote players to rank ", ", because your current rank is ", ""])), rank, sender.rank)); - if (rank == ranks_1.Rank.pi && !config_1.Mode.localDebug) - (0, commands_1.fail)(f(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Rank ", " is immutable."], ["Rank ", " is immutable."])), rank)); - if (player.immutable() && !config_1.Mode.localDebug) - (0, commands_1.fail)(f(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Player ", " is immutable."], ["Player ", " is immutable."])), player)); - if (!(player == sender && rank.level < sender.rank.level)) return [3 /*break*/, 2]; - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "[red] ARE YOU SURE YOU WANT TO SELF DEMOTE. THIS ACTION CANNOT BE UNDONE!")]; - case 1: - _d.sent(); - _d.label = 2; - case 2: return [4 /*yield*/, player.setRank(rank)]; - case 3: - _d.sent(); - (0, utils_1.logAction)("set rank to ".concat(rank.name, " for"), sender, player); - outputSuccess(f(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Set rank of player ", " to ", ""], ["Set rank of player ", " to ", ""])), player, rank)); - return [2 /*return*/]; - } - }); - }); - } - }, - setflag: { - args: ["player:player", "flag:roleflag", "value:boolean"], - description: "Set a player's role flags.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("player")], - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var _c = _b.args, flag = _c.flag, player = _c.player, value = _c.value, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - if (!sender.hasPerm("admin") && !flag.assignableByModerators) - (0, commands_1.fail)(f(templateObject_19 || (templateObject_19 = __makeTemplateObject(["You do not have permission to change the value of role flag ", ""], ["You do not have permission to change the value of role flag ", ""])), flag)); - return [4 /*yield*/, player.setFlag(flag, value)]; - case 1: - _d.sent(); - (0, utils_1.logAction)("set roleflag ".concat(flag.name, " to ").concat(value, " for"), sender, player); - outputSuccess(f(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Set role flag ", " of player ", " to ", ""], ["Set role flag ", " of player ", " to ", ""])), flag, player, value)); - return [2 /*return*/]; - } - }); - }); - } - }, - murder: { - args: [], - description: 'Kills all ohno units', - perm: commands_1.Perm.mod, - customUnauthorizedMessage: "[yellow]You're a [scarlet]monster[].", - handler: function (_a) { - var output = _a.output, f = _a.f, allCommands = _a.allCommands; - var Ohnos = allCommands["ohno"].data; //this is not ideal... TODO commit omega shenanigans - var numOhnos = Ohnos.amount(); - Ohnos.killAll(); - output(f(templateObject_21 || (templateObject_21 = __makeTemplateObject(["[orange]You massacred ", " helpless ohno crawlers."], ["[orange]You massacred ", " helpless ohno crawlers."])), numOhnos)); - } - }, - stop_offline: { - args: ["time:time?", "name:string?"], - description: "Stops an offline player.", - perm: commands_1.Perm.mod, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - function stop(option, time) { - return __awaiter(this, void 0, void 0, function () { - var fishP; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - fishP = players_1.FishPlayer.getFromInfo(option); - if (!sender.canModerate(fishP, true)) return [3 /*break*/, 2]; - (0, utils_1.logAction)(fishP.marked() ? time == 1000 ? "freed" : "updated stop time of" : "stopped", sender, option, undefined, time); - return [4 /*yield*/, fishP.stop(sender, time)]; - case 1: - _a.sent(); - outputSuccess(f(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Player ", " was marked for ", "."], ["Player ", " was marked for ", "."])), option, (0, utils_1.formatTime)(time))); - return [3 /*break*/, 3]; - case 2: - outputFail("You do not have permission to stop this player."); - _a.label = 3; - case 3: return [2 /*return*/]; - } - }); - }); - } - var maxPlayers, info, possiblePlayers, exactPlayers, score_1, optionPlayer, _c, _d, _e; - var _f, _g; - var args = _b.args, sender = _b.sender, outputFail = _b.outputFail, outputSuccess = _b.outputSuccess, f = _b.f, admins = _b.admins; - return __generator(this, function (_h) { - switch (_h.label) { - case 0: - maxPlayers = 60; - if (!(args.name && globals_1.uuidPattern.test(args.name))) return [3 /*break*/, 4]; - info = admins.getInfoOptional(args.name); - if (!(info != null)) return [3 /*break*/, 2]; - return [4 /*yield*/, stop(info, (_f = args.time) !== null && _f !== void 0 ? _f : (0, utils_1.untilForever)())]; - case 1: - _h.sent(); - return [3 /*break*/, 3]; - case 2: - outputFail(f(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); - _h.label = 3; - case 3: return [2 /*return*/]; - case 4: - if (args.name) { - possiblePlayers = (0, funcs_1.setToArray)(admins.searchNames(args.name)); - if (possiblePlayers.length > maxPlayers) { - exactPlayers = (0, funcs_1.setToArray)(admins.findByName(args.name)); - if (exactPlayers.length > 0) { - possiblePlayers = exactPlayers; - } - else { - (0, commands_1.fail)("Too many players with that name."); - } - } - else if (possiblePlayers.length == 0) { - (0, commands_1.fail)("No players with that name were found."); - } - score_1 = function (data) { - var fishP = players_1.FishPlayer.getById(data.id); - if (fishP) - return fishP.lastJoined; - return -data.timesJoined; - }; - possiblePlayers.sort(function (a, b) { return score_1(b) - score_1(a); }); - } - else { - possiblePlayers = players_1.FishPlayer.recentLeaves.map(function (p) { return p.info(); }); - } - return [4 /*yield*/, menus_1.Menu.menu("Stop", "Choose a player to mark", possiblePlayers, sender, { - includeCancel: true, - optionStringifier: function (p) { return p.lastName; } - })]; - case 5: - optionPlayer = _h.sent(); - if (!((_g = args.time) !== null && _g !== void 0)) return [3 /*break*/, 6]; - _c = _g; - return [3 /*break*/, 8]; - case 6: - _d = args; - _e = utils_1.match; - return [4 /*yield*/, menus_1.Menu.menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender)]; - case 7: - _c = (_d.time = _e.apply(void 0, [_h.sent(), { - "2 days": funcs_1.Duration.days(2), - "7 days": funcs_1.Duration.days(7), - "30 days": funcs_1.Duration.days(30), - "forever": globals_1.maxTime - Date.now() - 10000, - }])); - _h.label = 8; - case 8: - _c; - return [4 /*yield*/, stop(optionPlayer, args.time)]; - case 9: - _h.sent(); - return [2 /*return*/]; - } - }); - }); - } - }, - mute_offline: { - args: ["name:string?"], - description: "Mutes an offline player.", - perm: commands_1.Perm.mod, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - function mute(option) { - return __awaiter(this, void 0, void 0, function () { - var fishP; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - fishP = players_1.FishPlayer.getFromInfo(option); - if (!sender.canModerate(fishP, true)) - (0, commands_1.fail)("You do not have permission to mute this player."); - return [4 /*yield*/, menus_1.Menu.confirm(sender, "Are you sure you want to ".concat(fishP.muted ? "unmute" : "mute", " player ").concat(option.lastName, "?"), { - title: "Mute Offine Confirmation", - confirmText: "[green]Yes, ".concat(fishP.muted ? "unmute" : "mute", " them"), - })]; - case 1: - _a.sent(); - (0, utils_1.logAction)(fishP.muted ? "unmuted" : "muted", sender, fishP); - if (!fishP.muted) return [3 /*break*/, 3]; - return [4 /*yield*/, fishP.unmute(sender)]; - case 2: - _a.sent(); - return [3 /*break*/, 5]; - case 3: return [4 /*yield*/, fishP.mute(sender)]; - case 4: - _a.sent(); - _a.label = 5; - case 5: - outputSuccess("".concat(fishP.muted ? "Muted" : "Unmuted", " ").concat(option.lastName, ".")); - return [2 /*return*/]; - } - }); - }); - } - var maxPlayers, info, possiblePlayers, exactPlayers, score_2, option; - var _c; - var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f, admins = _b.admins; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - maxPlayers = 300; - if (!(args.name && globals_1.uuidPattern.test(args.name))) return [3 /*break*/, 2]; - info = (_c = admins.getInfoOptional(args.name)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(f(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); - return [4 /*yield*/, mute(info)]; - case 1: - _d.sent(); - return [2 /*return*/]; - case 2: - if (args.name) { - possiblePlayers = (0, funcs_1.setToArray)(admins.searchNames(args.name)); - if (possiblePlayers.length > maxPlayers) { - exactPlayers = (0, funcs_1.setToArray)(admins.findByName(args.name)); - if (exactPlayers.length > 0) { - possiblePlayers = exactPlayers; - } - else { - (0, commands_1.fail)("Too many players with that name."); - } - } - else if (possiblePlayers.length == 0) { - (0, commands_1.fail)("No players with that name were found."); - } - score_2 = function (data) { - var fishP = players_1.FishPlayer.getById(data.id); - if (fishP) - return fishP.lastJoined; - return -data.timesJoined; - }; - possiblePlayers.sort(function (a, b) { return score_2(b) - score_2(a); }); - } - else { - possiblePlayers = players_1.FishPlayer.recentLeaves.map(function (p) { return p.info(); }); - } - return [4 /*yield*/, menus_1.Menu.pagedList(sender, "Mute", "Choose a player to mute", possiblePlayers, { - optionStringifier: function (p) { return p.lastName; } - })]; - case 3: - option = _d.sent(); - return [4 /*yield*/, mute(option)]; - case 4: - _d.sent(); - return [2 /*return*/]; - } - }); - }); - } - }, - restart: { - args: ["time:number?"], - perm: commands_1.Perm.admin, - description: "Restarts the server.", - handler: function (_a) { - var _b; - var time = _a.args.time; - (_b = globals_1.fishState.restartLoopTask) === null || _b === void 0 ? void 0 : _b.cancel(); - if (Groups.player.isEmpty()) { - if (time == undefined) { - Log.info("Restarting immediately as no players are online."); - time !== null && time !== void 0 ? time : (time = 0); - } - } - else if (config_1.Gamemode.pvp()) { - time !== null && time !== void 0 ? time : (time = -1); - Log.info("PVP: restart will occur at the end of the current match. Specify a time to override, but &rthat would interrupt the current pvp match, and players would lose their teams.&fr"); - } - else { - time !== null && time !== void 0 ? time : (time = 60); - } - if (time == -1) { - Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart queued. The server will restart after the current match is over.[]\n[accent]---[[[coral]+++[]]---"); - globals_1.fishState.restartQueued = true; - } - else { - if (time < 0 || time > 100) - (0, commands_1.fail)("Invalid time: out of valid range."); - (0, utils_1.serverRestartLoop)(time); - if (config_1.Gamemode.pvp()) { - Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 15 seconds.[]\n[accent]---[[[coral]+++[]]---"); - } - else { - Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back with 20 seconds of downtime, and all progress will be saved.[]\n[accent]---[[[coral]+++[]]---"); - } - } - } - }, - history: { - args: ["player:player"], - description: "Shows moderation history for a player.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var args = _a.args, output = _a.output, f = _a.f; - if (args.player.history && args.player.history.length > 0) { - output("[yellow]_______________Player history_______________\n\n" + - (args.player).history.sort(function (a, b) { return a.time - b.time; }).map(function (e) { - return "".concat(e.by, " [yellow]").concat(e.action, " ").concat(args.player.prefixedName, " [white]").concat((0, utils_1.formatTimeRelative)(e.time)); - }).join("\n")); - } - else { - output(f(templateObject_25 || (templateObject_25 = __makeTemplateObject(["[yellow]No history was found for player ", "."], ["[yellow]No history was found for player ", "."])), args.player)); - } - } - }, - save: { - args: [], - description: "Saves the game state.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var outputSuccess = _a.outputSuccess; - players_1.FishPlayer.saveAll(); - players_1.FishPlayer.uploadAll(); - globals_1.FishEvents.fire("saveData", []); - var file = Vars.saveDirectory.child("1.".concat(Vars.saveExtension)); - SaveIO.save(file); - outputSuccess("Game saved."); - } - }, - wave: { - args: ["wave:number"], - description: "Sets the wave number.", - perm: commands_1.Perm.admin, - requirements: [commands_1.Req.positiveInteger("wave")], - handler: function (_a) { - var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; - Vars.state.wave = args.wave; - outputSuccess(f(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Set wave to ", ""], ["Set wave to ", ""])), Vars.state.wave)); - } - }, - label: { - args: ["time:time", "message:string"], - description: "Places a label at your position for a specified amount of time.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var _b; - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - if (args.time > funcs_1.Duration.hours(10)) - (0, commands_1.fail)("Time must be less than 10 hours."); - var unit = (_b = sender.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("You must be in a unit to use this command."); - var timeRemaining = args.time / 1000; - var labelx = unit.x; - var labely = unit.y; - var task = Timer.schedule(function () { - if (timeRemaining > 0) { - var timeseconds = timeRemaining % 60; - var timeminutes = (timeRemaining - timeseconds) / 60; - Call.label("".concat(sender.name, "\n\n[white]").concat(args.message, "\n\n[acid]").concat(timeminutes.toString().padStart(2, "0"), ":").concat(timeseconds.toString().padStart(2, "0")), 1, labelx, labely); - timeRemaining--; - } - }, 0, 1, args.time); - globals_1.fishState.labels.push({ x: labelx, y: labely, task: task }); - outputSuccess(f(templateObject_27 || (templateObject_27 = __makeTemplateObject(["Placed label \"", "\" for ", " seconds."], ["Placed label \"", "\" for ", " seconds."])), args.message, timeRemaining)); - } - }, - labelsticky: { - args: ["time:time", "message:string"], - description: "Places a label at the bottom left corner of everyone's screen.", - perm: commands_1.Perm.admin, - handler: function (_a) { - var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; - if (args.time > funcs_1.Duration.hours(10)) - (0, commands_1.fail)("Time must be less than 10 hours."); - var timeRemaining = args.time / 1000; - var task = Timer.schedule(function () { - if (timeRemaining > 0) { - Call.label(args.message, 5, NaN, NaN); - timeRemaining -= 5; - } - }, 0, 5, Math.ceil(args.time / 5)); - globals_1.fishState.labels.push({ task: task, x: null, y: null }); - outputSuccess(f(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Placed label \"", "\" for ", " seconds."], ["Placed label \"", "\" for ", " seconds."])), args.message, timeRemaining)); - } - }, - clearlabels: { - args: [], - description: "Removes all labels.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var outputSuccess = _a.outputSuccess; - if (globals_1.fishState.labels.length == 0) - (0, commands_1.fail)("No labels found."); - globals_1.fishState.labels.forEach(function (l) { return l.task.cancel(); }); - outputSuccess("Removed all labels."); - } - }, - clearlabel: { - args: ["sticky:boolean?"], - description: "Removes the closest label, or sticky label if specified", - perm: commands_1.Perm.mod, - handler: function (_a) { - var _b; - var _c = _a.args.sticky, sticky = _c === void 0 ? false : _c, sender = _a.sender, outputSuccess = _a.outputSuccess; - if (globals_1.fishState.labels.length == 0) - (0, commands_1.fail)("No labels found."); - var label; - if (sticky) { - var index = globals_1.fishState.labels.findIndex(function (l) { return l.x == null; }); - if (index == -1) - (0, commands_1.fail)("No sticky label found."); - label = globals_1.fishState.labels.splice(index, 1)[0]; - } - else { - var unit_1 = (_b = sender.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Cannot remove the closest label because you are dead."); - var dist_1 = function (label) { - if (label.x == null || label.y == null) - return Infinity; - return Mathf.dst(label.x, label.y, unit_1.x, unit_1.y); - }; - var index = __spreadArray([], __read(globals_1.fishState.labels.entries()), false).reduce(function (a, b) { return dist_1(a[1]) < dist_1(b[1]) ? a : b; })[0]; - label = globals_1.fishState.labels.splice(index, 1)[0]; - } - label.task.cancel(); - outputSuccess("Removed one label."); - } - }, - member: { - args: ["value:boolean", "player:player"], - description: "Sets a player's member status.", - perm: commands_1.Perm.admin, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: return [4 /*yield*/, args.player.setFlag("member", args.value)]; - case 1: - _c.sent(); - outputSuccess(f(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Set membership status of player ", " to ", "."], ["Set membership status of player ", " to ", "."])), args.player, args.value)); - return [2 /*return*/]; - } - }); - }); - } - }, - remind: { - args: ["rule:number", "target:player?"], - description: "Remind players in chat of a specific rule.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var _b; - var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; - var rule = (_b = config_1.rules[args.rule - 1]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("The rule you requested does not exist."); - if (args.target) { - args.target.sendMessage("A staff member wants to remind you of the following rule:\n" + rule); - outputSuccess(f(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Reminded ", " of rule ", ""], ["Reminded ", " of rule ", ""])), args.target, args.rule)); - } - else { - Call.sendMessage("A staff member wants to remind everyone of the following rule:\n" + rule); - } - }, - }, - ban: { - args: ["uuid_or_ip:string?"], - description: "Bans a player by UUID and IP.", - perm: commands_1.Perm.admin, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var uuid, data, name, ip, ip, info, alreadyBanned, option; - var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f, admins = _b.admins; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!(args.uuid_or_ip && globals_1.uuidPattern.test(args.uuid_or_ip))) return [3 /*break*/, 2]; - uuid = args.uuid_or_ip; - data = void 0; - if ((data = admins.getInfoOptional(uuid)) != null && data.admin) - (0, commands_1.fail)("Cannot ban an admin."); - name = data ? "".concat((0, funcs_1.escapeStringColorsClient)(data.lastName), " (").concat(uuid, "/").concat(data.lastIP, ")") : uuid; - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to ban ".concat(name, "?"))]; - case 1: - _c.sent(); - admins.banPlayerID(uuid); - if (data) { - ip = data.lastIP; - admins.banPlayerIP(ip); - api.ban({ ip: ip, uuid: uuid }); - Log.info("".concat(uuid, "/").concat(ip, " was banned.")); - (0, utils_1.logAction)("banned", sender, data); - outputSuccess(f(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Banned player ", " (", "/", ")"], ["Banned player ", " (", "/", ")"])), (0, funcs_1.escapeStringColorsClient)(data.lastName), uuid, ip)); - //TODO add way to specify whether to activate or escape color tags - } - else { - api.ban({ uuid: uuid }); - Log.info("".concat(uuid, " was banned.")); - (0, utils_1.logAction)("banned", sender, uuid); - outputSuccess(f(templateObject_32 || (templateObject_32 = __makeTemplateObject(["Banned player ", ". [yellow]Unable to determine IP.[]"], ["Banned player ", ". [yellow]Unable to determine IP.[]"])), uuid)); - } - (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - return [2 /*return*/]; - case 2: - if (!(args.uuid_or_ip && globals_1.ipPattern.test(args.uuid_or_ip))) return [3 /*break*/, 4]; - ip = args.uuid_or_ip; - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to ban IP ".concat(ip, "?"))]; - case 3: - _c.sent(); - api.ban({ ip: ip }); - info = admins.findByIP(ip); - if (info) - (0, utils_1.logAction)("banned", sender, info); - else - (0, utils_1.logAction)("banned ".concat(ip), sender); - alreadyBanned = admins.banPlayerIP(ip); - if (alreadyBanned) { - outputSuccess(f(templateObject_33 || (templateObject_33 = __makeTemplateObject(["IP ", " is already banned. Ban was synced to other servers."], ["IP ", " is already banned. Ban was synced to other servers."])), ip)); - } - else { - outputSuccess(f(templateObject_34 || (templateObject_34 = __makeTemplateObject(["IP ", " has been banned. Ban was synced to other servers."], ["IP ", " has been banned. Ban was synced to other servers."])), ip)); - } - (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - return [2 /*return*/]; - case 4: return [4 /*yield*/, menus_1.Menu.menu("[scarlet]BAN[]", "Choose a player to ban.", (0, funcs_1.setToArray)(Groups.player), sender, { - includeCancel: true, - optionStringifier: function (opt) { return opt.name; } - })]; - case 5: - option = _c.sent(); - if (option.admin) - (0, commands_1.fail)("Cannot ban an admin."); - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to ban ".concat(option.name, "?"))]; - case 6: - _c.sent(); - admins.banPlayerIP(option.ip()); //this also bans the UUID - api.ban({ ip: option.ip(), uuid: option.uuid() }); - Log.info("".concat(option.ip(), "/").concat(option.uuid(), " was banned.")); - (0, utils_1.logAction)("banned", sender, option.getInfo()); - outputSuccess(f(templateObject_35 || (templateObject_35 = __makeTemplateObject(["Banned player ", "."], ["Banned player ", "."])), option)); - (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - return [2 /*return*/]; - } - }); - }); - } - }, - kill: { - args: ["player:player"], - description: "Kills a player's unit.", - perm: commands_1.Perm.admin, - requirements: [commands_1.Req.moderate("player", true)], - handler: function (_a) { - var args = _a.args, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess, f = _a.f; - var unit = args.player.unit(); - if (unit) { - unit.kill(); - outputSuccess(f(templateObject_36 || (templateObject_36 = __makeTemplateObject(["Killed the unit of player ", "."], ["Killed the unit of player ", "."])), args.player)); - } - else { - outputFail(f(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Player ", " does not have a unit."], ["Player ", " does not have a unit."])), args.player)); - } - } - }, - killunits: { - args: ["team:team?", "unit:unittype?"], - description: "Kills all units, optionally specifying a team and unit type.", - perm: commands_1.Perm.massKill, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var i_1, before, i_2, before; - var _c = _b.args, team = _c.team, unit = _c.unit, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: - if (!team) return [3 /*break*/, 2]; - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every ".concat(unit ? unit.localizedName : "unit", "[] on the team ").concat(team.coloredName(), "."), { confirmText: "[orange]Kill units[]" })]; - case 1: - _d.sent(); - if (unit) { - i_1 = 0; - team.data().units.each(function (u) { return u.type == unit; }, function (u) { - u.kill(); - i_1++; - }); - outputSuccess(f(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Killed ", " units on ", "."], ["Killed ", " units on ", "."])), i_1, team)); - } - else { - before = team.data().units.size; - team.data().units.each(function (u) { return u.kill(); }); - outputSuccess(f(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Killed ", " units on ", "."], ["Killed ", " units on ", "."])), before, team)); - } - return [3 /*break*/, 4]; - case 2: return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every single ".concat(unit ? unit.localizedName : "unit", "[]."), { confirmText: "[orange]Kill all units[]" })]; - case 3: - _d.sent(); - if (unit) { - i_2 = 0; - Groups.unit.each(function (u) { return u.type == unit; }, function (u) { - u.kill(); - i_2++; - }); - outputSuccess(f(templateObject_40 || (templateObject_40 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), i_2)); - } - else { - before = Groups.unit.size(); - Groups.unit.each(function (u) { return u.kill(); }); - outputSuccess(f(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), before)); - } - _d.label = 4; - case 4: return [2 /*return*/]; - } - }); - }); - } - }, - killbuildings: { - args: ["team:team?"], - description: "Kills all buildings (except cores), optionally specifying a team.", - perm: commands_1.Perm.massKill, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var count, count; - var team = _b.args.team, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!team) return [3 /*break*/, 2]; - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every building[] on the team ".concat(team.coloredName(), ", except cores."), { confirmText: "[orange]Kill buildings[]" })]; - case 1: - _c.sent(); - count = team.data().buildings.size; - team.data().buildings.each(function (b) { return !(b.block instanceof CoreBlock); }, function (b) { return b.tile.remove(); }); - outputSuccess(f(templateObject_42 || (templateObject_42 = __makeTemplateObject(["Killed ", " buildings on ", "."], ["Killed ", " buildings on ", "."])), count, team)); - return [3 /*break*/, 4]; - case 2: return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every building[] except cores.", { confirmText: "[orange]Kill buildings[]" })]; - case 3: - _c.sent(); - count = Groups.build.size(); - Groups.build.each(function (b) { return !(b.block instanceof CoreBlock); }, function (b) { return b.tile.remove(); }); - outputSuccess(f(templateObject_43 || (templateObject_43 = __makeTemplateObject(["Killed ", " buildings."], ["Killed ", " buildings."])), count)); - _c.label = 4; - case 4: return [2 /*return*/]; - } - }); - }); - } - }, - respawn: { - args: ["player:player"], - description: "Forces a player to respawn.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("player", true, "mod", true)], - handler: function (_a) { - var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; - args.player.forceRespawn(); - outputSuccess(f(templateObject_44 || (templateObject_44 = __makeTemplateObject(["Respawned player ", "."], ["Respawned player ", "."])), args.player)); - } - }, - clearunit: { - args: ["target:player", "duration:time?"], - description: "Forces a player out of the unit they are controlling, and blocks them from possessing units for a specified duration.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("target", false, "mod", false)], - handler: function (_a) { - var _b = _a.args, target = _b.target, duration = _b.duration, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - if (Date.now() > 1000 + target.blockedFromPossessingUnitsUntil) - duration !== null && duration !== void 0 ? duration : (duration = funcs_1.Duration.minutes(1)); - else - duration !== null && duration !== void 0 ? duration : (duration = 0); - if (duration == 0) { - target.blockedFromPossessingUnitsUntil = 0; - target.sendMessage("You are allowed to control units again."); - outputSuccess(f(templateObject_45 || (templateObject_45 = __makeTemplateObject(["Restored ", "'s ability to control units."], ["Restored ", "'s ability to control units."])), target)); - (0, utils_1.logAction)("restored unit possession for", sender, target); - } - else { - target.forceRespawn(); - target.blockedFromPossessingUnitsUntil = Date.now() + duration; - target.sendMessage("You have been blocked from controlling units for ".concat((0, utils_1.formatTime)(duration), ".")); - outputSuccess(f(templateObject_46 || (templateObject_46 = __makeTemplateObject(["Blocked ", " from controlling units for ", "."], ["Blocked ", " from controlling units for ", "."])), target, (0, utils_1.formatTime)(duration))); - (0, utils_1.logAction)("revoked unit possession for", sender, target, undefined, duration); - } - } - }, - clearcommand: { - args: ["target:player", "duration:time?"], - description: "Blocks a player from commanding units for a specified duration.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("target", false, "mod", false)], - handler: function (_a) { - var _b = _a.args, target = _b.target, duration = _b.duration, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - if (Date.now() > 1000 + target.blockedFromCommandingUnitsUntil) - duration !== null && duration !== void 0 ? duration : (duration = funcs_1.Duration.minutes(1)); - else - duration !== null && duration !== void 0 ? duration : (duration = 0); - if (duration == 0) { - target.blockedFromCommandingUnitsUntil = 0; - target.sendMessage("You are allowed to command units again."); - outputSuccess(f(templateObject_47 || (templateObject_47 = __makeTemplateObject(["Restored ", "'s ability to command units."], ["Restored ", "'s ability to command units."])), target)); - (0, utils_1.logAction)("restored command mode for", sender, target, undefined, duration); - } - else { - target.blockedFromCommandingUnitsUntil = Date.now() + duration; - target.sendMessage("You have been blocked from commanding units for ".concat((0, utils_1.formatTime)(duration), ".")); - outputSuccess(f(templateObject_48 || (templateObject_48 = __makeTemplateObject(["Blocked ", " from commanding units for ", "."], ["Blocked ", " from commanding units for ", "."])), target, (0, utils_1.formatTime)(duration))); - (0, utils_1.logAction)("revoked command mode for", sender, target, undefined, duration); - } - } - }, - stealunit: { - args: ["target:player", "newcontroller:player?"], - description: "Steals the unit of a player, putting you in their unit and forcing them to respawn.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.moderate("target", true, "mod", true), commands_1.Req.moderate("newcontroller", true, "mod", true)], - handler: function (_a) { - var _b; - var sender = _a.sender, _c = _a.args, target = _c.target, _d = _c.newcontroller, newcontroller = _d === void 0 ? sender : _d, outputSuccess = _a.outputSuccess, f = _a.f; - var unit = (_b = target.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)(f(templateObject_49 || (templateObject_49 = __makeTemplateObject(["Targeted player ", " is not in a unit."], ["Targeted player ", " is not in a unit."])), target)); - if (target.team() !== newcontroller.team()) { - if (!sender.hasPerm("changeTeamExternal")) { - if (!sender.hasPerm("changeTeam")) - (0, commands_1.fail)("You do not have permission to change teams."); - newcontroller.setTeam(unit.team); - } - } - target.forceRespawn(); - newcontroller.unit(unit); - if (newcontroller == sender) { - outputSuccess(f(templateObject_50 || (templateObject_50 = __makeTemplateObject(["Commandeered the unit of player ", "."], ["Commandeered the unit of player ", "."])), target)); - } - else { - outputSuccess(f(templateObject_51 || (templateObject_51 = __makeTemplateObject(["Transferred player ", " into the unit of ", "."], ["Transferred player ", " into the unit of ", "."])), newcontroller, target)); - newcontroller.sendMessage(f(templateObject_52 || (templateObject_52 = __makeTemplateObject(["[green]You were transferred to the unit of player ", " by ", "."], ["[green]You were transferred to the unit of player ", " by ", "."])), target, sender)('[green]')); - } - } - }, - m: { - args: ["message:string"], - description: "Sends a message to muted players only.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var sender = _a.sender, args = _a.args; - players_1.FishPlayer.messageMuted(sender.prefixedName, args.message); - } - }, - info: { - args: ["target:player", "showColors:boolean?"], - description: "Displays information about an online player.", - perm: commands_1.Perm.none, - handler: function (_a) { - var sender = _a.sender, args = _a.args, output = _a.output, f = _a.f; - var info = args.target.info(); - var names = args.showColors - ? info.names.map(funcs_1.escapeStringColorsClient).toString(", ") - : __spreadArray([], __read(new Set(info.names.map(function (n) { return Strings.stripColors(n); }).toArray())), false).join(", "); - output(f(templateObject_53 || (templateObject_53 = __makeTemplateObject(["[accent]Info for player ", " [gray](", ") (#", ")\n\t[accent]Rank: ", "\n\t[accent]Role flags: ", "\n\t[accent]Stopped: ", "\n\t[accent]marked: ", "\n\t[accent]muted: ", "\n\t[accent]autoflagged: ", "\n\t[accent]VPN detected: ", "\n\t[accent]times joined / kicked: ", "/", "\n\t[accent]First joined: ", "\n\t[accent]Names used: [[", "]"], ["\\\n[accent]Info for player ", " [gray](", ") (#", ")\n\t[accent]Rank: ", "\n\t[accent]Role flags: ", "\n\t[accent]Stopped: ", "\n\t[accent]marked: ", "\n\t[accent]muted: ", "\n\t[accent]autoflagged: ", "\n\t[accent]VPN detected: ", "\n\t[accent]times joined / kicked: ", "/", "\n\t[accent]First joined: ", "\n\t[accent]Names used: [[", "]"])), args.target, (0, funcs_1.escapeStringColorsClient)(args.target.name), args.target.player.id.toString(), args.target.rank, Array.from(args.target.flags).map(function (f) { return f.coloredName(); }).join(" "), f.boolBad(!args.target.hasPerm("play")), args.target.marked() ? "until ".concat((0, utils_1.formatTimeRelative)(args.target.unmarkTime)) : "[green]false", f.boolBad(args.target.muted), f.boolBad(args.target.autoflagged), f.boolBad(args.target.ipDetectedVpn), info.timesJoined, info.timesKicked, args.target.firstJoined < 1 ? "unknown" : (0, utils_1.formatTimeRelative)(args.target.firstJoined), names)); - if (sender.hasPerm("viewUUIDs")) - output(f(templateObject_54 || (templateObject_54 = __makeTemplateObject(["\t[#FFAAAA]UUID: ", ""], ["\\t[#FFAAAA]UUID: ", ""])), args.target.uuid)); - if (sender.hasPerm("viewIPs")) - output(f(templateObject_55 || (templateObject_55 = __makeTemplateObject(["\t[#FFAAAA]IP: ", ""], ["\\t[#FFAAAA]IP: ", ""])), args.target.ip())); - } - }, - spawn: { - args: ["type:unittype", "x:number?", "y:number?", "count:number?", "team:team?", "effects:string?", "stack:boolean?"], - description: "Spawns a unit of specified type at your position. [scarlet]Usage will be logged.[]", - perm: commands_1.Perm.admin.exceptModes({ - testsrv: commands_1.Perm.trusted, - }), - data: [], - requirements: [commands_1.Req.positiveInteger("count")], - handler: function (_a) { - var _b, _c; - var sender = _a.sender, args = _a.args, data = _a.data, outputSuccess = _a.outputSuccess, f = _a.f; - var x = args.x ? (args.x * 8) : sender.player.x; - var y = args.y ? (args.y * 8) : sender.player.y; - var team = (_b = args.team) !== null && _b !== void 0 ? _b : sender.team(); - var count = Math.min((_c = args.count) !== null && _c !== void 0 ? _c : 1, 1000); - for (var i = 0; i < count; i++) { - var unit = args.type.create(team); - var xOffset = args.stack ? 0 : 0.01 * i; - var yOffset = args.stack ? 0 : 0.5 * (i % 10); - unit.set(x + xOffset, y + yOffset); - if (args.effects) - (0, utils_1.applyEffectMode)(args.effects, unit, 1e12); - unit.add(); - data.push(unit); - } - if (!(config_1.Gamemode.sandbox() || config_1.Gamemode.testsrv()) && args.effects !== 'paper') - (0, utils_1.logAction)("spawned unit ".concat(args.type.name).concat(count == 1 ? '' : " x".concat(count), " at ").concat(Math.round(x / 8), ", ").concat(Math.round(y / 8)) + (args.effects ? "with ".concat(args.effects, " effects") : ''), sender); - outputSuccess(f(templateObject_56 || (templateObject_56 = __makeTemplateObject(["Spawned unit ", " at (", ", ", ")"], ["Spawned unit ", " at (", ", ", ")"])), args.type, Math.round(x / 8), Math.round(y / 8))); - } - }, - setblock: { - args: ["x:number", "y:number", "block:block", "team:team?", "rotation:number?"], - description: "Sets the block at a location.", - perm: commands_1.Perm.admin.exceptModes({ - testsrv: commands_1.Perm.trusted, - }), - requirements: [commands_1.Req.integerRange("rotation", 0, 3)], - handler: function (_a) { - var _b, _c; - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - var team = (_b = args.team) !== null && _b !== void 0 ? _b : sender.team(); - var tile = Vars.world.tile(args.x, args.y); - if (tile == null) - (0, commands_1.fail)(f(templateObject_57 || (templateObject_57 = __makeTemplateObject(["Position (", ", ", ") is out of bounds."], ["Position (", ", ", ") is out of bounds."])), args.x, args.y)); - tile.setNet(args.block, team, (_c = args.rotation) !== null && _c !== void 0 ? _c : 0); - (0, utils_1.addToTileHistory)({ - pos: "".concat(args.x, ",").concat(args.y), - uuid: sender.uuid, - action: "setblocked", - type: args.block.localizedName - }); - if (!config_1.Gamemode.sandbox()) - (0, utils_1.logAction)("set block to ".concat(args.block.localizedName, " at ").concat(args.x, ",").concat(args.y), sender); - outputSuccess(f(templateObject_58 || (templateObject_58 = __makeTemplateObject(["Set block at ", ", ", " to ", ""], ["Set block at ", ", ", " to ", ""])), args.x, args.y, args.block)); - } - }, - setblockr: { - args: ["block:block?", "team:team?", "rotation:number?"], - description: "Sets the block at tapped locations, repeatedly.", - perm: commands_1.Perm.admin, - requirements: [commands_1.Req.integerRange("rotation", 0, 3)], - tapped: function (_a) { - var _b, _c; - var args = _a.args, sender = _a.sender, f = _a.f, x = _a.x, y = _a.y, outputSuccess = _a.outputSuccess; - if (!args.block) - (0, funcs_1.crash)("uh oh"); - var team = (_b = args.team) !== null && _b !== void 0 ? _b : sender.team(); - var tile = Vars.world.tile(x, y); - if (tile == null) - (0, commands_1.fail)(f(templateObject_59 || (templateObject_59 = __makeTemplateObject(["Position (", ", ", ") is out of bounds."], ["Position (", ", ", ") is out of bounds."])), x, y)); - tile.setNet(args.block, team, (_c = args.rotation) !== null && _c !== void 0 ? _c : 0); - (0, utils_1.addToTileHistory)({ - pos: "".concat(x, ",").concat(y), - uuid: sender.uuid, - action: "setblocked", - type: args.block.localizedName - }); - if (!config_1.Gamemode.sandbox()) - (0, utils_1.logAction)("set block to ".concat(args.block.localizedName, " at ").concat(x, ",").concat(y), sender); - outputSuccess(f(templateObject_60 || (templateObject_60 = __makeTemplateObject(["Set block at ", ", ", " to ", ""], ["Set block at ", ", ", " to ", ""])), x, y, args.block)); - }, - handler: function (_a) { - var args = _a.args, outputSuccess = _a.outputSuccess, handleTaps = _a.handleTaps, currentTapMode = _a.currentTapMode, f = _a.f; - if (args.block) { - handleTaps("on"); - if (currentTapMode == "off") { - outputSuccess("setblockr enabled.\n[scarlet]Be careful, you have the midas touch now![] Turn it off by running /setblockr again."); - } - else { - outputSuccess(f(templateObject_61 || (templateObject_61 = __makeTemplateObject(["Changed setblockr's block to ", ""], ["Changed setblockr's block to ", ""])), args.block)); - } - } - else { - if (currentTapMode == "off") { - (0, commands_1.fail)("Please specify the block to place."); - } - else { - handleTaps("off"); - outputSuccess("setblockr disabled."); - } - } - } - }, - exterminate: { - args: [], - description: "Removes all spawned units.", - perm: commands_1.Perm.admin.exceptModes({ - testsrv: commands_1.Perm.trusted, - }), - handler: function (_a) { - var sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f, allCommands = _a.allCommands; - var numKilled = 0; - allCommands.spawn.data.forEach(function (u) { - if (u.isAdded() && !u.dead) { - u.kill(); - numKilled++; - } - }); - if (!config_1.Gamemode.sandbox()) - (0, utils_1.logAction)("exterminated ".concat(numKilled, " units"), sender); - outputSuccess(f(templateObject_62 || (templateObject_62 = __makeTemplateObject(["Exterminated ", " units."], ["Exterminated ", " units."])), numKilled)); - } - }, - js: { - args: ["javascript:string"], - description: "Run arbitrary javascript.", - perm: commands_1.Perm.runJS, - customUnauthorizedMessage: "[scarlet]You are not in the jsers file. This incident will be reported.[]", - handler: function (_a) { - var javascript = _a.args.javascript, output = _a.output, outputFail = _a.outputFail, sender = _a.sender; - //Additional validation couldn't hurt... - var playerInfo_AdminUsid = sender.info().adminUsid; - if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid != sender.player.usid()) { - api.sendModerationMessage("# !!!!! /js authentication failed !!!!!\nServer: ".concat(config_1.Gamemode.name(), " Player: ").concat((0, funcs_1.escapeTextDiscord)(sender.cleanedName), "/`").concat(sender.uuid, "`\n<@!709904412033810533>")); - (0, commands_1.fail)("Authentication failure"); - } - if (javascript == "Timer.instance().clear()") - (0, commands_1.fail)("Are you really sure you want to do that? If so, prepend \"void\" to your command."); - try { - var scripts = Vars.mods.getScripts(); - var out = scripts.context.evaluateString(scripts.scope, javascript, "fish-js-console.js", 1); - if (out instanceof Array) { - output("[cyan]Array: [[[]" + out.join(", ") + "[cyan]]"); - } - else if (out === undefined) { - output("[blue]undefined[]"); - } - else if (out === null) { - output("[blue]null[]"); - } - else if (out instanceof Error) { - outputFail((0, funcs_1.parseError)(out)); - } - else if (typeof out == "number") { - output("[blue]".concat(out, "[]")); - } - else { - output(out); - } - } - catch (err) { - outputFail((0, funcs_1.parseError)(err)); - } - } - }, - fjs: { - args: ["javascript:string"], - description: "Run arbitrary javascript in the fish-commands context.", - perm: commands_1.Perm.runJS, - customUnauthorizedMessage: "[scarlet]You are not in the jsers file. This incident will be reported.[]", - handler: function (_a) { - var javascript = _a.args.javascript, output = _a.output, outputFail = _a.outputFail, sender = _a.sender; - //Additional validation couldn't hurt... - var playerInfo_AdminUsid = sender.info().adminUsid; - if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid != sender.player.usid()) { - api.sendModerationMessage("# !!!!! /js authentication failed !!!!!\nServer: ".concat(config_1.Gamemode.name(), " Player: ").concat((0, funcs_1.escapeTextDiscord)(sender.cleanedName), "/`").concat(sender.uuid, "`\n<@!709904412033810533>")); - (0, commands_1.fail)("Authentication failure"); - } - fjsContext.runJS(javascript, output, outputFail, sender); - } - }, - antibot: { - args: ["timeout:time?"], - description: "Checks anti bot stats, or force enables anti bot mode.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, output = _a.output, f = _a.f; - if (args.timeout != undefined) { - args.timeout = Math.min(args.timeout, sender.hasPerm("admin") ? funcs_1.Duration.hours(1) : funcs_1.Duration.minutes(10)); - players_1.FishPlayer.triggerAntibot(args.timeout, "Manually triggered by player ".concat(sender.name), "manual"); - outputSuccess("Set antibot mode override for ".concat((0, utils_1.formatTime)(args.timeout), ".")); - } - else { - output("[acid]Antibot status:\n[acid]Enabled: ".concat(f.boolBad(players_1.FishPlayer.antiBotMode()), "\n").concat((0, utils_1.getAntiBotInfo)("client"))); - } - } - }, - chatstrictness: { - args: ["player:player", "value:string"], - description: "Sets chat strictness for a player.", - perm: commands_1.Perm.mod, - handler: function (_a) { - var _b = _a.args, player = _b.player, value = _b.value, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - if (!sender.canModerate(player, true)) - (0, commands_1.fail)("You do not have permission to set the chat strictness level of this player."); - if (!(value == "chat" || value == "strict")) - (0, commands_1.fail)("Invalid chat strictness level: valid levels are \"chat\", \"strict\""); - player.chatStrictness = value; - (0, utils_1.logAction)("set chat strictness to ".concat(value, " for"), sender, player); - outputSuccess(f(templateObject_63 || (templateObject_63 = __makeTemplateObject(["Set chat strictness for player ", " to \"", "\"."], ["Set chat strictness for player ", " to \"", "\"."])), player, value)); - } - }, - emanate: (0, commands_1.command)(function () { - var unitMapping = {}; - Timer.schedule(function () { - var e_1, _a; - try { - for (var _b = __values(Object.entries(unitMapping)), _c = _b.next(); !_c.done; _c = _b.next()) { - var _d = __read(_c.value, 2), uuid = _d[0], unit = _d[1]; - var fishP = players_1.FishPlayer.getById(uuid); - if (!fishP || !fishP.connected() || (unit.getPlayer() != fishP.player)) { - delete unitMapping[uuid]; - unit === null || unit === void 0 ? void 0 : unit.kill(); - } - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_1) throw e_1.error; } - } - }, 1, 0.5); - return { - args: [], - description: "Puts you in an emanate.", - perm: commands_1.Perm.admin, - data: { unitMapping: unitMapping }, - requirements: [], - handler: function (_a) { - var sender = _a.sender, outputSuccess = _a.outputSuccess; - var emanate = UnitTypes.emanate.spawn(sender.team(), sender.player.x, sender.player.y); - sender.player.unit(emanate); - unitMapping[sender.uuid] = emanate; - if (!config_1.Gamemode.sandbox()) - (0, utils_1.logAction)("spawned an emanate", sender); - outputSuccess("Spawned an emanate."); - } - }; - }), - updatemaps: { - args: [], - description: 'Attempt to fetch and update all map files', - perm: commands_1.Perm.trusted, - requirements: function (_a) { - var sender = _a.sender; - return [commands_1.Req.cooldownGlobal(config_1.Gamemode.testsrv() || sender.hasPerm("mod") ? 15000 : funcs_1.Duration.minutes(5))]; - }, - handler: function (_a) { - var output = _a.output, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; - output("Updating maps... (this may take a while)"); - (0, files_1.updateMaps)() - .then(function (changed) { - Log.info("Maps updated."); - if (changed) { - outputSuccess("Map update completed."); - Call.sendMessage("[orange]Maps have been updated. Run [white]/maps[] to view available maps."); - } - else { - outputSuccess("Map update completed; already up to date."); - } - }) - .catch(function (message) { - outputFail("Map update failed: ".concat(String(message))); - Log.err("Map updates failed: ".concat(String(message))); - }); - } - }, - clearfire: { - args: [], - description: "Clears all the fires.", - perm: commands_1.Perm.admin, - handler: function (_a) { - var output = _a.output, outputSuccess = _a.outputSuccess; - output("Removing fires..."); - var totalRemoved = 0; - Call.sendMessage("[scarlet][[Fire Department]:[yellow] Fires were reported. Trucks are en-route. Removing all fires shortly."); - Timer.schedule(function () { - totalRemoved += Groups.fire.size(); - Groups.fire.each(function (f) { return f.remove(); }); - Groups.fire.clear(); - }, 2, 0.1, 40); - Timer.schedule(function () { - outputSuccess("Removed ".concat(totalRemoved, " fires.")); - Call.sendMessage("[scarlet][[Fire Department]:[yellow] We've extinguished ".concat(totalRemoved, " fires.")); - }, 6.1); - } - }, - search: { - args: ["input:string"], - description: "Searches playerinfo by name, IP, or UUID.", - perm: commands_1.Perm.admin, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var fishP, info, matches, matches_1, displayMatches; - var input = _b.args.input, admins = _b.admins, output = _b.output, f = _b.f, sender = _b.sender; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!globals_1.uuidPattern.test(input)) return [3 /*break*/, 1]; - fishP = players_1.FishPlayer.getById(input); - info = admins.getInfoOptional(input); - if (fishP == null && info == null) - (0, commands_1.fail)(f(templateObject_64 || (templateObject_64 = __makeTemplateObject(["No stored data matched uuid ", "."], ["No stored data matched uuid ", "."])), input)); - else if (fishP == null && info) - output(f(templateObject_65 || (templateObject_65 = __makeTemplateObject(["[accent]Found player info (but no fish player data) for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nFound player info (but no fish player data) for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), input, info.plainLastName(), (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); - else if (fishP && info) - output(f(templateObject_66 || (templateObject_66 = __makeTemplateObject(["[accent]Found fish player data for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nFound fish player data for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), input, fishP.name, (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); - else - (0, commands_1.fail)(f(templateObject_67 || (templateObject_67 = __makeTemplateObject(["Super weird edge case: found fish player data but no player info for uuid ", "."], ["Super weird edge case: found fish player data but no player info for uuid ", "."])), input)); - return [3 /*break*/, 5]; - case 1: - if (!globals_1.ipPattern.test(input)) return [3 /*break*/, 2]; - matches = admins.findByIPs(input); - if (matches.isEmpty()) - (0, commands_1.fail)(f(templateObject_68 || (templateObject_68 = __makeTemplateObject(["No stored data matched IP ", ""], ["No stored data matched IP ", ""])), input)); - output(f(templateObject_69 || (templateObject_69 = __makeTemplateObject(["[accent]Found ", " match", " for search \"", "\"."], ["[accent]Found ", " match", " for search \"", "\"."])), matches.size, matches.size == 1 ? "" : "es", input)); - matches.each(function (info) { return output(f(templateObject_70 || (templateObject_70 = __makeTemplateObject(["[accent]Player with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nPlayer with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), info.id, info.plainLastName(), (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); }); - return [3 /*break*/, 5]; - case 2: - matches_1 = Vars.netServer.admins.searchNames(input); - if (matches_1.isEmpty()) - (0, commands_1.fail)(f(templateObject_71 || (templateObject_71 = __makeTemplateObject(["No stored data matched name ", ""], ["No stored data matched name ", ""])), input)); - output(f(templateObject_72 || (templateObject_72 = __makeTemplateObject(["[accent]Found ", " match", " for search \"", "\"."], ["[accent]Found ", " match", " for search \"", "\"."])), matches_1.size, matches_1.size == 1 ? "" : "es", input)); - displayMatches = function () { - matches_1.each(function (info) { return output(f(templateObject_73 || (templateObject_73 = __makeTemplateObject(["[accent]Player with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nPlayer with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), info.id, info.plainLastName(), (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); }); - }; - if (!(matches_1.size > 20)) return [3 /*break*/, 4]; - return [4 /*yield*/, menus_1.Menu.confirm(sender, "Are you sure you want to view all ".concat(matches_1.size, " matches?"))]; - case 3: - _c.sent(); - _c.label = 4; - case 4: - displayMatches(); - _c.label = 5; - case 5: return [2 /*return*/]; - } - }); - }); - } - }, - peace: { - args: ["peace:boolean"], - description: "Toggles peaceful mode for sandbox.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.mode('sandbox')], - handler: function (_a) { - var args = _a.args; - if (args.peace) { - globals_1.fishState.peacefulMode = true; - Groups.player.each(function (p) { - if (p.team() != Vars.state.rules.defaultTeam) { - p.team(Vars.state.rules.defaultTeam); - } - }); - Call.sendMessage("[[Sandbox] [green]Enabled peaceful mode."); - } - else { - globals_1.fishState.peacefulMode = false; - Call.sendMessage("[[Sandbox] [red]Disabled peaceful mode."); - } - }, - }, - effects: { - args: ["mode:string", "player:player?", "duration:time?"], - description: "Applies effects to a player's unit.", - perm: commands_1.Perm.admin.exceptModes({ - testsrv: commands_1.Perm.trusted, - }), - handler: function (_a) { - var _b, _c, _d; - var args = _a.args, sender = _a.sender, f = _a.f, outputSuccess = _a.outputSuccess; - if ((_b = args.player) === null || _b === void 0 ? void 0 : _b.hasPerm("blockTrolling")) - (0, commands_1.fail)(f(templateObject_74 || (templateObject_74 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); - if (args.player && !sender.canModerate(args.player, false)) - (0, commands_1.fail)("You do not have permission to perform moderation actions on this player."); - var target = (_c = args.player) !== null && _c !== void 0 ? _c : sender; - var unit = target.unit(); - if (!unit || unit.dead) - (0, commands_1.fail)(f(templateObject_75 || (templateObject_75 = __makeTemplateObject(["", "'s unit is dead."], ["", "'s unit is dead."])), target)); - var ticks = ((_d = args.duration) !== null && _d !== void 0 ? _d : 1e12) / 1000 * 60; - (0, utils_1.applyEffectMode)(args.mode, unit, ticks); - outputSuccess("".concat(args.mode === "clear" ? "Cleared" : "Applied", " effects.")); - if (!config_1.Gamemode.sandbox()) - (0, utils_1.logAction)("applied **".concat(args.mode, "** effects to"), sender, target); - } - }, - items: { - args: ["team:team", "item:item", "amount:number"], - description: "Gives items to a team.", - perm: commands_1.Perm.admin, - requirements: [commands_1.Req.integer("amount")], - handler: function (_a) { - var _b; - var _c = _a.args, team = _c.team, item = _c.item, amount = _c.amount, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; - var core = (_b = team.data().cores.firstOpt()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)(f(templateObject_76 || (templateObject_76 = __makeTemplateObject(["Team ", " has no cores."], ["Team ", " has no cores."])), team)); - core.items.add(item, amount); - outputSuccess(f(templateObject_77 || (templateObject_77 = __makeTemplateObject(["Gave ", " ", " to ", "."], ["Gave ", " ", " to ", "."])), amount, item, team)); - if (!config_1.Gamemode.sandbox()) - (0, utils_1.logAction)("gave ".concat(amount, " ").concat(item.localizedName.toLowerCase(), " to ").concat(team.name), sender); - } - }, - explosion: { - args: ["radius:number", "x:number", "y:number", "team:team?", "damage:number?", "damageMode:string?"], - description: "Causes an explosion at specified coordinates.", - perm: commands_1.Perm.admin, - handler: function (_a) { - var _b; - var _c = _a.args, radius = _c.radius, x = _c.x, y = _c.y, _d = _c.team, team = _d === void 0 ? Team.derelict : _d, _e = _c.damage, damage = _e === void 0 ? 1e12 : _e, _f = _c.damageMode, damageMode = _f === void 0 ? "both" : _f, outputSuccess = _a.outputSuccess; - var _g = __read((_b = (0, utils_1.match)(damageMode, { - air: [true, false], - ground: [false, true], - both: [true, true], - none: [false, false], - })) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Valid values of damageMode: air, ground, both, none"), 2), air = _g[0], ground = _g[1]; - if (radius > 100) - (0, commands_1.fail)("Maximum radius is 100"); - if (damage < 0) - Call.effect(Fx.dynamicSpikes, x * 8, y * 8, radius * 8, Pal.heal); - else - Call.effect(Fx.dynamicExplosion, x * 8, y * 8, Math.max(radius, 8) / 7, Color.white); - Damage.damage(team, x * 8, y * 8, radius * 8, damage, true, air, ground); - outputSuccess("Created an explosion at (".concat(x, ", ").concat(y, ").")); - } - }, - memorycorruption: { - args: [], - description: "Triggers a fake memory corruption prank.", - perm: commands_1.Perm.mod, - requirements: [commands_1.Req.cooldownGlobal(funcs_1.Duration.minutes(30))], - handler: function () { - (0, utils_1.definitelyRealMemoryCorruption)(); - } - }, - editor: { - args: ["editor:boolean"], - description: "Toggles the in-game editor mode.", - perm: commands_1.Perm.trusted, - requirements: [commands_1.Req.mode("testsrv"), commands_1.Req.cooldownGlobal(20000)], - handler: function (_a) { - var editor = _a.args.editor; - Vars.state.rules.editor = editor; - Call.setRules(Vars.state.rules); - } - }, - mapruns: { - args: ["map:map", "lowestHighscores:boolean?"], - description: "Displays all map runs for a selected map, and allows deleting invalid/cheated runs.", - perm: commands_1.Perm.admin, - handler: function (_a) { - return __awaiter(this, arguments, void 0, function (_b) { - var fmap, _c, initialLength, runs, _d, index, _, deleted; - var _e; - var _f = _b.args, map = _f.map, lowestHighscores = _f.lowestHighscores, sender = _b.sender, outputSuccess = _b.outputSuccess; - return __generator(this, function (_g) { - switch (_g.label) { - case 0: - fmap = (_e = maps_1.FMap.getCreate(map)) !== null && _e !== void 0 ? _e : (0, commands_1.fail)("Map data is still loading, please try again."); - if (!(lowestHighscores !== null && lowestHighscores !== void 0)) return [3 /*break*/, 1]; - _c = lowestHighscores; - return [3 /*break*/, 3]; - case 1: return [4 /*yield*/, menus_1.Menu.buttons(sender, "[accent]Map runs", "Select a view", [ - [{ data: true, text: "Lowest highscores" }], - [{ data: false, text: "All runs" }], - ], { - includeCancel: true, - onCancel: "reject" - })]; - case 2: - _c = (lowestHighscores = _g.sent()); - _g.label = 3; - case 3: - _c; - initialLength = fmap.runs.length; - runs = fmap.runs.slice(); - if (lowestHighscores) - runs = runs.filter(function (r) { return r.success; }) - .sort(function (a, b) { return a.duration() - b.duration(); }); - return [4 /*yield*/, menus_1.Menu.textPages(sender, runs.map(function (r) { return [ - (0, utils_1.formatTimestamp)(r.startTime), - function () { - return "Duration: ".concat((0, utils_1.formatTime)(r.duration()), "\nMax player count: ").concat(r.maxPlayerCount, "\nOutcome: ").concat(r.outcome()[1], "\nWave: ").concat(r.wave); - } - ]; }), ["[scarlet]\uE86FDelete"], { - onCancel: "reject" - })]; - case 4: - _d = __read.apply(void 0, [_g.sent(), 2]), index = _d[0], _ = _d[1]; - return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to delete this map run? This action is irreversible.")]; - case 5: - _g.sent(); - if (initialLength != fmap.runs.length) - (0, commands_1.fail)("Someone else deleted a run, please try again."); - deleted = fmap.runs.splice(index, 1)[0]; - outputSuccess("Deleted run (".concat((0, utils_1.formatTimestamp)(deleted.startTime), ") with duration ").concat((0, utils_1.formatTime)(deleted.duration()), ".")); - return [2 /*return*/]; - } - }); - }); - } - } -}); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26, templateObject_27, templateObject_28, templateObject_29, templateObject_30, templateObject_31, templateObject_32, templateObject_33, templateObject_34, templateObject_35, templateObject_36, templateObject_37, templateObject_38, templateObject_39, templateObject_40, templateObject_41, templateObject_42, templateObject_43, templateObject_44, templateObject_45, templateObject_46, templateObject_47, templateObject_48, templateObject_49, templateObject_50, templateObject_51, templateObject_52, templateObject_53, templateObject_54, templateObject_55, templateObject_56, templateObject_57, templateObject_58, templateObject_59, templateObject_60, templateObject_61, templateObject_62, templateObject_63, templateObject_64, templateObject_65, templateObject_66, templateObject_67, templateObject_68, templateObject_69, templateObject_70, templateObject_71, templateObject_72, templateObject_73, templateObject_74, templateObject_75, templateObject_76, templateObject_77; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the in-game chat commands that can be run by trusted staff. +*/ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.commands = void 0; +var api = __importStar(require("/api")); +var config_1 = require("/config"); +var files_1 = require("/files"); +var fjsContext = __importStar(require("/fjsContext")); +var commands_1 = require("/frameworks/commands"); +var menus_1 = require("/frameworks/menus"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var maps_1 = require("/maps"); +var players_1 = require("/players"); +var ranks_1 = require("/ranks"); +var utils_1 = require("/utils"); +exports.commands = (0, commands_1.commandList)({ + warn: { + args: ['player:player', 'message:string?'], + description: 'Sends the player a warning (menu popup).', + perm: commands_1.Perm.warn, + requirements: [commands_1.Req.cooldown(3000)], + handler: function (_a) { + var _b; + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + if (args.player.hasPerm("blockTrolling")) + (0, commands_1.fail)(f(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); + var message = (_b = args.message) !== null && _b !== void 0 ? _b : "You have been warned. I suggest you stop what you're doing"; + void menus_1.Menu.menu('Warning', message, ["[green]Accept"], args.player, { onCancel: 'null' }) + .then(function () { return outputSuccess('Player acknowledged the warning.'); }); + (0, utils_1.logAction)('warned', sender, args.player, message); + outputSuccess(f(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Warned player ", " for \"", "\""], ["Warned player ", " for \"", "\""])), args.player, message)); + } + }, + mute: { + args: ['player:player'], + description: 'Stops a player from chatting.', + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("player")], + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (args.player.muted) + (0, commands_1.fail)(f(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Player ", " is already muted."], ["Player ", " is already muted."])), args.player)); + return [4 /*yield*/, args.player.mute(sender)]; + case 1: + _c.sent(); + (0, utils_1.logAction)('muted', sender, args.player); + outputSuccess(f(templateObject_4 || (templateObject_4 = __makeTemplateObject(["Muted player ", "."], ["Muted player ", "."])), args.player)); + return [2 /*return*/]; + } + }); + }); + } + }, + unmute: { + args: ['player:player'], + description: 'Unmutes a player', + perm: commands_1.Perm.mod, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!args.player.muted && args.player.autoflagged) + (0, commands_1.fail)(f(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."], ["Player ", " is not muted, but they are autoflagged. You probably want to free them with /free."])), args.player)); + if (!args.player.muted) + (0, commands_1.fail)(f(templateObject_6 || (templateObject_6 = __makeTemplateObject(["Player ", " is not muted."], ["Player ", " is not muted."])), args.player)); + return [4 /*yield*/, args.player.unmute(sender)]; + case 1: + _c.sent(); + (0, utils_1.logAction)('unmuted', sender, args.player); + outputSuccess(f(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Unmuted player ", "."], ["Unmuted player ", "."])), args.player)); + return [2 /*return*/]; + } + }); + }); + } + }, + kick: { + args: ["player:player", "duration:time?", "reason:string?"], + description: 'Kick a player with optional reason.', + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("player")], + handler: function (_a) { + var _b, _c, _d; + var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f, sender = _a.sender; + if (!sender.hasPerm("admin") && args.duration && args.duration > funcs_1.Duration.hours(6)) + (0, commands_1.fail)("Maximum kick duration is 6 hours."); + var reason = (_b = args.reason) !== null && _b !== void 0 ? _b : "A staff member did not like your actions."; + var duration = (_c = args.duration) !== null && _c !== void 0 ? _c : 60000; + args.player.kick(reason, duration); + (0, utils_1.logAction)("kicked", sender, args.player, (_d = args.reason) !== null && _d !== void 0 ? _d : undefined, duration); + if (duration > 60000) + args.player.setPunishedIP(config_1.stopAntiEvadeTime); + outputSuccess(f(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Kicked player ", " for ", " with reason \"", "\""], ["Kicked player ", " for ", " with reason \"", "\""])), args.player, (0, utils_1.formatTime)(duration), reason)); + } + }, + pardon: { + args: ["player:offlinePlayer"], + description: 'Pardons a votekicked player.', + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("player")], + handler: function (_a) { + var player = _a.args.player, admins = _a.admins, outputSuccess = _a.outputSuccess, f = _a.f; + var info = admins.getInfo(player.uuid); + if (Time.millis() > info.lastKicked && !admins.kickedIPs.containsKey(info.lastIP)) + (0, commands_1.fail)("That player is not kicked."); + info.lastKicked = 0; + admins.kickedIPs.remove(info.lastIP); + outputSuccess(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Pardoned player ", "."], ["Pardoned player ", "."])), player)); + } + }, + stop: { + args: ['player:player', "time:time?", "message:string?"], + description: 'Stops a player.', + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("player", true)], + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var previousTime, time; + var _c, _d, _e, _f; + var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + if (!args.player.marked()) return [3 /*break*/, 2]; + //overload: overwrite stoptime + if (!args.time) + (0, commands_1.fail)(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Player ", " is already marked."], ["Player ", " is already marked."])), args.player)); + previousTime = (0, utils_1.formatTimeRelative)(args.player.unmarkTime, true); + return [4 /*yield*/, args.player.updateStopTime(args.time)]; + case 1: + _g.sent(); + outputSuccess(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Player ", "'s stop time has been updated to ", " (was ", ")."], ["Player ", "'s stop time has been updated to ", " (was ", ")."])), args.player, (0, utils_1.formatTime)(args.time), previousTime)); + (0, utils_1.logAction)("updated stop time of", sender, args.player, (_c = args.message) !== null && _c !== void 0 ? _c : undefined, args.time); + return [3 /*break*/, 4]; + case 2: + time = (_d = args.time) !== null && _d !== void 0 ? _d : (0, utils_1.untilForever)(); + if (time + Date.now() > globals_1.maxTime) + (0, commands_1.fail)("Error: time too high."); + return [4 /*yield*/, args.player.stop(sender, time, (_e = args.message) !== null && _e !== void 0 ? _e : undefined)]; + case 3: + _g.sent(); + (0, utils_1.logAction)('stopped', sender, args.player, (_f = args.message) !== null && _f !== void 0 ? _f : undefined, time); + //TODO outputGlobal() + Call.sendMessage("[orange]Player \"".concat(args.player.prefixedName, "[orange]\" has been marked for ").concat((0, utils_1.formatTime)(time)).concat(args.message ? " with reason: [white]".concat(args.message, "[]") : "", ".")); + _g.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); + } + }, + free: { + args: ['player:player'], + description: 'Frees a player.', + perm: commands_1.Perm.mod, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, outputFail = _b.outputFail, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!args.player.marked()) return [3 /*break*/, 2]; + return [4 /*yield*/, args.player.free(sender)]; + case 1: + _c.sent(); + (0, utils_1.logAction)('freed', sender, args.player); + outputSuccess(f(templateObject_12 || (templateObject_12 = __makeTemplateObject(["Player ", " has been unmarked."], ["Player ", " has been unmarked."])), args.player)); + return [3 /*break*/, 3]; + case 2: + if (args.player.autoflagged) { + args.player.autoflagged = false; + args.player.sendMessage("[yellow]You have been freed! Enjoy!"); + args.player.updateName(); + args.player.forceRespawn(); + outputSuccess(f(templateObject_13 || (templateObject_13 = __makeTemplateObject(["Player ", " has been unflagged."], ["Player ", " has been unflagged."])), args.player)); + } + else { + outputFail(f(templateObject_14 || (templateObject_14 = __makeTemplateObject(["Player ", " is not marked or autoflagged."], ["Player ", " is not marked or autoflagged."])), args.player)); + } + _c.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); + } + }, + setrank: { + args: ["player:player", "rank:rank"], + description: "Set a player's rank.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("player")], + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var _c = _b.args, rank = _c.rank, player = _c.player, outputSuccess = _b.outputSuccess, f = _b.f, sender = _b.sender; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + if (rank.level >= sender.rank.level) + (0, commands_1.fail)(f(templateObject_15 || (templateObject_15 = __makeTemplateObject(["You do not have permission to promote players to rank ", ", because your current rank is ", ""], ["You do not have permission to promote players to rank ", ", because your current rank is ", ""])), rank, sender.rank)); + if (rank == ranks_1.Rank.pi && !config_1.Mode.localDebug) + (0, commands_1.fail)(f(templateObject_16 || (templateObject_16 = __makeTemplateObject(["Rank ", " is immutable."], ["Rank ", " is immutable."])), rank)); + if (player.immutable() && !config_1.Mode.localDebug) + (0, commands_1.fail)(f(templateObject_17 || (templateObject_17 = __makeTemplateObject(["Player ", " is immutable."], ["Player ", " is immutable."])), player)); + if (!(player == sender && rank.level < sender.rank.level)) return [3 /*break*/, 2]; + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "[red] ARE YOU SURE YOU WANT TO SELF DEMOTE. THIS ACTION CANNOT BE UNDONE!")]; + case 1: + _d.sent(); + _d.label = 2; + case 2: return [4 /*yield*/, player.setRank(rank)]; + case 3: + _d.sent(); + (0, utils_1.logAction)("set rank to ".concat(rank.name, " for"), sender, player); + outputSuccess(f(templateObject_18 || (templateObject_18 = __makeTemplateObject(["Set rank of player ", " to ", ""], ["Set rank of player ", " to ", ""])), player, rank)); + return [2 /*return*/]; + } + }); + }); + } + }, + setflag: { + args: ["player:player", "flag:roleflag", "value:boolean"], + description: "Set a player's role flags.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("player")], + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var _c = _b.args, flag = _c.flag, player = _c.player, value = _c.value, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + if (!sender.hasPerm("admin") && !flag.assignableByModerators) + (0, commands_1.fail)(f(templateObject_19 || (templateObject_19 = __makeTemplateObject(["You do not have permission to change the value of role flag ", ""], ["You do not have permission to change the value of role flag ", ""])), flag)); + return [4 /*yield*/, player.setFlag(flag, value)]; + case 1: + _d.sent(); + (0, utils_1.logAction)("set roleflag ".concat(flag.name, " to ").concat(value, " for"), sender, player); + outputSuccess(f(templateObject_20 || (templateObject_20 = __makeTemplateObject(["Set role flag ", " of player ", " to ", ""], ["Set role flag ", " of player ", " to ", ""])), flag, player, value)); + return [2 /*return*/]; + } + }); + }); + } + }, + murder: { + args: [], + description: 'Kills all ohno units', + perm: commands_1.Perm.mod, + customUnauthorizedMessage: "[yellow]You're a [scarlet]monster[].", + handler: function (_a) { + var output = _a.output, f = _a.f, allCommands = _a.allCommands; + var Ohnos = allCommands["ohno"].data; //this is not ideal... TODO commit omega shenanigans + var numOhnos = Ohnos.amount(); + Ohnos.killAll(); + output(f(templateObject_21 || (templateObject_21 = __makeTemplateObject(["[orange]You massacred ", " helpless ohno crawlers."], ["[orange]You massacred ", " helpless ohno crawlers."])), numOhnos)); + } + }, + stop_offline: { + args: ["time:time?", "name:string?"], + description: "Stops an offline player.", + perm: commands_1.Perm.mod, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + function stop(option, time) { + return __awaiter(this, void 0, void 0, function () { + var fishP; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + fishP = players_1.FishPlayer.getFromInfo(option); + if (!sender.canModerate(fishP, true)) return [3 /*break*/, 2]; + (0, utils_1.logAction)(fishP.marked() ? time == 1000 ? "freed" : "updated stop time of" : "stopped", sender, option, undefined, time); + return [4 /*yield*/, fishP.stop(sender, time)]; + case 1: + _a.sent(); + outputSuccess(f(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Player ", " was marked for ", "."], ["Player ", " was marked for ", "."])), option, (0, utils_1.formatTime)(time))); + return [3 /*break*/, 3]; + case 2: + outputFail("You do not have permission to stop this player."); + _a.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); + } + var maxPlayers, info, possiblePlayers, exactPlayers, score_1, optionPlayer, _c, _d, _e; + var _f, _g; + var args = _b.args, sender = _b.sender, outputFail = _b.outputFail, outputSuccess = _b.outputSuccess, f = _b.f, admins = _b.admins; + return __generator(this, function (_h) { + switch (_h.label) { + case 0: + maxPlayers = 60; + if (!(args.name && globals_1.uuidPattern.test(args.name))) return [3 /*break*/, 4]; + info = admins.getInfoOptional(args.name); + if (!(info != null)) return [3 /*break*/, 2]; + return [4 /*yield*/, stop(info, (_f = args.time) !== null && _f !== void 0 ? _f : (0, utils_1.untilForever)())]; + case 1: + _h.sent(); + return [3 /*break*/, 3]; + case 2: + outputFail(f(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); + _h.label = 3; + case 3: return [2 /*return*/]; + case 4: + if (args.name) { + possiblePlayers = (0, funcs_1.setToArray)(admins.searchNames(args.name)); + if (possiblePlayers.length > maxPlayers) { + exactPlayers = (0, funcs_1.setToArray)(admins.findByName(args.name)); + if (exactPlayers.length > 0) { + possiblePlayers = exactPlayers; + } + else { + (0, commands_1.fail)("Too many players with that name."); + } + } + else if (possiblePlayers.length == 0) { + (0, commands_1.fail)("No players with that name were found."); + } + score_1 = function (data) { + var fishP = players_1.FishPlayer.getById(data.id); + if (fishP) + return fishP.lastJoined; + return -data.timesJoined; + }; + possiblePlayers.sort(function (a, b) { return score_1(b) - score_1(a); }); + } + else { + possiblePlayers = players_1.FishPlayer.recentLeaves.map(function (p) { return p.info(); }); + } + return [4 /*yield*/, menus_1.Menu.menu("Stop", "Choose a player to mark", possiblePlayers, sender, { + includeCancel: true, + optionStringifier: function (p) { return p.lastName; } + })]; + case 5: + optionPlayer = _h.sent(); + if (!((_g = args.time) !== null && _g !== void 0)) return [3 /*break*/, 6]; + _c = _g; + return [3 /*break*/, 8]; + case 6: + _d = args; + _e = utils_1.match; + return [4 /*yield*/, menus_1.Menu.menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender)]; + case 7: + _c = (_d.time = _e.apply(void 0, [_h.sent(), { + "2 days": funcs_1.Duration.days(2), + "7 days": funcs_1.Duration.days(7), + "30 days": funcs_1.Duration.days(30), + "forever": globals_1.maxTime - Date.now() - 10000, + }])); + _h.label = 8; + case 8: + _c; + return [4 /*yield*/, stop(optionPlayer, args.time)]; + case 9: + _h.sent(); + return [2 /*return*/]; + } + }); + }); + } + }, + mute_offline: { + args: ["name:string?"], + description: "Mutes an offline player.", + perm: commands_1.Perm.mod, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + function mute(option) { + return __awaiter(this, void 0, void 0, function () { + var fishP; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + fishP = players_1.FishPlayer.getFromInfo(option); + if (!sender.canModerate(fishP, true)) + (0, commands_1.fail)("You do not have permission to mute this player."); + return [4 /*yield*/, menus_1.Menu.confirm(sender, "Are you sure you want to ".concat(fishP.muted ? "unmute" : "mute", " player ").concat(option.lastName, "?"), { + title: "Mute Offine Confirmation", + confirmText: "[green]Yes, ".concat(fishP.muted ? "unmute" : "mute", " them"), + })]; + case 1: + _a.sent(); + (0, utils_1.logAction)(fishP.muted ? "unmuted" : "muted", sender, fishP); + if (!fishP.muted) return [3 /*break*/, 3]; + return [4 /*yield*/, fishP.unmute(sender)]; + case 2: + _a.sent(); + return [3 /*break*/, 5]; + case 3: return [4 /*yield*/, fishP.mute(sender)]; + case 4: + _a.sent(); + _a.label = 5; + case 5: + outputSuccess("".concat(fishP.muted ? "Muted" : "Unmuted", " ").concat(option.lastName, ".")); + return [2 /*return*/]; + } + }); + }); + } + var maxPlayers, info, possiblePlayers, exactPlayers, score_2, option; + var _c; + var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f, admins = _b.admins; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + maxPlayers = 300; + if (!(args.name && globals_1.uuidPattern.test(args.name))) return [3 /*break*/, 2]; + info = (_c = admins.getInfoOptional(args.name)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(f(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); + return [4 /*yield*/, mute(info)]; + case 1: + _d.sent(); + return [2 /*return*/]; + case 2: + if (args.name) { + possiblePlayers = (0, funcs_1.setToArray)(admins.searchNames(args.name)); + if (possiblePlayers.length > maxPlayers) { + exactPlayers = (0, funcs_1.setToArray)(admins.findByName(args.name)); + if (exactPlayers.length > 0) { + possiblePlayers = exactPlayers; + } + else { + (0, commands_1.fail)("Too many players with that name."); + } + } + else if (possiblePlayers.length == 0) { + (0, commands_1.fail)("No players with that name were found."); + } + score_2 = function (data) { + var fishP = players_1.FishPlayer.getById(data.id); + if (fishP) + return fishP.lastJoined; + return -data.timesJoined; + }; + possiblePlayers.sort(function (a, b) { return score_2(b) - score_2(a); }); + } + else { + possiblePlayers = players_1.FishPlayer.recentLeaves.map(function (p) { return p.info(); }); + } + return [4 /*yield*/, menus_1.Menu.pagedList(sender, "Mute", "Choose a player to mute", possiblePlayers, { + optionStringifier: function (p) { return p.lastName; } + })]; + case 3: + option = _d.sent(); + return [4 /*yield*/, mute(option)]; + case 4: + _d.sent(); + return [2 /*return*/]; + } + }); + }); + } + }, + restart: { + args: ["time:number?"], + perm: commands_1.Perm.admin, + description: "Restarts the server.", + handler: function (_a) { + var _b; + var time = _a.args.time; + (_b = globals_1.fishState.restartLoopTask) === null || _b === void 0 ? void 0 : _b.cancel(); + if (Groups.player.isEmpty()) { + if (time == undefined) { + Log.info("Restarting immediately as no players are online."); + time !== null && time !== void 0 ? time : (time = 0); + } + } + else if (config_1.Gamemode.pvp()) { + time !== null && time !== void 0 ? time : (time = -1); + Log.info("PVP: restart will occur at the end of the current match. Specify a time to override, but &rthat would interrupt the current pvp match, and players would lose their teams.&fr"); + } + else { + time !== null && time !== void 0 ? time : (time = 60); + } + if (time == -1) { + Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart queued. The server will restart after the current match is over.[]\n[accent]---[[[coral]+++[]]---"); + globals_1.fishState.restartQueued = true; + } + else { + if (time < 0 || time > 100) + (0, commands_1.fail)("Invalid time: out of valid range."); + (0, utils_1.serverRestartLoop)(time); + if (config_1.Gamemode.pvp()) { + Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 15 seconds.[]\n[accent]---[[[coral]+++[]]---"); + } + else { + Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back with 20 seconds of downtime, and all progress will be saved.[]\n[accent]---[[[coral]+++[]]---"); + } + } + } + }, + history: { + args: ["player:player"], + description: "Shows moderation history for a player.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var args = _a.args, output = _a.output, f = _a.f; + if (args.player.history && args.player.history.length > 0) { + output("[yellow]_______________Player history_______________\n\n" + + (args.player).history.sort(function (a, b) { return a.time - b.time; }).map(function (e) { + return "".concat(e.by, " [yellow]").concat(e.action, " ").concat(args.player.prefixedName, " [white]").concat((0, utils_1.formatTimeRelative)(e.time)); + }).join("\n")); + } + else { + output(f(templateObject_25 || (templateObject_25 = __makeTemplateObject(["[yellow]No history was found for player ", "."], ["[yellow]No history was found for player ", "."])), args.player)); + } + } + }, + save: { + args: [], + description: "Saves the game state.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var outputSuccess = _a.outputSuccess; + players_1.FishPlayer.saveAll(); + players_1.FishPlayer.uploadAll(); + globals_1.FishEvents.fire("saveData", []); + var file = Vars.saveDirectory.child("1.".concat(Vars.saveExtension)); + SaveIO.save(file); + outputSuccess("Game saved."); + } + }, + wave: { + args: ["wave:number"], + description: "Sets the wave number.", + perm: commands_1.Perm.admin, + requirements: [commands_1.Req.positiveInteger("wave")], + handler: function (_a) { + var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; + Vars.state.wave = args.wave; + outputSuccess(f(templateObject_26 || (templateObject_26 = __makeTemplateObject(["Set wave to ", ""], ["Set wave to ", ""])), Vars.state.wave)); + } + }, + label: { + args: ["time:time", "message:string"], + description: "Places a label at your position for a specified amount of time.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var _b; + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + if (args.time > funcs_1.Duration.hours(10)) + (0, commands_1.fail)("Time must be less than 10 hours."); + var unit = (_b = sender.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("You must be in a unit to use this command."); + var timeRemaining = args.time / 1000; + var labelx = unit.x; + var labely = unit.y; + var task = Timer.schedule(function () { + if (timeRemaining > 0) { + var timeseconds = timeRemaining % 60; + var timeminutes = (timeRemaining - timeseconds) / 60; + Call.label("".concat(sender.name, "\n\n[white]").concat(args.message, "\n\n[acid]").concat(timeminutes.toString().padStart(2, "0"), ":").concat(timeseconds.toString().padStart(2, "0")), 1, labelx, labely); + timeRemaining--; + } + }, 0, 1, args.time); + globals_1.fishState.labels.push({ x: labelx, y: labely, task: task }); + outputSuccess(f(templateObject_27 || (templateObject_27 = __makeTemplateObject(["Placed label \"", "\" for ", " seconds."], ["Placed label \"", "\" for ", " seconds."])), args.message, timeRemaining)); + } + }, + labelsticky: { + args: ["time:time", "message:string"], + description: "Places a label at the bottom left corner of everyone's screen.", + perm: commands_1.Perm.admin, + handler: function (_a) { + var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; + if (args.time > funcs_1.Duration.hours(10)) + (0, commands_1.fail)("Time must be less than 10 hours."); + var timeRemaining = args.time / 1000; + var task = Timer.schedule(function () { + if (timeRemaining > 0) { + Call.label(args.message, 5, NaN, NaN); + timeRemaining -= 5; + } + }, 0, 5, Math.ceil(args.time / 5)); + globals_1.fishState.labels.push({ task: task, x: null, y: null }); + outputSuccess(f(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Placed label \"", "\" for ", " seconds."], ["Placed label \"", "\" for ", " seconds."])), args.message, timeRemaining)); + } + }, + clearlabels: { + args: [], + description: "Removes all labels.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var outputSuccess = _a.outputSuccess; + if (globals_1.fishState.labels.length == 0) + (0, commands_1.fail)("No labels found."); + globals_1.fishState.labels.forEach(function (l) { return l.task.cancel(); }); + outputSuccess("Removed all labels."); + } + }, + clearlabel: { + args: ["sticky:boolean?"], + description: "Removes the closest label, or sticky label if specified", + perm: commands_1.Perm.mod, + handler: function (_a) { + var _b; + var _c = _a.args.sticky, sticky = _c === void 0 ? false : _c, sender = _a.sender, outputSuccess = _a.outputSuccess; + if (globals_1.fishState.labels.length == 0) + (0, commands_1.fail)("No labels found."); + var label; + if (sticky) { + var index = globals_1.fishState.labels.findIndex(function (l) { return l.x == null; }); + if (index == -1) + (0, commands_1.fail)("No sticky label found."); + label = globals_1.fishState.labels.splice(index, 1)[0]; + } + else { + var unit_1 = (_b = sender.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Cannot remove the closest label because you are dead."); + var dist_1 = function (label) { + if (label.x == null || label.y == null) + return Infinity; + return Mathf.dst(label.x, label.y, unit_1.x, unit_1.y); + }; + var index = __spreadArray([], __read(globals_1.fishState.labels.entries()), false).reduce(function (a, b) { return dist_1(a[1]) < dist_1(b[1]) ? a : b; })[0]; + label = globals_1.fishState.labels.splice(index, 1)[0]; + } + label.task.cancel(); + outputSuccess("Removed one label."); + } + }, + member: { + args: ["value:boolean", "player:player"], + description: "Sets a player's member status.", + perm: commands_1.Perm.admin, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var args = _b.args, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, args.player.setFlag("member", args.value)]; + case 1: + _c.sent(); + outputSuccess(f(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Set membership status of player ", " to ", "."], ["Set membership status of player ", " to ", "."])), args.player, args.value)); + return [2 /*return*/]; + } + }); + }); + } + }, + remind: { + args: ["rule:number", "target:player?"], + description: "Remind players in chat of a specific rule.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var _b; + var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; + var rule = (_b = config_1.rules[args.rule - 1]) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("The rule you requested does not exist."); + if (args.target) { + args.target.sendMessage("A staff member wants to remind you of the following rule:\n" + rule); + outputSuccess(f(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Reminded ", " of rule ", ""], ["Reminded ", " of rule ", ""])), args.target, args.rule)); + } + else { + Call.sendMessage("A staff member wants to remind everyone of the following rule:\n" + rule); + } + }, + }, + ban: { + args: ["uuid_or_ip:string?"], + description: "Bans a player by UUID and IP.", + perm: commands_1.Perm.admin, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var uuid, data, name, ip, ip, info, alreadyBanned, option; + var args = _b.args, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f, admins = _b.admins; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!(args.uuid_or_ip && globals_1.uuidPattern.test(args.uuid_or_ip))) return [3 /*break*/, 2]; + uuid = args.uuid_or_ip; + data = void 0; + if ((data = admins.getInfoOptional(uuid)) != null && data.admin) + (0, commands_1.fail)("Cannot ban an admin."); + name = data ? "".concat((0, funcs_1.escapeStringColorsClient)(data.lastName), " (").concat(uuid, "/").concat(data.lastIP, ")") : uuid; + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to ban ".concat(name, "?"))]; + case 1: + _c.sent(); + admins.banPlayerID(uuid); + if (data) { + ip = data.lastIP; + admins.banPlayerIP(ip); + api.ban({ ip: ip, uuid: uuid }); + Log.info("".concat(uuid, "/").concat(ip, " was banned.")); + (0, utils_1.logAction)("banned", sender, data); + outputSuccess(f(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Banned player ", " (", "/", ")"], ["Banned player ", " (", "/", ")"])), (0, funcs_1.escapeStringColorsClient)(data.lastName), uuid, ip)); + //TODO add way to specify whether to activate or escape color tags + } + else { + api.ban({ uuid: uuid }); + Log.info("".concat(uuid, " was banned.")); + (0, utils_1.logAction)("banned", sender, uuid); + outputSuccess(f(templateObject_32 || (templateObject_32 = __makeTemplateObject(["Banned player ", ". [yellow]Unable to determine IP.[]"], ["Banned player ", ". [yellow]Unable to determine IP.[]"])), uuid)); + } + (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); + return [2 /*return*/]; + case 2: + if (!(args.uuid_or_ip && globals_1.ipPattern.test(args.uuid_or_ip))) return [3 /*break*/, 4]; + ip = args.uuid_or_ip; + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to ban IP ".concat(ip, "?"))]; + case 3: + _c.sent(); + api.ban({ ip: ip }); + info = admins.findByIP(ip); + if (info) + (0, utils_1.logAction)("banned", sender, info); + else + (0, utils_1.logAction)("banned ".concat(ip), sender); + alreadyBanned = admins.banPlayerIP(ip); + if (alreadyBanned) { + outputSuccess(f(templateObject_33 || (templateObject_33 = __makeTemplateObject(["IP ", " is already banned. Ban was synced to other servers."], ["IP ", " is already banned. Ban was synced to other servers."])), ip)); + } + else { + outputSuccess(f(templateObject_34 || (templateObject_34 = __makeTemplateObject(["IP ", " has been banned. Ban was synced to other servers."], ["IP ", " has been banned. Ban was synced to other servers."])), ip)); + } + (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); + return [2 /*return*/]; + case 4: return [4 /*yield*/, menus_1.Menu.menu("[scarlet]BAN[]", "Choose a player to ban.", (0, funcs_1.setToArray)(Groups.player), sender, { + includeCancel: true, + optionStringifier: function (opt) { return opt.name; } + })]; + case 5: + option = _c.sent(); + if (option.admin) + (0, commands_1.fail)("Cannot ban an admin."); + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to ban ".concat(option.name, "?"))]; + case 6: + _c.sent(); + admins.banPlayerIP(option.ip()); //this also bans the UUID + api.ban({ ip: option.ip(), uuid: option.uuid() }); + Log.info("".concat(option.ip(), "/").concat(option.uuid(), " was banned.")); + (0, utils_1.logAction)("banned", sender, option.getInfo()); + outputSuccess(f(templateObject_35 || (templateObject_35 = __makeTemplateObject(["Banned player ", "."], ["Banned player ", "."])), option)); + (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); + return [2 /*return*/]; + } + }); + }); + } + }, + kill: { + args: ["player:player"], + description: "Kills a player's unit.", + perm: commands_1.Perm.admin, + requirements: [commands_1.Req.moderate("player", true)], + handler: function (_a) { + var args = _a.args, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess, f = _a.f; + var unit = args.player.unit(); + if (unit) { + unit.kill(); + outputSuccess(f(templateObject_36 || (templateObject_36 = __makeTemplateObject(["Killed the unit of player ", "."], ["Killed the unit of player ", "."])), args.player)); + } + else { + outputFail(f(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Player ", " does not have a unit."], ["Player ", " does not have a unit."])), args.player)); + } + } + }, + killunits: { + args: ["team:team?", "unit:unittype?"], + description: "Kills all units, optionally specifying a team and unit type.", + perm: commands_1.Perm.massKill, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var i_1, before, i_2, before; + var _c = _b.args, team = _c.team, unit = _c.unit, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + if (!team) return [3 /*break*/, 2]; + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every ".concat(unit ? unit.localizedName : "unit", "[] on the team ").concat(team.coloredName(), "."), { confirmText: "[orange]Kill units[]" })]; + case 1: + _d.sent(); + if (unit) { + i_1 = 0; + team.data().units.each(function (u) { return u.type == unit; }, function (u) { + u.kill(); + i_1++; + }); + outputSuccess(f(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Killed ", " units on ", "."], ["Killed ", " units on ", "."])), i_1, team)); + } + else { + before = team.data().units.size; + team.data().units.each(function (u) { return u.kill(); }); + outputSuccess(f(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Killed ", " units on ", "."], ["Killed ", " units on ", "."])), before, team)); + } + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every single ".concat(unit ? unit.localizedName : "unit", "[]."), { confirmText: "[orange]Kill all units[]" })]; + case 3: + _d.sent(); + if (unit) { + i_2 = 0; + Groups.unit.each(function (u) { return u.type == unit; }, function (u) { + u.kill(); + i_2++; + }); + outputSuccess(f(templateObject_40 || (templateObject_40 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), i_2)); + } + else { + before = Groups.unit.size(); + Groups.unit.each(function (u) { return u.kill(); }); + outputSuccess(f(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), before)); + } + _d.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); + } + }, + killbuildings: { + args: ["team:team?"], + description: "Kills all buildings (except cores), optionally specifying a team.", + perm: commands_1.Perm.massKill, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var count, count; + var team = _b.args.team, sender = _b.sender, outputSuccess = _b.outputSuccess, f = _b.f; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!team) return [3 /*break*/, 2]; + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every building[] on the team ".concat(team.coloredName(), ", except cores."), { confirmText: "[orange]Kill buildings[]" })]; + case 1: + _c.sent(); + count = team.data().buildings.size; + team.data().buildings.each(function (b) { return !(b.block instanceof CoreBlock); }, function (b) { return b.tile.remove(); }); + outputSuccess(f(templateObject_42 || (templateObject_42 = __makeTemplateObject(["Killed ", " buildings on ", "."], ["Killed ", " buildings on ", "."])), count, team)); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "This will kill [scarlet]every building[] except cores.", { confirmText: "[orange]Kill buildings[]" })]; + case 3: + _c.sent(); + count = Groups.build.size(); + Groups.build.each(function (b) { return !(b.block instanceof CoreBlock); }, function (b) { return b.tile.remove(); }); + outputSuccess(f(templateObject_43 || (templateObject_43 = __makeTemplateObject(["Killed ", " buildings."], ["Killed ", " buildings."])), count)); + _c.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); + } + }, + respawn: { + args: ["player:player"], + description: "Forces a player to respawn.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("player", true, "mod", true)], + handler: function (_a) { + var args = _a.args, outputSuccess = _a.outputSuccess, f = _a.f; + args.player.forceRespawn(); + outputSuccess(f(templateObject_44 || (templateObject_44 = __makeTemplateObject(["Respawned player ", "."], ["Respawned player ", "."])), args.player)); + } + }, + clearunit: { + args: ["target:player", "duration:time?"], + description: "Forces a player out of the unit they are controlling, and blocks them from possessing units for a specified duration.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("target", false, "mod", false)], + handler: function (_a) { + var _b = _a.args, target = _b.target, duration = _b.duration, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + if (Date.now() > 1000 + target.blockedFromPossessingUnitsUntil) + duration !== null && duration !== void 0 ? duration : (duration = funcs_1.Duration.minutes(1)); + else + duration !== null && duration !== void 0 ? duration : (duration = 0); + if (duration == 0) { + target.blockedFromPossessingUnitsUntil = 0; + target.sendMessage("You are allowed to control units again."); + outputSuccess(f(templateObject_45 || (templateObject_45 = __makeTemplateObject(["Restored ", "'s ability to control units."], ["Restored ", "'s ability to control units."])), target)); + (0, utils_1.logAction)("restored unit possession for", sender, target); + } + else { + target.forceRespawn(); + target.blockedFromPossessingUnitsUntil = Date.now() + duration; + target.sendMessage("You have been blocked from controlling units for ".concat((0, utils_1.formatTime)(duration), ".")); + outputSuccess(f(templateObject_46 || (templateObject_46 = __makeTemplateObject(["Blocked ", " from controlling units for ", "."], ["Blocked ", " from controlling units for ", "."])), target, (0, utils_1.formatTime)(duration))); + (0, utils_1.logAction)("revoked unit possession for", sender, target, undefined, duration); + } + } + }, + clearcommand: { + args: ["target:player", "duration:time?"], + description: "Blocks a player from commanding units for a specified duration.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("target", false, "mod", false)], + handler: function (_a) { + var _b = _a.args, target = _b.target, duration = _b.duration, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + if (Date.now() > 1000 + target.blockedFromCommandingUnitsUntil) + duration !== null && duration !== void 0 ? duration : (duration = funcs_1.Duration.minutes(1)); + else + duration !== null && duration !== void 0 ? duration : (duration = 0); + if (duration == 0) { + target.blockedFromCommandingUnitsUntil = 0; + target.sendMessage("You are allowed to command units again."); + outputSuccess(f(templateObject_47 || (templateObject_47 = __makeTemplateObject(["Restored ", "'s ability to command units."], ["Restored ", "'s ability to command units."])), target)); + (0, utils_1.logAction)("restored command mode for", sender, target, undefined, duration); + } + else { + target.blockedFromCommandingUnitsUntil = Date.now() + duration; + target.sendMessage("You have been blocked from commanding units for ".concat((0, utils_1.formatTime)(duration), ".")); + outputSuccess(f(templateObject_48 || (templateObject_48 = __makeTemplateObject(["Blocked ", " from commanding units for ", "."], ["Blocked ", " from commanding units for ", "."])), target, (0, utils_1.formatTime)(duration))); + (0, utils_1.logAction)("revoked command mode for", sender, target, undefined, duration); + } + } + }, + stealunit: { + args: ["target:player", "newcontroller:player?"], + description: "Steals the unit of a player, putting you in their unit and forcing them to respawn.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.moderate("target", true, "mod", true), commands_1.Req.moderate("newcontroller", true, "mod", true)], + handler: function (_a) { + var _b; + var sender = _a.sender, _c = _a.args, target = _c.target, _d = _c.newcontroller, newcontroller = _d === void 0 ? sender : _d, outputSuccess = _a.outputSuccess, f = _a.f; + var unit = (_b = target.unit()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)(f(templateObject_49 || (templateObject_49 = __makeTemplateObject(["Targeted player ", " is not in a unit."], ["Targeted player ", " is not in a unit."])), target)); + if (target.team() !== newcontroller.team()) { + if (!sender.hasPerm("changeTeamExternal")) { + if (!sender.hasPerm("changeTeam")) + (0, commands_1.fail)("You do not have permission to change teams."); + newcontroller.setTeam(unit.team); + } + } + target.forceRespawn(); + newcontroller.unit(unit); + if (newcontroller == sender) { + outputSuccess(f(templateObject_50 || (templateObject_50 = __makeTemplateObject(["Commandeered the unit of player ", "."], ["Commandeered the unit of player ", "."])), target)); + } + else { + outputSuccess(f(templateObject_51 || (templateObject_51 = __makeTemplateObject(["Transferred player ", " into the unit of ", "."], ["Transferred player ", " into the unit of ", "."])), newcontroller, target)); + newcontroller.sendMessage(f(templateObject_52 || (templateObject_52 = __makeTemplateObject(["[green]You were transferred to the unit of player ", " by ", "."], ["[green]You were transferred to the unit of player ", " by ", "."])), target, sender)('[green]')); + } + } + }, + m: { + args: ["message:string"], + description: "Sends a message to muted players only.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var sender = _a.sender, args = _a.args; + players_1.FishPlayer.messageMuted(sender.prefixedName, args.message); + } + }, + info: { + args: ["target:player", "showColors:boolean?"], + description: "Displays information about an online player.", + perm: commands_1.Perm.none, + handler: function (_a) { + var sender = _a.sender, args = _a.args, output = _a.output, f = _a.f; + var info = args.target.info(); + var names = args.showColors + ? info.names.map(funcs_1.escapeStringColorsClient).toString(", ") + : __spreadArray([], __read(new Set(info.names.map(function (n) { return Strings.stripColors(n); }).toArray())), false).join(", "); + output(f(templateObject_53 || (templateObject_53 = __makeTemplateObject(["[accent]Info for player ", " [gray](", ") (#", ")\n\t[accent]Rank: ", "\n\t[accent]Role flags: ", "\n\t[accent]Stopped: ", "\n\t[accent]marked: ", "\n\t[accent]muted: ", "\n\t[accent]autoflagged: ", "\n\t[accent]VPN detected: ", "\n\t[accent]times joined / kicked: ", "/", "\n\t[accent]First joined: ", "\n\t[accent]Names used: [[", "]"], ["\\\n[accent]Info for player ", " [gray](", ") (#", ")\n\t[accent]Rank: ", "\n\t[accent]Role flags: ", "\n\t[accent]Stopped: ", "\n\t[accent]marked: ", "\n\t[accent]muted: ", "\n\t[accent]autoflagged: ", "\n\t[accent]VPN detected: ", "\n\t[accent]times joined / kicked: ", "/", "\n\t[accent]First joined: ", "\n\t[accent]Names used: [[", "]"])), args.target, (0, funcs_1.escapeStringColorsClient)(args.target.name), args.target.player.id.toString(), args.target.rank, Array.from(args.target.flags).map(function (f) { return f.coloredName(); }).join(" "), f.boolBad(!args.target.hasPerm("play")), args.target.marked() ? "until ".concat((0, utils_1.formatTimeRelative)(args.target.unmarkTime)) : "[green]false", f.boolBad(args.target.muted), f.boolBad(args.target.autoflagged), f.boolBad(args.target.ipDetectedVpn), info.timesJoined, info.timesKicked, args.target.firstJoined < 1 ? "unknown" : (0, utils_1.formatTimeRelative)(args.target.firstJoined), names)); + if (sender.hasPerm("viewUUIDs")) + output(f(templateObject_54 || (templateObject_54 = __makeTemplateObject(["\t[#FFAAAA]UUID: ", ""], ["\\t[#FFAAAA]UUID: ", ""])), args.target.uuid)); + if (sender.hasPerm("viewIPs")) + output(f(templateObject_55 || (templateObject_55 = __makeTemplateObject(["\t[#FFAAAA]IP: ", ""], ["\\t[#FFAAAA]IP: ", ""])), args.target.ip())); + } + }, + spawn: { + args: ["type:unittype", "x:number?", "y:number?", "count:number?", "team:team?", "effects:string?", "stack:boolean?"], + description: "Spawns a unit of specified type at your position. [scarlet]Usage will be logged.[]", + perm: commands_1.Perm.admin.exceptModes({ + testsrv: commands_1.Perm.trusted, + }), + data: [], + requirements: [commands_1.Req.positiveInteger("count")], + handler: function (_a) { + var _b, _c; + var sender = _a.sender, args = _a.args, data = _a.data, outputSuccess = _a.outputSuccess, f = _a.f; + var x = args.x ? (args.x * 8) : sender.player.x; + var y = args.y ? (args.y * 8) : sender.player.y; + var team = (_b = args.team) !== null && _b !== void 0 ? _b : sender.team(); + var count = Math.min((_c = args.count) !== null && _c !== void 0 ? _c : 1, 1000); + for (var i = 0; i < count; i++) { + var unit = args.type.create(team); + var xOffset = args.stack ? 0 : 0.01 * i; + var yOffset = args.stack ? 0 : 0.5 * (i % 10); + unit.set(x + xOffset, y + yOffset); + if (args.effects) + (0, utils_1.applyEffectMode)(args.effects, unit, 1e12); + unit.add(); + data.push(unit); + } + if (!(config_1.Gamemode.sandbox() || config_1.Gamemode.testsrv()) && args.effects !== 'paper') + (0, utils_1.logAction)("spawned unit ".concat(args.type.name).concat(count == 1 ? '' : " x".concat(count), " at ").concat(Math.round(x / 8), ", ").concat(Math.round(y / 8)) + (args.effects ? "with ".concat(args.effects, " effects") : ''), sender); + outputSuccess(f(templateObject_56 || (templateObject_56 = __makeTemplateObject(["Spawned unit ", " at (", ", ", ")"], ["Spawned unit ", " at (", ", ", ")"])), args.type, Math.round(x / 8), Math.round(y / 8))); + } + }, + setblock: { + args: ["x:number", "y:number", "block:block", "team:team?", "rotation:number?"], + description: "Sets the block at a location.", + perm: commands_1.Perm.admin.exceptModes({ + testsrv: commands_1.Perm.trusted, + }), + requirements: [commands_1.Req.integerRange("rotation", 0, 3)], + handler: function (_a) { + var _b, _c; + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + var team = (_b = args.team) !== null && _b !== void 0 ? _b : sender.team(); + var tile = Vars.world.tile(args.x, args.y); + if (tile == null) + (0, commands_1.fail)(f(templateObject_57 || (templateObject_57 = __makeTemplateObject(["Position (", ", ", ") is out of bounds."], ["Position (", ", ", ") is out of bounds."])), args.x, args.y)); + tile.setNet(args.block, team, (_c = args.rotation) !== null && _c !== void 0 ? _c : 0); + (0, utils_1.addToTileHistory)({ + pos: "".concat(args.x, ",").concat(args.y), + uuid: sender.uuid, + action: "setblocked", + type: args.block.localizedName + }); + if (!config_1.Gamemode.sandbox()) + (0, utils_1.logAction)("set block to ".concat(args.block.localizedName, " at ").concat(args.x, ",").concat(args.y), sender); + outputSuccess(f(templateObject_58 || (templateObject_58 = __makeTemplateObject(["Set block at ", ", ", " to ", ""], ["Set block at ", ", ", " to ", ""])), args.x, args.y, args.block)); + } + }, + setblockr: { + args: ["block:block?", "team:team?", "rotation:number?"], + description: "Sets the block at tapped locations, repeatedly.", + perm: commands_1.Perm.admin, + requirements: [commands_1.Req.integerRange("rotation", 0, 3)], + tapped: function (_a) { + var _b, _c; + var args = _a.args, sender = _a.sender, f = _a.f, x = _a.x, y = _a.y, outputSuccess = _a.outputSuccess; + if (!args.block) + (0, funcs_1.crash)("uh oh"); + var team = (_b = args.team) !== null && _b !== void 0 ? _b : sender.team(); + var tile = Vars.world.tile(x, y); + if (tile == null) + (0, commands_1.fail)(f(templateObject_59 || (templateObject_59 = __makeTemplateObject(["Position (", ", ", ") is out of bounds."], ["Position (", ", ", ") is out of bounds."])), x, y)); + tile.setNet(args.block, team, (_c = args.rotation) !== null && _c !== void 0 ? _c : 0); + (0, utils_1.addToTileHistory)({ + pos: "".concat(x, ",").concat(y), + uuid: sender.uuid, + action: "setblocked", + type: args.block.localizedName + }); + if (!config_1.Gamemode.sandbox()) + (0, utils_1.logAction)("set block to ".concat(args.block.localizedName, " at ").concat(x, ",").concat(y), sender); + outputSuccess(f(templateObject_60 || (templateObject_60 = __makeTemplateObject(["Set block at ", ", ", " to ", ""], ["Set block at ", ", ", " to ", ""])), x, y, args.block)); + }, + handler: function (_a) { + var args = _a.args, outputSuccess = _a.outputSuccess, handleTaps = _a.handleTaps, currentTapMode = _a.currentTapMode, f = _a.f; + if (args.block) { + handleTaps("on"); + if (currentTapMode == "off") { + outputSuccess("setblockr enabled.\n[scarlet]Be careful, you have the midas touch now![] Turn it off by running /setblockr again."); + } + else { + outputSuccess(f(templateObject_61 || (templateObject_61 = __makeTemplateObject(["Changed setblockr's block to ", ""], ["Changed setblockr's block to ", ""])), args.block)); + } + } + else { + if (currentTapMode == "off") { + (0, commands_1.fail)("Please specify the block to place."); + } + else { + handleTaps("off"); + outputSuccess("setblockr disabled."); + } + } + } + }, + exterminate: { + args: [], + description: "Removes all spawned units.", + perm: commands_1.Perm.admin.exceptModes({ + testsrv: commands_1.Perm.trusted, + }), + handler: function (_a) { + var sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f, allCommands = _a.allCommands; + var numKilled = 0; + allCommands.spawn.data.forEach(function (u) { + if (u.isAdded() && !u.dead) { + u.kill(); + numKilled++; + } + }); + if (!config_1.Gamemode.sandbox()) + (0, utils_1.logAction)("exterminated ".concat(numKilled, " units"), sender); + outputSuccess(f(templateObject_62 || (templateObject_62 = __makeTemplateObject(["Exterminated ", " units."], ["Exterminated ", " units."])), numKilled)); + } + }, + js: { + args: ["javascript:string"], + description: "Run arbitrary javascript.", + perm: commands_1.Perm.runJS, + customUnauthorizedMessage: "[scarlet]You are not in the jsers file. This incident will be reported.[]", + handler: function (_a) { + var javascript = _a.args.javascript, output = _a.output, outputFail = _a.outputFail, sender = _a.sender; + //Additional validation couldn't hurt... + var playerInfo_AdminUsid = sender.info().adminUsid; + if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid != sender.player.usid()) { + api.sendModerationMessage("# !!!!! /js authentication failed !!!!!\nServer: ".concat(config_1.Gamemode.name(), " Player: ").concat((0, funcs_1.escapeTextDiscord)(sender.cleanedName), "/`").concat(sender.uuid, "`\n<@!709904412033810533>")); + (0, commands_1.fail)("Authentication failure"); + } + if (javascript == "Timer.instance().clear()") + (0, commands_1.fail)("Are you really sure you want to do that? If so, prepend \"void\" to your command."); + try { + var scripts = Vars.mods.getScripts(); + var out = scripts.context.evaluateString(scripts.scope, javascript, "fish-js-console.js", 1); + if (out instanceof Array) { + output("[cyan]Array: [[[]" + out.join(", ") + "[cyan]]"); + } + else if (out === undefined) { + output("[blue]undefined[]"); + } + else if (out === null) { + output("[blue]null[]"); + } + else if (out instanceof Error) { + outputFail((0, funcs_1.parseError)(out)); + } + else if (typeof out == "number") { + output("[blue]".concat(out, "[]")); + } + else { + output(out); + } + } + catch (err) { + outputFail((0, funcs_1.parseError)(err)); + } + } + }, + fjs: { + args: ["javascript:string"], + description: "Run arbitrary javascript in the fish-commands context.", + perm: commands_1.Perm.runJS, + customUnauthorizedMessage: "[scarlet]You are not in the jsers file. This incident will be reported.[]", + handler: function (_a) { + var javascript = _a.args.javascript, output = _a.output, outputFail = _a.outputFail, sender = _a.sender; + //Additional validation couldn't hurt... + var playerInfo_AdminUsid = sender.info().adminUsid; + if (!playerInfo_AdminUsid || playerInfo_AdminUsid != sender.player.usid() || sender.usid != sender.player.usid()) { + api.sendModerationMessage("# !!!!! /js authentication failed !!!!!\nServer: ".concat(config_1.Gamemode.name(), " Player: ").concat((0, funcs_1.escapeTextDiscord)(sender.cleanedName), "/`").concat(sender.uuid, "`\n<@!709904412033810533>")); + (0, commands_1.fail)("Authentication failure"); + } + fjsContext.runJS(javascript, output, outputFail, sender); + } + }, + antibot: { + args: ["timeout:time?"], + description: "Checks anti bot stats, or force enables anti bot mode.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, output = _a.output, f = _a.f; + if (args.timeout != undefined) { + args.timeout = Math.min(args.timeout, sender.hasPerm("admin") ? funcs_1.Duration.hours(1) : funcs_1.Duration.minutes(10)); + players_1.FishPlayer.triggerAntibot(args.timeout, "Manually triggered by player ".concat(sender.name), "manual"); + outputSuccess("Set antibot mode override for ".concat((0, utils_1.formatTime)(args.timeout), ".")); + } + else { + output("[acid]Antibot status:\n[acid]Enabled: ".concat(f.boolBad(players_1.FishPlayer.antiBotMode()), "\n").concat((0, utils_1.getAntiBotInfo)("client"))); + } + } + }, + chatstrictness: { + args: ["player:player", "value:string"], + description: "Sets chat strictness for a player.", + perm: commands_1.Perm.mod, + handler: function (_a) { + var _b = _a.args, player = _b.player, value = _b.value, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + if (!sender.canModerate(player, true)) + (0, commands_1.fail)("You do not have permission to set the chat strictness level of this player."); + if (!(value == "chat" || value == "strict")) + (0, commands_1.fail)("Invalid chat strictness level: valid levels are \"chat\", \"strict\""); + player.chatStrictness = value; + (0, utils_1.logAction)("set chat strictness to ".concat(value, " for"), sender, player); + outputSuccess(f(templateObject_63 || (templateObject_63 = __makeTemplateObject(["Set chat strictness for player ", " to \"", "\"."], ["Set chat strictness for player ", " to \"", "\"."])), player, value)); + } + }, + emanate: (0, commands_1.command)(function () { + var unitMapping = {}; + Timer.schedule(function () { + var e_1, _a; + try { + for (var _b = __values(Object.entries(unitMapping)), _c = _b.next(); !_c.done; _c = _b.next()) { + var _d = __read(_c.value, 2), uuid = _d[0], unit = _d[1]; + var fishP = players_1.FishPlayer.getById(uuid); + if (!fishP || !fishP.connected() || (unit.getPlayer() != fishP.player)) { + delete unitMapping[uuid]; + unit === null || unit === void 0 ? void 0 : unit.kill(); + } + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_1) throw e_1.error; } + } + }, 1, 0.5); + return { + args: [], + description: "Puts you in an emanate.", + perm: commands_1.Perm.admin, + data: { unitMapping: unitMapping }, + requirements: [], + handler: function (_a) { + var sender = _a.sender, outputSuccess = _a.outputSuccess; + var emanate = UnitTypes.emanate.spawn(sender.team(), sender.player.x, sender.player.y); + sender.player.unit(emanate); + unitMapping[sender.uuid] = emanate; + if (!config_1.Gamemode.sandbox()) + (0, utils_1.logAction)("spawned an emanate", sender); + outputSuccess("Spawned an emanate."); + } + }; + }), + updatemaps: { + args: [], + description: 'Attempt to fetch and update all map files', + perm: commands_1.Perm.trusted, + requirements: function (_a) { + var sender = _a.sender; + return [commands_1.Req.cooldownGlobal(config_1.Gamemode.testsrv() || sender.hasPerm("mod") ? 15000 : funcs_1.Duration.minutes(5))]; + }, + handler: function (_a) { + var output = _a.output, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail; + output("Updating maps... (this may take a while)"); + (0, files_1.updateMaps)() + .then(function (changed) { + Log.info("Maps updated."); + if (changed) { + outputSuccess("Map update completed."); + Call.sendMessage("[orange]Maps have been updated. Run [white]/maps[] to view available maps."); + } + else { + outputSuccess("Map update completed; already up to date."); + } + }) + .catch(function (message) { + outputFail("Map update failed: ".concat(String(message))); + Log.err("Map updates failed: ".concat(String(message))); + }); + } + }, + clearfire: { + args: [], + description: "Clears all the fires.", + perm: commands_1.Perm.admin, + handler: function (_a) { + var output = _a.output, outputSuccess = _a.outputSuccess; + output("Removing fires..."); + var totalRemoved = 0; + Call.sendMessage("[scarlet][[Fire Department]:[yellow] Fires were reported. Trucks are en-route. Removing all fires shortly."); + Timer.schedule(function () { + totalRemoved += Groups.fire.size(); + Groups.fire.each(function (f) { return f.remove(); }); + Groups.fire.clear(); + }, 2, 0.1, 40); + Timer.schedule(function () { + outputSuccess("Removed ".concat(totalRemoved, " fires.")); + Call.sendMessage("[scarlet][[Fire Department]:[yellow] We've extinguished ".concat(totalRemoved, " fires.")); + }, 6.1); + } + }, + search: { + args: ["input:string"], + description: "Searches playerinfo by name, IP, or UUID.", + perm: commands_1.Perm.admin, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var fishP, info, matches, matches_1, displayMatches; + var input = _b.args.input, admins = _b.admins, output = _b.output, f = _b.f, sender = _b.sender; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!globals_1.uuidPattern.test(input)) return [3 /*break*/, 1]; + fishP = players_1.FishPlayer.getById(input); + info = admins.getInfoOptional(input); + if (fishP == null && info == null) + (0, commands_1.fail)(f(templateObject_64 || (templateObject_64 = __makeTemplateObject(["No stored data matched uuid ", "."], ["No stored data matched uuid ", "."])), input)); + else if (fishP == null && info) + output(f(templateObject_65 || (templateObject_65 = __makeTemplateObject(["[accent]Found player info (but no fish player data) for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nFound player info (but no fish player data) for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), input, info.plainLastName(), (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); + else if (fishP && info) + output(f(templateObject_66 || (templateObject_66 = __makeTemplateObject(["[accent]Found fish player data for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nFound fish player data for uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), input, fishP.name, (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); + else + (0, commands_1.fail)(f(templateObject_67 || (templateObject_67 = __makeTemplateObject(["Super weird edge case: found fish player data but no player info for uuid ", "."], ["Super weird edge case: found fish player data but no player info for uuid ", "."])), input)); + return [3 /*break*/, 5]; + case 1: + if (!globals_1.ipPattern.test(input)) return [3 /*break*/, 2]; + matches = admins.findByIPs(input); + if (matches.isEmpty()) + (0, commands_1.fail)(f(templateObject_68 || (templateObject_68 = __makeTemplateObject(["No stored data matched IP ", ""], ["No stored data matched IP ", ""])), input)); + output(f(templateObject_69 || (templateObject_69 = __makeTemplateObject(["[accent]Found ", " match", " for search \"", "\"."], ["[accent]Found ", " match", " for search \"", "\"."])), matches.size, matches.size == 1 ? "" : "es", input)); + matches.each(function (info) { return output(f(templateObject_70 || (templateObject_70 = __makeTemplateObject(["[accent]Player with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nPlayer with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), info.id, info.plainLastName(), (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); }); + return [3 /*break*/, 5]; + case 2: + matches_1 = Vars.netServer.admins.searchNames(input); + if (matches_1.isEmpty()) + (0, commands_1.fail)(f(templateObject_71 || (templateObject_71 = __makeTemplateObject(["No stored data matched name ", ""], ["No stored data matched name ", ""])), input)); + output(f(templateObject_72 || (templateObject_72 = __makeTemplateObject(["[accent]Found ", " match", " for search \"", "\"."], ["[accent]Found ", " match", " for search \"", "\"."])), matches_1.size, matches_1.size == 1 ? "" : "es", input)); + displayMatches = function () { + matches_1.each(function (info) { return output(f(templateObject_73 || (templateObject_73 = __makeTemplateObject(["[accent]Player with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nPlayer with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), info.id, info.plainLastName(), (0, funcs_1.escapeStringColorsClient)(info.lastName), info.names.map(funcs_1.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); }); + }; + if (!(matches_1.size > 20)) return [3 /*break*/, 4]; + return [4 /*yield*/, menus_1.Menu.confirm(sender, "Are you sure you want to view all ".concat(matches_1.size, " matches?"))]; + case 3: + _c.sent(); + _c.label = 4; + case 4: + displayMatches(); + _c.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); + } + }, + peace: { + args: ["peace:boolean"], + description: "Toggles peaceful mode for sandbox.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.mode('sandbox')], + handler: function (_a) { + var args = _a.args; + if (args.peace) { + globals_1.fishState.peacefulMode = true; + Groups.player.each(function (p) { + if (p.team() != Vars.state.rules.defaultTeam) { + p.team(Vars.state.rules.defaultTeam); + } + }); + Call.sendMessage("[[Sandbox] [green]Enabled peaceful mode."); + } + else { + globals_1.fishState.peacefulMode = false; + Call.sendMessage("[[Sandbox] [red]Disabled peaceful mode."); + } + }, + }, + effects: { + args: ["mode:string", "player:player?", "duration:time?"], + description: "Applies effects to a player's unit.", + perm: commands_1.Perm.admin.exceptModes({ + testsrv: commands_1.Perm.trusted, + }), + handler: function (_a) { + var _b, _c, _d; + var args = _a.args, sender = _a.sender, f = _a.f, outputSuccess = _a.outputSuccess; + if ((_b = args.player) === null || _b === void 0 ? void 0 : _b.hasPerm("blockTrolling")) + (0, commands_1.fail)(f(templateObject_74 || (templateObject_74 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); + if (args.player && !sender.canModerate(args.player, false)) + (0, commands_1.fail)("You do not have permission to perform moderation actions on this player."); + var target = (_c = args.player) !== null && _c !== void 0 ? _c : sender; + var unit = target.unit(); + if (!unit || unit.dead) + (0, commands_1.fail)(f(templateObject_75 || (templateObject_75 = __makeTemplateObject(["", "'s unit is dead."], ["", "'s unit is dead."])), target)); + var ticks = ((_d = args.duration) !== null && _d !== void 0 ? _d : 1e12) / 1000 * 60; + (0, utils_1.applyEffectMode)(args.mode, unit, ticks); + outputSuccess("".concat(args.mode === "clear" ? "Cleared" : "Applied", " effects.")); + if (!config_1.Gamemode.sandbox()) + (0, utils_1.logAction)("applied **".concat(args.mode, "** effects to"), sender, target); + } + }, + items: { + args: ["team:team", "item:item", "amount:number"], + description: "Gives items to a team.", + perm: commands_1.Perm.admin, + requirements: [commands_1.Req.integer("amount")], + handler: function (_a) { + var _b; + var _c = _a.args, team = _c.team, item = _c.item, amount = _c.amount, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f; + var core = (_b = team.data().cores.firstOpt()) !== null && _b !== void 0 ? _b : (0, commands_1.fail)(f(templateObject_76 || (templateObject_76 = __makeTemplateObject(["Team ", " has no cores."], ["Team ", " has no cores."])), team)); + core.items.add(item, amount); + outputSuccess(f(templateObject_77 || (templateObject_77 = __makeTemplateObject(["Gave ", " ", " to ", "."], ["Gave ", " ", " to ", "."])), amount, item, team)); + if (!config_1.Gamemode.sandbox()) + (0, utils_1.logAction)("gave ".concat(amount, " ").concat(item.localizedName.toLowerCase(), " to ").concat(team.name), sender); + } + }, + explosion: { + args: ["radius:number", "x:number", "y:number", "team:team?", "damage:number?", "damageMode:string?"], + description: "Causes an explosion at specified coordinates.", + perm: commands_1.Perm.admin, + handler: function (_a) { + var _b; + var _c = _a.args, radius = _c.radius, x = _c.x, y = _c.y, _d = _c.team, team = _d === void 0 ? Team.derelict : _d, _e = _c.damage, damage = _e === void 0 ? 1e12 : _e, _f = _c.damageMode, damageMode = _f === void 0 ? "both" : _f, outputSuccess = _a.outputSuccess; + var _g = __read((_b = (0, utils_1.match)(damageMode, { + air: [true, false], + ground: [false, true], + both: [true, true], + none: [false, false], + })) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("Valid values of damageMode: air, ground, both, none"), 2), air = _g[0], ground = _g[1]; + if (radius > 100) + (0, commands_1.fail)("Maximum radius is 100"); + if (damage < 0) + Call.effect(Fx.dynamicSpikes, x * 8, y * 8, radius * 8, Pal.heal); + else + Call.effect(Fx.dynamicExplosion, x * 8, y * 8, Math.max(radius, 8) / 7, Color.white); + Damage.damage(team, x * 8, y * 8, radius * 8, damage, true, air, ground); + outputSuccess("Created an explosion at (".concat(x, ", ").concat(y, ").")); + } + }, + memorycorruption: { + args: [], + description: "Triggers a fake memory corruption prank.", + perm: commands_1.Perm.mod, + requirements: [commands_1.Req.cooldownGlobal(funcs_1.Duration.minutes(30))], + handler: function () { + (0, utils_1.definitelyRealMemoryCorruption)(); + } + }, + editor: { + args: ["editor:boolean"], + description: "Toggles the in-game editor mode.", + perm: commands_1.Perm.trusted, + requirements: [commands_1.Req.mode("testsrv"), commands_1.Req.cooldownGlobal(20000)], + handler: function (_a) { + var editor = _a.args.editor; + Vars.state.rules.editor = editor; + Call.setRules(Vars.state.rules); + } + }, + mapruns: { + args: ["map:map", "lowestHighscores:boolean?"], + description: "Displays all map runs for a selected map, and allows deleting invalid/cheated runs.", + perm: commands_1.Perm.admin, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var fmap, _c, initialLength, runs, _d, index, _, deleted; + var _e; + var _f = _b.args, map = _f.map, lowestHighscores = _f.lowestHighscores, sender = _b.sender, outputSuccess = _b.outputSuccess; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + fmap = (_e = maps_1.FMap.getCreate(map)) !== null && _e !== void 0 ? _e : (0, commands_1.fail)("Map data is still loading, please try again."); + if (!(lowestHighscores !== null && lowestHighscores !== void 0)) return [3 /*break*/, 1]; + _c = lowestHighscores; + return [3 /*break*/, 3]; + case 1: return [4 /*yield*/, menus_1.Menu.buttons(sender, "[accent]Map runs", "Select a view", [ + [{ data: true, text: "Lowest highscores" }], + [{ data: false, text: "All runs" }], + ], { + includeCancel: true, + onCancel: "reject" + })]; + case 2: + _c = (lowestHighscores = _g.sent()); + _g.label = 3; + case 3: + _c; + initialLength = fmap.runs.length; + runs = fmap.runs.slice(); + if (lowestHighscores) + runs = runs.filter(function (r) { return r.success; }) + .sort(function (a, b) { return a.duration() - b.duration(); }); + return [4 /*yield*/, menus_1.Menu.textPages(sender, runs.map(function (r) { return [ + (0, utils_1.formatTimestamp)(r.startTime), + function () { + return "Duration: ".concat((0, utils_1.formatTime)(r.duration()), "\nMax player count: ").concat(r.maxPlayerCount, "\nOutcome: ").concat(r.outcome()[1], "\nWave: ").concat(r.wave); + } + ]; }), ["[scarlet]\uE86FDelete"], { + onCancel: "reject" + })]; + case 4: + _d = __read.apply(void 0, [_g.sent(), 2]), index = _d[0], _ = _d[1]; + return [4 /*yield*/, menus_1.Menu.confirmDangerous(sender, "Are you sure you want to delete this map run? This action is irreversible.")]; + case 5: + _g.sent(); + if (initialLength != fmap.runs.length) + (0, commands_1.fail)("Someone else deleted a run, please try again."); + deleted = fmap.runs.splice(index, 1)[0]; + outputSuccess("Deleted run (".concat((0, utils_1.formatTimestamp)(deleted.startTime), ") with duration ").concat((0, utils_1.formatTime)(deleted.duration()), ".")); + return [2 /*return*/]; + } + }); + }); + } + } +}); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24, templateObject_25, templateObject_26, templateObject_27, templateObject_28, templateObject_29, templateObject_30, templateObject_31, templateObject_32, templateObject_33, templateObject_34, templateObject_35, templateObject_36, templateObject_37, templateObject_38, templateObject_39, templateObject_40, templateObject_41, templateObject_42, templateObject_43, templateObject_44, templateObject_45, templateObject_46, templateObject_47, templateObject_48, templateObject_49, templateObject_50, templateObject_51, templateObject_52, templateObject_53, templateObject_54, templateObject_55, templateObject_56, templateObject_57, templateObject_58, templateObject_59, templateObject_60, templateObject_61, templateObject_62, templateObject_63, templateObject_64, templateObject_65, templateObject_66, templateObject_67, templateObject_68, templateObject_69, templateObject_70, templateObject_71, templateObject_72, templateObject_73, templateObject_74, templateObject_75, templateObject_76, templateObject_77; diff --git a/build/scripts/config.js b/build/scripts/config.js index 0f8151f4..988a9e8c 100644 --- a/build/scripts/config.js +++ b/build/scripts/config.js @@ -1,358 +1,358 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains configurable constants. -*/ -var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { - if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } - return cooked; -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.rules = exports.tips = exports.FColor = exports.text = exports.prefixes = exports.GamemodeNames = exports.Gamemode = exports.FishServer = exports.mapRepoURLs = exports.Mode = exports.backendIP = exports.stopAntiEvadeTime = exports.heuristics = exports.adminNames = exports.multiCharSubstitutions = exports.substitutions = exports.bannedWords = void 0; -var globals_1 = require("/globals"); -var ranks_1 = require("/ranks"); -var funcs_1 = require("/funcs"); -function processBannedWordList(words) { - return words.map(function (word) { - return (typeof word == "string" || word instanceof RegExp) ? - [word, []] - : [word[0], word.slice(1)]; - }); -} -exports.bannedWords = { - // README: Information on how to update this list - // All words must be in *lowercase*. - // Words need to be separated by commas, even if they are on a new line. - // If a word can be contained in another word that should be allowed (the scunthorpe problem), - // surround the entire thing in square brackets, then list out the words after - // like this: ["badw", "goodbadw"] - /** Normal: banned always. */ - normal: processBannedWordList([ - "fanum tax", "gyatt", ["rizz", "grizzly", "frizz", "horizzon"], "skibidi", //With love, DarthScion - //>:( -dart - // "uwu", //lol - "nig" + "ger", "nig" + "ga", "niger", "ni8" + "8er", "nig" + "gre", "негр", "ниг" + "гер", "нигер", "нігер", "ніг" + "гер", /\bnegr\b/, //our apologies to citizens of the Republic of Niger - ["ni" + "ga", "anniga", "inniga", "unniga", "aniga", "iniga", "eniga", "oniga"], - "re" + "tard", - 'kill yourself', 'kill urself', /\bkys\b/, - "kill blacks", "heil hitler", "heil nazis", "heil the nazis", "sieg heil", "hail hitler", "hail nazis", "hail the nazis", "sieg hail", /\b1488\b/, //nazi-related words - ["co" + "ck", "cockroach", "poppycock", "cocktail"], "suck dick", "sucking dick", - "iamasussyimposter", - ["cu" + "nt", "scunthorpe"], - ["penis", "peniston"], - "hawk tuah", - ["rape", "grape", "therap", "drape", "scrape", "trapez", "earrape", "atrape", "traped"], - ["raping", "draping", "graping", "scraping", "craping"], - /\bf(a)g\b/, "fa" + "gg" + "ot", - /\bc(u)m\b/, ["semen", "sement", "horsemen", "housemen", "defensemen", "those", "menders"], - ["porn", "maporn"], - "futa" + "nari", "futa", - "ur gay", "your gay", "youre gay", "you're gay", - "gooning", "gooner", "dildo", "loli", /\banal\b/, "cunny" - ]), - /** Strict: banned in names and for players with a chat strictness level of 'strict'. */ - strict: processBannedWordList([ - "fu" + "ck", "bi" + "tch", ["sh" + "it", "harshit"], /\ba(s)s\b/, "as" + "shole", ["dick", "medick", "dickens"], - ]), - /** Names: banned only in names. */ - names: processBannedWordList([ - "sex", /\bgoldberg\b/, "hitler", "stalin", "putin", "lenin", /^something$/, "[something]", "[[something]", "卐", "diddy", "epstein", - globals_1.uuidPattern, globals_1.ipPattern, globals_1.ipPortPattern - ]), - /** autoWhack: new players saying one of these words will be automatically stopped and muted. Comes with \b so no need to add it. */ - autoWhack: [ - "nig" + "ger", "nig" + "ga", "ni8" + "8er", "nig" + "g3r", "hit" + "ler", "fa" + "gg" + "ot", "nazis", "негр", "ниг" + "гер", "нигер", "нігер", "ніг" + "гер", "negr" - ], -}; -//for some reason the external mindustry server does not read the files correctly, so we can only use ASCII -exports.substitutions = Object.fromEntries(Object.entries({ - "a": "\u0430\u1E9A\u1EA1\u1E01\u00E4\u03B1@\u0101\u0103\u0105\u03AC", - "b": "\u1E03\u1E07\u1E03\u0253\u0185", - "c": "\u0441\u217D\u00E7\u03C2\u010B", - "d": "\u217E\u1E0B\u1E11\u010F\u1E13\u1E0D\u1E0F\u0257\u20AB\u0256\u056A", - "e": "\u0435\u1E1B\u0113\u1E17\u0229\u0451\u011B\u0205\u03F5\u03B5\u025B3", - "f": "\u1E1F\u0493\u0192", - "g": "\u0581\u0123\u01F5\u0260\u011F\u011D\u01E5\u1E21", - "h": "\u1E23\u021F\u1E25\u1E2B\u0570\u056B\u1E29\u0266\u1E27\u1E23\u0266\u1E96\u0127", - "i": "\u0456\u012F\u03B9\u1EC9\u1F31\u1F77\u012B1\u00A1\u0457\u0390\u03CA", - "j": "\u0458\u029D\u0575\u025F\u0135\u0237\u01F0", - "k": "\u049F\u1E31\u0137\u0138\u043A\u0199\u049D", - "l": "\u217C\u1E3D\u1E3B\u013E\u0140\u013C\u1E39\u0142\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u03B9", - "m": "\u217F\u1E43\u0271\u1E41\u1E3F", - "n": "\u00F1\u0144\u0146\u0148\u0149\u01F9\u03AE\u03B7\u0578\u057C\u0580\u1E45\u1E47\u03A0", - "o": "\u00F2\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1F40\u1F41\u1F42\u1F43\u1F44\u1F45\u1F78\u1F79\u03C3\u0E50\u00F6\u014D\u014F\u0151\u01A1\u01D2\u03BF\u03CC0", - "p": "\u03C1\u0440\u048F\u1E55\u1E57\u1FE4\u1FE5\u2374", - "q": "\u051B\u0563\u0566\u0563\u0566", - "r": "\u0155\u0157\u0159\u0211\u0213\u027C\u027D\u0433\u0453\u0491\u04F7\u1E59\u1E5B\u1E5D", - "s": "\u015B\u015D\u015F\u0161\u0219\u0282\u0455\u1E61\u1E63\u1E65\u1E67\u1E69\u03C2", - "t": "\u0163\u0165\u01AB\u021B\u0288\u1E6B\u1E6D\u1E6F\u1E71\u1E97\u0236\u2020\u04AD", - "u": "\u00B5\u03BC\u00F9\u00FA\u00FB\u00FC\u0169\u016B\u016D\u016F\u0171\u0173\u01B0\u01D4\u0215\u0217\u0265\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u03BC\u03C5\u03CB\u03CD", - "v": "\u03BD\u0475\u0477\u1E7D\u1E7F\u2174\u2228\u03C5\u03CB\u03CD", - "w": "\u0175\u051D\u1E81\u1E83\u1E85\u1E87\u1E89\u1E98\u03C9\u03CE", - "x": "\u0445\u04B3\u1E8B\u1E8D\u03C7", - "y": "\u00FD\u00FF\u0177\u01B4\u0233\u03B3\u0443\u045E\u04EF\u04F1\u04F3\u1E8F\u1E99\u1EF3\u1EF5\u1EF7\u1EF9\u04AF\u04B1", - "z": "\u017A\u017C\u017E\u01B6\u0225\u0290\u1E91\u1E93\u1E95", - "A": "\u1E00\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAC\u1F08\u1F09\u1F88\u1F89\u1FB8\u1FB9\u1FBA\u1FBC\u212B\u0100\u0102\u0104\u0386\u0391\u0410", - "B": "\u0181\u0392\u0412\u1E02\u1E04\u1E06", - "C": "\u00C7\u0106\u0108\u010A\u010C\u0187\u0421\u04AA\u1E08\u216D\u03F9", - "D": "\u00D0\u010E\u0110\u0189\u018A\u1E0A\u1E0C\u1E0E\u216E", - "E": "\u00C8\u00C9\u00CA\u00CB\u0112\u0114\u0116\u0118\u011A\u0204\u0206\u0228\u0395\u0400\u0415\u04D6\u1E18\u0510\u2107\u0190\u1F19\u1FC8\u0404\u0388\u03AD\u03B5\u03B7\u0415", - "F": "\u03DC\u1E1E\u0492\u0191\u0492\u0493", - "G": "\u011C\u011E\u0120\u0122\u0193\u01E6\u01F4\u1E20", - "H": "\u0124\u021E\u0397\u041D\u04A2\u04A4\u04C7\u04C9\u1E22\u1E24\u1E26\u1E28\u1E2A\u1FCC\uA726\u0389", - "I": "\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u217C\u1E3D\u1E3B\u026B\u013E\u0140\u013C\u1E39\u038A", - "J": "\u0134\u0408\u037F", - "K": "\u0136\u0198\u01E8\u039A\u040C\u041A\u051E\u1E30\u1E32\u1E34\u20AD\u212A\u03BA", - "L": "\u0139\u013B\u013D\u013F\u0141\u053C\u1E36\u1E38\u1E3A\u1E3C\u216C", - "M": "\u039C\u041C\u04CD\u1E3E\u1E40\u1E42\u216F", - "N": "\u00D1\u0143\u0145\u0147\u01F8\u039D\u1E44\u1E46\u1E48\u1E4A\u019D", - "O": "\u03B8\u236C\u00D2\u00D3\u00D4\u00D5\u00D6\u014C\u014E\u0150\u019F\u01A0\u01D1\u020E\u022E\u0230\u0398\u039F\u041E\u04E6\u0555\u1ECC\u1ECE\u1ED4\u1FF9\u038C", - "P": "\u01A4\u03A1\u0420\u048E\u1E54\u1E56\u1FEC", - "Q": "\u051A", - "R": "\u0154\u0156\u0158\u0210\u0212\u1E58\u1E5A\u1E5C\u1E5E\u211E\u024C\u2C64", - "S": "\u015A\u015C\u015E\u0160\u0218\u0405\u054F\u1E60\u1E62\u1E68\u1E64\u1E66", - "T": "\u0162\u0164\u0166\u01AE\u021A\u03A4\u0422\u04AC\u1E6A\u1E6C\u1E6E\u1E70\u038A\u1FDB\uA68C\u0372\u0373\u03C4", - "U": "\u016A\u016C\u016E\u0170\u0172\u01AF\u01D3\u1EE8\u1EEA\u1EEC\u1EEE\u0544", - "V": "\u0474\u0476\u1E7C\u1E7E\u22C1\u2164", - "W": "\u051C\u1E80\u1E82\u1E84\u1E86\u1E88\u019C", - "X": "\u03A7\u0425\u04B2\u1E8A\u1E8C\u2169", - "Y": "\u01B3\u0232\u03A5\u03AB\u03D3\u0423\u04AE\u04B0\u1E8E\u1EF2\u1EF4\u038E", - "Z": "\u0179\u017B\u017D\u0224\u0396\u1E90\u1E92\u1E94", - "": "\u200B\u200C\u200D", -}).map(function (_a) { - var _b = __read(_a, 2), char = _b[0], alts = _b[1]; - return alts.split("").map(function (alt) { return [alt, char]; }); -}).flat(1)); -exports.multiCharSubstitutions = [ - [/\|-\|/g, "H"] -]; -//#endregion -//#region misc -/** Used for anti-impersonation. Make sure to replace numbers with letters, for example, balam314 -> balamei4. */ -exports.adminNames = ["fish", "balamei4", "clashgone", "darthscion", "firefridge", "aricia", "rawsewage", "skeledragon", "edh8e", "everydayhuman8e", "benjamonsrl"]; -exports.heuristics = { - /** Will trip if more than this many blocks are broken within 25 seconds of joining. */ - blocksBrokenAfterJoin: 40, -}; -exports.stopAntiEvadeTime = funcs_1.Duration.minutes(30); -exports.backendIP = '45.79.202.111:5082'; -exports.Mode = { - localDebug: new Fi("config/.debug").exists(), - noBackend: new Fi("config/.debug").exists() && !exports.backendIP.startsWith("127.0.0.1:"), - isChristmas: new Date().getMonth() == 11, - isAprilFools: new Date().getMonth() == 3 && new Date().getDate() == 1, -}; -//#endregion -//#region servers -/** Stores the repository url for the maps for each gamemode. */ -exports.mapRepoURLs = { - attack: "https://api.github.com/repos/Fish-Community/fish-maps/contents/attack", - survival: "https://api.github.com/repos/Fish-Community/fish-maps/contents/survival", - pvp: "https://api.github.com/repos/Fish-Community/fish-maps/contents/pvp", - hexed: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hexed", - sandbox: "https://api.github.com/repos/Fish-Community/fish-maps/contents/sandbox", - hardcore: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hardcore", - testsrv: "https://api.github.com/repos/Fish-Community/fish-maps/contents/testsrv", - minigame: "https://api.github.com/repos/Fish-Community/fish-maps/contents/minigame", -}; -/** Stores the names and addresses of each active server. */ -var FishServer = /** @class */ (function () { - function FishServer(name, ip, port, aliases, - /** If set, this permission is required to switch to or get information about this server. */ - requiredPerm) { - this.name = name; - this.ip = ip; - this.port = port; - this.aliases = aliases; - this.requiredPerm = requiredPerm; - FishServer.all.push(this); - } - FishServer.byName = function (input) { - var _a; - input = input.toLowerCase(); - return (_a = FishServer.all.find(function (s) { return s.aliases.concat(s.name).includes(input); })) !== null && _a !== void 0 ? _a : null; - }; - FishServer.all = []; - FishServer.attack = new FishServer("attack", "162.248.100.98", "6567", ["attac", "atack", "atak", "atck", "atk", "a"]); - FishServer.survival = new FishServer("survival", "162.248.101.95", "6567", ["surviv", "surv", "sur", "su", "s", "sl"]); - FishServer.pvp = new FishServer("pvp", "162.248.102.101", "6567", ["pv", "p", "v", "playerversusplayer"]); - FishServer.sandbox = new FishServer("sandbox", "162.248.101.53", "6567", ["sand", "box", "sa", "sb"]); - FishServer.hexed = new FishServer("hexed", "162.248.100.133", "6567", ["h", "hx", "hxd", "hpvp", "hxpvp", "hexpvp"]); - FishServer.minigame = new FishServer("minigame", "162.248.101.116", "6567", ["m", "mg", "mini", "minig", "mgame", "mng", "minigame", "mpvp"]); - FishServer.testing = new FishServer("testing", "162.248.101.52", "6567", ["test", "testsrv", "t", "testingserver", "testserver"]); - return FishServer; -}()); -exports.FishServer = FishServer; -; -/** Stores functions that return whether the specified gamemode is the current gamemode. */ -exports.Gamemode = { - attack: function () { return exports.Gamemode.name() == "attack"; }, - survival: function () { return exports.Gamemode.name() == "survival"; }, - pvp: function () { return exports.Gamemode.name() == "pvp" || exports.Gamemode.name() == "hexed" || exports.Gamemode.name() == "minigame"; }, - sandbox: function () { return exports.Gamemode.name() == "sandbox"; }, - hexed: function () { return exports.Gamemode.name() == "hexed"; }, - hardcore: function () { return exports.Gamemode.name() == "hardcore"; }, - testsrv: function () { return exports.Gamemode.name() == "testsrv"; }, - minigame: function () { return exports.Gamemode.name() == "minigame"; }, - name: function () { return Core.settings.get("mode", Vars.state.rules.mode().name()); }, -}; -exports.GamemodeNames = Object.keys(exports.Gamemode).filter(function (x) { return x !== "name"; }); -//#endregion -//#region text content -exports.prefixes = { - marked: '[yellow]\u26A0[scarlet]Marked Griefer[]\u26A0[]', - flagged: '[yellow]\u26A0[orange]Flagged[]\u26A0[]', - muted: '[white](muted)', -}; -exports.text = { - discordURL: "https://discord.gg/VpzcYSQ33Y", - membershipURL: "https://patreon.com/FishServers", - reportsPing: "<@&1040193678817378305>", - welcomeMessage: function () { return (0, funcs_1.random)([ - "[gold]Welcome![]" - ]); }, - chatFilterReplacement: { - message: function () { return "I really hope everyone is having a fun time :) <3"; }, - messageShort: function () { return "I hope we're all having a fun time :) <3"; }, - highlight: function () { return "[#f456f]"; }, - // `[#22AA22]Merry [#EC4444]Christmas!`, - // `[gold]Happy Holidays! [white]•*•☃*•`, - // `[gold]Happy Hanukkah!`, - // `[#EC4444]May your days be merry and bright!`, - // `[gold]Merry Fishmas! >|||> [white]☃`, - // `[gold]Deck the halls with lots of fun!`, - // `[gold]>|||> Fish wishes you [#22AA22]a merry Christmas!`, - // ]), - // chatFilterReplacement: { - // message: () => random([ - // `Have a holly jolly Christmas :) <3`, - // `I really hope everyone is jolly for the season! :D`, - // `All I want for Christmaaaaaaas is everyone having a fun time! :)`, - // `Remember to be nice in chat: Santa is watching! <3`, - // `All I want for Christmas is Fish! >|||>`, - // ]), - // highlight: () => random([ - // `[#22AA22]`, `[#EC4444]`, `[#FFFFFF]` - // ]), - }, - dataFetchFailed: "[scarlet]\u26A0 Data fetch failed!\n[white]Please disconnect and rejoin the server if you encounter further issues, such as missing rank or statistics.", -}; -//TODO use this -exports.FColor = (function (data) { - return Object.fromEntries(Object.entries(data).map(function (_a) { - var _b = __read(_a, 2), k = _b[0], c = _b[1]; - return [k, function (str) { - var varChunks = []; - for (var _i = 1; _i < arguments.length; _i++) { - varChunks[_i - 1] = arguments[_i]; - } - return str != null ? - "".concat(c).concat(Array.isArray(str) ? String.raw.apply(String, __spreadArray([{ raw: str }], __read(varChunks.map(function (v) { return String(v) + c; })), false)) : str, "[]") - : c; - } - ]; - })); -})({ - discord: "[#7289DA]", - /** Used for tips and welcome messages. */ - tip: "[gold]", - member: "[pink]", - achievement: "[lime]", -}); -/** Tips that are shown to players randomly. */ -exports.tips = { - ads: [ - "".concat(exports.FColor.member(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " subscribers can access the ").concat(exports.FColor.member(templateObject_2 || (templateObject_2 = __makeTemplateObject(["/pet"], ["/pet"]))), " command, which spawns a merui that follows you around. Get a Fish Membership at[sky] ").concat(exports.text.membershipURL, " []"), - "".concat(exports.FColor.member(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " subscribers can use the ").concat(exports.FColor.member(templateObject_4 || (templateObject_4 = __makeTemplateObject(["/highlight"], ["/highlight"]))), " command, which turns your chat messages to a color of your choice. Get a Fish Membership at[sky] ").concat(exports.text.membershipURL, " []"), - "".concat(exports.FColor.member(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " subscribers can use the ").concat(exports.FColor.member(templateObject_6 || (templateObject_6 = __makeTemplateObject(["/rainbow"], ["/rainbow"]))), " command, which makes your name flash different colors. Get a Fish Membership at[sky] ").concat(exports.text.membershipURL, " []"), - "Want to support the server and get some perks? Get a ".concat(exports.FColor.member(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " at[sky] ").concat(exports.text.membershipURL, " []"), - "Join our ".concat(exports.FColor.discord(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Discord server"], ["Discord server"]))), "[]! ").concat(exports.FColor.discord(exports.text.discordURL), " or type ").concat(exports.FColor.discord(templateObject_9 || (templateObject_9 = __makeTemplateObject(["/discord"], ["/discord"])))), - ], - normal: [ - //commands - "You can spawn an [scarlet]Ohno[] with the [scarlet]/ohno[] command. Ohnos are harmless creatures that were created by fusing an alpha and an atrax.", - "Ohnos cannot be spawned near enemy buildings, because they are peaceful and do not want to be used for attacks.", - "You can use [white]/tp[] to teleport directly to any other player! (But only when you're in a core unit)", - "You can unload bulk conveyors (\uF819 or \uF732) with unloaders (\uF864 or \uF731).", - "Hate boulders? You can remove them with [white]/clean[].", - "You can check our rules at any time by running [white]/rules[].", - // `You can kill your unit by running [white]/die[].`, - "We have a tilelog system to help catch griefers. Run [white]/tilelog[], then click a tile to see what's happened there.", - "Run [white]/tilelog 1[] to check the tile history of multiple tiles.", - "Tilelog stores when a building is placed, broken, rotated, configured, and picked up/dropped by a payload unit. Access it with [white]/tilelog[]", - "Tilelog doesn't just log tile actions, it also logs unit deaths! Access it with [white]/tilelog[]", - "Did someone kill a T5 with commands? Run [white]/aoelog 0 15 killed[] to check tilelogs for unit deaths in a large area.", - "Aoelog can show the history of tiles in an area. Select the opposite corners of a rectangle to view the history of its tiles.", - "Aoelog is the plural version of tilelog, access it via [white]/aoelog[]", - "You can mark yourself as AFK(away from keyboard) with [white]/afk[].", - "Run /survival, /attack, /pvp, /sandbox, /hexed or /minigame to quickly change to another server.", - "Need to get rid of an active griefer? Use [#6FFC7C]/s[] to send a message to all staff members across all servers.", - "Use [white]/help to get more information about a specific command.", - "If you want to send a message to just one player, you can use the [white]/msg[] command.", - "Use [white]/r[] to reply to a message sent by another player.", - "[white]/trail[] can be used to give your unit a trail of particle effects.", - "Run [white]/ranks[] to see all the ranks on our server.", - "Is someone impersonating a staff member? Run [white]/rank[] to see their real rank.", - "Don't like the map? Vote to change it with [white]/rtv[].", - "If you want to end the current map, DO NOT BREAK DEFENCES! Vote to change the map with [white]/rtv[].", - //misc - "Anyone attempting to impersonate a ranked player, or the server, will have [scarlet]SUSSY IMPOSTOR[] prepended to their name. Beware!", - "Griefers will often be found with the text ".concat(exports.prefixes.marked, " prepended to their name: they are harmless and cannot grief again."), - "Don't votekick ".concat(exports.prefixes.marked.slice(0, -3), "[][scarlet]s [gold]if they aren't breaking the rules: they are incapable of griefing more."), - "Players marked as ".concat(exports.prefixes.flagged, " have been flagged as suspicious by our detection systems, but they may not be griefers."), - "Need to appeal a moderation action? Join the discord at ".concat(exports.FColor.discord(exports.text.discordURL), " or type /discord"), - "Want to send the phrase [white]\"/command\"[] in chat? Type [white]\"./command\"[] and the [white].[] will be removed.", - "All commands with a player as an argument support using a menu to specify the player. Just run the command leaving the argument blank (using two spaces if necessary), and a menu will show up.", - "Players with a ".concat(ranks_1.Rank.trusted.prefix, " in front of their name aren't staff members, but they do have extra powers."), - "Staff members will have the following prefixes in front of their name: ".concat(ranks_1.Rank.manager.prefix, ", ").concat(ranks_1.Rank.admin.prefix, ", ").concat(ranks_1.Rank.mod.prefix), - "Wave cooldown too long? Skip the wait with [white]/vnw[]", - "You can tell new players not to break power voids with [white]/void[]", - "You can add [pink]color[] to things with color tags! Try typing \"[[".concat(["pink", "green", "cyan", "acid", "royal", "coral"][Math.floor(Math.random() * 6)], "]Hello\" in chat, and see what happens!") - ], - christmas: [ - "Remember to be nice in-game, Santa is watching!", - "Santa's checking his list, so be nice!", - "Have a merry christmas and a happy new year!", - "Fish becomes a bit more jolly around Christmastime!", - "Many server maps have been changed for the season.", - ], - staff: [], -}; -exports.rules = [ - "# 1: [red]No griefing. This refers to intentionally hurting your own team in any way.", - "# 2: [orange]False votekicking isn't allowed. Avoid votekicking if there's an active staff member in the server.", - "# 3: [yellow]Gore, pornography, suggestive content and jokes, and flashing images aren't allowed here. Being horny and a creep in chat will result in a ban.", - "# 4: [green]Do not harass other people. We have zero tolerance for any bigotry. Please respect everyone.", - "# 5: [#00D8D8]Spamming is prohibited. Be reasonable with messaging staff in-game. Misuse may result in a mute.", - "# 6: [blue]Impersonating people or ranks is prohibited.", - "# 7: [purple]Talking about controversial or sensitive topics is not allowed in-game. Hate symbols, such as swastikas, are not permitted.", - "# 8: [pink]No uncomfortable trolling or intentionally causing chaos. This includes any actions or messages that create an unpleasant atmosphere.", - "Failure to follow these rules will result in consequences: likely a ".concat(exports.prefixes.marked, " tag for any game disruption, mute for broken chat rules, and bans for repeated offenses or bypasses.") -].map(function (r) { return "[white]".concat(r); }); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; -//#endregion +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains configurable constants. +*/ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.rules = exports.tips = exports.FColor = exports.text = exports.prefixes = exports.GamemodeNames = exports.Gamemode = exports.FishServer = exports.mapRepoURLs = exports.Mode = exports.backendIP = exports.stopAntiEvadeTime = exports.heuristics = exports.adminNames = exports.multiCharSubstitutions = exports.substitutions = exports.bannedWords = void 0; +var globals_1 = require("/globals"); +var ranks_1 = require("/ranks"); +var funcs_1 = require("/funcs"); +function processBannedWordList(words) { + return words.map(function (word) { + return (typeof word == "string" || word instanceof RegExp) ? + [word, []] + : [word[0], word.slice(1)]; + }); +} +exports.bannedWords = { + // README: Information on how to update this list + // All words must be in *lowercase*. + // Words need to be separated by commas, even if they are on a new line. + // If a word can be contained in another word that should be allowed (the scunthorpe problem), + // surround the entire thing in square brackets, then list out the words after + // like this: ["badw", "goodbadw"] + /** Normal: banned always. */ + normal: processBannedWordList([ + "fanum tax", "gyatt", ["rizz", "grizzly", "frizz", "horizzon"], "skibidi", //With love, DarthScion + //>:( -dart + // "uwu", //lol + "nig" + "ger", "nig" + "ga", "niger", "ni8" + "8er", "nig" + "gre", "негр", "ниг" + "гер", "нигер", "нігер", "ніг" + "гер", /\bnegr\b/, //our apologies to citizens of the Republic of Niger + ["ni" + "ga", "anniga", "inniga", "unniga", "aniga", "iniga", "eniga", "oniga"], + "re" + "tard", + 'kill yourself', 'kill urself', /\bkys\b/, + "kill blacks", "heil hitler", "heil nazis", "heil the nazis", "sieg heil", "hail hitler", "hail nazis", "hail the nazis", "sieg hail", /\b1488\b/, //nazi-related words + ["co" + "ck", "cockroach", "poppycock", "cocktail"], "suck dick", "sucking dick", + "iamasussyimposter", + ["cu" + "nt", "scunthorpe"], + ["penis", "peniston"], + "hawk tuah", + ["rape", "grape", "therap", "drape", "scrape", "trapez", "earrape", "atrape", "traped"], + ["raping", "draping", "graping", "scraping", "craping"], + /\bf(a)g\b/, "fa" + "gg" + "ot", + /\bc(u)m\b/, ["semen", "sement", "horsemen", "housemen", "defensemen", "those", "menders"], + ["porn", "maporn"], + "futa" + "nari", "futa", + "ur gay", "your gay", "youre gay", "you're gay", + "gooning", "gooner", "dildo", "loli", /\banal\b/, "cunny" + ]), + /** Strict: banned in names and for players with a chat strictness level of 'strict'. */ + strict: processBannedWordList([ + "fu" + "ck", "bi" + "tch", ["sh" + "it", "harshit"], /\ba(s)s\b/, "as" + "shole", ["dick", "medick", "dickens"], + ]), + /** Names: banned only in names. */ + names: processBannedWordList([ + "sex", /\bgoldberg\b/, "hitler", "stalin", "putin", "lenin", /^something$/, "[something]", "[[something]", "卐", "diddy", "epstein", + globals_1.uuidPattern, globals_1.ipPattern, globals_1.ipPortPattern + ]), + /** autoWhack: new players saying one of these words will be automatically stopped and muted. Comes with \b so no need to add it. */ + autoWhack: [ + "nig" + "ger", "nig" + "ga", "ni8" + "8er", "nig" + "g3r", "hit" + "ler", "fa" + "gg" + "ot", "nazis", "негр", "ниг" + "гер", "нигер", "нігер", "ніг" + "гер", "negr" + ], +}; +//for some reason the external mindustry server does not read the files correctly, so we can only use ASCII +exports.substitutions = Object.fromEntries(Object.entries({ + "a": "\u0430\u1E9A\u1EA1\u1E01\u00E4\u03B1@\u0101\u0103\u0105\u03AC", + "b": "\u1E03\u1E07\u1E03\u0253\u0185", + "c": "\u0441\u217D\u00E7\u03C2\u010B", + "d": "\u217E\u1E0B\u1E11\u010F\u1E13\u1E0D\u1E0F\u0257\u20AB\u0256\u056A", + "e": "\u0435\u1E1B\u0113\u1E17\u0229\u0451\u011B\u0205\u03F5\u03B5\u025B3", + "f": "\u1E1F\u0493\u0192", + "g": "\u0581\u0123\u01F5\u0260\u011F\u011D\u01E5\u1E21", + "h": "\u1E23\u021F\u1E25\u1E2B\u0570\u056B\u1E29\u0266\u1E27\u1E23\u0266\u1E96\u0127", + "i": "\u0456\u012F\u03B9\u1EC9\u1F31\u1F77\u012B1\u00A1\u0457\u0390\u03CA", + "j": "\u0458\u029D\u0575\u025F\u0135\u0237\u01F0", + "k": "\u049F\u1E31\u0137\u0138\u043A\u0199\u049D", + "l": "\u217C\u1E3D\u1E3B\u013E\u0140\u013C\u1E39\u0142\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u03B9", + "m": "\u217F\u1E43\u0271\u1E41\u1E3F", + "n": "\u00F1\u0144\u0146\u0148\u0149\u01F9\u03AE\u03B7\u0578\u057C\u0580\u1E45\u1E47\u03A0", + "o": "\u00F2\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1F40\u1F41\u1F42\u1F43\u1F44\u1F45\u1F78\u1F79\u03C3\u0E50\u00F6\u014D\u014F\u0151\u01A1\u01D2\u03BF\u03CC0", + "p": "\u03C1\u0440\u048F\u1E55\u1E57\u1FE4\u1FE5\u2374", + "q": "\u051B\u0563\u0566\u0563\u0566", + "r": "\u0155\u0157\u0159\u0211\u0213\u027C\u027D\u0433\u0453\u0491\u04F7\u1E59\u1E5B\u1E5D", + "s": "\u015B\u015D\u015F\u0161\u0219\u0282\u0455\u1E61\u1E63\u1E65\u1E67\u1E69\u03C2", + "t": "\u0163\u0165\u01AB\u021B\u0288\u1E6B\u1E6D\u1E6F\u1E71\u1E97\u0236\u2020\u04AD", + "u": "\u00B5\u03BC\u00F9\u00FA\u00FB\u00FC\u0169\u016B\u016D\u016F\u0171\u0173\u01B0\u01D4\u0215\u0217\u0265\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u03BC\u03C5\u03CB\u03CD", + "v": "\u03BD\u0475\u0477\u1E7D\u1E7F\u2174\u2228\u03C5\u03CB\u03CD", + "w": "\u0175\u051D\u1E81\u1E83\u1E85\u1E87\u1E89\u1E98\u03C9\u03CE", + "x": "\u0445\u04B3\u1E8B\u1E8D\u03C7", + "y": "\u00FD\u00FF\u0177\u01B4\u0233\u03B3\u0443\u045E\u04EF\u04F1\u04F3\u1E8F\u1E99\u1EF3\u1EF5\u1EF7\u1EF9\u04AF\u04B1", + "z": "\u017A\u017C\u017E\u01B6\u0225\u0290\u1E91\u1E93\u1E95", + "A": "\u1E00\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAC\u1F08\u1F09\u1F88\u1F89\u1FB8\u1FB9\u1FBA\u1FBC\u212B\u0100\u0102\u0104\u0386\u0391\u0410", + "B": "\u0181\u0392\u0412\u1E02\u1E04\u1E06", + "C": "\u00C7\u0106\u0108\u010A\u010C\u0187\u0421\u04AA\u1E08\u216D\u03F9", + "D": "\u00D0\u010E\u0110\u0189\u018A\u1E0A\u1E0C\u1E0E\u216E", + "E": "\u00C8\u00C9\u00CA\u00CB\u0112\u0114\u0116\u0118\u011A\u0204\u0206\u0228\u0395\u0400\u0415\u04D6\u1E18\u0510\u2107\u0190\u1F19\u1FC8\u0404\u0388\u03AD\u03B5\u03B7\u0415", + "F": "\u03DC\u1E1E\u0492\u0191\u0492\u0493", + "G": "\u011C\u011E\u0120\u0122\u0193\u01E6\u01F4\u1E20", + "H": "\u0124\u021E\u0397\u041D\u04A2\u04A4\u04C7\u04C9\u1E22\u1E24\u1E26\u1E28\u1E2A\u1FCC\uA726\u0389", + "I": "\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u217C\u1E3D\u1E3B\u026B\u013E\u0140\u013C\u1E39\u038A", + "J": "\u0134\u0408\u037F", + "K": "\u0136\u0198\u01E8\u039A\u040C\u041A\u051E\u1E30\u1E32\u1E34\u20AD\u212A\u03BA", + "L": "\u0139\u013B\u013D\u013F\u0141\u053C\u1E36\u1E38\u1E3A\u1E3C\u216C", + "M": "\u039C\u041C\u04CD\u1E3E\u1E40\u1E42\u216F", + "N": "\u00D1\u0143\u0145\u0147\u01F8\u039D\u1E44\u1E46\u1E48\u1E4A\u019D", + "O": "\u03B8\u236C\u00D2\u00D3\u00D4\u00D5\u00D6\u014C\u014E\u0150\u019F\u01A0\u01D1\u020E\u022E\u0230\u0398\u039F\u041E\u04E6\u0555\u1ECC\u1ECE\u1ED4\u1FF9\u038C", + "P": "\u01A4\u03A1\u0420\u048E\u1E54\u1E56\u1FEC", + "Q": "\u051A", + "R": "\u0154\u0156\u0158\u0210\u0212\u1E58\u1E5A\u1E5C\u1E5E\u211E\u024C\u2C64", + "S": "\u015A\u015C\u015E\u0160\u0218\u0405\u054F\u1E60\u1E62\u1E68\u1E64\u1E66", + "T": "\u0162\u0164\u0166\u01AE\u021A\u03A4\u0422\u04AC\u1E6A\u1E6C\u1E6E\u1E70\u038A\u1FDB\uA68C\u0372\u0373\u03C4", + "U": "\u016A\u016C\u016E\u0170\u0172\u01AF\u01D3\u1EE8\u1EEA\u1EEC\u1EEE\u0544", + "V": "\u0474\u0476\u1E7C\u1E7E\u22C1\u2164", + "W": "\u051C\u1E80\u1E82\u1E84\u1E86\u1E88\u019C", + "X": "\u03A7\u0425\u04B2\u1E8A\u1E8C\u2169", + "Y": "\u01B3\u0232\u03A5\u03AB\u03D3\u0423\u04AE\u04B0\u1E8E\u1EF2\u1EF4\u038E", + "Z": "\u0179\u017B\u017D\u0224\u0396\u1E90\u1E92\u1E94", + "": "\u200B\u200C\u200D", +}).map(function (_a) { + var _b = __read(_a, 2), char = _b[0], alts = _b[1]; + return alts.split("").map(function (alt) { return [alt, char]; }); +}).flat(1)); +exports.multiCharSubstitutions = [ + [/\|-\|/g, "H"] +]; +//#endregion +//#region misc +/** Used for anti-impersonation. Make sure to replace numbers with letters, for example, balam314 -> balamei4. */ +exports.adminNames = ["fish", "balamei4", "clashgone", "darthscion", "firefridge", "aricia", "rawsewage", "skeledragon", "edh8e", "everydayhuman8e", "benjamonsrl"]; +exports.heuristics = { + /** Will trip if more than this many blocks are broken within 25 seconds of joining. */ + blocksBrokenAfterJoin: 40, +}; +exports.stopAntiEvadeTime = funcs_1.Duration.minutes(30); +exports.backendIP = '45.79.202.111:5082'; +exports.Mode = { + localDebug: new Fi("config/.debug").exists(), + noBackend: new Fi("config/.debug").exists() && !exports.backendIP.startsWith("127.0.0.1:"), + isChristmas: new Date().getMonth() == 11, + isAprilFools: new Date().getMonth() == 3 && new Date().getDate() == 1, +}; +//#endregion +//#region servers +/** Stores the repository url for the maps for each gamemode. */ +exports.mapRepoURLs = { + attack: "https://api.github.com/repos/Fish-Community/fish-maps/contents/attack", + survival: "https://api.github.com/repos/Fish-Community/fish-maps/contents/survival", + pvp: "https://api.github.com/repos/Fish-Community/fish-maps/contents/pvp", + hexed: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hexed", + sandbox: "https://api.github.com/repos/Fish-Community/fish-maps/contents/sandbox", + hardcore: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hardcore", + testsrv: "https://api.github.com/repos/Fish-Community/fish-maps/contents/testsrv", + minigame: "https://api.github.com/repos/Fish-Community/fish-maps/contents/minigame", +}; +/** Stores the names and addresses of each active server. */ +var FishServer = /** @class */ (function () { + function FishServer(name, ip, port, aliases, + /** If set, this permission is required to switch to or get information about this server. */ + requiredPerm) { + this.name = name; + this.ip = ip; + this.port = port; + this.aliases = aliases; + this.requiredPerm = requiredPerm; + FishServer.all.push(this); + } + FishServer.byName = function (input) { + var _a; + input = input.toLowerCase(); + return (_a = FishServer.all.find(function (s) { return s.aliases.concat(s.name).includes(input); })) !== null && _a !== void 0 ? _a : null; + }; + FishServer.all = []; + FishServer.attack = new FishServer("attack", "162.248.100.98", "6567", ["attac", "atack", "atak", "atck", "atk", "a"]); + FishServer.survival = new FishServer("survival", "162.248.101.95", "6567", ["surviv", "surv", "sur", "su", "s", "sl"]); + FishServer.pvp = new FishServer("pvp", "162.248.102.101", "6567", ["pv", "p", "v", "playerversusplayer"]); + FishServer.sandbox = new FishServer("sandbox", "162.248.101.53", "6567", ["sand", "box", "sa", "sb"]); + FishServer.hexed = new FishServer("hexed", "162.248.100.133", "6567", ["h", "hx", "hxd", "hpvp", "hxpvp", "hexpvp"]); + FishServer.minigame = new FishServer("minigame", "162.248.101.116", "6567", ["m", "mg", "mini", "minig", "mgame", "mng", "minigame", "mpvp"]); + FishServer.testing = new FishServer("testing", "162.248.101.52", "6567", ["test", "testsrv", "t", "testingserver", "testserver"]); + return FishServer; +}()); +exports.FishServer = FishServer; +; +/** Stores functions that return whether the specified gamemode is the current gamemode. */ +exports.Gamemode = { + attack: function () { return exports.Gamemode.name() == "attack"; }, + survival: function () { return exports.Gamemode.name() == "survival"; }, + pvp: function () { return exports.Gamemode.name() == "pvp" || exports.Gamemode.name() == "hexed" || exports.Gamemode.name() == "minigame"; }, + sandbox: function () { return exports.Gamemode.name() == "sandbox"; }, + hexed: function () { return exports.Gamemode.name() == "hexed"; }, + hardcore: function () { return exports.Gamemode.name() == "hardcore"; }, + testsrv: function () { return exports.Gamemode.name() == "testsrv"; }, + minigame: function () { return exports.Gamemode.name() == "minigame"; }, + name: function () { return Core.settings.get("mode", Vars.state.rules.mode().name()); }, +}; +exports.GamemodeNames = Object.keys(exports.Gamemode).filter(function (x) { return x !== "name"; }); +//#endregion +//#region text content +exports.prefixes = { + marked: '[yellow]\u26A0[scarlet]Marked Griefer[]\u26A0[]', + flagged: '[yellow]\u26A0[orange]Flagged[]\u26A0[]', + muted: '[white](muted)', +}; +exports.text = { + discordURL: "https://discord.gg/VpzcYSQ33Y", + membershipURL: "https://patreon.com/FishServers", + reportsPing: "<@&1040193678817378305>", + welcomeMessage: function () { return (0, funcs_1.random)([ + "[gold]Welcome![]" + ]); }, + chatFilterReplacement: { + message: function () { return "I really hope everyone is having a fun time :) <3"; }, + messageShort: function () { return "I hope we're all having a fun time :) <3"; }, + highlight: function () { return "[#f456f]"; }, + // `[#22AA22]Merry [#EC4444]Christmas!`, + // `[gold]Happy Holidays! [white]•*•☃*•`, + // `[gold]Happy Hanukkah!`, + // `[#EC4444]May your days be merry and bright!`, + // `[gold]Merry Fishmas! >|||> [white]☃`, + // `[gold]Deck the halls with lots of fun!`, + // `[gold]>|||> Fish wishes you [#22AA22]a merry Christmas!`, + // ]), + // chatFilterReplacement: { + // message: () => random([ + // `Have a holly jolly Christmas :) <3`, + // `I really hope everyone is jolly for the season! :D`, + // `All I want for Christmaaaaaaas is everyone having a fun time! :)`, + // `Remember to be nice in chat: Santa is watching! <3`, + // `All I want for Christmas is Fish! >|||>`, + // ]), + // highlight: () => random([ + // `[#22AA22]`, `[#EC4444]`, `[#FFFFFF]` + // ]), + }, + dataFetchFailed: "[scarlet]\u26A0 Data fetch failed!\n[white]Please disconnect and rejoin the server if you encounter further issues, such as missing rank or statistics.", +}; +//TODO use this +exports.FColor = (function (data) { + return Object.fromEntries(Object.entries(data).map(function (_a) { + var _b = __read(_a, 2), k = _b[0], c = _b[1]; + return [k, function (str) { + var varChunks = []; + for (var _i = 1; _i < arguments.length; _i++) { + varChunks[_i - 1] = arguments[_i]; + } + return str != null ? + "".concat(c).concat(Array.isArray(str) ? String.raw.apply(String, __spreadArray([{ raw: str }], __read(varChunks.map(function (v) { return String(v) + c; })), false)) : str, "[]") + : c; + } + ]; + })); +})({ + discord: "[#7289DA]", + /** Used for tips and welcome messages. */ + tip: "[gold]", + member: "[pink]", + achievement: "[lime]", +}); +/** Tips that are shown to players randomly. */ +exports.tips = { + ads: [ + "".concat(exports.FColor.member(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " subscribers can access the ").concat(exports.FColor.member(templateObject_2 || (templateObject_2 = __makeTemplateObject(["/pet"], ["/pet"]))), " command, which spawns a merui that follows you around. Get a Fish Membership at[sky] ").concat(exports.text.membershipURL, " []"), + "".concat(exports.FColor.member(templateObject_3 || (templateObject_3 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " subscribers can use the ").concat(exports.FColor.member(templateObject_4 || (templateObject_4 = __makeTemplateObject(["/highlight"], ["/highlight"]))), " command, which turns your chat messages to a color of your choice. Get a Fish Membership at[sky] ").concat(exports.text.membershipURL, " []"), + "".concat(exports.FColor.member(templateObject_5 || (templateObject_5 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " subscribers can use the ").concat(exports.FColor.member(templateObject_6 || (templateObject_6 = __makeTemplateObject(["/rainbow"], ["/rainbow"]))), " command, which makes your name flash different colors. Get a Fish Membership at[sky] ").concat(exports.text.membershipURL, " []"), + "Want to support the server and get some perks? Get a ".concat(exports.FColor.member(templateObject_7 || (templateObject_7 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " at[sky] ").concat(exports.text.membershipURL, " []"), + "Join our ".concat(exports.FColor.discord(templateObject_8 || (templateObject_8 = __makeTemplateObject(["Discord server"], ["Discord server"]))), "[]! ").concat(exports.FColor.discord(exports.text.discordURL), " or type ").concat(exports.FColor.discord(templateObject_9 || (templateObject_9 = __makeTemplateObject(["/discord"], ["/discord"])))), + ], + normal: [ + //commands + "You can spawn an [scarlet]Ohno[] with the [scarlet]/ohno[] command. Ohnos are harmless creatures that were created by fusing an alpha and an atrax.", + "Ohnos cannot be spawned near enemy buildings, because they are peaceful and do not want to be used for attacks.", + "You can use [white]/tp[] to teleport directly to any other player! (But only when you're in a core unit)", + "You can unload bulk conveyors (\uF819 or \uF732) with unloaders (\uF864 or \uF731).", + "Hate boulders? You can remove them with [white]/clean[].", + "You can check our rules at any time by running [white]/rules[].", + // `You can kill your unit by running [white]/die[].`, + "We have a tilelog system to help catch griefers. Run [white]/tilelog[], then click a tile to see what's happened there.", + "Run [white]/tilelog 1[] to check the tile history of multiple tiles.", + "Tilelog stores when a building is placed, broken, rotated, configured, and picked up/dropped by a payload unit. Access it with [white]/tilelog[]", + "Tilelog doesn't just log tile actions, it also logs unit deaths! Access it with [white]/tilelog[]", + "Did someone kill a T5 with commands? Run [white]/aoelog 0 15 killed[] to check tilelogs for unit deaths in a large area.", + "Aoelog can show the history of tiles in an area. Select the opposite corners of a rectangle to view the history of its tiles.", + "Aoelog is the plural version of tilelog, access it via [white]/aoelog[]", + "You can mark yourself as AFK(away from keyboard) with [white]/afk[].", + "Run /survival, /attack, /pvp, /sandbox, /hexed or /minigame to quickly change to another server.", + "Need to get rid of an active griefer? Use [#6FFC7C]/s[] to send a message to all staff members across all servers.", + "Use [white]/help to get more information about a specific command.", + "If you want to send a message to just one player, you can use the [white]/msg[] command.", + "Use [white]/r[] to reply to a message sent by another player.", + "[white]/trail[] can be used to give your unit a trail of particle effects.", + "Run [white]/ranks[] to see all the ranks on our server.", + "Is someone impersonating a staff member? Run [white]/rank[] to see their real rank.", + "Don't like the map? Vote to change it with [white]/rtv[].", + "If you want to end the current map, DO NOT BREAK DEFENCES! Vote to change the map with [white]/rtv[].", + //misc + "Anyone attempting to impersonate a ranked player, or the server, will have [scarlet]SUSSY IMPOSTOR[] prepended to their name. Beware!", + "Griefers will often be found with the text ".concat(exports.prefixes.marked, " prepended to their name: they are harmless and cannot grief again."), + "Don't votekick ".concat(exports.prefixes.marked.slice(0, -3), "[][scarlet]s [gold]if they aren't breaking the rules: they are incapable of griefing more."), + "Players marked as ".concat(exports.prefixes.flagged, " have been flagged as suspicious by our detection systems, but they may not be griefers."), + "Need to appeal a moderation action? Join the discord at ".concat(exports.FColor.discord(exports.text.discordURL), " or type /discord"), + "Want to send the phrase [white]\"/command\"[] in chat? Type [white]\"./command\"[] and the [white].[] will be removed.", + "All commands with a player as an argument support using a menu to specify the player. Just run the command leaving the argument blank (using two spaces if necessary), and a menu will show up.", + "Players with a ".concat(ranks_1.Rank.trusted.prefix, " in front of their name aren't staff members, but they do have extra powers."), + "Staff members will have the following prefixes in front of their name: ".concat(ranks_1.Rank.manager.prefix, ", ").concat(ranks_1.Rank.admin.prefix, ", ").concat(ranks_1.Rank.mod.prefix), + "Wave cooldown too long? Skip the wait with [white]/vnw[]", + "You can tell new players not to break power voids with [white]/void[]", + "You can add [pink]color[] to things with color tags! Try typing \"[[".concat(["pink", "green", "cyan", "acid", "royal", "coral"][Math.floor(Math.random() * 6)], "]Hello\" in chat, and see what happens!") + ], + christmas: [ + "Remember to be nice in-game, Santa is watching!", + "Santa's checking his list, so be nice!", + "Have a merry christmas and a happy new year!", + "Fish becomes a bit more jolly around Christmastime!", + "Many server maps have been changed for the season.", + ], + staff: [], +}; +exports.rules = [ + "# 1: [red]No griefing. This refers to intentionally hurting your own team in any way.", + "# 2: [orange]False votekicking isn't allowed. Avoid votekicking if there's an active staff member in the server.", + "# 3: [yellow]Gore, pornography, suggestive content and jokes, and flashing images aren't allowed here. Being horny and a creep in chat will result in a ban.", + "# 4: [green]Do not harass other people. We have zero tolerance for any bigotry. Please respect everyone.", + "# 5: [#00D8D8]Spamming is prohibited. Be reasonable with messaging staff in-game. Misuse may result in a mute.", + "# 6: [blue]Impersonating people or ranks is prohibited.", + "# 7: [purple]Talking about controversial or sensitive topics is not allowed in-game. Hate symbols, such as swastikas, are not permitted.", + "# 8: [pink]No uncomfortable trolling or intentionally causing chaos. This includes any actions or messages that create an unpleasant atmosphere.", + "Failure to follow these rules will result in consequences: likely a ".concat(exports.prefixes.marked, " tag for any game disruption, mute for broken chat rules, and bans for repeated offenses or bypasses.") +].map(function (r) { return "[white]".concat(r); }); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9; +//#endregion diff --git a/build/scripts/files.js b/build/scripts/files.js index f08cfe8f..350e2975 100644 --- a/build/scripts/files.js +++ b/build/scripts/files.js @@ -1,96 +1,96 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the code for automated map syncing. -Original contributor: @author Jurorno9 -Maintenance: @author BalaM314 -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.updateMaps = updateMaps; -var config_1 = require("/config"); -var funcs_1 = require("/funcs"); -var promise_1 = require("/promise"); -var utils_1 = require("/utils"); -//if we switch to a self-hosted setup, just make it respond with the githubfile object for a drop-in replacement -function fetchGithubContents() { - return new promise_1.Promise(function (resolve, reject) { - var url = config_1.mapRepoURLs[config_1.Gamemode.name()]; - if (!url) - return reject("No recognized gamemode detected. please enter \"host \" and try again"); - Http.get(url, function (res) { - try { - //Trust github to return valid JSON data - resolve(JSON.parse(res.getResultAsString())); - } - catch (e) { - reject("Failed to parse GitHub repository contents: ".concat(String(e))); - } - }, function () { return reject("Network error while fetching github repository contents"); }); - }); -} -function downloadFile(address, filename) { - if (!/^https?:\/\//i.test(address)) { - (0, funcs_1.crash)("Invalid address, please start with 'http://' or 'https://'"); - } - return new promise_1.Promise(function (resolve, reject) { - var instream = null; - var outstream = null; - Log.info("Downloading ".concat(filename, "...")); - Http.get(address, function (res) { - try { - instream = res.getResultAsStream(); - outstream = new Fi(filename).write(); - instream.transferTo(outstream); - resolve(); - } - finally { - instream === null || instream === void 0 ? void 0 : instream.close(); - outstream === null || outstream === void 0 ? void 0 : outstream.close(); - } - }, function () { - Log.err("Download failed."); - reject("Network error while downloading a map file: ".concat(address)); - }); - }); -} -function downloadMaps(githubListing) { - return promise_1.Promise.all(githubListing.map(function (fileEntry) { - if (!(typeof fileEntry.download_url == "string")) { - Log.warn("Map ".concat(fileEntry.name, " has no valid download link, skipped.")); - return promise_1.Promise.resolve(null); - } - return downloadFile(fileEntry.download_url, Vars.customMapDirectory.child(fileEntry.name).absolutePath()); - })).then(function (v) { }); -} -/** - * @returns whether any maps were changed - */ -function updateMaps() { - //get github map listing - return fetchGithubContents().then(function (listing) { - //filter only valid mindustry maps - var mapList = listing - .filter(function (entry) { return entry.type == 'file'; }) - .filter(function (entry) { return entry.name.endsWith(".msav"); }); - var mapFiles = Vars.customMapDirectory.list(); - var mapsToDelete = mapFiles.filter(function (localFile) { - return !mapList.some(function (remoteFile) { - return remoteFile.name === localFile.name(); - }) - && !localFile.name().startsWith("$$"); - }); - mapsToDelete.forEach(function (map) { return map.delete(); }); - var mapsToDownload = mapList - .filter(function (entry) { - var file = Vars.customMapDirectory.child(entry.name); - return !file.exists() || entry.sha !== (0, utils_1.getHash)(file); //sha'd - }); - if (mapsToDownload.length == 0) { - return mapsToDelete.length > 0 ? true : false; - } - return downloadMaps(mapsToDownload).then(function () { - Vars.maps.reload(); - return true; - }); - }); -} +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the code for automated map syncing. +Original contributor: @author Jurorno9 +Maintenance: @author BalaM314 +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateMaps = updateMaps; +var config_1 = require("/config"); +var funcs_1 = require("/funcs"); +var promise_1 = require("/promise"); +var utils_1 = require("/utils"); +//if we switch to a self-hosted setup, just make it respond with the githubfile object for a drop-in replacement +function fetchGithubContents() { + return new promise_1.Promise(function (resolve, reject) { + var url = config_1.mapRepoURLs[config_1.Gamemode.name()]; + if (!url) + return reject("No recognized gamemode detected. please enter \"host \" and try again"); + Http.get(url, function (res) { + try { + //Trust github to return valid JSON data + resolve(JSON.parse(res.getResultAsString())); + } + catch (e) { + reject("Failed to parse GitHub repository contents: ".concat(String(e))); + } + }, function () { return reject("Network error while fetching github repository contents"); }); + }); +} +function downloadFile(address, filename) { + if (!/^https?:\/\//i.test(address)) { + (0, funcs_1.crash)("Invalid address, please start with 'http://' or 'https://'"); + } + return new promise_1.Promise(function (resolve, reject) { + var instream = null; + var outstream = null; + Log.info("Downloading ".concat(filename, "...")); + Http.get(address, function (res) { + try { + instream = res.getResultAsStream(); + outstream = new Fi(filename).write(); + instream.transferTo(outstream); + resolve(); + } + finally { + instream === null || instream === void 0 ? void 0 : instream.close(); + outstream === null || outstream === void 0 ? void 0 : outstream.close(); + } + }, function () { + Log.err("Download failed."); + reject("Network error while downloading a map file: ".concat(address)); + }); + }); +} +function downloadMaps(githubListing) { + return promise_1.Promise.all(githubListing.map(function (fileEntry) { + if (!(typeof fileEntry.download_url == "string")) { + Log.warn("Map ".concat(fileEntry.name, " has no valid download link, skipped.")); + return promise_1.Promise.resolve(null); + } + return downloadFile(fileEntry.download_url, Vars.customMapDirectory.child(fileEntry.name).absolutePath()); + })).then(function (v) { }); +} +/** + * @returns whether any maps were changed + */ +function updateMaps() { + //get github map listing + return fetchGithubContents().then(function (listing) { + //filter only valid mindustry maps + var mapList = listing + .filter(function (entry) { return entry.type == 'file'; }) + .filter(function (entry) { return entry.name.endsWith(".msav"); }); + var mapFiles = Vars.customMapDirectory.list(); + var mapsToDelete = mapFiles.filter(function (localFile) { + return !mapList.some(function (remoteFile) { + return remoteFile.name === localFile.name(); + }) + && !localFile.name().startsWith("$$"); + }); + mapsToDelete.forEach(function (map) { return map.delete(); }); + var mapsToDownload = mapList + .filter(function (entry) { + var file = Vars.customMapDirectory.child(entry.name); + return !file.exists() || entry.sha !== (0, utils_1.getHash)(file); //sha'd + }); + if (mapsToDownload.length == 0) { + return mapsToDelete.length > 0 ? true : false; + } + return downloadMaps(mapsToDownload).then(function () { + Vars.maps.reload(); + return true; + }); + }); +} diff --git a/build/scripts/fjsContext.js b/build/scripts/fjsContext.js index 50f5cfdd..ba28a49d 100644 --- a/build/scripts/fjsContext.js +++ b/build/scripts/fjsContext.js @@ -1,101 +1,101 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the context for the "fjs" command, -which executes code with access to the plugin's internals. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.runJS = runJS; -var achievements = require("/achievements"); -var api = require("/api"); -var commands = require("/frameworks/commands"); -var config = require("/config"); -var consoleCommands = require("/commands/console").commands; -var files = require("/files"); -var funcs = require("/funcs"); -var globals = require("/globals"); -var io = require("/frameworks/io"); -var maps = require("/maps"); -var memberCommands = require("/commands/member").commands; -var menus = require("/frameworks/menus"); -var packetHandlers = require("/packetHandlers"); -var playerCommands = require("/commands/general").commands; -var players = require("/players"); -var ranks = require("/ranks"); -var staffCommands = require("/commands/staff").commands; -var timers = require("/timers"); -var utils = require("/utils"); -var votes = require("/votes"); -var Promise = require("/promise").Promise; -var Achievement = achievements.Achievement, Achievements = achievements.Achievements; -var Perm = commands.Perm, allCommands = commands.allCommands; -var bannedWords = config.bannedWords, FishServer = config.FishServer, Mode = config.Mode, Gamemode = config.Gamemode, FColor = config.FColor, mapRepoURLs = config.mapRepoURLs; -var FishPlayer = players.FishPlayer; -var Serializer = io.Serializer; -var FishEvents = globals.FishEvents, fishPlugin = globals.fishPlugin, fishState = globals.fishState, tileHistory = globals.tileHistory; -var FMap = maps.FMap; -var Rank = ranks.Rank, RoleFlag = ranks.RoleFlag; -var Menu = menus.Menu; -Object.assign(this, utils, funcs); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever -var Ranks = null; -var $ = Object.assign(function $(input) { - if (typeof input == "string") { - if (Pattern.matches("[a-zA-Z0-9+/]{22}==", input)) { - return FishPlayer.getById(input); - } - } - return null; -}, { - sussy: true, - info: function (input) { - if (typeof input == "string") { - if (Pattern.matches("[a-zA-Z0-9+/]{22}==", input)) { - return Vars.netServer.admins.getInfo(input); - } - } - return null; - }, - create: function (input) { - if (typeof input == "string") { - if (Pattern.matches("[a-zA-Z0-9+/]{22}==", input)) { - return FishPlayer.getFromInfo(Vars.netServer.admins.getInfo(input)); - } - } - return null; - }, - me: null, - meM: null, -}); -/** Used to persist variables. */ -var vars = {}; -function runJS(input, outputFunction, errorFunction, player) { - if (outputFunction === void 0) { outputFunction = Log.info; } - if (errorFunction === void 0) { errorFunction = Log.err; } - if (player) { - $.me = player; - $.meM = player.player; - } - else if (Groups.player.size() == 1) { - $.meM = Groups.player.first(); - $.me = players.FishPlayer.get($.meM); - } - try { - var admins = Vars.netServer.admins; - var output = eval(input); - if (output instanceof Array) { - outputFunction("&cArray: [&fr" + output.join(", ") + "&c]&fr"); - } - else if (output === undefined) { - outputFunction("undefined"); - } - else if (output === null) { - outputFunction("null"); - } - else { - outputFunction(output); - } - } - catch (err) { - errorFunction(err); - } -} +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the context for the "fjs" command, +which executes code with access to the plugin's internals. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runJS = runJS; +var achievements = require("/achievements"); +var api = require("/api"); +var commands = require("/frameworks/commands"); +var config = require("/config"); +var consoleCommands = require("/commands/console").commands; +var files = require("/files"); +var funcs = require("/funcs"); +var globals = require("/globals"); +var io = require("/frameworks/io"); +var maps = require("/maps"); +var memberCommands = require("/commands/member").commands; +var menus = require("/frameworks/menus"); +var packetHandlers = require("/packetHandlers"); +var playerCommands = require("/commands/general").commands; +var players = require("/players"); +var ranks = require("/ranks"); +var staffCommands = require("/commands/staff").commands; +var timers = require("/timers"); +var utils = require("/utils"); +var votes = require("/votes"); +var Promise = require("/promise").Promise; +var Achievement = achievements.Achievement, Achievements = achievements.Achievements; +var Perm = commands.Perm, allCommands = commands.allCommands; +var bannedWords = config.bannedWords, FishServer = config.FishServer, Mode = config.Mode, Gamemode = config.Gamemode, FColor = config.FColor, mapRepoURLs = config.mapRepoURLs; +var FishPlayer = players.FishPlayer; +var Serializer = io.Serializer; +var FishEvents = globals.FishEvents, fishPlugin = globals.fishPlugin, fishState = globals.fishState, tileHistory = globals.tileHistory; +var FMap = maps.FMap; +var Rank = ranks.Rank, RoleFlag = ranks.RoleFlag; +var Menu = menus.Menu; +Object.assign(this, utils, funcs); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever +var Ranks = null; +var $ = Object.assign(function $(input) { + if (typeof input == "string") { + if (Pattern.matches("[a-zA-Z0-9+/]{22}==", input)) { + return FishPlayer.getById(input); + } + } + return null; +}, { + sussy: true, + info: function (input) { + if (typeof input == "string") { + if (Pattern.matches("[a-zA-Z0-9+/]{22}==", input)) { + return Vars.netServer.admins.getInfo(input); + } + } + return null; + }, + create: function (input) { + if (typeof input == "string") { + if (Pattern.matches("[a-zA-Z0-9+/]{22}==", input)) { + return FishPlayer.getFromInfo(Vars.netServer.admins.getInfo(input)); + } + } + return null; + }, + me: null, + meM: null, +}); +/** Used to persist variables. */ +var vars = {}; +function runJS(input, outputFunction, errorFunction, player) { + if (outputFunction === void 0) { outputFunction = Log.info; } + if (errorFunction === void 0) { errorFunction = Log.err; } + if (player) { + $.me = player; + $.meM = player.player; + } + else if (Groups.player.size() == 1) { + $.meM = Groups.player.first(); + $.me = players.FishPlayer.get($.meM); + } + try { + var admins = Vars.netServer.admins; + var output = eval(input); + if (output instanceof Array) { + outputFunction("&cArray: [&fr" + output.join(", ") + "&c]&fr"); + } + else if (output === undefined) { + outputFunction("undefined"); + } + else if (output === null) { + outputFunction("null"); + } + else { + outputFunction(output); + } + } + catch (err) { + errorFunction(err); + } +} diff --git a/build/scripts/frameworks/commands.js b/build/scripts/frameworks/commands.js index 50e79d93..63ab5097 100644 --- a/build/scripts/frameworks/commands.js +++ b/build/scripts/frameworks/commands.js @@ -1,22 +1,22 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("/frameworks/commands/commands"), exports); -__exportStar(require("/frameworks/commands/errors"), exports); -__exportStar(require("/frameworks/commands/formatting"), exports); -__exportStar(require("/frameworks/commands/perm"), exports); -__exportStar(require("/frameworks/commands/requirements"), exports); -__exportStar(require("/frameworks/commands/types"), exports); +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("/frameworks/commands/commands"), exports); +__exportStar(require("/frameworks/commands/errors"), exports); +__exportStar(require("/frameworks/commands/formatting"), exports); +__exportStar(require("/frameworks/commands/perm"), exports); +__exportStar(require("/frameworks/commands/requirements"), exports); +__exportStar(require("/frameworks/commands/types"), exports); diff --git a/build/scripts/frameworks/commands/commands.js b/build/scripts/frameworks/commands/commands.js index 28bf248b..fdad59da 100644 --- a/build/scripts/frameworks/commands/commands.js +++ b/build/scripts/frameworks/commands/commands.js @@ -1,767 +1,767 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the commands framework. -For usage information, see docs/framework-usage-guide.md -For maintenance information, see docs/frameworks.md -*/ -//Behold, the power of typescript! -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.consoleCommandList = exports.commandList = exports.allConsoleCommands = exports.allCommands = void 0; -exports.command = command; -exports.processArgString = processArgString; -exports.formatArg = formatArg; -exports.joinArgs = joinArgs; -exports.disambiguateArgument = disambiguateArgument; -exports.processArgs = processArgs; -exports.convertArgs = convertArgs; -exports.handleTapEvent = handleTapEvent; -exports.register = register; -exports.registerConsole = registerConsole; -exports.initialize = initialize; -exports.reset = reset; -var config_1 = require("/config"); -var errors_1 = require("/frameworks/commands/errors"); -var formatting_1 = require("/frameworks/commands/formatting"); -var types_1 = require("/frameworks/commands/types"); -var menus_1 = require("/frameworks/menus"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var players_1 = require("/players"); -var ranks_1 = require("/ranks"); -var utils_1 = require("/utils"); -var hiddenUnauthorizedMessage = "[scarlet]Unknown command. Check [lightgray]/help[scarlet]."; -/** Flag to prevent double initialization */ -var initialized = false; -/** Stores all chat comamnds by their name. */ -exports.allCommands = {}; -/** Stores all console commands by their name. */ -exports.allConsoleCommands = {}; -/** Stores the last usage data for chat commands by their name. */ -var globalUsageData = {}; -/** Helper function to get the correct type for command lists. */ -var commandList = function (list) { return list; }; -exports.commandList = commandList; -/** Helper function to get the correct type for command lists. */ -var consoleCommandList = function (list) { return list; }; -exports.consoleCommandList = consoleCommandList; -/** - * Helper function to get the correct type definitions for commands that use "data" or init(). - * Necessary because, while typescript is capable of inferring A1, A2... - * ``` - * { - * prop1: Type; - * prop2: Type; - * } - * ``` - * it cannot handle inferring A1 and B1. - * ``` - * { - * prop1: Type; - * prop2: Type; - * } - * ``` - */ -function command(input) { - return input; -} -/** Takes an arg string, like `reason:string?` and converts it to a CommandArg. */ -function processArgString(str) { - //this was copypasted from mlogx haha - var matchResult = str.match(/(\w+):(\w+)(\?)?/); - if (!matchResult) { - (0, funcs_1.crash)("Bad arg string ".concat(str, ": does not match pattern word:word(?)")); - } - var _a = __read(matchResult, 4), name = _a[1], type = _a[2], isOptional = _a[3]; - if (types_1.commandArgTypes.includes(type)) { - return { name: name, type: type, isOptional: !!isOptional }; - } - else { - (0, funcs_1.crash)("Bad arg string ".concat(str, ": invalid type ").concat(type)); - } -} -function formatArg(a) { - var isOptional = a.at(-1) == "?"; - var brackets = isOptional ? ["[", "]"] : ["<", ">"]; - return brackets[0] + a.split(":")[0] + brackets[1]; -} -/** Joins multi-word arguments that have been groups with quotes. Ex: turns [`"a`, `b"`] into [`a b`]*/ -function joinArgs(rawArgs) { - var e_1, _a; - var outputArgs = []; - var groupedArg = null; - try { - for (var rawArgs_1 = __values(rawArgs), rawArgs_1_1 = rawArgs_1.next(); !rawArgs_1_1.done; rawArgs_1_1 = rawArgs_1.next()) { - var arg = rawArgs_1_1.value; - if (arg.startsWith("\"") && groupedArg == null) { - groupedArg = []; - } - if (groupedArg) { - groupedArg.push(arg); - if (arg.endsWith("\"")) { - outputArgs.push(groupedArg.join(" ").slice(1, -1)); - groupedArg = null; - } - } - else { - outputArgs.push(arg); - } - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (rawArgs_1_1 && !rawArgs_1_1.done && (_a = rawArgs_1.return)) _a.call(rawArgs_1); - } - finally { if (e_1) throw e_1.error; } - } - if (groupedArg != null) { - //return `Unterminated string literal.`; - outputArgs.push(groupedArg.join(" ")); - } - return outputArgs; -} -function disambiguateArgument(options_1, arg_1, _a, sender_1, outputArgs_1, optionStringifier_1) { - return __awaiter(this, arguments, void 0, function (options, arg, _b, sender, outputArgs, optionStringifier, columns) { - var word, a_an_word, _c, _d; - var name = _b.name, type = _b.type; - if (columns === void 0) { columns = 3; } - return __generator(this, function (_e) { - switch (_e.label) { - case 0: - if (!(options == null)) return [3 /*break*/, 1]; - (0, errors_1.fail)("".concat((0, funcs_1.capitalizeText)(types_1.commandArgNames[type]), " \"").concat(arg, "\" not found.")); - return [3 /*break*/, 4]; - case 1: - if (!(options instanceof Array)) return [3 /*break*/, 3]; - word = types_1.commandArgNames[type]; - if (!sender) - (0, errors_1.fail)("Name \"".concat(arg, "\" could refer to more than one ").concat(word, ".")); - a_an_word = (0, funcs_1.indefiniteArticle)(word); - _c = outputArgs; - _d = name; - return [4 /*yield*/, menus_1.Menu.menu("Select ".concat(a_an_word), "Select ".concat(a_an_word, " for the argument \"").concat(name, "\""), options, sender, { - includeCancel: true, - optionStringifier: optionStringifier, - columns: columns, - })]; - case 2: - _c[_d] = _e.sent(); - return [3 /*break*/, 4]; - case 3: - outputArgs[name] = options; - _e.label = 4; - case 4: return [2 /*return*/]; - } - }); - }); -} -var argsSupportingBlank = ["player", "offlinePlayer", "unittype", "map", "mapOrRandom", "rank", "roleflag", "item"]; -/** Takes a list of joined args passed to the command, and processes it, turning it into a kwargs style object. */ -function processArgs(args, processedCmdArgs, sender) { - return __awaiter(this, void 0, void 0, function () { - var outputArgs, _a, _b, _c, i, cmdArg, commonArgs, _d, player, team, number, milliseconds, block, e_2_1; - var e_2, _e; - return __generator(this, function (_f) { - switch (_f.label) { - case 0: - outputArgs = {}; - _f.label = 1; - case 1: - _f.trys.push([1, 32, 33, 34]); - _a = __values(processedCmdArgs.entries()), _b = _a.next(); - _f.label = 2; - case 2: - if (!!_b.done) return [3 /*break*/, 31]; - _c = __read(_b.value, 2), i = _c[0], cmdArg = _c[1]; - if (!(i in args) || args[i] === "") { - //if the arg was not provided or it was empty - if (cmdArg.isOptional) { - outputArgs[cmdArg.name] = undefined; - return [3 /*break*/, 30]; - } - else if (sender && argsSupportingBlank.includes(cmdArg.type)) { - //it will be resolved later - } - else { - (0, errors_1.fail)("No value specified for arg ".concat(cmdArg.name, ". Did you type two spaces instead of one?")); - } - } - commonArgs = [args[i], cmdArg, sender, outputArgs]; - _d = cmdArg.type; - switch (_d) { - case "player": return [3 /*break*/, 3]; - case "offlinePlayer": return [3 /*break*/, 5]; - case "team": return [3 /*break*/, 10]; - case "number": return [3 /*break*/, 11]; - case "time": return [3 /*break*/, 12]; - case "string": return [3 /*break*/, 13]; - case "boolean": return [3 /*break*/, 14]; - case "block": return [3 /*break*/, 15]; - case "unittype": return [3 /*break*/, 16]; - case "uuid": return [3 /*break*/, 18]; - case "map": return [3 /*break*/, 19]; - case "mapOrRandom": return [3 /*break*/, 21]; - case "rank": return [3 /*break*/, 23]; - case "roleflag": return [3 /*break*/, 25]; - case "item": return [3 /*break*/, 27]; - } - return [3 /*break*/, 29]; - case 3: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([players_1.FishPlayer.search(players_1.FishPlayer.getAllOnline(), args[i])], __read(commonArgs), false), [function (player) { return (player.marked() ? config_1.prefixes.marked : player.autoflagged ? config_1.prefixes.flagged : "") + (Strings.stripColors(player.name).length >= 3 ? - player.name - : (0, funcs_1.escapeStringColorsClient)(player.name)); }, - 2], false))]; - case 4: - _f.sent(); - return [3 /*break*/, 30]; - case 5: - if (!globals_1.uuidPattern.test(args[i])) return [3 /*break*/, 6]; - player = players_1.FishPlayer.getById(args[i]); - if (player == null) - (0, errors_1.fail)("Player with uuid \"".concat(args[i], "\" not found. Specify \"create:").concat(args[i], "\" to create the player.")); - outputArgs[cmdArg.name] = player; - return [3 /*break*/, 9]; - case 6: - if (!(args[i].startsWith("create:") && globals_1.uuidPattern.test(args[i].split("create:")[1]))) return [3 /*break*/, 7]; - outputArgs[cmdArg.name] = players_1.FishPlayer.getFromInfo(Vars.netServer.admins.getInfo(args[i].split("create:")[1])); - return [3 /*break*/, 9]; - case 7: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([players_1.FishPlayer.search(Object.values(players_1.FishPlayer.cachedPlayers), args[i])], __read(commonArgs), false), [function (player) { return Strings.stripColors(player.name).length >= 3 ? - player.name - : (0, funcs_1.escapeStringColorsClient)(player.name); }, - 2], false))]; - case 8: - _f.sent(); - _f.label = 9; - case 9: return [3 /*break*/, 30]; - case 10: - { - team = (0, utils_1.getTeam)(args[i]); - if (typeof team == "string") - (0, errors_1.fail)(team); - outputArgs[cmdArg.name] = team; - return [3 /*break*/, 30]; - } - _f.label = 11; - case 11: - { - number = Number(args[i]); - if (isNaN(number)) { - if (/\(\d+,/.test(args[i])) - number = Number(args[i].slice(1, -1)); - else if (/\d+\)/.test(args[i])) - number = Number(args[i].slice(0, -1)); - if (isNaN(number)) - (0, errors_1.fail)("Invalid number \"".concat(args[i], "\"")); - } - outputArgs[cmdArg.name] = number; - return [3 /*break*/, 30]; - } - _f.label = 12; - case 12: - { - milliseconds = (0, utils_1.parseTimeString)(args[i]); - if (milliseconds == null) - (0, errors_1.fail)("Invalid time string \"".concat(args[i], "\"")); - outputArgs[cmdArg.name] = milliseconds; - return [3 /*break*/, 30]; - } - _f.label = 13; - case 13: - outputArgs[cmdArg.name] = args[i]; - return [3 /*break*/, 30]; - case 14: - switch (args[i].toLowerCase()) { - case "true": - case "yes": - case "yeah": - case "ya": - case "ye": - case "t": - case "y": - case "1": - outputArgs[cmdArg.name] = true; - break; - case "false": - case "no": - case "nah": - case "nay": - case "nope": - case "f": - case "n": - case "0": - outputArgs[cmdArg.name] = false; - break; - default: (0, errors_1.fail)("Argument ".concat(args[i], " is not a boolean. Try \"true\" or \"false\".")); - } - return [3 /*break*/, 30]; - case 15: - { - block = (0, utils_1.getBlock)(args[i], "air"); - if (typeof block == "string") - (0, errors_1.fail)(block); - outputArgs[cmdArg.name] = block; - return [3 /*break*/, 30]; - } - _f.label = 16; - case 16: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getUnitType)(args[i])], __read(commonArgs), false), [function (u) { return u.emoji() + (0, funcs_1.capitalizeText)(u.name); }], false))]; - case 17: - _f.sent(); - return [3 /*break*/, 30]; - case 18: - if (!globals_1.uuidPattern.test(args[i])) - (0, errors_1.fail)("Invalid uuid string \"".concat(args[i], "\"")); - outputArgs[cmdArg.name] = args[i]; - return [3 /*break*/, 30]; - case 19: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getMap)(args[i])], __read(commonArgs), false), [function (r) { return r.name(); }, - 2], false))]; - case 20: - _f.sent(); - return [3 /*break*/, 30]; - case 21: - if (["rand", "random"].includes(args[i].toLowerCase())) { - outputArgs[cmdArg.name] = "random"; - return [3 /*break*/, 30]; - } - return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getMap)(args[i])], __read(commonArgs), false), [function (r) { return r.name(); }, - 2], false))]; - case 22: - _f.sent(); - return [3 /*break*/, 30]; - case 23: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([ranks_1.Rank.search(args[i])], __read(commonArgs), false), [function (r) { return r.coloredName(); }], false))]; - case 24: - _f.sent(); - return [3 /*break*/, 30]; - case 25: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([ranks_1.RoleFlag.search(args[i])], __read(commonArgs), false), [function (f) { return f.coloredName(); }], false))]; - case 26: - _f.sent(); - return [3 /*break*/, 30]; - case 27: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getItem)(args[i])], __read(commonArgs), false), [function (i) { return i.emoji() + (0, funcs_1.capitalizeText)(i.name, "-"); }, - 2], false))]; - case 28: - _f.sent(); - return [3 /*break*/, 30]; - case 29: - cmdArg.type; - (0, funcs_1.crash)("impossible"); - _f.label = 30; - case 30: - _b = _a.next(); - return [3 /*break*/, 2]; - case 31: return [3 /*break*/, 34]; - case 32: - e_2_1 = _f.sent(); - e_2 = { error: e_2_1 }; - return [3 /*break*/, 34]; - case 33: - try { - if (_b && !_b.done && (_e = _a.return)) _e.call(_a); - } - finally { if (e_2) throw e_2.error; } - return [7 /*endfinally*/]; - case 34: return [2 /*return*/, outputArgs]; - } - }); - }); -} -var variadicArgumentTypes = ["player", "string", "map", "mapOrRandom"]; -function isArgOptional(arg, allowMenus) { - return arg.isOptional || (argsSupportingBlank.includes(arg.type) && allowMenus); -} -/** Converts the CommandArg[] to the format accepted by Arc CommandHandler */ -function convertArgs(processedCmdArgs, allowMenus) { - return processedCmdArgs.map(function (arg, index, array) { - var isOptional = isArgOptional(arg, allowMenus) && - !array.slice(index + 1).some(function (c) { return !isArgOptional(c, allowMenus); }); //this is enforced by the arc command handler - //TODO internalize command handler - var brackets = isOptional ? ["[", "]"] : ["<", ">"]; - //if the arg is a string and last argument, make it variadic (so if `/warn player a b c d` is run, the last arg is "a b c d" not "a") - return brackets[0] + arg.name + (variadicArgumentTypes.includes(arg.type) && index + 1 == array.length ? "..." : "") + brackets[1]; - }).join(" "); -} -function handleTapEvent(event) { - var _a; - var sender = players_1.FishPlayer.get(event.player); - if (sender.tapInfo.commandName == null) - return; - var command = exports.allCommands[sender.tapInfo.commandName]; - var usageData = sender.getUsageData(sender.tapInfo.commandName); - var handleTapsUpdated = false; - try { - var failed_1 = false; - (_a = command.tapped) === null || _a === void 0 ? void 0 : _a.call(command, { - args: sender.tapInfo.lastArgs, - data: command.data, - outputFail: function (message) { (0, utils_1.outputFail)(message, sender); failed_1 = true; }, - outputSuccess: function (message) { return (0, utils_1.outputSuccess)(message, sender); }, - output: function (message) { return (0, utils_1.outputMessage)(message, sender); }, - f: formatting_1.outputFormatter_client, - admins: Vars.netServer.admins, - commandLastUsed: usageData.lastUsed, - commandLastUsedSuccessfully: usageData.lastUsedSuccessfully, - lastUsed: usageData.tapLastUsed, - lastUsedSuccessfully: usageData.tapLastUsedSuccessfully, - sender: sender, - tile: event.tile, - x: event.tile.x, - y: event.tile.y, - currentTapMode: sender.tapInfo.commandName == null ? "off" : sender.tapInfo.mode, - handleTaps: function (mode) { - if (mode == "off") { - sender.tapInfo.commandName = null; - return; - } - sender.tapInfo.mode = mode; - handleTapsUpdated = true; - }, - }); - if (!failed_1) - usageData.tapLastUsedSuccessfully = Date.now(); - } - catch (err) { - (0, utils_1.handleError)(err, sender, utils_1.outputFail, "".concat(sender.cleanedName, " ran /").concat(sender.tapInfo.commandName, " and tapped")); - } - finally { - if (sender.tapInfo.mode == "once" && !handleTapsUpdated) { - sender.tapInfo.commandName = null; - } - usageData.tapLastUsed = Date.now(); - } -} -/** - * Registers all commands in a list to a client command handler. - **/ -function register(commands, clientHandler, serverHandler) { - var e_3, _a; - var _loop_1 = function (name, _data) { - //Invoke thunk if necessary - var data = typeof _data == "function" ? _data() : _data; - //Process the args - var processedCmdArgs = data.args.map(processArgString); - clientHandler.removeCommand(name); //The function silently fails if the argument doesn't exist so this is safe - clientHandler.register(name, convertArgs(processedCmdArgs, true), data.description, new CommandHandler.CommandRunner({ accept: function (unjoinedRawArgs, sender) { - return __awaiter(this, void 0, void 0, function () { - var fishSender, rawArgs, resolvedArgs, err_1, usageData, failed, args_1, requirements, err_2; - var _a; - return __generator(this, function (_b) { - switch (_b.label) { - case 0: - if (!initialized) - (0, funcs_1.crash)("Commands not initialized!"); - fishSender = players_1.FishPlayer.get(sender); - players_1.FishPlayer.onPlayerCommand(fishSender, name, unjoinedRawArgs); - //Verify authorization - //as a bonus, this crashes if data.perm is undefined - if (!data.perm.check(fishSender)) { - if (data.customUnauthorizedMessage) { - (0, utils_1.outputFail)(data.customUnauthorizedMessage, sender); - globals_1.FishEvents.fire("commandUnauthorized", [fishSender, name]); - } - else if (data.isHidden) - (0, utils_1.outputMessage)(hiddenUnauthorizedMessage, sender); - else - (0, utils_1.outputFail)(data.perm.unauthorizedMessage, sender); - return [2 /*return*/]; - } - rawArgs = joinArgs(unjoinedRawArgs); - _b.label = 1; - case 1: - _b.trys.push([1, 3, , 4]); - return [4 /*yield*/, processArgs(rawArgs, processedCmdArgs, fishSender)]; - case 2: - resolvedArgs = _b.sent(); - return [3 /*break*/, 4]; - case 3: - err_1 = _b.sent(); - (0, utils_1.handleError)(err_1, fishSender, utils_1.outputFail, "".concat(fishSender.cleanedName, " ran /").concat(name)); - return [2 /*return*/]; - case 4: - usageData = fishSender.getUsageData(name); - failed = false; - _b.label = 5; - case 5: - _b.trys.push([5, 7, 8, 9]); - args_1 = { - rawArgs: rawArgs, - args: resolvedArgs, - sender: fishSender, - data: data.data, - outputFail: function (message) { (0, utils_1.outputFail)(message, sender); failed = true; }, - outputSuccess: function (message) { return (0, utils_1.outputSuccess)(message, sender); }, - output: function (message) { return (0, utils_1.outputMessage)(message, sender); }, - f: formatting_1.f_client, - execServer: function (command) { return serverHandler.handleMessage(command); }, - admins: Vars.netServer.admins, - lastUsedSender: usageData.lastUsed, - lastUsedSuccessfullySender: usageData.lastUsedSuccessfully, - lastUsedSuccessfully: ((_a = globalUsageData[name]) !== null && _a !== void 0 ? _a : (globalUsageData[name] = { lastUsed: -1, lastUsedSuccessfully: -1 })).lastUsedSuccessfully, - allCommands: exports.allCommands, - currentTapMode: fishSender.tapInfo.commandName == null ? "off" : fishSender.tapInfo.mode, - handleTaps: function (mode) { - if (data.tapped == undefined) - (0, funcs_1.crash)("No tap handler to activate: command \"".concat(name, "\"")); - if (mode == "off") { - fishSender.tapInfo.commandName = null; - } - else { - fishSender.tapInfo.commandName = name; - fishSender.tapInfo.mode = mode; - } - fishSender.tapInfo.lastArgs = resolvedArgs; - }, - }; - requirements = typeof data.requirements == "function" ? data.requirements(args_1) : data.requirements; - requirements === null || requirements === void 0 ? void 0 : requirements.forEach(function (r) { return r(args_1); }); - return [4 /*yield*/, data.handler(args_1)]; - case 6: - _b.sent(); - //Update usage data - if (!failed) { - usageData.lastUsedSuccessfully = globalUsageData[name].lastUsedSuccessfully = Date.now(); - } - return [3 /*break*/, 9]; - case 7: - err_2 = _b.sent(); - (0, utils_1.handleError)(err_2, fishSender, utils_1.outputFail, "".concat(fishSender.cleanedName, " ran /").concat(name)); - return [3 /*break*/, 9]; - case 8: - usageData.lastUsed = globalUsageData[name].lastUsed = Date.now(); - return [7 /*endfinally*/]; - case 9: return [2 /*return*/]; - } - }); - }); - } })); - exports.allCommands[name] = data; - }; - try { - for (var _b = __values(Object.entries(commands)), _c = _b.next(); !_c.done; _c = _b.next()) { - var _d = __read(_c.value, 2), name = _d[0], _data = _d[1]; - _loop_1(name, _data); - } - } - catch (e_3_1) { e_3 = { error: e_3_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_3) throw e_3.error; } - } -} -function registerConsole(commands, serverHandler) { - var e_4, _a; - var _loop_2 = function (name, data) { - //Process the args - var processedCmdArgs = data.args.map(processArgString); - serverHandler.removeCommand(name); //The function silently fails if the argument doesn't exist so this is safe - serverHandler.register(name, convertArgs(processedCmdArgs, false), data.description, new CommandHandler.CommandRunner({ accept: function (rawArgs) { - return __awaiter(this, void 0, void 0, function () { - var resolvedArgs, err_3, usageData, failed_2; - var _a; - var _b; - return __generator(this, function (_c) { - switch (_c.label) { - case 0: - if (!initialized) - (0, funcs_1.crash)("Commands not initialized!"); - _c.label = 1; - case 1: - _c.trys.push([1, 3, , 4]); - return [4 /*yield*/, processArgs(rawArgs, processedCmdArgs, null)]; - case 2: - resolvedArgs = _c.sent(); - return [3 /*break*/, 4]; - case 3: - err_3 = _c.sent(); - //if args are invalid - Log.err(err_3); - return [2 /*return*/]; - case 4: - usageData = ((_a = globalUsageData[_b = "_console_" + name]) !== null && _a !== void 0 ? _a : (globalUsageData[_b] = { lastUsed: -1, lastUsedSuccessfully: -1 })); - try { - failed_2 = false; - data.handler(__assign({ rawArgs: rawArgs, args: resolvedArgs, data: data.data, outputFail: function (message) { (0, utils_1.outputConsole)(message, Log.err); failed_2 = true; }, outputSuccess: utils_1.outputConsole, output: utils_1.outputConsole, f: formatting_1.f_server, execServer: function (command) { return serverHandler.handleMessage(command); }, admins: Vars.netServer.admins }, usageData)); - usageData.lastUsed = Date.now(); - if (!failed_2) - usageData.lastUsedSuccessfully = Date.now(); - } - catch (err) { - usageData.lastUsed = Date.now(); - if (err instanceof errors_1.CommandError) { - Log.warn(typeof err.data == "function" ? err.data("&fr") : err.data); - } - else { - Log.err("&lrAn error occured while executing the command!&fr"); - Log.err((0, funcs_1.parseError)(err)); - } - } - return [2 /*return*/]; - } - }); - }); - } })); - exports.allConsoleCommands[name] = data; - }; - try { - for (var _b = __values(Object.entries(commands)), _c = _b.next(); !_c.done; _c = _b.next()) { - var _d = __read(_c.value, 2), name = _d[0], data = _d[1]; - _loop_2(name, data); - } - } - catch (e_4_1) { e_4 = { error: e_4_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_4) throw e_4.error; } - } -} -function initialize() { - var e_5, _a, e_6, _b; - if (initialized) { - (0, funcs_1.crash)("Already initialized commands."); - } - try { - for (var _c = __values(Object.entries(exports.allConsoleCommands)), _d = _c.next(); !_d.done; _d = _c.next()) { - var _e = __read(_d.value, 2), key = _e[0], command_1 = _e[1]; - if (command_1.init) - command_1.data = command_1.init(); - } - } - catch (e_5_1) { e_5 = { error: e_5_1 }; } - finally { - try { - if (_d && !_d.done && (_a = _c.return)) _a.call(_c); - } - finally { if (e_5) throw e_5.error; } - } - try { - for (var _f = __values(Object.entries(exports.allCommands)), _g = _f.next(); !_g.done; _g = _f.next()) { - var _h = __read(_g.value, 2), key = _h[0], command_2 = _h[1]; - if (command_2.init) - command_2.data = command_2.init(); - } - } - catch (e_6_1) { e_6 = { error: e_6_1 }; } - finally { - try { - if (_g && !_g.done && (_b = _f.return)) _b.call(_f); - } - finally { if (e_6) throw e_6.error; } - } - initialized = true; -} -function reset() { - var e_7, _a, e_8, _b; - initialized = false; - try { - for (var _c = __values(Object.entries(exports.allConsoleCommands)), _d = _c.next(); !_d.done; _d = _c.next()) { - var _e = __read(_d.value, 2), key = _e[0], command_3 = _e[1]; - if (command_3.init) - command_3.data = undefined; - } - } - catch (e_7_1) { e_7 = { error: e_7_1 }; } - finally { - try { - if (_d && !_d.done && (_a = _c.return)) _a.call(_c); - } - finally { if (e_7) throw e_7.error; } - } - try { - for (var _f = __values(Object.entries(exports.allCommands)), _g = _f.next(); !_g.done; _g = _f.next()) { - var _h = __read(_g.value, 2), key = _h[0], command_4 = _h[1]; - if (command_4.init) - command_4.data = undefined; - } - } - catch (e_8_1) { e_8 = { error: e_8_1 }; } - finally { - try { - if (_g && !_g.done && (_b = _f.return)) _b.call(_f); - } - finally { if (e_8) throw e_8.error; } - } -} +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the commands framework. +For usage information, see docs/framework-usage-guide.md +For maintenance information, see docs/frameworks.md +*/ +//Behold, the power of typescript! +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.consoleCommandList = exports.commandList = exports.allConsoleCommands = exports.allCommands = void 0; +exports.command = command; +exports.processArgString = processArgString; +exports.formatArg = formatArg; +exports.joinArgs = joinArgs; +exports.disambiguateArgument = disambiguateArgument; +exports.processArgs = processArgs; +exports.convertArgs = convertArgs; +exports.handleTapEvent = handleTapEvent; +exports.register = register; +exports.registerConsole = registerConsole; +exports.initialize = initialize; +exports.reset = reset; +var config_1 = require("/config"); +var errors_1 = require("/frameworks/commands/errors"); +var formatting_1 = require("/frameworks/commands/formatting"); +var types_1 = require("/frameworks/commands/types"); +var menus_1 = require("/frameworks/menus"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var players_1 = require("/players"); +var ranks_1 = require("/ranks"); +var utils_1 = require("/utils"); +var hiddenUnauthorizedMessage = "[scarlet]Unknown command. Check [lightgray]/help[scarlet]."; +/** Flag to prevent double initialization */ +var initialized = false; +/** Stores all chat comamnds by their name. */ +exports.allCommands = {}; +/** Stores all console commands by their name. */ +exports.allConsoleCommands = {}; +/** Stores the last usage data for chat commands by their name. */ +var globalUsageData = {}; +/** Helper function to get the correct type for command lists. */ +var commandList = function (list) { return list; }; +exports.commandList = commandList; +/** Helper function to get the correct type for command lists. */ +var consoleCommandList = function (list) { return list; }; +exports.consoleCommandList = consoleCommandList; +/** + * Helper function to get the correct type definitions for commands that use "data" or init(). + * Necessary because, while typescript is capable of inferring A1, A2... + * ``` + * { + * prop1: Type; + * prop2: Type; + * } + * ``` + * it cannot handle inferring A1 and B1. + * ``` + * { + * prop1: Type; + * prop2: Type; + * } + * ``` + */ +function command(input) { + return input; +} +/** Takes an arg string, like `reason:string?` and converts it to a CommandArg. */ +function processArgString(str) { + //this was copypasted from mlogx haha + var matchResult = str.match(/(\w+):(\w+)(\?)?/); + if (!matchResult) { + (0, funcs_1.crash)("Bad arg string ".concat(str, ": does not match pattern word:word(?)")); + } + var _a = __read(matchResult, 4), name = _a[1], type = _a[2], isOptional = _a[3]; + if (types_1.commandArgTypes.includes(type)) { + return { name: name, type: type, isOptional: !!isOptional }; + } + else { + (0, funcs_1.crash)("Bad arg string ".concat(str, ": invalid type ").concat(type)); + } +} +function formatArg(a) { + var isOptional = a.at(-1) == "?"; + var brackets = isOptional ? ["[", "]"] : ["<", ">"]; + return brackets[0] + a.split(":")[0] + brackets[1]; +} +/** Joins multi-word arguments that have been groups with quotes. Ex: turns [`"a`, `b"`] into [`a b`]*/ +function joinArgs(rawArgs) { + var e_1, _a; + var outputArgs = []; + var groupedArg = null; + try { + for (var rawArgs_1 = __values(rawArgs), rawArgs_1_1 = rawArgs_1.next(); !rawArgs_1_1.done; rawArgs_1_1 = rawArgs_1.next()) { + var arg = rawArgs_1_1.value; + if (arg.startsWith("\"") && groupedArg == null) { + groupedArg = []; + } + if (groupedArg) { + groupedArg.push(arg); + if (arg.endsWith("\"")) { + outputArgs.push(groupedArg.join(" ").slice(1, -1)); + groupedArg = null; + } + } + else { + outputArgs.push(arg); + } + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (rawArgs_1_1 && !rawArgs_1_1.done && (_a = rawArgs_1.return)) _a.call(rawArgs_1); + } + finally { if (e_1) throw e_1.error; } + } + if (groupedArg != null) { + //return `Unterminated string literal.`; + outputArgs.push(groupedArg.join(" ")); + } + return outputArgs; +} +function disambiguateArgument(options_1, arg_1, _a, sender_1, outputArgs_1, optionStringifier_1) { + return __awaiter(this, arguments, void 0, function (options, arg, _b, sender, outputArgs, optionStringifier, columns) { + var word, a_an_word, _c, _d; + var name = _b.name, type = _b.type; + if (columns === void 0) { columns = 3; } + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + if (!(options == null)) return [3 /*break*/, 1]; + (0, errors_1.fail)("".concat((0, funcs_1.capitalizeText)(types_1.commandArgNames[type]), " \"").concat(arg, "\" not found.")); + return [3 /*break*/, 4]; + case 1: + if (!(options instanceof Array)) return [3 /*break*/, 3]; + word = types_1.commandArgNames[type]; + if (!sender) + (0, errors_1.fail)("Name \"".concat(arg, "\" could refer to more than one ").concat(word, ".")); + a_an_word = (0, funcs_1.indefiniteArticle)(word); + _c = outputArgs; + _d = name; + return [4 /*yield*/, menus_1.Menu.menu("Select ".concat(a_an_word), "Select ".concat(a_an_word, " for the argument \"").concat(name, "\""), options, sender, { + includeCancel: true, + optionStringifier: optionStringifier, + columns: columns, + })]; + case 2: + _c[_d] = _e.sent(); + return [3 /*break*/, 4]; + case 3: + outputArgs[name] = options; + _e.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); +} +var argsSupportingBlank = ["player", "offlinePlayer", "unittype", "map", "mapOrRandom", "rank", "roleflag", "item"]; +/** Takes a list of joined args passed to the command, and processes it, turning it into a kwargs style object. */ +function processArgs(args, processedCmdArgs, sender) { + return __awaiter(this, void 0, void 0, function () { + var outputArgs, _a, _b, _c, i, cmdArg, commonArgs, _d, player, team, number, milliseconds, block, e_2_1; + var e_2, _e; + return __generator(this, function (_f) { + switch (_f.label) { + case 0: + outputArgs = {}; + _f.label = 1; + case 1: + _f.trys.push([1, 32, 33, 34]); + _a = __values(processedCmdArgs.entries()), _b = _a.next(); + _f.label = 2; + case 2: + if (!!_b.done) return [3 /*break*/, 31]; + _c = __read(_b.value, 2), i = _c[0], cmdArg = _c[1]; + if (!(i in args) || args[i] === "") { + //if the arg was not provided or it was empty + if (cmdArg.isOptional) { + outputArgs[cmdArg.name] = undefined; + return [3 /*break*/, 30]; + } + else if (sender && argsSupportingBlank.includes(cmdArg.type)) { + //it will be resolved later + } + else { + (0, errors_1.fail)("No value specified for arg ".concat(cmdArg.name, ". Did you type two spaces instead of one?")); + } + } + commonArgs = [args[i], cmdArg, sender, outputArgs]; + _d = cmdArg.type; + switch (_d) { + case "player": return [3 /*break*/, 3]; + case "offlinePlayer": return [3 /*break*/, 5]; + case "team": return [3 /*break*/, 10]; + case "number": return [3 /*break*/, 11]; + case "time": return [3 /*break*/, 12]; + case "string": return [3 /*break*/, 13]; + case "boolean": return [3 /*break*/, 14]; + case "block": return [3 /*break*/, 15]; + case "unittype": return [3 /*break*/, 16]; + case "uuid": return [3 /*break*/, 18]; + case "map": return [3 /*break*/, 19]; + case "mapOrRandom": return [3 /*break*/, 21]; + case "rank": return [3 /*break*/, 23]; + case "roleflag": return [3 /*break*/, 25]; + case "item": return [3 /*break*/, 27]; + } + return [3 /*break*/, 29]; + case 3: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([players_1.FishPlayer.search(players_1.FishPlayer.getAllOnline(), args[i])], __read(commonArgs), false), [function (player) { return (player.marked() ? config_1.prefixes.marked : player.autoflagged ? config_1.prefixes.flagged : "") + (Strings.stripColors(player.name).length >= 3 ? + player.name + : (0, funcs_1.escapeStringColorsClient)(player.name)); }, + 2], false))]; + case 4: + _f.sent(); + return [3 /*break*/, 30]; + case 5: + if (!globals_1.uuidPattern.test(args[i])) return [3 /*break*/, 6]; + player = players_1.FishPlayer.getById(args[i]); + if (player == null) + (0, errors_1.fail)("Player with uuid \"".concat(args[i], "\" not found. Specify \"create:").concat(args[i], "\" to create the player.")); + outputArgs[cmdArg.name] = player; + return [3 /*break*/, 9]; + case 6: + if (!(args[i].startsWith("create:") && globals_1.uuidPattern.test(args[i].split("create:")[1]))) return [3 /*break*/, 7]; + outputArgs[cmdArg.name] = players_1.FishPlayer.getFromInfo(Vars.netServer.admins.getInfo(args[i].split("create:")[1])); + return [3 /*break*/, 9]; + case 7: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([players_1.FishPlayer.search(Object.values(players_1.FishPlayer.cachedPlayers), args[i])], __read(commonArgs), false), [function (player) { return Strings.stripColors(player.name).length >= 3 ? + player.name + : (0, funcs_1.escapeStringColorsClient)(player.name); }, + 2], false))]; + case 8: + _f.sent(); + _f.label = 9; + case 9: return [3 /*break*/, 30]; + case 10: + { + team = (0, utils_1.getTeam)(args[i]); + if (typeof team == "string") + (0, errors_1.fail)(team); + outputArgs[cmdArg.name] = team; + return [3 /*break*/, 30]; + } + _f.label = 11; + case 11: + { + number = Number(args[i]); + if (isNaN(number)) { + if (/\(\d+,/.test(args[i])) + number = Number(args[i].slice(1, -1)); + else if (/\d+\)/.test(args[i])) + number = Number(args[i].slice(0, -1)); + if (isNaN(number)) + (0, errors_1.fail)("Invalid number \"".concat(args[i], "\"")); + } + outputArgs[cmdArg.name] = number; + return [3 /*break*/, 30]; + } + _f.label = 12; + case 12: + { + milliseconds = (0, utils_1.parseTimeString)(args[i]); + if (milliseconds == null) + (0, errors_1.fail)("Invalid time string \"".concat(args[i], "\"")); + outputArgs[cmdArg.name] = milliseconds; + return [3 /*break*/, 30]; + } + _f.label = 13; + case 13: + outputArgs[cmdArg.name] = args[i]; + return [3 /*break*/, 30]; + case 14: + switch (args[i].toLowerCase()) { + case "true": + case "yes": + case "yeah": + case "ya": + case "ye": + case "t": + case "y": + case "1": + outputArgs[cmdArg.name] = true; + break; + case "false": + case "no": + case "nah": + case "nay": + case "nope": + case "f": + case "n": + case "0": + outputArgs[cmdArg.name] = false; + break; + default: (0, errors_1.fail)("Argument ".concat(args[i], " is not a boolean. Try \"true\" or \"false\".")); + } + return [3 /*break*/, 30]; + case 15: + { + block = (0, utils_1.getBlock)(args[i], "air"); + if (typeof block == "string") + (0, errors_1.fail)(block); + outputArgs[cmdArg.name] = block; + return [3 /*break*/, 30]; + } + _f.label = 16; + case 16: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getUnitType)(args[i])], __read(commonArgs), false), [function (u) { return u.emoji() + (0, funcs_1.capitalizeText)(u.name); }], false))]; + case 17: + _f.sent(); + return [3 /*break*/, 30]; + case 18: + if (!globals_1.uuidPattern.test(args[i])) + (0, errors_1.fail)("Invalid uuid string \"".concat(args[i], "\"")); + outputArgs[cmdArg.name] = args[i]; + return [3 /*break*/, 30]; + case 19: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getMap)(args[i])], __read(commonArgs), false), [function (r) { return r.name(); }, + 2], false))]; + case 20: + _f.sent(); + return [3 /*break*/, 30]; + case 21: + if (["rand", "random"].includes(args[i].toLowerCase())) { + outputArgs[cmdArg.name] = "random"; + return [3 /*break*/, 30]; + } + return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getMap)(args[i])], __read(commonArgs), false), [function (r) { return r.name(); }, + 2], false))]; + case 22: + _f.sent(); + return [3 /*break*/, 30]; + case 23: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([ranks_1.Rank.search(args[i])], __read(commonArgs), false), [function (r) { return r.coloredName(); }], false))]; + case 24: + _f.sent(); + return [3 /*break*/, 30]; + case 25: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([ranks_1.RoleFlag.search(args[i])], __read(commonArgs), false), [function (f) { return f.coloredName(); }], false))]; + case 26: + _f.sent(); + return [3 /*break*/, 30]; + case 27: return [4 /*yield*/, disambiguateArgument.apply(void 0, __spreadArray(__spreadArray([(0, utils_1.getItem)(args[i])], __read(commonArgs), false), [function (i) { return i.emoji() + (0, funcs_1.capitalizeText)(i.name, "-"); }, + 2], false))]; + case 28: + _f.sent(); + return [3 /*break*/, 30]; + case 29: + cmdArg.type; + (0, funcs_1.crash)("impossible"); + _f.label = 30; + case 30: + _b = _a.next(); + return [3 /*break*/, 2]; + case 31: return [3 /*break*/, 34]; + case 32: + e_2_1 = _f.sent(); + e_2 = { error: e_2_1 }; + return [3 /*break*/, 34]; + case 33: + try { + if (_b && !_b.done && (_e = _a.return)) _e.call(_a); + } + finally { if (e_2) throw e_2.error; } + return [7 /*endfinally*/]; + case 34: return [2 /*return*/, outputArgs]; + } + }); + }); +} +var variadicArgumentTypes = ["player", "string", "map", "mapOrRandom"]; +function isArgOptional(arg, allowMenus) { + return arg.isOptional || (argsSupportingBlank.includes(arg.type) && allowMenus); +} +/** Converts the CommandArg[] to the format accepted by Arc CommandHandler */ +function convertArgs(processedCmdArgs, allowMenus) { + return processedCmdArgs.map(function (arg, index, array) { + var isOptional = isArgOptional(arg, allowMenus) && + !array.slice(index + 1).some(function (c) { return !isArgOptional(c, allowMenus); }); //this is enforced by the arc command handler + //TODO internalize command handler + var brackets = isOptional ? ["[", "]"] : ["<", ">"]; + //if the arg is a string and last argument, make it variadic (so if `/warn player a b c d` is run, the last arg is "a b c d" not "a") + return brackets[0] + arg.name + (variadicArgumentTypes.includes(arg.type) && index + 1 == array.length ? "..." : "") + brackets[1]; + }).join(" "); +} +function handleTapEvent(event) { + var _a; + var sender = players_1.FishPlayer.get(event.player); + if (sender.tapInfo.commandName == null) + return; + var command = exports.allCommands[sender.tapInfo.commandName]; + var usageData = sender.getUsageData(sender.tapInfo.commandName); + var handleTapsUpdated = false; + try { + var failed_1 = false; + (_a = command.tapped) === null || _a === void 0 ? void 0 : _a.call(command, { + args: sender.tapInfo.lastArgs, + data: command.data, + outputFail: function (message) { (0, utils_1.outputFail)(message, sender); failed_1 = true; }, + outputSuccess: function (message) { return (0, utils_1.outputSuccess)(message, sender); }, + output: function (message) { return (0, utils_1.outputMessage)(message, sender); }, + f: formatting_1.outputFormatter_client, + admins: Vars.netServer.admins, + commandLastUsed: usageData.lastUsed, + commandLastUsedSuccessfully: usageData.lastUsedSuccessfully, + lastUsed: usageData.tapLastUsed, + lastUsedSuccessfully: usageData.tapLastUsedSuccessfully, + sender: sender, + tile: event.tile, + x: event.tile.x, + y: event.tile.y, + currentTapMode: sender.tapInfo.commandName == null ? "off" : sender.tapInfo.mode, + handleTaps: function (mode) { + if (mode == "off") { + sender.tapInfo.commandName = null; + return; + } + sender.tapInfo.mode = mode; + handleTapsUpdated = true; + }, + }); + if (!failed_1) + usageData.tapLastUsedSuccessfully = Date.now(); + } + catch (err) { + (0, utils_1.handleError)(err, sender, utils_1.outputFail, "".concat(sender.cleanedName, " ran /").concat(sender.tapInfo.commandName, " and tapped")); + } + finally { + if (sender.tapInfo.mode == "once" && !handleTapsUpdated) { + sender.tapInfo.commandName = null; + } + usageData.tapLastUsed = Date.now(); + } +} +/** + * Registers all commands in a list to a client command handler. + **/ +function register(commands, clientHandler, serverHandler) { + var e_3, _a; + var _loop_1 = function (name, _data) { + //Invoke thunk if necessary + var data = typeof _data == "function" ? _data() : _data; + //Process the args + var processedCmdArgs = data.args.map(processArgString); + clientHandler.removeCommand(name); //The function silently fails if the argument doesn't exist so this is safe + clientHandler.register(name, convertArgs(processedCmdArgs, true), data.description, new CommandHandler.CommandRunner({ accept: function (unjoinedRawArgs, sender) { + return __awaiter(this, void 0, void 0, function () { + var fishSender, rawArgs, resolvedArgs, err_1, usageData, failed, args_1, requirements, err_2; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!initialized) + (0, funcs_1.crash)("Commands not initialized!"); + fishSender = players_1.FishPlayer.get(sender); + players_1.FishPlayer.onPlayerCommand(fishSender, name, unjoinedRawArgs); + //Verify authorization + //as a bonus, this crashes if data.perm is undefined + if (!data.perm.check(fishSender)) { + if (data.customUnauthorizedMessage) { + (0, utils_1.outputFail)(data.customUnauthorizedMessage, sender); + globals_1.FishEvents.fire("commandUnauthorized", [fishSender, name]); + } + else if (data.isHidden) + (0, utils_1.outputMessage)(hiddenUnauthorizedMessage, sender); + else + (0, utils_1.outputFail)(data.perm.unauthorizedMessage, sender); + return [2 /*return*/]; + } + rawArgs = joinArgs(unjoinedRawArgs); + _b.label = 1; + case 1: + _b.trys.push([1, 3, , 4]); + return [4 /*yield*/, processArgs(rawArgs, processedCmdArgs, fishSender)]; + case 2: + resolvedArgs = _b.sent(); + return [3 /*break*/, 4]; + case 3: + err_1 = _b.sent(); + (0, utils_1.handleError)(err_1, fishSender, utils_1.outputFail, "".concat(fishSender.cleanedName, " ran /").concat(name)); + return [2 /*return*/]; + case 4: + usageData = fishSender.getUsageData(name); + failed = false; + _b.label = 5; + case 5: + _b.trys.push([5, 7, 8, 9]); + args_1 = { + rawArgs: rawArgs, + args: resolvedArgs, + sender: fishSender, + data: data.data, + outputFail: function (message) { (0, utils_1.outputFail)(message, sender); failed = true; }, + outputSuccess: function (message) { return (0, utils_1.outputSuccess)(message, sender); }, + output: function (message) { return (0, utils_1.outputMessage)(message, sender); }, + f: formatting_1.f_client, + execServer: function (command) { return serverHandler.handleMessage(command); }, + admins: Vars.netServer.admins, + lastUsedSender: usageData.lastUsed, + lastUsedSuccessfullySender: usageData.lastUsedSuccessfully, + lastUsedSuccessfully: ((_a = globalUsageData[name]) !== null && _a !== void 0 ? _a : (globalUsageData[name] = { lastUsed: -1, lastUsedSuccessfully: -1 })).lastUsedSuccessfully, + allCommands: exports.allCommands, + currentTapMode: fishSender.tapInfo.commandName == null ? "off" : fishSender.tapInfo.mode, + handleTaps: function (mode) { + if (data.tapped == undefined) + (0, funcs_1.crash)("No tap handler to activate: command \"".concat(name, "\"")); + if (mode == "off") { + fishSender.tapInfo.commandName = null; + } + else { + fishSender.tapInfo.commandName = name; + fishSender.tapInfo.mode = mode; + } + fishSender.tapInfo.lastArgs = resolvedArgs; + }, + }; + requirements = typeof data.requirements == "function" ? data.requirements(args_1) : data.requirements; + requirements === null || requirements === void 0 ? void 0 : requirements.forEach(function (r) { return r(args_1); }); + return [4 /*yield*/, data.handler(args_1)]; + case 6: + _b.sent(); + //Update usage data + if (!failed) { + usageData.lastUsedSuccessfully = globalUsageData[name].lastUsedSuccessfully = Date.now(); + } + return [3 /*break*/, 9]; + case 7: + err_2 = _b.sent(); + (0, utils_1.handleError)(err_2, fishSender, utils_1.outputFail, "".concat(fishSender.cleanedName, " ran /").concat(name)); + return [3 /*break*/, 9]; + case 8: + usageData.lastUsed = globalUsageData[name].lastUsed = Date.now(); + return [7 /*endfinally*/]; + case 9: return [2 /*return*/]; + } + }); + }); + } })); + exports.allCommands[name] = data; + }; + try { + for (var _b = __values(Object.entries(commands)), _c = _b.next(); !_c.done; _c = _b.next()) { + var _d = __read(_c.value, 2), name = _d[0], _data = _d[1]; + _loop_1(name, _data); + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_3) throw e_3.error; } + } +} +function registerConsole(commands, serverHandler) { + var e_4, _a; + var _loop_2 = function (name, data) { + //Process the args + var processedCmdArgs = data.args.map(processArgString); + serverHandler.removeCommand(name); //The function silently fails if the argument doesn't exist so this is safe + serverHandler.register(name, convertArgs(processedCmdArgs, false), data.description, new CommandHandler.CommandRunner({ accept: function (rawArgs) { + return __awaiter(this, void 0, void 0, function () { + var resolvedArgs, err_3, usageData, failed_2; + var _a; + var _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!initialized) + (0, funcs_1.crash)("Commands not initialized!"); + _c.label = 1; + case 1: + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, processArgs(rawArgs, processedCmdArgs, null)]; + case 2: + resolvedArgs = _c.sent(); + return [3 /*break*/, 4]; + case 3: + err_3 = _c.sent(); + //if args are invalid + Log.err(err_3); + return [2 /*return*/]; + case 4: + usageData = ((_a = globalUsageData[_b = "_console_" + name]) !== null && _a !== void 0 ? _a : (globalUsageData[_b] = { lastUsed: -1, lastUsedSuccessfully: -1 })); + try { + failed_2 = false; + data.handler(__assign({ rawArgs: rawArgs, args: resolvedArgs, data: data.data, outputFail: function (message) { (0, utils_1.outputConsole)(message, Log.err); failed_2 = true; }, outputSuccess: utils_1.outputConsole, output: utils_1.outputConsole, f: formatting_1.f_server, execServer: function (command) { return serverHandler.handleMessage(command); }, admins: Vars.netServer.admins }, usageData)); + usageData.lastUsed = Date.now(); + if (!failed_2) + usageData.lastUsedSuccessfully = Date.now(); + } + catch (err) { + usageData.lastUsed = Date.now(); + if (err instanceof errors_1.CommandError) { + Log.warn(typeof err.data == "function" ? err.data("&fr") : err.data); + } + else { + Log.err("&lrAn error occured while executing the command!&fr"); + Log.err((0, funcs_1.parseError)(err)); + } + } + return [2 /*return*/]; + } + }); + }); + } })); + exports.allConsoleCommands[name] = data; + }; + try { + for (var _b = __values(Object.entries(commands)), _c = _b.next(); !_c.done; _c = _b.next()) { + var _d = __read(_c.value, 2), name = _d[0], data = _d[1]; + _loop_2(name, data); + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_4) throw e_4.error; } + } +} +function initialize() { + var e_5, _a, e_6, _b; + if (initialized) { + (0, funcs_1.crash)("Already initialized commands."); + } + try { + for (var _c = __values(Object.entries(exports.allConsoleCommands)), _d = _c.next(); !_d.done; _d = _c.next()) { + var _e = __read(_d.value, 2), key = _e[0], command_1 = _e[1]; + if (command_1.init) + command_1.data = command_1.init(); + } + } + catch (e_5_1) { e_5 = { error: e_5_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_5) throw e_5.error; } + } + try { + for (var _f = __values(Object.entries(exports.allCommands)), _g = _f.next(); !_g.done; _g = _f.next()) { + var _h = __read(_g.value, 2), key = _h[0], command_2 = _h[1]; + if (command_2.init) + command_2.data = command_2.init(); + } + } + catch (e_6_1) { e_6 = { error: e_6_1 }; } + finally { + try { + if (_g && !_g.done && (_b = _f.return)) _b.call(_f); + } + finally { if (e_6) throw e_6.error; } + } + initialized = true; +} +function reset() { + var e_7, _a, e_8, _b; + initialized = false; + try { + for (var _c = __values(Object.entries(exports.allConsoleCommands)), _d = _c.next(); !_d.done; _d = _c.next()) { + var _e = __read(_d.value, 2), key = _e[0], command_3 = _e[1]; + if (command_3.init) + command_3.data = undefined; + } + } + catch (e_7_1) { e_7 = { error: e_7_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_7) throw e_7.error; } + } + try { + for (var _f = __values(Object.entries(exports.allCommands)), _g = _f.next(); !_g.done; _g = _f.next()) { + var _h = __read(_g.value, 2), key = _h[0], command_4 = _h[1]; + if (command_4.init) + command_4.data = undefined; + } + } + catch (e_8_1) { e_8 = { error: e_8_1 }; } + finally { + try { + if (_g && !_g.done && (_b = _f.return)) _b.call(_f); + } + finally { if (e_8) throw e_8.error; } + } +} diff --git a/build/scripts/frameworks/commands/errors.js b/build/scripts/frameworks/commands/errors.js index a7fa23f2..ba915110 100644 --- a/build/scripts/frameworks/commands/errors.js +++ b/build/scripts/frameworks/commands/errors.js @@ -1,20 +1,20 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the error handling framework. -For usage information, see docs/framework-usage-guide.md -For maintenance information, see docs/frameworks.md -*/ -//Behold, the power of typescript! -Object.defineProperty(exports, "__esModule", { value: true }); -exports.CommandError = void 0; -exports.fail = fail; -exports.CommandError = (function () { }); -Object.setPrototypeOf(exports.CommandError.prototype, Error.prototype); -function fail(message) { - var err = new Error(typeof message == "string" ? message : ""); - //oh no it's even worse now because i have to smuggle a function through here - err.data = message; - Object.setPrototypeOf(err, exports.CommandError.prototype); - throw err; -} +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the error handling framework. +For usage information, see docs/framework-usage-guide.md +For maintenance information, see docs/frameworks.md +*/ +//Behold, the power of typescript! +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CommandError = void 0; +exports.fail = fail; +exports.CommandError = (function () { }); +Object.setPrototypeOf(exports.CommandError.prototype, Error.prototype); +function fail(message) { + var err = new Error(typeof message == "string" ? message : ""); + //oh no it's even worse now because i have to smuggle a function through here + err.data = message; + Object.setPrototypeOf(err, exports.CommandError.prototype); + throw err; +} diff --git a/build/scripts/frameworks/commands/formatting.js b/build/scripts/frameworks/commands/formatting.js index ecd73206..08e169af 100644 --- a/build/scripts/frameworks/commands/formatting.js +++ b/build/scripts/frameworks/commands/formatting.js @@ -1,183 +1,183 @@ -"use strict"; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.f_server = exports.f_client = exports.processedFFunctions = exports.fFunctions = exports.outputFormatter_client = exports.outputFormatter_server = void 0; -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var players_1 = require("/players"); -var ranks_1 = require("/ranks"); -exports.outputFormatter_server = (0, funcs_1.tagProcessorPartial)(function (chunk) { - if (chunk instanceof players_1.FishPlayer) { - return "&c(".concat((0, funcs_1.escapeStringColorsServer)(chunk.cleanedName), ")&fr"); - } - else if (chunk instanceof ranks_1.Rank) { - return "&p".concat(chunk.name, "&fr"); - } - else if (chunk instanceof ranks_1.RoleFlag) { - return "&p".concat(chunk.name, "&fr"); - } - else if (chunk instanceof Error) { - return "&r".concat((0, funcs_1.escapeStringColorsServer)(chunk.toString()), "&fr"); - } - else if (chunk instanceof Player) { - var player = chunk; //not sure why this is necessary, typescript randomly converts any to unknown - return "&cPlayer#".concat(player.id, " (").concat((0, funcs_1.escapeStringColorsServer)(Strings.stripColors(player.name)), ")&fr"); - } - else if (typeof chunk == "string") { - if (globals_1.uuidPattern.test(chunk)) { - return "&b".concat(chunk, "&fr"); - } - else if (globals_1.ipPattern.test(chunk)) { - return "&b".concat(chunk, "&fr"); - } - else { - return "".concat(chunk); - } - } - else if (typeof chunk == "boolean") { - return "&b".concat(chunk.toString(), "&fr"); - } - else if (typeof chunk == "number") { - return "&b".concat(chunk.toString(), "&fr"); - } - else if (chunk instanceof Administration.PlayerInfo) { - return "&c".concat((0, funcs_1.escapeStringColorsServer)(chunk.plainLastName()), "&fr"); - } - else if (chunk instanceof UnitType) { - return "&c".concat(chunk.localizedName, "&fr"); - } - else if (chunk instanceof Block) { - return "&c".concat(chunk.localizedName, "&fr"); - } - else if (chunk instanceof Team) { - return "&c".concat(chunk.name, "&fr"); - } - else if (chunk instanceof Item) { - return "&c".concat((0, funcs_1.capitalizeText)(chunk.name, "-"), "&fr"); - } - else { - chunk; - Log.err("Invalid format object!"); - Log.info(chunk); - return chunk; //let it get stringified by the JS engine - } -}); -exports.outputFormatter_client = (0, funcs_1.tagProcessorPartial)(function (chunk, i, data, stringChunks) { - var _a, _b; - var reset = (_b = data !== null && data !== void 0 ? data : (_a = stringChunks[0].match(/^\[.+?\]/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : ""; - if (chunk instanceof players_1.FishPlayer) { - return "[cyan](".concat(chunk.name, "[cyan])") + reset; - } - else if (chunk instanceof ranks_1.Rank) { - return "".concat(chunk.color).concat(chunk.name, "[]") + reset; - } - else if (chunk instanceof ranks_1.RoleFlag) { - return "".concat(chunk.color).concat(chunk.name, "[]") + reset; - } - else if (chunk instanceof Error) { - return "[red]".concat(chunk.toString()) + reset; - } - else if (chunk instanceof Player) { - var fishP = players_1.FishPlayer.get(chunk); - return "[cyan](".concat(fishP.name, "[cyan])") + reset; - } - else if (typeof chunk == "string") { - if (globals_1.uuidPattern.test(chunk)) { - return "[blue]".concat(chunk, "[]"); - } - else if (globals_1.ipPattern.test(chunk)) { - return "[blue]".concat(chunk, "[]"); - } - else { - //TODO reset color? - return chunk; - } - } - else if (typeof chunk == "boolean") { - return "[blue]".concat(chunk.toString(), "[]"); - } - else if (typeof chunk == "number") { - return "[blue]".concat(chunk.toString(), "[]"); - } - else if (chunk instanceof Administration.PlayerInfo) { - return chunk.lastName + reset; - } - else if (chunk instanceof UnitType) { - return "[cyan]".concat(chunk.localizedName, "[]"); - } - else if (chunk instanceof Block) { - return "[cyan]".concat(chunk.localizedName, "[]"); - } - else if (chunk instanceof Team) { - return "[white]".concat(chunk.coloredName(), "[][]"); - } - else if (chunk instanceof Item) { - return "[cyan]".concat((0, funcs_1.capitalizeText)(chunk.name, "-"), "[]"); - } - else { - chunk; - Log.err("Invalid format object!"); - Log.info(chunk); - return chunk; //allow it to get stringified by the engine - } -}); -exports.fFunctions = { - boolGood: function (value) { - return [ - value ? "[green]true[]" : "[red]false[]", - value ? "&lgtrue&fr" : "&lrfalse&fr", - ]; - }, - boolBad: function (value) { - return [ - value ? "[red]true[]" : "[green]false[]", - value ? "&lrtrue&fr" : "&lgfalse&fr", - ]; - }, - percent: function (value, decimals) { - if (decimals === void 0) { decimals = 0; } - if (isNaN(value) || !isFinite(value)) - return ["[gray]N/A[]", "N/A"]; - var percent = (value * 100).toFixed(decimals) + "%"; - return ["".concat(percent), "".concat(percent)]; - }, - number: function (value, decimals) { - if (decimals === void 0) { decimals = null; } - if (isNaN(value) || !isFinite(value)) - return ["[gray]N/A[]", "N/A"]; - if (decimals !== null) - return [value.toFixed(decimals), value.toFixed(decimals)]; - return [value.toString(), value.toString()]; - } -}; -exports.processedFFunctions = [0, 1].map(function (i) { - return Object.fromEntries(Object.entries(exports.fFunctions).map(function (_a) { - var _b = __read(_a, 2), k = _b[0], v = _b[1]; - return [k, - function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - return v.apply(exports.processedFFunctions[i], args)[i]; - } - ]; - })); -}); -exports.f_client = Object.assign(exports.outputFormatter_client, exports.processedFFunctions[0]); -exports.f_server = Object.assign(exports.outputFormatter_server, exports.processedFFunctions[1]); +"use strict"; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.f_server = exports.f_client = exports.processedFFunctions = exports.fFunctions = exports.outputFormatter_client = exports.outputFormatter_server = void 0; +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var players_1 = require("/players"); +var ranks_1 = require("/ranks"); +exports.outputFormatter_server = (0, funcs_1.tagProcessorPartial)(function (chunk) { + if (chunk instanceof players_1.FishPlayer) { + return "&c(".concat((0, funcs_1.escapeStringColorsServer)(chunk.cleanedName), ")&fr"); + } + else if (chunk instanceof ranks_1.Rank) { + return "&p".concat(chunk.name, "&fr"); + } + else if (chunk instanceof ranks_1.RoleFlag) { + return "&p".concat(chunk.name, "&fr"); + } + else if (chunk instanceof Error) { + return "&r".concat((0, funcs_1.escapeStringColorsServer)(chunk.toString()), "&fr"); + } + else if (chunk instanceof Player) { + var player = chunk; //not sure why this is necessary, typescript randomly converts any to unknown + return "&cPlayer#".concat(player.id, " (").concat((0, funcs_1.escapeStringColorsServer)(Strings.stripColors(player.name)), ")&fr"); + } + else if (typeof chunk == "string") { + if (globals_1.uuidPattern.test(chunk)) { + return "&b".concat(chunk, "&fr"); + } + else if (globals_1.ipPattern.test(chunk)) { + return "&b".concat(chunk, "&fr"); + } + else { + return "".concat(chunk); + } + } + else if (typeof chunk == "boolean") { + return "&b".concat(chunk.toString(), "&fr"); + } + else if (typeof chunk == "number") { + return "&b".concat(chunk.toString(), "&fr"); + } + else if (chunk instanceof Administration.PlayerInfo) { + return "&c".concat((0, funcs_1.escapeStringColorsServer)(chunk.plainLastName()), "&fr"); + } + else if (chunk instanceof UnitType) { + return "&c".concat(chunk.localizedName, "&fr"); + } + else if (chunk instanceof Block) { + return "&c".concat(chunk.localizedName, "&fr"); + } + else if (chunk instanceof Team) { + return "&c".concat(chunk.name, "&fr"); + } + else if (chunk instanceof Item) { + return "&c".concat((0, funcs_1.capitalizeText)(chunk.name, "-"), "&fr"); + } + else { + chunk; + Log.err("Invalid format object!"); + Log.info(chunk); + return chunk; //let it get stringified by the JS engine + } +}); +exports.outputFormatter_client = (0, funcs_1.tagProcessorPartial)(function (chunk, i, data, stringChunks) { + var _a, _b; + var reset = (_b = data !== null && data !== void 0 ? data : (_a = stringChunks[0].match(/^\[.+?\]/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : ""; + if (chunk instanceof players_1.FishPlayer) { + return "[cyan](".concat(chunk.name, "[cyan])") + reset; + } + else if (chunk instanceof ranks_1.Rank) { + return "".concat(chunk.color).concat(chunk.name, "[]") + reset; + } + else if (chunk instanceof ranks_1.RoleFlag) { + return "".concat(chunk.color).concat(chunk.name, "[]") + reset; + } + else if (chunk instanceof Error) { + return "[red]".concat(chunk.toString()) + reset; + } + else if (chunk instanceof Player) { + var fishP = players_1.FishPlayer.get(chunk); + return "[cyan](".concat(fishP.name, "[cyan])") + reset; + } + else if (typeof chunk == "string") { + if (globals_1.uuidPattern.test(chunk)) { + return "[blue]".concat(chunk, "[]"); + } + else if (globals_1.ipPattern.test(chunk)) { + return "[blue]".concat(chunk, "[]"); + } + else { + //TODO reset color? + return chunk; + } + } + else if (typeof chunk == "boolean") { + return "[blue]".concat(chunk.toString(), "[]"); + } + else if (typeof chunk == "number") { + return "[blue]".concat(chunk.toString(), "[]"); + } + else if (chunk instanceof Administration.PlayerInfo) { + return chunk.lastName + reset; + } + else if (chunk instanceof UnitType) { + return "[cyan]".concat(chunk.localizedName, "[]"); + } + else if (chunk instanceof Block) { + return "[cyan]".concat(chunk.localizedName, "[]"); + } + else if (chunk instanceof Team) { + return "[white]".concat(chunk.coloredName(), "[][]"); + } + else if (chunk instanceof Item) { + return "[cyan]".concat((0, funcs_1.capitalizeText)(chunk.name, "-"), "[]"); + } + else { + chunk; + Log.err("Invalid format object!"); + Log.info(chunk); + return chunk; //allow it to get stringified by the engine + } +}); +exports.fFunctions = { + boolGood: function (value) { + return [ + value ? "[green]true[]" : "[red]false[]", + value ? "&lgtrue&fr" : "&lrfalse&fr", + ]; + }, + boolBad: function (value) { + return [ + value ? "[red]true[]" : "[green]false[]", + value ? "&lrtrue&fr" : "&lgfalse&fr", + ]; + }, + percent: function (value, decimals) { + if (decimals === void 0) { decimals = 0; } + if (isNaN(value) || !isFinite(value)) + return ["[gray]N/A[]", "N/A"]; + var percent = (value * 100).toFixed(decimals) + "%"; + return ["".concat(percent), "".concat(percent)]; + }, + number: function (value, decimals) { + if (decimals === void 0) { decimals = null; } + if (isNaN(value) || !isFinite(value)) + return ["[gray]N/A[]", "N/A"]; + if (decimals !== null) + return [value.toFixed(decimals), value.toFixed(decimals)]; + return [value.toString(), value.toString()]; + } +}; +exports.processedFFunctions = [0, 1].map(function (i) { + return Object.fromEntries(Object.entries(exports.fFunctions).map(function (_a) { + var _b = __read(_a, 2), k = _b[0], v = _b[1]; + return [k, + function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return v.apply(exports.processedFFunctions[i], args)[i]; + } + ]; + })); +}); +exports.f_client = Object.assign(exports.outputFormatter_client, exports.processedFFunctions[0]); +exports.f_server = Object.assign(exports.outputFormatter_server, exports.processedFFunctions[1]); diff --git a/build/scripts/frameworks/commands/perm.js b/build/scripts/frameworks/commands/perm.js index 33ef8dca..04b73ef1 100644 --- a/build/scripts/frameworks/commands/perm.js +++ b/build/scripts/frameworks/commands/perm.js @@ -1,95 +1,95 @@ -"use strict"; -var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { - if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } - return cooked; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Perm = void 0; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the Perm class. -*/ -var config_1 = require("/config"); -var funcs_1 = require("/funcs"); -var ranks_1 = require("/ranks"); -/** Represents a permission that is required to do something. */ -var Perm = /** @class */ (function () { - function Perm(name, check, color, unauthorizedMessage) { - if (color === void 0) { color = ""; } - if (unauthorizedMessage === void 0) { unauthorizedMessage = "You do not have the required permission (".concat(name, ") to execute this command"); } - this.name = name; - this.color = color; - this.unauthorizedMessage = unauthorizedMessage; - if (typeof check == "string") { - if (ranks_1.Rank.getByName(check) == null) - (0, funcs_1.crash)("Invalid perm ".concat(name, ": invalid rank name ").concat(check)); - this.check = function (fishP) { return fishP.ranksAtLeast(check); }; - } - else { - this.check = check; - } - Perm.perms[name] = this; - } - /** Creates a new Perm with overrides for specified gamemodes. */ - Perm.prototype.exceptModes = function (modes, unauthorizedMessage) { - var _this = this; - if (unauthorizedMessage === void 0) { unauthorizedMessage = this.unauthorizedMessage; } - return new Perm(this.name, function (fishP) { - var _a; - var effectivePerm = (_a = modes[config_1.Gamemode.name()]) !== null && _a !== void 0 ? _a : _this; - return effectivePerm.check(fishP); - }, this.color, unauthorizedMessage); - }; - Perm.fromRank = function (rank) { - return new Perm(rank.name, function (fishP) { return fishP.ranksAtLeast(rank); }, rank.color); - }; - Perm.getByName = function (name) { - var _a; - return (_a = Perm.perms[name]) !== null && _a !== void 0 ? _a : (0, funcs_1.crash)("Invalid requiredPerm"); - }; - Perm.perms = {}; - Perm.none = new Perm("none", function (fishP) { return true; }, "[sky]"); - Perm.trusted = Perm.fromRank(ranks_1.Rank.trusted); - Perm.mod = Perm.fromRank(ranks_1.Rank.mod); - Perm.admin = Perm.fromRank(ranks_1.Rank.admin); - Perm.member = new Perm("member", function (fishP) { return fishP.hasFlag("member"); }, "[pink]", "You must have a ".concat(config_1.FColor.member(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " to use this command. Get a Fish Membership at[sky] ").concat(config_1.text.membershipURL, " []")); - Perm.chat = new Perm("chat", function (fishP) { return (!fishP.muted && !fishP.autoflagged) || fishP.ranksAtLeast("mod"); }); - Perm.bypassChatFilter = new Perm("bypassChatFilter", "admin"); - Perm.seeMutedMessages = new Perm("seeMutedMessages", function (fishP) { return fishP.muted || fishP.autoflagged || fishP.ranksAtLeast("mod"); }); - Perm.play = new Perm("play", function (fishP) { return !fishP.stelled() || fishP.ranksAtLeast("mod"); }); - Perm.seeErrorMessages = new Perm("seeErrorMessages", "admin"); - Perm.viewUUIDs = new Perm("viewUUIDs", "admin"); - Perm.viewIPs = new Perm("viewIPs", "admin"); - Perm.blockTrolling = new Perm("blockTrolling", function (fishP) { return fishP.rank === ranks_1.Rank.pi; }); - Perm.visualEffects = new Perm("visualEffects", function (fishP) { return (!fishP.stelled() || fishP.ranksAtLeast("mod")) && !fishP.hasFlag("no_effects"); }); - Perm.bulkVisualEffects = new Perm("bulkVisualEffects", function (fishP) { return ((fishP.hasFlag("developer") || fishP.hasFlag("illusionist") || fishP.hasFlag("member")) && !fishP.stelled()) - || fishP.ranksAtLeast("mod"); }); - Perm.bypassVoteFreeze = new Perm("bypassVoteFreeze", "trusted"); - Perm.bypassVotekick = new Perm("bypassVotekick", "mod"); - Perm.warn = new Perm("warn", "mod"); - Perm.vanish = new Perm("vanish", "mod"); - Perm.changeTeam = new Perm("changeTeam", "admin").exceptModes({ - sandbox: Perm.trusted, - attack: Perm.admin, - hexed: Perm.mod, - pvp: Perm.trusted, - minigame: Perm.trusted, - testsrv: Perm.trusted, - }); - /** Whether players should be allowed to change the team of a unit or building. If not, they will be kicked out of their current unit or building before switching teams. */ - Perm.changeTeamExternal = new Perm("changeTeamExternal", "admin").exceptModes({ - sandbox: Perm.trusted, - }); - Perm.usidCheck = new Perm("usidCheck", "trusted"); - Perm.runJS = new Perm("runJS", "manager"); - Perm.bypassNameCheck = new Perm("bypassNameCheck", "fish"); - Perm.hardcore = new Perm("hardcore", "trusted"); - Perm.massKill = new Perm("massKill", "admin").exceptModes({ - sandbox: Perm.mod, - }); - Perm.voteOtherTeams = new Perm("voteOtherTeams", "trusted"); - Perm.immediatelyVotekickNewPlayers = new Perm("immediatelyVotekickNewPlayers", "trusted"); - return Perm; -}()); -exports.Perm = Perm; -var templateObject_1; +"use strict"; +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Perm = void 0; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the Perm class. +*/ +var config_1 = require("/config"); +var funcs_1 = require("/funcs"); +var ranks_1 = require("/ranks"); +/** Represents a permission that is required to do something. */ +var Perm = /** @class */ (function () { + function Perm(name, check, color, unauthorizedMessage) { + if (color === void 0) { color = ""; } + if (unauthorizedMessage === void 0) { unauthorizedMessage = "You do not have the required permission (".concat(name, ") to execute this command"); } + this.name = name; + this.color = color; + this.unauthorizedMessage = unauthorizedMessage; + if (typeof check == "string") { + if (ranks_1.Rank.getByName(check) == null) + (0, funcs_1.crash)("Invalid perm ".concat(name, ": invalid rank name ").concat(check)); + this.check = function (fishP) { return fishP.ranksAtLeast(check); }; + } + else { + this.check = check; + } + Perm.perms[name] = this; + } + /** Creates a new Perm with overrides for specified gamemodes. */ + Perm.prototype.exceptModes = function (modes, unauthorizedMessage) { + var _this = this; + if (unauthorizedMessage === void 0) { unauthorizedMessage = this.unauthorizedMessage; } + return new Perm(this.name, function (fishP) { + var _a; + var effectivePerm = (_a = modes[config_1.Gamemode.name()]) !== null && _a !== void 0 ? _a : _this; + return effectivePerm.check(fishP); + }, this.color, unauthorizedMessage); + }; + Perm.fromRank = function (rank) { + return new Perm(rank.name, function (fishP) { return fishP.ranksAtLeast(rank); }, rank.color); + }; + Perm.getByName = function (name) { + var _a; + return (_a = Perm.perms[name]) !== null && _a !== void 0 ? _a : (0, funcs_1.crash)("Invalid requiredPerm"); + }; + Perm.perms = {}; + Perm.none = new Perm("none", function (fishP) { return true; }, "[sky]"); + Perm.trusted = Perm.fromRank(ranks_1.Rank.trusted); + Perm.mod = Perm.fromRank(ranks_1.Rank.mod); + Perm.admin = Perm.fromRank(ranks_1.Rank.admin); + Perm.member = new Perm("member", function (fishP) { return fishP.hasFlag("member"); }, "[pink]", "You must have a ".concat(config_1.FColor.member(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Fish Membership"], ["Fish Membership"]))), " to use this command. Get a Fish Membership at[sky] ").concat(config_1.text.membershipURL, " []")); + Perm.chat = new Perm("chat", function (fishP) { return (!fishP.muted && !fishP.autoflagged) || fishP.ranksAtLeast("mod"); }); + Perm.bypassChatFilter = new Perm("bypassChatFilter", "admin"); + Perm.seeMutedMessages = new Perm("seeMutedMessages", function (fishP) { return fishP.muted || fishP.autoflagged || fishP.ranksAtLeast("mod"); }); + Perm.play = new Perm("play", function (fishP) { return !fishP.stelled() || fishP.ranksAtLeast("mod"); }); + Perm.seeErrorMessages = new Perm("seeErrorMessages", "admin"); + Perm.viewUUIDs = new Perm("viewUUIDs", "admin"); + Perm.viewIPs = new Perm("viewIPs", "admin"); + Perm.blockTrolling = new Perm("blockTrolling", function (fishP) { return fishP.rank === ranks_1.Rank.pi; }); + Perm.visualEffects = new Perm("visualEffects", function (fishP) { return (!fishP.stelled() || fishP.ranksAtLeast("mod")) && !fishP.hasFlag("no_effects"); }); + Perm.bulkVisualEffects = new Perm("bulkVisualEffects", function (fishP) { return ((fishP.hasFlag("developer") || fishP.hasFlag("illusionist") || fishP.hasFlag("member")) && !fishP.stelled()) + || fishP.ranksAtLeast("mod"); }); + Perm.bypassVoteFreeze = new Perm("bypassVoteFreeze", "trusted"); + Perm.bypassVotekick = new Perm("bypassVotekick", "mod"); + Perm.warn = new Perm("warn", "mod"); + Perm.vanish = new Perm("vanish", "mod"); + Perm.changeTeam = new Perm("changeTeam", "admin").exceptModes({ + sandbox: Perm.trusted, + attack: Perm.admin, + hexed: Perm.mod, + pvp: Perm.trusted, + minigame: Perm.trusted, + testsrv: Perm.trusted, + }); + /** Whether players should be allowed to change the team of a unit or building. If not, they will be kicked out of their current unit or building before switching teams. */ + Perm.changeTeamExternal = new Perm("changeTeamExternal", "admin").exceptModes({ + sandbox: Perm.trusted, + }); + Perm.usidCheck = new Perm("usidCheck", "trusted"); + Perm.runJS = new Perm("runJS", "manager"); + Perm.bypassNameCheck = new Perm("bypassNameCheck", "fish"); + Perm.hardcore = new Perm("hardcore", "trusted"); + Perm.massKill = new Perm("massKill", "admin").exceptModes({ + sandbox: Perm.mod, + }); + Perm.voteOtherTeams = new Perm("voteOtherTeams", "trusted"); + Perm.immediatelyVotekickNewPlayers = new Perm("immediatelyVotekickNewPlayers", "trusted"); + return Perm; +}()); +exports.Perm = Perm; +var templateObject_1; diff --git a/build/scripts/frameworks/commands/requirements.js b/build/scripts/frameworks/commands/requirements.js index 6ecb4aab..444ca5d8 100644 --- a/build/scripts/frameworks/commands/requirements.js +++ b/build/scripts/frameworks/commands/requirements.js @@ -1,92 +1,92 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the requirements system, which is part of the commands framework. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Req = void 0; -var config_1 = require("/config"); -var errors_1 = require("/frameworks/commands/errors"); -var utils_1 = require("/utils"); -exports.Req = { - mode: function () { - var modes = []; - for (var _i = 0; _i < arguments.length; _i++) { - modes[_i] = arguments[_i]; - } - return function () { - return modes.map(function (mode) { return config_1.Gamemode[mode](); }).some(Boolean) - || (0, errors_1.fail)("This command is only available in ".concat(modes.map(utils_1.formatModeName).join(" or "))); - }; - }, - modeNot: function (mode) { return function () { - return !config_1.Gamemode[mode]() - || (0, errors_1.fail)("This command is disabled in ".concat((0, utils_1.formatModeName)(mode))); - }; }, - moderate: function (argName, allowSameRank, minimumLevel, allowSelfIfUnauthorized) { - if (allowSameRank === void 0) { allowSameRank = false; } - if (minimumLevel === void 0) { minimumLevel = "mod"; } - if (allowSelfIfUnauthorized === void 0) { allowSelfIfUnauthorized = false; } - return function (_a) { - var args = _a.args, sender = _a.sender; - return (args[argName] == undefined || sender.canModerate(args[argName], !allowSameRank, minimumLevel, allowSelfIfUnauthorized) - || (0, errors_1.fail)("You do not have permission to perform moderation actions on this player.")); - }; - }, - cooldown: function (durationMS) { return function (_a) { - var lastUsedSuccessfullySender = _a.lastUsedSuccessfullySender; - return Date.now() - lastUsedSuccessfullySender >= durationMS - || (0, errors_1.fail)("This command was run recently and is on cooldown."); - }; }, - cooldownGlobal: function (durationMS) { return function (_a) { - var lastUsedSuccessfully = _a.lastUsedSuccessfully; - return Date.now() - lastUsedSuccessfully >= durationMS - || (0, errors_1.fail)("This command was run recently and is on cooldown."); - }; }, - gameRunning: function () { - return !Vars.state.gameOver - || (0, errors_1.fail)("This game is over, please wait for the next map to load."); - }, - teamAlive: function (_a) { - var sender = _a.sender; - return sender.team().isAlive() - || (0, errors_1.fail)(Math.random() > 0.9 ? "You are already dead." : "Your team is dead."); - }, - unitExists: function (message) { - if (message === void 0) { message = "You must be in a unit to use this command."; } - return function (_a) { - var _b; - var sender = _a.sender; - return (sender.connected() && ((_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.added) && !sender.unit().dead) - || (0, errors_1.fail)(message); - }; - }, - numberRange: function (argName, min, max) { - return function (_a) { - var args = _a.args; - return args[argName] == undefined || min <= args[argName] && args[argName] <= max - || (0, errors_1.fail)("".concat(argName, " must be between ").concat(min, " and ").concat(max)); - }; - }, - integer: function (argName) { - return function (_a) { - var args = _a.args; - return args[argName] == undefined || Number.isSafeInteger(args[argName]) - || (0, errors_1.fail)("".concat(argName, " must be an integer")); - }; - }, - integerRange: function (argName, min, max) { - return function (_a) { - var args = _a.args; - return exports.Req.integer(argName)({ args: args }) && exports.Req.numberRange(argName, min, max)({ args: args }); - }; - }, - positiveInteger: function (argName) { - return function (_a) { - var args = _a.args; - return exports.Req.integer(argName)({ args: args }) && - (args[argName] == undefined || args[argName] > 0 - || (0, errors_1.fail)("".concat(argName, " must be positive"))); - }; - }, -}; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the requirements system, which is part of the commands framework. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Req = void 0; +var config_1 = require("/config"); +var errors_1 = require("/frameworks/commands/errors"); +var utils_1 = require("/utils"); +exports.Req = { + mode: function () { + var modes = []; + for (var _i = 0; _i < arguments.length; _i++) { + modes[_i] = arguments[_i]; + } + return function () { + return modes.map(function (mode) { return config_1.Gamemode[mode](); }).some(Boolean) + || (0, errors_1.fail)("This command is only available in ".concat(modes.map(utils_1.formatModeName).join(" or "))); + }; + }, + modeNot: function (mode) { return function () { + return !config_1.Gamemode[mode]() + || (0, errors_1.fail)("This command is disabled in ".concat((0, utils_1.formatModeName)(mode))); + }; }, + moderate: function (argName, allowSameRank, minimumLevel, allowSelfIfUnauthorized) { + if (allowSameRank === void 0) { allowSameRank = false; } + if (minimumLevel === void 0) { minimumLevel = "mod"; } + if (allowSelfIfUnauthorized === void 0) { allowSelfIfUnauthorized = false; } + return function (_a) { + var args = _a.args, sender = _a.sender; + return (args[argName] == undefined || sender.canModerate(args[argName], !allowSameRank, minimumLevel, allowSelfIfUnauthorized) + || (0, errors_1.fail)("You do not have permission to perform moderation actions on this player.")); + }; + }, + cooldown: function (durationMS) { return function (_a) { + var lastUsedSuccessfullySender = _a.lastUsedSuccessfullySender; + return Date.now() - lastUsedSuccessfullySender >= durationMS + || (0, errors_1.fail)("This command was run recently and is on cooldown."); + }; }, + cooldownGlobal: function (durationMS) { return function (_a) { + var lastUsedSuccessfully = _a.lastUsedSuccessfully; + return Date.now() - lastUsedSuccessfully >= durationMS + || (0, errors_1.fail)("This command was run recently and is on cooldown."); + }; }, + gameRunning: function () { + return !Vars.state.gameOver + || (0, errors_1.fail)("This game is over, please wait for the next map to load."); + }, + teamAlive: function (_a) { + var sender = _a.sender; + return sender.team().isAlive() + || (0, errors_1.fail)(Math.random() > 0.9 ? "You are already dead." : "Your team is dead."); + }, + unitExists: function (message) { + if (message === void 0) { message = "You must be in a unit to use this command."; } + return function (_a) { + var _b; + var sender = _a.sender; + return (sender.connected() && ((_b = sender.unit()) === null || _b === void 0 ? void 0 : _b.added) && !sender.unit().dead) + || (0, errors_1.fail)(message); + }; + }, + numberRange: function (argName, min, max) { + return function (_a) { + var args = _a.args; + return args[argName] == undefined || min <= args[argName] && args[argName] <= max + || (0, errors_1.fail)("".concat(argName, " must be between ").concat(min, " and ").concat(max)); + }; + }, + integer: function (argName) { + return function (_a) { + var args = _a.args; + return args[argName] == undefined || Number.isSafeInteger(args[argName]) + || (0, errors_1.fail)("".concat(argName, " must be an integer")); + }; + }, + integerRange: function (argName, min, max) { + return function (_a) { + var args = _a.args; + return exports.Req.integer(argName)({ args: args }) && exports.Req.numberRange(argName, min, max)({ args: args }); + }; + }, + positiveInteger: function (argName) { + return function (_a) { + var args = _a.args; + return exports.Req.integer(argName)({ args: args }) && + (args[argName] == undefined || args[argName] > 0 + || (0, errors_1.fail)("".concat(argName, " must be positive"))); + }; + }, +}; diff --git a/build/scripts/frameworks/commands/types.js b/build/scripts/frameworks/commands/types.js index d5e540cc..6f4b15c0 100644 --- a/build/scripts/frameworks/commands/types.js +++ b/build/scripts/frameworks/commands/types.js @@ -1,29 +1,29 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains types for the commands framework. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.commandArgNames = exports.commandArgTypes = void 0; -/** All valid command arg types. */ -exports.commandArgTypes = [ - "string", "number", "boolean", "player", /*"menuPlayer",*/ "team", "time", "unittype", "block", - "uuid", "offlinePlayer", "map", "mapOrRandom", "rank", "roleflag", "item" -]; -exports.commandArgNames = { - string: "text", - number: "number", - boolean: "boolean", - player: "player", - team: "team", - time: "duration", - unittype: "unit type", - block: "block", - uuid: "UUID", - offlinePlayer: "player", - map: "map", - mapOrRandom: "map", - rank: "rank", - roleflag: "role flag", - item: "item" -}; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains types for the commands framework. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.commandArgNames = exports.commandArgTypes = void 0; +/** All valid command arg types. */ +exports.commandArgTypes = [ + "string", "number", "boolean", "player", /*"menuPlayer",*/ "team", "time", "unittype", "block", + "uuid", "offlinePlayer", "map", "mapOrRandom", "rank", "roleflag", "item" +]; +exports.commandArgNames = { + string: "text", + number: "number", + boolean: "boolean", + player: "player", + team: "team", + time: "duration", + unittype: "unit type", + block: "block", + uuid: "UUID", + offlinePlayer: "player", + map: "map", + mapOrRandom: "map", + rank: "rank", + roleflag: "role flag", + item: "item" +}; diff --git a/build/scripts/frameworks/io.js b/build/scripts/frameworks/io.js index f4ad2059..05f31124 100644 --- a/build/scripts/frameworks/io.js +++ b/build/scripts/frameworks/io.js @@ -1,388 +1,388 @@ -"use strict"; -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SettingsSerializer = exports.Serializer = exports.DataClass = void 0; -exports.dataClass = dataClass; -exports.serialize = serialize; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the serialization framework. -For usage information, see docs/framework-usage-guide.md -For maintenance information, see docs/frameworks.md -*/ -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var DataClass = /** @class */ (function () { - function DataClass(data) { - Object.assign(this, data); - } - return DataClass; -}()); -exports.DataClass = DataClass; -function dataClass() { - return DataClass; -} -function checkBounds(type, value, min, max) { - if (value < min) { - Log.warn("Integer underflow when serializing ".concat(type, ": value ").concat(value, " was less than ").concat(min)); - return min; - } - if (value >= max) { - Log.warn("Integer overflow when serializing ".concat(type, ": value ").concat(value, " was greater than ").concat(max)); - return max; - } - return value; -} -var Serializer = /** @class */ (function () { - function Serializer(schema, oldSchema) { - this.schema = schema; - this.oldSchema = oldSchema; - } - Serializer.prototype.write = function (object, output) { - Serializer.writeNode(this.schema, object, output); - }; - Serializer.prototype.read = function (input) { - input.mark(0xFFFF); //1MB - try { - return Serializer.readNode(this.schema, input); - } - catch (err) { - Log.warn("Using fallback schema: this message should go away after a restart"); - input.reset(); - if (this.oldSchema) - return Serializer.readNode(this.oldSchema, input); - else - throw err; - } - }; - Serializer.writeNode = function (schema, value, output) { - var e_1, _a, e_2, _b; - var checkNumbers = false; - switch (schema[0]) { - case 'string': - output.writeUTF(value); - break; - case 'number': - if (checkNumbers) { - switch (schema[1]) { - case 'u8': - output.writeByte(checkBounds(schema[1], value, 0, Math.pow(2, 8))); - break; - case 'u16': - output.writeShort(checkBounds(schema[1], value, 0, Math.pow(2, 16))); - break; - case 'u32': - output.writeInt(checkBounds(schema[1], value, 0, Math.pow(2, 32)) & 0xFFFFFFFF); - break; - case 'i8': - output.writeByte(checkBounds(schema[1], value, -(Math.pow(2, 7)), (Math.pow(2, 7)))); - break; - case 'i16': - output.writeShort(checkBounds(schema[1], value, -(Math.pow(2, 15)), (Math.pow(2, 15)))); - break; - case 'i32': - output.writeInt(checkBounds(schema[1], value, -(Math.pow(2, 31)), (Math.pow(2, 31)))); - break; - case 'i64': - output.writeLong(checkBounds(schema[1], value, -(Math.pow(2, 63)), (Math.pow(2, 63)))); - break; - case 'f32': - output.writeFloat(isNaN(value) || !isFinite(value) ? (Log.warn('Attempted to write a NaN floating-point value, defaulting to 0'), 0) : value); - break; - case 'f64': - output.writeDouble(isNaN(value) || !isFinite(value) ? (Log.warn('Attempted to write a NaN floating-point value, defaulting to 0'), 0) : value); - break; - } - } - else { - switch (schema[1]) { - case 'u8': - output.writeByte(value); - break; - case 'u16': - output.writeShort(value); - break; - case 'u32': - output.writeInt(value & 0xFFFFFFFF); - break; //If the value is greater than 0x7FFFFFFF, make it negative so Java writes it correctly - case 'i8': - output.writeByte(value); - break; - case 'i16': - output.writeShort(value); - break; - case 'i32': - output.writeInt(value); - break; - case 'i64': - output.writeLong(value); - break; - case 'f32': - output.writeFloat(value); - break; - case 'f64': - output.writeDouble(value); - break; - } - } - break; - case 'boolean': - output.writeBoolean(value); - break; - case 'team': - if (!value) - Log.err("attempting to serialize a Team, but it was null"); //temporary debug message - output.writeByte(value.id); - break; - case 'object': - try { - for (var _c = __values(schema[1]), _d = _c.next(); !_d.done; _d = _c.next()) { - var _e = __read(_d.value, 2), key = _e[0], childSchema = _e[1]; - //correspondence - this.writeNode(childSchema, value[key], output); - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_d && !_d.done && (_a = _c.return)) _a.call(_c); - } - finally { if (e_1) throw e_1.error; } - } - break; - case 'class': - try { - for (var _f = __values(schema[2]), _g = _f.next(); !_g.done; _g = _f.next()) { - var _h = __read(_g.value, 2), key = _h[0], childSchema = _h[1]; - //correspondence - this.writeNode(childSchema, value[key], output); - } - } - catch (e_2_1) { e_2 = { error: e_2_1 }; } - finally { - try { - if (_g && !_g.done && (_b = _f.return)) _b.call(_f); - } - finally { if (e_2) throw e_2.error; } - } - break; - case 'array': - if (typeof schema[1] == "string") { - this.writeNode(["number", schema[1]], value.length, output); - } - else { - if (schema[1] !== value.length) { - Log.err('SERIALIZATION WARNING: received invalid data: array with greater length than specified by schema'); - value.length = schema[1]; - } - } - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (var i = 0; i < value.length; i++) { - this.writeNode(schema[2], value[i], output); - } - break; - case 'version': - output.writeByte(schema[1]); - this.writeNode(schema[2], value, output); - break; - } - }; - Serializer.readNode = function (schema, input) { - var e_3, _a, e_4, _b; - switch (schema[0]) { - case 'string': - return input.readUTF(); - case 'number': - switch (schema[1]) { - case 'u8': return input.readUnsignedByte(); - case 'u16': return input.readUnsignedShort(); - case 'u32': { - var value = input.readInt(); //Java does not support unsigned ints - return value < 0 ? value + Math.pow(2, 32) : value; - } - case 'i8': return input.readByte(); - case 'i16': return input.readShort(); - case 'i32': return input.readInt(); - case 'i64': return input.readLong(); - case 'f32': return input.readFloat(); - case 'f64': return input.readDouble(); - } - schema[1]; - break; - case 'boolean': - return input.readBoolean(); - case 'team': - return Team.all[input.readByte()]; - case 'object': { - var output = {}; - try { - for (var _c = __values(schema[1]), _d = _c.next(); !_d.done; _d = _c.next()) { - var _e = __read(_d.value, 2), key = _e[0], childSchema = _e[1]; - output[key] = this.readNode(childSchema, input); - } - } - catch (e_3_1) { e_3 = { error: e_3_1 }; } - finally { - try { - if (_d && !_d.done && (_a = _c.return)) _a.call(_c); - } - finally { if (e_3) throw e_3.error; } - } - return output; - } - case 'class': { - var classData = {}; - try { - for (var _f = __values(schema[2]), _g = _f.next(); !_g.done; _g = _f.next()) { - var _h = __read(_g.value, 2), key = _h[0], childSchema = _h[1]; - classData[key] = this.readNode(childSchema, input); - } - } - catch (e_4_1) { e_4 = { error: e_4_1 }; } - finally { - try { - if (_g && !_g.done && (_b = _f.return)) _b.call(_f); - } - finally { if (e_4) throw e_4.error; } - } - return new schema[1](classData); - } - case 'array': { - var length = typeof schema[1] === "number" ? - schema[1] - : this.readNode(["number", schema[1]], input); - var array = new Array(length); - for (var i = 0; i < length; i++) { - array[i] = this.readNode(schema[2], input); - } - return array; - } - case 'version': { - var version = input.readByte(); - if (version !== schema[1]) - (0, funcs_1.crash)("Expected version ".concat(schema[1], ", but read ").concat(version)); - return this.readNode(schema[2], input); - } - } - }; - return Serializer; -}()); -exports.Serializer = Serializer; -var SettingsSerializer = /** @class */ (function (_super) { - __extends(SettingsSerializer, _super); - function SettingsSerializer(settingsKey, schema, oldSchema) { - var _this = _super.call(this, schema, oldSchema) || this; - _this.settingsKey = settingsKey; - _this.schema = schema; - _this.oldSchema = oldSchema; - return _this; - } - SettingsSerializer.prototype.writeSettings = function (object) { - var output = new ByteArrayOutputStream(); - this.write(object, new DataOutputStream(output)); - Core.settings.put(this.settingsKey, output.toByteArray()); - }; - SettingsSerializer.prototype.readSettings = function () { - var data = Core.settings.getBytes(this.settingsKey); - if (data) - return this.read(new DataInputStream(new ByteArrayInputStream(data))); - else - return null; - }; - return SettingsSerializer; -}(Serializer)); -exports.SettingsSerializer = SettingsSerializer; -if (!Symbol.metadata) - Object.defineProperty(Symbol, "metadata", { - writable: false, - enumerable: false, - configurable: false, - value: Symbol("Symbol.metadata") - }); -var valuesToSerialize = new AtomicInteger(); -function serialize(settingsKey, schema, oldSchema, fixer, saveEvent) { - if (saveEvent === void 0) { saveEvent = "saveData"; } - return function decorate(_, _a) { - var addInitializer = _a.addInitializer, access = _a.access, name = _a.name; - valuesToSerialize.getAndIncrement(); - addInitializer(function () { - var _this = this; - var serializer = (0, funcs_1.lazy)(function () { - return new SettingsSerializer(settingsKey, schema(), oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema()); - }); - globals_1.FishEvents.on("loadData", function () { return Threads.daemon(function () { - //Load data multithreaded - var start = Time.nanos(); - var value = serializer().readSettings(); - if (value) { - if (fixer) - value = fixer(value); - access.set(_this, value); - } - if (valuesToSerialize.decrementAndGet() == 0) - globals_1.FishEvents.fire("dataLoaded", []); - Log.debug("serialize read @ @", settingsKey, (Time.nanos() - start) / 1e6); - }); }); - globals_1.FishEvents.on(saveEvent, function () { - try { - Time.mark(); - var value = access.get(_this); - if (value == null) - return; - serializer().writeSettings(value); - Log.debug("serialize save @ @", settingsKey, Time.elapsed()); - } - catch (err) { - Log.err("Error while saving field ".concat(String(name), " on ").concat(String(_this === null || _this === void 0 ? void 0 : _this.name), " using settings key ").concat(settingsKey)); - Log.info(JSON.stringify(access.get(_this))); - throw err; - } - }); - }); - }; -} -globals_1.FishEvents.on("loadData", function () { - if (valuesToSerialize.get() == 0) - globals_1.FishEvents.fire("dataLoaded", []); -}); +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SettingsSerializer = exports.Serializer = exports.DataClass = void 0; +exports.dataClass = dataClass; +exports.serialize = serialize; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the serialization framework. +For usage information, see docs/framework-usage-guide.md +For maintenance information, see docs/frameworks.md +*/ +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var DataClass = /** @class */ (function () { + function DataClass(data) { + Object.assign(this, data); + } + return DataClass; +}()); +exports.DataClass = DataClass; +function dataClass() { + return DataClass; +} +function checkBounds(type, value, min, max) { + if (value < min) { + Log.warn("Integer underflow when serializing ".concat(type, ": value ").concat(value, " was less than ").concat(min)); + return min; + } + if (value >= max) { + Log.warn("Integer overflow when serializing ".concat(type, ": value ").concat(value, " was greater than ").concat(max)); + return max; + } + return value; +} +var Serializer = /** @class */ (function () { + function Serializer(schema, oldSchema) { + this.schema = schema; + this.oldSchema = oldSchema; + } + Serializer.prototype.write = function (object, output) { + Serializer.writeNode(this.schema, object, output); + }; + Serializer.prototype.read = function (input) { + input.mark(0xFFFF); //1MB + try { + return Serializer.readNode(this.schema, input); + } + catch (err) { + Log.warn("Using fallback schema: this message should go away after a restart"); + input.reset(); + if (this.oldSchema) + return Serializer.readNode(this.oldSchema, input); + else + throw err; + } + }; + Serializer.writeNode = function (schema, value, output) { + var e_1, _a, e_2, _b; + var checkNumbers = false; + switch (schema[0]) { + case 'string': + output.writeUTF(value); + break; + case 'number': + if (checkNumbers) { + switch (schema[1]) { + case 'u8': + output.writeByte(checkBounds(schema[1], value, 0, Math.pow(2, 8))); + break; + case 'u16': + output.writeShort(checkBounds(schema[1], value, 0, Math.pow(2, 16))); + break; + case 'u32': + output.writeInt(checkBounds(schema[1], value, 0, Math.pow(2, 32)) & 0xFFFFFFFF); + break; + case 'i8': + output.writeByte(checkBounds(schema[1], value, -(Math.pow(2, 7)), (Math.pow(2, 7)))); + break; + case 'i16': + output.writeShort(checkBounds(schema[1], value, -(Math.pow(2, 15)), (Math.pow(2, 15)))); + break; + case 'i32': + output.writeInt(checkBounds(schema[1], value, -(Math.pow(2, 31)), (Math.pow(2, 31)))); + break; + case 'i64': + output.writeLong(checkBounds(schema[1], value, -(Math.pow(2, 63)), (Math.pow(2, 63)))); + break; + case 'f32': + output.writeFloat(isNaN(value) || !isFinite(value) ? (Log.warn('Attempted to write a NaN floating-point value, defaulting to 0'), 0) : value); + break; + case 'f64': + output.writeDouble(isNaN(value) || !isFinite(value) ? (Log.warn('Attempted to write a NaN floating-point value, defaulting to 0'), 0) : value); + break; + } + } + else { + switch (schema[1]) { + case 'u8': + output.writeByte(value); + break; + case 'u16': + output.writeShort(value); + break; + case 'u32': + output.writeInt(value & 0xFFFFFFFF); + break; //If the value is greater than 0x7FFFFFFF, make it negative so Java writes it correctly + case 'i8': + output.writeByte(value); + break; + case 'i16': + output.writeShort(value); + break; + case 'i32': + output.writeInt(value); + break; + case 'i64': + output.writeLong(value); + break; + case 'f32': + output.writeFloat(value); + break; + case 'f64': + output.writeDouble(value); + break; + } + } + break; + case 'boolean': + output.writeBoolean(value); + break; + case 'team': + if (!value) + Log.err("attempting to serialize a Team, but it was null"); //temporary debug message + output.writeByte(value.id); + break; + case 'object': + try { + for (var _c = __values(schema[1]), _d = _c.next(); !_d.done; _d = _c.next()) { + var _e = __read(_d.value, 2), key = _e[0], childSchema = _e[1]; + //correspondence + this.writeNode(childSchema, value[key], output); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_1) throw e_1.error; } + } + break; + case 'class': + try { + for (var _f = __values(schema[2]), _g = _f.next(); !_g.done; _g = _f.next()) { + var _h = __read(_g.value, 2), key = _h[0], childSchema = _h[1]; + //correspondence + this.writeNode(childSchema, value[key], output); + } + } + catch (e_2_1) { e_2 = { error: e_2_1 }; } + finally { + try { + if (_g && !_g.done && (_b = _f.return)) _b.call(_f); + } + finally { if (e_2) throw e_2.error; } + } + break; + case 'array': + if (typeof schema[1] == "string") { + this.writeNode(["number", schema[1]], value.length, output); + } + else { + if (schema[1] !== value.length) { + Log.err('SERIALIZATION WARNING: received invalid data: array with greater length than specified by schema'); + value.length = schema[1]; + } + } + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (var i = 0; i < value.length; i++) { + this.writeNode(schema[2], value[i], output); + } + break; + case 'version': + output.writeByte(schema[1]); + this.writeNode(schema[2], value, output); + break; + } + }; + Serializer.readNode = function (schema, input) { + var e_3, _a, e_4, _b; + switch (schema[0]) { + case 'string': + return input.readUTF(); + case 'number': + switch (schema[1]) { + case 'u8': return input.readUnsignedByte(); + case 'u16': return input.readUnsignedShort(); + case 'u32': { + var value = input.readInt(); //Java does not support unsigned ints + return value < 0 ? value + Math.pow(2, 32) : value; + } + case 'i8': return input.readByte(); + case 'i16': return input.readShort(); + case 'i32': return input.readInt(); + case 'i64': return input.readLong(); + case 'f32': return input.readFloat(); + case 'f64': return input.readDouble(); + } + schema[1]; + break; + case 'boolean': + return input.readBoolean(); + case 'team': + return Team.all[input.readByte()]; + case 'object': { + var output = {}; + try { + for (var _c = __values(schema[1]), _d = _c.next(); !_d.done; _d = _c.next()) { + var _e = __read(_d.value, 2), key = _e[0], childSchema = _e[1]; + output[key] = this.readNode(childSchema, input); + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_3) throw e_3.error; } + } + return output; + } + case 'class': { + var classData = {}; + try { + for (var _f = __values(schema[2]), _g = _f.next(); !_g.done; _g = _f.next()) { + var _h = __read(_g.value, 2), key = _h[0], childSchema = _h[1]; + classData[key] = this.readNode(childSchema, input); + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (_g && !_g.done && (_b = _f.return)) _b.call(_f); + } + finally { if (e_4) throw e_4.error; } + } + return new schema[1](classData); + } + case 'array': { + var length = typeof schema[1] === "number" ? + schema[1] + : this.readNode(["number", schema[1]], input); + var array = new Array(length); + for (var i = 0; i < length; i++) { + array[i] = this.readNode(schema[2], input); + } + return array; + } + case 'version': { + var version = input.readByte(); + if (version !== schema[1]) + (0, funcs_1.crash)("Expected version ".concat(schema[1], ", but read ").concat(version)); + return this.readNode(schema[2], input); + } + } + }; + return Serializer; +}()); +exports.Serializer = Serializer; +var SettingsSerializer = /** @class */ (function (_super) { + __extends(SettingsSerializer, _super); + function SettingsSerializer(settingsKey, schema, oldSchema) { + var _this = _super.call(this, schema, oldSchema) || this; + _this.settingsKey = settingsKey; + _this.schema = schema; + _this.oldSchema = oldSchema; + return _this; + } + SettingsSerializer.prototype.writeSettings = function (object) { + var output = new ByteArrayOutputStream(); + this.write(object, new DataOutputStream(output)); + Core.settings.put(this.settingsKey, output.toByteArray()); + }; + SettingsSerializer.prototype.readSettings = function () { + var data = Core.settings.getBytes(this.settingsKey); + if (data) + return this.read(new DataInputStream(new ByteArrayInputStream(data))); + else + return null; + }; + return SettingsSerializer; +}(Serializer)); +exports.SettingsSerializer = SettingsSerializer; +if (!Symbol.metadata) + Object.defineProperty(Symbol, "metadata", { + writable: false, + enumerable: false, + configurable: false, + value: Symbol("Symbol.metadata") + }); +var valuesToSerialize = new AtomicInteger(); +function serialize(settingsKey, schema, oldSchema, fixer, saveEvent) { + if (saveEvent === void 0) { saveEvent = "saveData"; } + return function decorate(_, _a) { + var addInitializer = _a.addInitializer, access = _a.access, name = _a.name; + valuesToSerialize.getAndIncrement(); + addInitializer(function () { + var _this = this; + var serializer = (0, funcs_1.lazy)(function () { + return new SettingsSerializer(settingsKey, schema(), oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema()); + }); + globals_1.FishEvents.on("loadData", function () { return Threads.daemon(function () { + //Load data multithreaded + var start = Time.nanos(); + var value = serializer().readSettings(); + if (value) { + if (fixer) + value = fixer(value); + access.set(_this, value); + } + if (valuesToSerialize.decrementAndGet() == 0) + globals_1.FishEvents.fire("dataLoaded", []); + Log.debug("serialize read @ @", settingsKey, (Time.nanos() - start) / 1e6); + }); }); + globals_1.FishEvents.on(saveEvent, function () { + try { + Time.mark(); + var value = access.get(_this); + if (value == null) + return; + serializer().writeSettings(value); + Log.debug("serialize save @ @", settingsKey, Time.elapsed()); + } + catch (err) { + Log.err("Error while saving field ".concat(String(name), " on ").concat(String(_this === null || _this === void 0 ? void 0 : _this.name), " using settings key ").concat(settingsKey)); + Log.info(JSON.stringify(access.get(_this))); + throw err; + } + }); + }); + }; +} +globals_1.FishEvents.on("loadData", function () { + if (valuesToSerialize.get() == 0) + globals_1.FishEvents.fire("dataLoaded", []); +}); diff --git a/build/scripts/frameworks/menus.js b/build/scripts/frameworks/menus.js index 8af7338f..1f6cc55a 100644 --- a/build/scripts/frameworks/menus.js +++ b/build/scripts/frameworks/menus.js @@ -1,382 +1,382 @@ -"use strict"; -/* eslint-disable @typescript-eslint/array-type */ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the menu framework. -For usage information, see docs/framework-usage-guide.md -For maintenance information, see docs/frameworks.md -*/ -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -var __rest = (this && this.__rest) || function (s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.listeners = exports.Menu = exports.Cancel = void 0; -exports.registerListeners = registerListeners; -var commands_1 = require("/frameworks/commands"); -var funcs_1 = require("/funcs"); -var players_1 = require("/players"); -var promise_1 = require("/promise"); -var utils_1 = require("/utils"); -/** Used to change the behavior of adding another menu when being run in a menu callback. */ -var isInMenuCallback = false; -/** Stores a mapping from name to the numeric id of a listener that has been registered. */ -var registeredListeners = {}; -exports.listeners = registeredListeners; -/** Stores all listeners in use by fish-commands. */ -var listeners = { - generic: function (player, option) { - var fishSender = players_1.FishPlayer.get(player); - var prevCallback = fishSender.activeMenus.shift(); - if (!prevCallback) - return; //No menu to process, do nothing - isInMenuCallback = true; - prevCallback.callback(option); - isInMenuCallback = false; - }, - none: function (player, option) { - //do nothing - } -}; -exports.Cancel = Symbol("Cancel"); -/** Registers all listeners, should be called on server load. */ -function registerListeners() { - var e_1, _a; - var _b; - try { - for (var _c = __values(Object.entries(listeners)), _d = _c.next(); !_d.done; _d = _c.next()) { - var _e = __read(_d.value, 2), key = _e[0], listener = _e[1]; - (_b = registeredListeners[key]) !== null && _b !== void 0 ? _b : (registeredListeners[key] = Menus.registerMenu(listener)); - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_d && !_d.done && (_a = _c.return)) _a.call(_c); - } - finally { if (e_1) throw e_1.error; } - } -} -exports.Menu = { - /** Displays a menu to a player, returning a Promise. */ - raw: function (title, description, arrangedOptions, target, _a) { - var _b = _a === void 0 ? {} : _a, _c = _b.optionStringifier, optionStringifier = _c === void 0 ? String : _c, _d = _b.onCancel, onCancel = _d === void 0 ? "reject" : _d, _e = _b.cancelOptionId, cancelOptionId = _e === void 0 ? -1 : _e; - var _f = promise_1.Promise.withResolvers(), promise = _f.promise, reject = _f.reject, resolve = _f.resolve; - //The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered. - //If menu() is being called from a menu calback, add it to the front of the queue so it is processed before any other menus. - //Otherwise, two multi-step menus queued together would alternate, which would confuse the player. - target.activeMenus[isInMenuCallback ? "unshift" : "push"]({ callback: function (option) { - //Additional permission validation could be done here, but the only way that callback() can be called is if the above statement executed, - //and on sensitive menus such as the stop menu, the only way to reach that is if menu() was called by the /stop command, - //which already checks permissions. - //Additionally, the callback is cleared by the generic menu listener after it is executed. - try { - var options = arrangedOptions.flat(); - //We do need to validate option though, as it can be any number. - if (option === -1 || option === cancelOptionId || !(option in options)) { - //Consider any invalid option to be a cancellation - if (onCancel == "null") - resolve(null); - else if (onCancel == "reject") - reject(exports.Cancel); - else - return; - } - else { - resolve(options[option]); - } - } - catch (err) { - (0, utils_1.handleError)(err, target, utils_1.outputFail, "".concat(target.cleanedName, " submitted menu \"").concat(title, "\" \"").concat(description, "\"")); - } - } }); - var i = 0; - var stringifiedOptions = arrangedOptions.map(function (r) { return r.map(function (item) { - if (i === cancelOptionId) - return item; - i++; - return optionStringifier(item); - }); }); - Call.menu(target.con, registeredListeners.generic, title, description, stringifiedOptions); - return promise; - }, - /** Displays a menu to a player, returning a Promise. Arranges provided options into a 2D array, and can add a Cancel option. */ - menu: function (title, description, options, target, _a) { - var _b = _a === void 0 ? {} : _a, _c = _b.includeCancel, includeCancel = _c === void 0 ? false : _c, _d = _b.optionStringifier, optionStringifier = _d === void 0 ? String : _d, _e = _b.columns, columns = _e === void 0 ? 3 : _e, _f = _b.onCancel, onCancel = _f === void 0 ? "reject" : _f, _g = _b.cancelOptionId, cancelOptionId = _g === void 0 ? -1 : _g; - //Set up the 2D array of options, and maybe add cancel - //Call.menu() with [[]] will cause a client crash, make sure to pass [] instead - var arrangedOptions = (options.length == 0 && !includeCancel) ? [] : (0, funcs_1.to2DArray)(options, columns); - if (includeCancel) { - arrangedOptions.push(["[red]Cancel[]"]); - //This is safe because cancelOptionId is set, - //so the handler will never get called with "Cancel". - cancelOptionId = options.length; - } - return exports.Menu.raw(title, description, arrangedOptions, target, { - cancelOptionId: cancelOptionId, - onCancel: onCancel, - optionStringifier: optionStringifier - }); - }, - /** Rejects with a CommandError if the user chooses to cancel. */ - confirm: function (target, description, _a) { - var _b = _a === void 0 ? {} : _a, _c = _b.cancelOutput, cancelOutput = _c === void 0 ? "Cancelled." : _c, _d = _b.title, title = _d === void 0 ? "Confirm" : _d, _e = _b.confirmText, confirmText = _e === void 0 ? "[green]Confirm" : _e, _f = _b.cancelText, cancelText = _f === void 0 ? "[red]Cancel" : _f; - return exports.Menu.menu(title, description, [confirmText, cancelText], target, { onCancel: "reject", cancelOptionId: 1 }).catch(function (e) { - if (e === exports.Cancel) - (0, commands_1.fail)(cancelOutput); - throw e; //some random error, rethrow it - }); - }, - /** Same as confirm(), but with inverted colors, for potentially dangerous actions. */ - confirmDangerous: function (target, description, _a) { - if (_a === void 0) { _a = {}; } - var _b = _a.confirmText, confirmText = _b === void 0 ? "[red]Confirm" : _b, _c = _a.cancelText, cancelText = _c === void 0 ? "[green]Cancel" : _c, rest = __rest(_a, ["confirmText", "cancelText"]); - return exports.Menu.confirm(target, description, __assign({ cancelText: cancelText, confirmText: confirmText }, rest)); - }, - /** - * Displays a menu to a player, returning a Promise. - * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. - */ - buttons: function (target, title, description, options, cfg) { - if (cfg === void 0) { cfg = {}; } - return exports.Menu.raw(title, description, options, target, __assign(__assign({}, cfg), { optionStringifier: function (o) { return o.text; } })).then(function (o) { return o === null || o === void 0 ? void 0 : o.data; }); - }, - /** - * Displays a menu to a player, returning a Promise. - * Adds left and right arrows to switch pages. - * Shows different options based on the page. - */ - pages: function (target, title, description, options, cfg) { - var _a = promise_1.Promise.withResolvers(), promise = _a.promise, reject = _a.reject, resolve = _a.resolve; - function showPage(index) { - var opts = __spreadArray(__spreadArray([], __read(options[index].map(function (r) { return r.map(function (d) { return ({ text: d.text, data: [d.data] }); }); })), false), [ - [ - { data: "left", text: "[".concat(index == 0 ? "gray" : "accent", "]<--") }, - { data: "numbers", text: "[accent]".concat(index + 1, "/").concat(options.length) }, - { data: "right", text: "[".concat(index == options.length - 1 ? "gray" : "accent", "]-->") } - ] - ], false); - void exports.Menu.buttons(target, title, description, opts, __assign(__assign({}, cfg), { onCancel: "null" })).then(function (response) { - if (response instanceof Array) - resolve(response[0]); - else if (response === "right") - showPage(Math.min(index + 1, options.length - 1)); - else if (response === "left") - showPage(Math.max(index - 1, 0)); - else { - //Treat numbers as cancel - if (cfg.onCancel == "null") - resolve(null); - else if (cfg.onCancel == "reject") - reject(exports.Cancel); - //otherwise, just let the promise hang - } - }); - } - showPage(0); - return promise; - }, - /** - * Displays a menu to a player, returning a Promise. - * Adds left and right arrows to switch pages. - * Shows different text based on the current page. - */ - textPages: function (target, pages, options, cfg) { - if (options === void 0) { options = []; } - if (cfg === void 0) { cfg = {}; } - var _a = promise_1.Promise.withResolvers(), promise = _a.promise, reject = _a.reject, resolve = _a.resolve; - var pageSkipSize = Math.max(Math.floor(pages.length / 8), 5); - function showPage(index) { - var opts = __spreadArray(__spreadArray([ - [ - { data: ["left", pageSkipSize], text: "[".concat(index == 0 ? "gray" : "accent", "]<<<") }, - { data: ["left", 1], text: "[".concat(index == 0 ? "gray" : "accent", "]<--") }, - { data: ["right", 1], text: "[".concat(index == pages.length - 1 ? "gray" : "accent", "]-->") }, - { data: ["right", pageSkipSize], text: "[".concat(index == pages.length - 1 ? "gray" : "accent", "]>>>") }, - ], [ - { data: ["numbers"], text: "[accent]Page ".concat(index + 1, "/").concat(pages.length) }, - ] - ], __read(options.map(function (d, i) { return [{ data: [i], text: d }]; })), false), [ - [ - { data: ["cancel"], text: "[lightgray]Close" }, - ], - ], false); - void exports.Menu.buttons(target, pages[index][0], pages[index][1](), opts, __assign(__assign({}, cfg), { onCancel: "null" })).then(function (response) { - if ((response === null || response === void 0 ? void 0 : response[0]) === "right") - showPage(Math.min(index + response[1], pages.length - 1)); - else if ((response === null || response === void 0 ? void 0 : response[0]) === "left") - showPage(Math.max(index - response[1], 0)); - else if (typeof (response === null || response === void 0 ? void 0 : response[0]) === "number") { - resolve([index, options[response[0]]]); - } - else { - //Treat numbers as cancel - if (cfg.onCancel == "null") - resolve(null); - else if (cfg.onCancel == "reject") - reject(exports.Cancel); - //otherwise, just let the promise hang - } - }); - } - var index = (function () { - if (cfg.startPage == undefined) - return 0; - if (typeof cfg.startPage === 'number') { - if (cfg.startPage < 0) - return 0; - return cfg.startPage; - } - var index = pages.findIndex(function (_a) { - var _b = __read(_a, 1), title = _b[0]; - return title === cfg.startPage; - }); - if (index === -1) - return 0; - return index; - })(); - showPage(index); - return promise; - }, - /** - * Displays a menu to a player, returning a Promise. - * Accepts a 2D array of options and shows a region of that 2D grid. - * Adds arrows to scroll left/right/up/down. - * Resolves to the selected option. - */ - scroll2D: function (target, title, description, options, cfg) { - var _a, _b; - if (cfg === void 0) { cfg = {}; } - var _c = promise_1.Promise.withResolvers(), promise = _c.promise, reject = _c.reject, resolve = _c.resolve; - var _d = cfg.rows, rows = _d === void 0 ? 5 : _d, _e = cfg.columns, cols = _e === void 0 ? 5 : _e; - var height = options.length; - var width = options[0].length; - function showPage(x, y) { - var _a, _b; - var opts = __spreadArray(__spreadArray([], __read(options.slice(y, y + rows).map(function (r) { return r.concat(Array(width - r.length).fill({ data: "blank", text: "" })); }).map(function (r) { - return r.slice(x, x + cols).map(function (d) { return ({ text: d.text, data: [d.data] }); }); - })), false), [ - [ - { data: "blank", text: "" }, - ], - [ - { data: "blank", text: "" }, - { data: "up", text: "[".concat(y == 0 ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.up)) }, - { data: "blank", text: "" }, - ], [ - { data: "left", text: "[".concat(x == 0 ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.left)) }, - { data: "blank", text: (_b = (_a = cfg.getCenterText) === null || _a === void 0 ? void 0 : _a.call(cfg, x, y)) !== null && _b !== void 0 ? _b : '' }, - { data: "right", text: "[".concat(x == width - cols ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.right)) }, - ], [ - { data: "blank", text: "" }, - { data: "down", text: "[".concat(y == height - rows ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.down)) }, - { data: "blank", text: "" }, - ] - ], false); - void exports.Menu.buttons(target, title, description, opts, cfg).then(function (response) { - if (response instanceof Array) - resolve([response[0], x, y]); - else if (response === "right") - showPage(Math.min(x + 1, width - cols), y); - else if (response === "left") - showPage(Math.max(x - 1, 0), y); - else if (response === "up") - showPage(x, Math.max(y - 1, 0)); - else if (response === "down") - showPage(x, Math.min(y + 1, height - rows)); - else { - //Treat numbers as cancel - if (cfg.onCancel == "null") - resolve(null); - else if (cfg.onCancel == "reject") - reject(exports.Cancel); - //otherwise, just let the promise hang - } - }); - } - showPage(Math.min((_a = cfg.x) !== null && _a !== void 0 ? _a : 0, width - cols), Math.min((_b = cfg.y) !== null && _b !== void 0 ? _b : 0, height - rows)); - return promise; - }, - /** - * Displays a menu to a player, returning a Promise. - * Adds left and right arrows to switch pages. Automatically paginates provided options. - * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. - */ - pagedListButtons: function (target, title, description, options, _a) { - var _b; - var _c = _a.rowsPerPage, rowsPerPage = _c === void 0 ? 10 : _c, _d = _a.columns, columns = _d === void 0 ? 3 : _d, cfg = __rest(_a, ["rowsPerPage", "columns"]); - //Generate pages - var pages = (0, funcs_1.to2DArray)((0, funcs_1.to2DArray)(options, columns), rowsPerPage); - if (pages.length <= 1) - return exports.Menu.buttons(target, title, description, (_b = pages[0]) !== null && _b !== void 0 ? _b : [], cfg); - return exports.Menu.pages(target, title, description, pages, cfg); - }, - /** - * Displays a menu to a player, returning a Promise. - * Adds left and right arrows to switch pages. Automatically paginates provided options. - */ - pagedList: function (target, title, description, options, _a) { - var _b; - if (_a === void 0) { _a = {}; } - var _c = _a.rowsPerPage, rowsPerPage = _c === void 0 ? 10 : _c, _d = _a.columns, columns = _d === void 0 ? 3 : _d, _e = _a.optionStringifier, optionStringifier = _e === void 0 ? String : _e, cfg = __rest(_a, ["rowsPerPage", "columns", "optionStringifier"]); - //Generate pages - var pages = (0, funcs_1.to2DArray)((0, funcs_1.to2DArray)(options.map(function (o) { return ({ data: o, get text() { return optionStringifier(o); } }); }), columns), rowsPerPage); - if (pages.length <= 1) - return exports.Menu.buttons(target, title, description, (_b = pages[0]) !== null && _b !== void 0 ? _b : [], cfg); - return exports.Menu.pages(target, title, description, pages, cfg); - } -}; +"use strict"; +/* eslint-disable @typescript-eslint/array-type */ +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the menu framework. +For usage information, see docs/framework-usage-guide.md +For maintenance information, see docs/frameworks.md +*/ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.listeners = exports.Menu = exports.Cancel = void 0; +exports.registerListeners = registerListeners; +var commands_1 = require("/frameworks/commands"); +var funcs_1 = require("/funcs"); +var players_1 = require("/players"); +var promise_1 = require("/promise"); +var utils_1 = require("/utils"); +/** Used to change the behavior of adding another menu when being run in a menu callback. */ +var isInMenuCallback = false; +/** Stores a mapping from name to the numeric id of a listener that has been registered. */ +var registeredListeners = {}; +exports.listeners = registeredListeners; +/** Stores all listeners in use by fish-commands. */ +var listeners = { + generic: function (player, option) { + var fishSender = players_1.FishPlayer.get(player); + var prevCallback = fishSender.activeMenus.shift(); + if (!prevCallback) + return; //No menu to process, do nothing + isInMenuCallback = true; + prevCallback.callback(option); + isInMenuCallback = false; + }, + none: function (player, option) { + //do nothing + } +}; +exports.Cancel = Symbol("Cancel"); +/** Registers all listeners, should be called on server load. */ +function registerListeners() { + var e_1, _a; + var _b; + try { + for (var _c = __values(Object.entries(listeners)), _d = _c.next(); !_d.done; _d = _c.next()) { + var _e = __read(_d.value, 2), key = _e[0], listener = _e[1]; + (_b = registeredListeners[key]) !== null && _b !== void 0 ? _b : (registeredListeners[key] = Menus.registerMenu(listener)); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_1) throw e_1.error; } + } +} +exports.Menu = { + /** Displays a menu to a player, returning a Promise. */ + raw: function (title, description, arrangedOptions, target, _a) { + var _b = _a === void 0 ? {} : _a, _c = _b.optionStringifier, optionStringifier = _c === void 0 ? String : _c, _d = _b.onCancel, onCancel = _d === void 0 ? "reject" : _d, _e = _b.cancelOptionId, cancelOptionId = _e === void 0 ? -1 : _e; + var _f = promise_1.Promise.withResolvers(), promise = _f.promise, reject = _f.reject, resolve = _f.resolve; + //The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered. + //If menu() is being called from a menu calback, add it to the front of the queue so it is processed before any other menus. + //Otherwise, two multi-step menus queued together would alternate, which would confuse the player. + target.activeMenus[isInMenuCallback ? "unshift" : "push"]({ callback: function (option) { + //Additional permission validation could be done here, but the only way that callback() can be called is if the above statement executed, + //and on sensitive menus such as the stop menu, the only way to reach that is if menu() was called by the /stop command, + //which already checks permissions. + //Additionally, the callback is cleared by the generic menu listener after it is executed. + try { + var options = arrangedOptions.flat(); + //We do need to validate option though, as it can be any number. + if (option === -1 || option === cancelOptionId || !(option in options)) { + //Consider any invalid option to be a cancellation + if (onCancel == "null") + resolve(null); + else if (onCancel == "reject") + reject(exports.Cancel); + else + return; + } + else { + resolve(options[option]); + } + } + catch (err) { + (0, utils_1.handleError)(err, target, utils_1.outputFail, "".concat(target.cleanedName, " submitted menu \"").concat(title, "\" \"").concat(description, "\"")); + } + } }); + var i = 0; + var stringifiedOptions = arrangedOptions.map(function (r) { return r.map(function (item) { + if (i === cancelOptionId) + return item; + i++; + return optionStringifier(item); + }); }); + Call.menu(target.con, registeredListeners.generic, title, description, stringifiedOptions); + return promise; + }, + /** Displays a menu to a player, returning a Promise. Arranges provided options into a 2D array, and can add a Cancel option. */ + menu: function (title, description, options, target, _a) { + var _b = _a === void 0 ? {} : _a, _c = _b.includeCancel, includeCancel = _c === void 0 ? false : _c, _d = _b.optionStringifier, optionStringifier = _d === void 0 ? String : _d, _e = _b.columns, columns = _e === void 0 ? 3 : _e, _f = _b.onCancel, onCancel = _f === void 0 ? "reject" : _f, _g = _b.cancelOptionId, cancelOptionId = _g === void 0 ? -1 : _g; + //Set up the 2D array of options, and maybe add cancel + //Call.menu() with [[]] will cause a client crash, make sure to pass [] instead + var arrangedOptions = (options.length == 0 && !includeCancel) ? [] : (0, funcs_1.to2DArray)(options, columns); + if (includeCancel) { + arrangedOptions.push(["[red]Cancel[]"]); + //This is safe because cancelOptionId is set, + //so the handler will never get called with "Cancel". + cancelOptionId = options.length; + } + return exports.Menu.raw(title, description, arrangedOptions, target, { + cancelOptionId: cancelOptionId, + onCancel: onCancel, + optionStringifier: optionStringifier + }); + }, + /** Rejects with a CommandError if the user chooses to cancel. */ + confirm: function (target, description, _a) { + var _b = _a === void 0 ? {} : _a, _c = _b.cancelOutput, cancelOutput = _c === void 0 ? "Cancelled." : _c, _d = _b.title, title = _d === void 0 ? "Confirm" : _d, _e = _b.confirmText, confirmText = _e === void 0 ? "[green]Confirm" : _e, _f = _b.cancelText, cancelText = _f === void 0 ? "[red]Cancel" : _f; + return exports.Menu.menu(title, description, [confirmText, cancelText], target, { onCancel: "reject", cancelOptionId: 1 }).catch(function (e) { + if (e === exports.Cancel) + (0, commands_1.fail)(cancelOutput); + throw e; //some random error, rethrow it + }); + }, + /** Same as confirm(), but with inverted colors, for potentially dangerous actions. */ + confirmDangerous: function (target, description, _a) { + if (_a === void 0) { _a = {}; } + var _b = _a.confirmText, confirmText = _b === void 0 ? "[red]Confirm" : _b, _c = _a.cancelText, cancelText = _c === void 0 ? "[green]Cancel" : _c, rest = __rest(_a, ["confirmText", "cancelText"]); + return exports.Menu.confirm(target, description, __assign({ cancelText: cancelText, confirmText: confirmText }, rest)); + }, + /** + * Displays a menu to a player, returning a Promise. + * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. + */ + buttons: function (target, title, description, options, cfg) { + if (cfg === void 0) { cfg = {}; } + return exports.Menu.raw(title, description, options, target, __assign(__assign({}, cfg), { optionStringifier: function (o) { return o.text; } })).then(function (o) { return o === null || o === void 0 ? void 0 : o.data; }); + }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. + * Shows different options based on the page. + */ + pages: function (target, title, description, options, cfg) { + var _a = promise_1.Promise.withResolvers(), promise = _a.promise, reject = _a.reject, resolve = _a.resolve; + function showPage(index) { + var opts = __spreadArray(__spreadArray([], __read(options[index].map(function (r) { return r.map(function (d) { return ({ text: d.text, data: [d.data] }); }); })), false), [ + [ + { data: "left", text: "[".concat(index == 0 ? "gray" : "accent", "]<--") }, + { data: "numbers", text: "[accent]".concat(index + 1, "/").concat(options.length) }, + { data: "right", text: "[".concat(index == options.length - 1 ? "gray" : "accent", "]-->") } + ] + ], false); + void exports.Menu.buttons(target, title, description, opts, __assign(__assign({}, cfg), { onCancel: "null" })).then(function (response) { + if (response instanceof Array) + resolve(response[0]); + else if (response === "right") + showPage(Math.min(index + 1, options.length - 1)); + else if (response === "left") + showPage(Math.max(index - 1, 0)); + else { + //Treat numbers as cancel + if (cfg.onCancel == "null") + resolve(null); + else if (cfg.onCancel == "reject") + reject(exports.Cancel); + //otherwise, just let the promise hang + } + }); + } + showPage(0); + return promise; + }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. + * Shows different text based on the current page. + */ + textPages: function (target, pages, options, cfg) { + if (options === void 0) { options = []; } + if (cfg === void 0) { cfg = {}; } + var _a = promise_1.Promise.withResolvers(), promise = _a.promise, reject = _a.reject, resolve = _a.resolve; + var pageSkipSize = Math.max(Math.floor(pages.length / 8), 5); + function showPage(index) { + var opts = __spreadArray(__spreadArray([ + [ + { data: ["left", pageSkipSize], text: "[".concat(index == 0 ? "gray" : "accent", "]<<<") }, + { data: ["left", 1], text: "[".concat(index == 0 ? "gray" : "accent", "]<--") }, + { data: ["right", 1], text: "[".concat(index == pages.length - 1 ? "gray" : "accent", "]-->") }, + { data: ["right", pageSkipSize], text: "[".concat(index == pages.length - 1 ? "gray" : "accent", "]>>>") }, + ], [ + { data: ["numbers"], text: "[accent]Page ".concat(index + 1, "/").concat(pages.length) }, + ] + ], __read(options.map(function (d, i) { return [{ data: [i], text: d }]; })), false), [ + [ + { data: ["cancel"], text: "[lightgray]Close" }, + ], + ], false); + void exports.Menu.buttons(target, pages[index][0], pages[index][1](), opts, __assign(__assign({}, cfg), { onCancel: "null" })).then(function (response) { + if ((response === null || response === void 0 ? void 0 : response[0]) === "right") + showPage(Math.min(index + response[1], pages.length - 1)); + else if ((response === null || response === void 0 ? void 0 : response[0]) === "left") + showPage(Math.max(index - response[1], 0)); + else if (typeof (response === null || response === void 0 ? void 0 : response[0]) === "number") { + resolve([index, options[response[0]]]); + } + else { + //Treat numbers as cancel + if (cfg.onCancel == "null") + resolve(null); + else if (cfg.onCancel == "reject") + reject(exports.Cancel); + //otherwise, just let the promise hang + } + }); + } + var index = (function () { + if (cfg.startPage == undefined) + return 0; + if (typeof cfg.startPage === 'number') { + if (cfg.startPage < 0) + return 0; + return cfg.startPage; + } + var index = pages.findIndex(function (_a) { + var _b = __read(_a, 1), title = _b[0]; + return title === cfg.startPage; + }); + if (index === -1) + return 0; + return index; + })(); + showPage(index); + return promise; + }, + /** + * Displays a menu to a player, returning a Promise. + * Accepts a 2D array of options and shows a region of that 2D grid. + * Adds arrows to scroll left/right/up/down. + * Resolves to the selected option. + */ + scroll2D: function (target, title, description, options, cfg) { + var _a, _b; + if (cfg === void 0) { cfg = {}; } + var _c = promise_1.Promise.withResolvers(), promise = _c.promise, reject = _c.reject, resolve = _c.resolve; + var _d = cfg.rows, rows = _d === void 0 ? 5 : _d, _e = cfg.columns, cols = _e === void 0 ? 5 : _e; + var height = options.length; + var width = options[0].length; + function showPage(x, y) { + var _a, _b; + var opts = __spreadArray(__spreadArray([], __read(options.slice(y, y + rows).map(function (r) { return r.concat(Array(width - r.length).fill({ data: "blank", text: "" })); }).map(function (r) { + return r.slice(x, x + cols).map(function (d) { return ({ text: d.text, data: [d.data] }); }); + })), false), [ + [ + { data: "blank", text: "" }, + ], + [ + { data: "blank", text: "" }, + { data: "up", text: "[".concat(y == 0 ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.up)) }, + { data: "blank", text: "" }, + ], [ + { data: "left", text: "[".concat(x == 0 ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.left)) }, + { data: "blank", text: (_b = (_a = cfg.getCenterText) === null || _a === void 0 ? void 0 : _a.call(cfg, x, y)) !== null && _b !== void 0 ? _b : '' }, + { data: "right", text: "[".concat(x == width - cols ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.right)) }, + ], [ + { data: "blank", text: "" }, + { data: "down", text: "[".concat(y == height - rows ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.down)) }, + { data: "blank", text: "" }, + ] + ], false); + void exports.Menu.buttons(target, title, description, opts, cfg).then(function (response) { + if (response instanceof Array) + resolve([response[0], x, y]); + else if (response === "right") + showPage(Math.min(x + 1, width - cols), y); + else if (response === "left") + showPage(Math.max(x - 1, 0), y); + else if (response === "up") + showPage(x, Math.max(y - 1, 0)); + else if (response === "down") + showPage(x, Math.min(y + 1, height - rows)); + else { + //Treat numbers as cancel + if (cfg.onCancel == "null") + resolve(null); + else if (cfg.onCancel == "reject") + reject(exports.Cancel); + //otherwise, just let the promise hang + } + }); + } + showPage(Math.min((_a = cfg.x) !== null && _a !== void 0 ? _a : 0, width - cols), Math.min((_b = cfg.y) !== null && _b !== void 0 ? _b : 0, height - rows)); + return promise; + }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. Automatically paginates provided options. + * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. + */ + pagedListButtons: function (target, title, description, options, _a) { + var _b; + var _c = _a.rowsPerPage, rowsPerPage = _c === void 0 ? 10 : _c, _d = _a.columns, columns = _d === void 0 ? 3 : _d, cfg = __rest(_a, ["rowsPerPage", "columns"]); + //Generate pages + var pages = (0, funcs_1.to2DArray)((0, funcs_1.to2DArray)(options, columns), rowsPerPage); + if (pages.length <= 1) + return exports.Menu.buttons(target, title, description, (_b = pages[0]) !== null && _b !== void 0 ? _b : [], cfg); + return exports.Menu.pages(target, title, description, pages, cfg); + }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. Automatically paginates provided options. + */ + pagedList: function (target, title, description, options, _a) { + var _b; + if (_a === void 0) { _a = {}; } + var _c = _a.rowsPerPage, rowsPerPage = _c === void 0 ? 10 : _c, _d = _a.columns, columns = _d === void 0 ? 3 : _d, _e = _a.optionStringifier, optionStringifier = _e === void 0 ? String : _e, cfg = __rest(_a, ["rowsPerPage", "columns", "optionStringifier"]); + //Generate pages + var pages = (0, funcs_1.to2DArray)((0, funcs_1.to2DArray)(options.map(function (o) { return ({ data: o, get text() { return optionStringifier(o); } }); }), columns), rowsPerPage); + if (pages.length <= 1) + return exports.Menu.buttons(target, title, description, (_b = pages[0]) !== null && _b !== void 0 ? _b : [], cfg); + return exports.Menu.pages(target, title, description, pages, cfg); + } +}; diff --git a/build/scripts/funcs.js b/build/scripts/funcs.js index 4a811ba2..ee7f6bac 100644 --- a/build/scripts/funcs.js +++ b/build/scripts/funcs.js @@ -1,474 +1,474 @@ -"use strict"; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DurationSecs = exports.Duration = exports.EventEmitter = exports.StringIO = exports.StringBuilder = void 0; -exports.memoize = memoize; -exports.to2DArray = to2DArray; -exports.setToArray = setToArray; -exports.crash = crash; -exports.capitalizeText = capitalizeText; -exports.indefiniteArticle = indefiniteArticle; -exports.escapeTextDiscord = escapeTextDiscord; -exports.escapeStringColorsClient = escapeStringColorsClient; -exports.escapeStringColorsServer = escapeStringColorsServer; -exports.parseError = parseError; -exports.tagProcessor = tagProcessor; -exports.tagProcessorPartial = tagProcessorPartial; -exports.random = random; -exports.getIPAddress = getIPAddress; -exports.lazy = lazy; -exports.invalidtoNull = invalidtoNull; -exports.cleanColors = cleanColors; -exports.computeStatistics = computeStatistics; -exports.search = search; -exports.searchFixed = searchFixed; -exports.delay = delay; -var storedValues = {}; -/** - * Stores the output of a function and returns that value - * instead of running the function again unless any - * dependencies have changed to improve performance with - * functions that have expensive computation. - * @param callback function to run if a dependancy has changed - * @param dep dependency array of values to monitor - * @param id arbitrary unique id of the function for storage purposes. - */ -function memoize(callback, dep, id) { - if (!storedValues[id]) { - storedValues[id] = { value: callback(), dep: dep }; - } - else if (dep.some(function (d, ind) { return d !== storedValues[id].dep[ind]; })) { - //If the value changed - storedValues[id].value = callback(); - storedValues[id].dep = dep; - } - return storedValues[id].value; -} /** - * Converts a 1D array into a 2D array. - * @param width the max length of each row. - * The last row may not be full. - */ -function to2DArray(array, width) { - if (array.length == 0) - return []; - var output = [[]]; - array.forEach(function (el) { - if (output.at(-1).length >= width) { - output.push([]); - } - output.at(-1).push(el); - }); - return output; -} -function setToArray(set) { - var array = []; - set.each(function (item) { return array.push(item); }); - return array; -} -var StringBuilder = /** @class */ (function () { - function StringBuilder(str) { - if (str === void 0) { str = ""; } - this.str = str; - } - StringBuilder.prototype.add = function (str) { - this.str += str; - return this; - }; - StringBuilder.prototype.chunk = function (str) { - if (Strings.stripColors(str).length > 0) { - this.str = this.str + " " + str; - } - return this; - }; - return StringBuilder; -}()); -exports.StringBuilder = StringBuilder; -/** - * Used for serialization to strings. - * @deprecated use Serializer instead, which serializes to byte[] - */ -var StringIO = /** @class */ (function () { - function StringIO(string) { - if (string === void 0) { string = ""; } - this.string = string; - this.offset = 0; - } - StringIO.prototype.read = function (length) { - if (length === void 0) { length = 1; } - if (this.offset + length > this.string.length) - crash("Unexpected EOF"); - return this.string.slice(this.offset, this.offset += length); - }; - StringIO.prototype.write = function (str) { - this.string += str; - }; - StringIO.prototype.readString = function (/** The length of the written length. */ lenlen) { - if (lenlen === void 0) { lenlen = 3; } - var length = parseInt(this.read(lenlen)); - if (length == 0) - return null; - return this.read(length); - }; - StringIO.prototype.writeString = function (str, lenlen, truncate) { - if (lenlen === void 0) { lenlen = 3; } - if (truncate === void 0) { truncate = false; } - if (str === null) { - this.string += "0".repeat(lenlen); - } - else if (typeof str !== "string") { - crash("Attempted to serialize string ".concat(String(str), ", but it was not a string")); - } - else if (str.length > (Math.pow(10, lenlen) - 1)) { - if (truncate) { - Log.err("Cannot write strings with length greater than ".concat((Math.pow(10, lenlen) - 1), " (was ").concat(str.length, "), truncating")); - this.string += (Math.pow(10, lenlen) - 1).toString().padStart(lenlen, "0"); - this.string += str.slice(0, (Math.pow(10, lenlen) - 1)); - } - else { - crash("Cannot write strings with length greater than ".concat((Math.pow(10, lenlen) - 1), " (was ").concat(str.length, ")\n String was: \"").concat(str, "\"")); - } - } - else { - this.string += str.length.toString().padStart(lenlen, "0"); - this.string += str; - } - }; - StringIO.prototype.readEnumString = function (options) { - var length = (options.length - 1).toString().length; - var option = this.readNumber(length); - return options[option]; - }; - StringIO.prototype.writeEnumString = function (value, options) { - var length = (options.length - 1).toString().length; - var option = options.indexOf(value); - if (option == -1) - crash("Attempted to write invalid value \"".concat(String(value), "\" for enum, valid values are (").concat(options.join(", "), ")")); - this.writeNumber(option, length); - }; - StringIO.prototype.readNumber = function (size) { - if (size === void 0) { size = 4; } - var data = this.read(size); - if (/^0*-\d+$/.test(data)) { - //negative numbers were incorrectly stored in previous versions - data = "-" + data.split("-")[1]; - } - if (isNaN(Number(data))) - crash("Attempted to read invalid number: ".concat(data)); - return Number(data); - }; - StringIO.prototype.writeNumber = function (num, size, clamp) { - if (size === void 0) { size = 4; } - if (clamp === void 0) { clamp = false; } - if (typeof num != "number") - crash("".concat(String(num), " was not a number!")); - if (num.toString().length > size) { - if (clamp) { - if (num > (Math.pow(10, size)) - 1) - this.string += (Math.pow(10, size)) - 1; - else - this.string += num.toString().slice(0, size); - } - else - crash("Cannot write number ".concat(num, " with length ").concat(size, ": too long")); - } - this.string += num.toString().padStart(size, "0"); - }; - StringIO.prototype.readBool = function () { - return this.read(1) == "T" ? true : false; - }; - StringIO.prototype.writeBool = function (val) { - this.write(val ? "T" : "F"); - }; - StringIO.prototype.writeArray = function (array, func, lenlen) { - var _this = this; - this.writeNumber(array.length, lenlen); - array.forEach(function (e) { return func(e, _this); }); - }; - StringIO.prototype.readArray = function (func, lenlen) { - var length = this.readNumber(lenlen); - var array = []; - for (var i = 0; i < length; i++) { - array[i] = func(this); - } - return array; - }; - StringIO.prototype.expectEOF = function () { - if (this.string.length > this.offset) - crash("Expected EOF, but found extra data: \"".concat(this.string.slice(this.offset), "\"")); - }; - StringIO.read = function (data, func) { - var str = new StringIO(data); - try { - return func(str); - } - catch (err) { - Log.err("Error while reading compressed data!"); - Log.err(data); - throw err; - } - }; - StringIO.write = function (data, func) { - var str = new StringIO(); - func(str, data); - return str.string; - }; - return StringIO; -}()); -exports.StringIO = StringIO; -/** Something that emits events. */ -var EventEmitter = /** @class */ (function () { - function EventEmitter() { - this.listeners = {}; - } - EventEmitter.prototype.on = function (event, callback) { - var _a; - var _b; - ((_a = (_b = this.listeners)[event]) !== null && _a !== void 0 ? _a : (_b[event] = [])).push(callback); - return this; - }; - EventEmitter.prototype.fire = function (event, args) { - var _a; - var listeners = (_a = this.listeners[event]) !== null && _a !== void 0 ? _a : []; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (var i = 0; i < listeners.length; i++) { - listeners[i].apply(listeners, __spreadArray([this], __read(args), false)); - } - }; - return EventEmitter; -}()); -exports.EventEmitter = EventEmitter; -function crash(message) { - throw new Error(message); -} -/** Best effort title-capitalization of a word. */ -function capitalizeText(text, separator) { - if (separator === void 0) { separator = " "; } - return text - .split(separator) - .map(function (word, i, arr) { return (["a", "an", "the", "in", "and", "of", "it", "is"].includes(word) && - i !== 0 && i !== arr.length - 1) ? word - : word[0].toUpperCase() + word.substring(1).toLowerCase(); }).join(" "); -} -/** Best effort prepends an indefinite article (either "a" or "an") to provided text. */ -function indefiniteArticle(text) { - var cText = Strings.stripColors(text); - if (/^[aeiou]/.test(cText) || cText == "hour") - return "an " + text; - else - return "a " + text; -} -var pattern = Pattern.compile("([*\\_~`|:])"); -function escapeTextDiscord(text) { - return pattern.matcher(text).replaceAll("\\\\$1\u200B"); -} -/** Prevents Mindustry from displaying color tags in a string by escaping them. Example: turns [scarlet]red to [[scarlet]red. */ -function escapeStringColorsClient(str) { - return str.replace(/\[/g, "[["); -} -// export function highlightStringColorsClient(str:string):string { -// return str.replace(/(? 1) - return { value: result }; - }; - try { - for (var filters_1 = __values(filters), filters_1_1 = filters_1.next(); !filters_1_1.done; filters_1_1 = filters_1.next()) { - var filter = filters_1_1.value; - var state_1 = _loop_1(filter); - if (typeof state_1 === "object") - return state_1.value; - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (filters_1_1 && !filters_1_1.done && (_a = filters_1.return)) _a.call(filters_1); - } - finally { if (e_1) throw e_1.error; } - } - return null; - }; -} -function searchFixed(options, filters, recomputeOptions) { - var func = search.apply(void 0, __spreadArray([], __read(filters), false)); - var _options = options; - return function (query) { - if (typeof _options == "function") { - if (recomputeOptions) - return func(_options(), query); - else - return func(_options = _options(), query); - } - else - return func(_options, query); - }; -} -function delay(millis) { - return new Promise(function (res) { return Timer.schedule(res, millis / 1000); }); -} -exports.Duration = { - seconds: function (x) { return x * 1000; }, - minutes: function (x) { return x * 60000; }, - hours: function (x) { return x * 3600000; }, - days: function (x) { return x * 86400000; }, - months: function (x) { return x * 2592000000; }, -}; -exports.DurationSecs = { - minutes: function (x) { return x * 60; }, - hours: function (x) { return x * 3600; }, - days: function (x) { return x * 86400; }, - months: function (x) { return x * 2592000; }, -}; +"use strict"; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DurationSecs = exports.Duration = exports.EventEmitter = exports.StringIO = exports.StringBuilder = void 0; +exports.memoize = memoize; +exports.to2DArray = to2DArray; +exports.setToArray = setToArray; +exports.crash = crash; +exports.capitalizeText = capitalizeText; +exports.indefiniteArticle = indefiniteArticle; +exports.escapeTextDiscord = escapeTextDiscord; +exports.escapeStringColorsClient = escapeStringColorsClient; +exports.escapeStringColorsServer = escapeStringColorsServer; +exports.parseError = parseError; +exports.tagProcessor = tagProcessor; +exports.tagProcessorPartial = tagProcessorPartial; +exports.random = random; +exports.getIPAddress = getIPAddress; +exports.lazy = lazy; +exports.invalidtoNull = invalidtoNull; +exports.cleanColors = cleanColors; +exports.computeStatistics = computeStatistics; +exports.search = search; +exports.searchFixed = searchFixed; +exports.delay = delay; +var storedValues = {}; +/** + * Stores the output of a function and returns that value + * instead of running the function again unless any + * dependencies have changed to improve performance with + * functions that have expensive computation. + * @param callback function to run if a dependancy has changed + * @param dep dependency array of values to monitor + * @param id arbitrary unique id of the function for storage purposes. + */ +function memoize(callback, dep, id) { + if (!storedValues[id]) { + storedValues[id] = { value: callback(), dep: dep }; + } + else if (dep.some(function (d, ind) { return d !== storedValues[id].dep[ind]; })) { + //If the value changed + storedValues[id].value = callback(); + storedValues[id].dep = dep; + } + return storedValues[id].value; +} /** + * Converts a 1D array into a 2D array. + * @param width the max length of each row. + * The last row may not be full. + */ +function to2DArray(array, width) { + if (array.length == 0) + return []; + var output = [[]]; + array.forEach(function (el) { + if (output.at(-1).length >= width) { + output.push([]); + } + output.at(-1).push(el); + }); + return output; +} +function setToArray(set) { + var array = []; + set.each(function (item) { return array.push(item); }); + return array; +} +var StringBuilder = /** @class */ (function () { + function StringBuilder(str) { + if (str === void 0) { str = ""; } + this.str = str; + } + StringBuilder.prototype.add = function (str) { + this.str += str; + return this; + }; + StringBuilder.prototype.chunk = function (str) { + if (Strings.stripColors(str).length > 0) { + this.str = this.str + " " + str; + } + return this; + }; + return StringBuilder; +}()); +exports.StringBuilder = StringBuilder; +/** + * Used for serialization to strings. + * @deprecated use Serializer instead, which serializes to byte[] + */ +var StringIO = /** @class */ (function () { + function StringIO(string) { + if (string === void 0) { string = ""; } + this.string = string; + this.offset = 0; + } + StringIO.prototype.read = function (length) { + if (length === void 0) { length = 1; } + if (this.offset + length > this.string.length) + crash("Unexpected EOF"); + return this.string.slice(this.offset, this.offset += length); + }; + StringIO.prototype.write = function (str) { + this.string += str; + }; + StringIO.prototype.readString = function (/** The length of the written length. */ lenlen) { + if (lenlen === void 0) { lenlen = 3; } + var length = parseInt(this.read(lenlen)); + if (length == 0) + return null; + return this.read(length); + }; + StringIO.prototype.writeString = function (str, lenlen, truncate) { + if (lenlen === void 0) { lenlen = 3; } + if (truncate === void 0) { truncate = false; } + if (str === null) { + this.string += "0".repeat(lenlen); + } + else if (typeof str !== "string") { + crash("Attempted to serialize string ".concat(String(str), ", but it was not a string")); + } + else if (str.length > (Math.pow(10, lenlen) - 1)) { + if (truncate) { + Log.err("Cannot write strings with length greater than ".concat((Math.pow(10, lenlen) - 1), " (was ").concat(str.length, "), truncating")); + this.string += (Math.pow(10, lenlen) - 1).toString().padStart(lenlen, "0"); + this.string += str.slice(0, (Math.pow(10, lenlen) - 1)); + } + else { + crash("Cannot write strings with length greater than ".concat((Math.pow(10, lenlen) - 1), " (was ").concat(str.length, ")\n String was: \"").concat(str, "\"")); + } + } + else { + this.string += str.length.toString().padStart(lenlen, "0"); + this.string += str; + } + }; + StringIO.prototype.readEnumString = function (options) { + var length = (options.length - 1).toString().length; + var option = this.readNumber(length); + return options[option]; + }; + StringIO.prototype.writeEnumString = function (value, options) { + var length = (options.length - 1).toString().length; + var option = options.indexOf(value); + if (option == -1) + crash("Attempted to write invalid value \"".concat(String(value), "\" for enum, valid values are (").concat(options.join(", "), ")")); + this.writeNumber(option, length); + }; + StringIO.prototype.readNumber = function (size) { + if (size === void 0) { size = 4; } + var data = this.read(size); + if (/^0*-\d+$/.test(data)) { + //negative numbers were incorrectly stored in previous versions + data = "-" + data.split("-")[1]; + } + if (isNaN(Number(data))) + crash("Attempted to read invalid number: ".concat(data)); + return Number(data); + }; + StringIO.prototype.writeNumber = function (num, size, clamp) { + if (size === void 0) { size = 4; } + if (clamp === void 0) { clamp = false; } + if (typeof num != "number") + crash("".concat(String(num), " was not a number!")); + if (num.toString().length > size) { + if (clamp) { + if (num > (Math.pow(10, size)) - 1) + this.string += (Math.pow(10, size)) - 1; + else + this.string += num.toString().slice(0, size); + } + else + crash("Cannot write number ".concat(num, " with length ").concat(size, ": too long")); + } + this.string += num.toString().padStart(size, "0"); + }; + StringIO.prototype.readBool = function () { + return this.read(1) == "T" ? true : false; + }; + StringIO.prototype.writeBool = function (val) { + this.write(val ? "T" : "F"); + }; + StringIO.prototype.writeArray = function (array, func, lenlen) { + var _this = this; + this.writeNumber(array.length, lenlen); + array.forEach(function (e) { return func(e, _this); }); + }; + StringIO.prototype.readArray = function (func, lenlen) { + var length = this.readNumber(lenlen); + var array = []; + for (var i = 0; i < length; i++) { + array[i] = func(this); + } + return array; + }; + StringIO.prototype.expectEOF = function () { + if (this.string.length > this.offset) + crash("Expected EOF, but found extra data: \"".concat(this.string.slice(this.offset), "\"")); + }; + StringIO.read = function (data, func) { + var str = new StringIO(data); + try { + return func(str); + } + catch (err) { + Log.err("Error while reading compressed data!"); + Log.err(data); + throw err; + } + }; + StringIO.write = function (data, func) { + var str = new StringIO(); + func(str, data); + return str.string; + }; + return StringIO; +}()); +exports.StringIO = StringIO; +/** Something that emits events. */ +var EventEmitter = /** @class */ (function () { + function EventEmitter() { + this.listeners = {}; + } + EventEmitter.prototype.on = function (event, callback) { + var _a; + var _b; + ((_a = (_b = this.listeners)[event]) !== null && _a !== void 0 ? _a : (_b[event] = [])).push(callback); + return this; + }; + EventEmitter.prototype.fire = function (event, args) { + var _a; + var listeners = (_a = this.listeners[event]) !== null && _a !== void 0 ? _a : []; + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (var i = 0; i < listeners.length; i++) { + listeners[i].apply(listeners, __spreadArray([this], __read(args), false)); + } + }; + return EventEmitter; +}()); +exports.EventEmitter = EventEmitter; +function crash(message) { + throw new Error(message); +} +/** Best effort title-capitalization of a word. */ +function capitalizeText(text, separator) { + if (separator === void 0) { separator = " "; } + return text + .split(separator) + .map(function (word, i, arr) { return (["a", "an", "the", "in", "and", "of", "it", "is"].includes(word) && + i !== 0 && i !== arr.length - 1) ? word + : word[0].toUpperCase() + word.substring(1).toLowerCase(); }).join(" "); +} +/** Best effort prepends an indefinite article (either "a" or "an") to provided text. */ +function indefiniteArticle(text) { + var cText = Strings.stripColors(text); + if (/^[aeiou]/.test(cText) || cText == "hour") + return "an " + text; + else + return "a " + text; +} +var pattern = Pattern.compile("([*\\_~`|:])"); +function escapeTextDiscord(text) { + return pattern.matcher(text).replaceAll("\\\\$1\u200B"); +} +/** Prevents Mindustry from displaying color tags in a string by escaping them. Example: turns [scarlet]red to [[scarlet]red. */ +function escapeStringColorsClient(str) { + return str.replace(/\[/g, "[["); +} +// export function highlightStringColorsClient(str:string):string { +// return str.replace(/(? 1) + return { value: result }; + }; + try { + for (var filters_1 = __values(filters), filters_1_1 = filters_1.next(); !filters_1_1.done; filters_1_1 = filters_1.next()) { + var filter = filters_1_1.value; + var state_1 = _loop_1(filter); + if (typeof state_1 === "object") + return state_1.value; + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (filters_1_1 && !filters_1_1.done && (_a = filters_1.return)) _a.call(filters_1); + } + finally { if (e_1) throw e_1.error; } + } + return null; + }; +} +function searchFixed(options, filters, recomputeOptions) { + var func = search.apply(void 0, __spreadArray([], __read(filters), false)); + var _options = options; + return function (query) { + if (typeof _options == "function") { + if (recomputeOptions) + return func(_options(), query); + else + return func(_options = _options(), query); + } + else + return func(_options, query); + }; +} +function delay(millis) { + return new Promise(function (res) { return Timer.schedule(res, millis / 1000); }); +} +exports.Duration = { + seconds: function (x) { return x * 1000; }, + minutes: function (x) { return x * 60000; }, + hours: function (x) { return x * 3600000; }, + days: function (x) { return x * 86400000; }, + months: function (x) { return x * 2592000000; }, +}; +exports.DurationSecs = { + minutes: function (x) { return x * 60; }, + hours: function (x) { return x * 3600; }, + days: function (x) { return x * 86400; }, + months: function (x) { return x * 2592000; }, +}; diff --git a/build/scripts/globals.js b/build/scripts/globals.js index d606d314..10e7e366 100644 --- a/build/scripts/globals.js +++ b/build/scripts/globals.js @@ -1,34 +1,34 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains mutable global variables, and global constants. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.FishEvents = exports.unitsT5 = exports.maxTime = exports.ipRangeWildcardPattern = exports.ipRangeCIDRPattern = exports.ipPortPattern = exports.ipPattern = exports.uuidPattern = exports.ipJoins = exports.fishPlugin = exports.fishState = exports.recentWhispers = exports.tileHistory = void 0; -var funcs_1 = require("/funcs"); -exports.tileHistory = {}; -exports.recentWhispers = {}; -exports.fishState = { - restartQueued: false, - restartLoopTask: null, - corruption_t1: null, - corruption_t2: null, - lastPranked: Date.now(), - labels: [], - peacefulMode: false, - joinBell: false, - startTime: Date.now(), -}; -exports.fishPlugin = { - directory: null, - version: null, -}; -exports.ipJoins = new ObjectIntMap(); -exports.uuidPattern = /^[a-zA-Z0-9+/]{22}==$/; -exports.ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; -exports.ipPortPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$/; -exports.ipRangeCIDRPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(1[2-9]|2[0-4])$/; //Disallow anything bigger than a /12 -exports.ipRangeWildcardPattern = /^(\d{1,3}\.\d{1,3})\.(?:(\d{1,3}\.\*)|\*)$/; //Disallow anything bigger than a /16 -exports.maxTime = 9999999999999; -exports.unitsT5 = [UnitTypes.reign, UnitTypes.toxopid, UnitTypes.corvus, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.omura, UnitTypes.navanax, UnitTypes.conquer, UnitTypes.collaris, UnitTypes.disrupt]; -exports.FishEvents = new funcs_1.EventEmitter(); +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains mutable global variables, and global constants. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FishEvents = exports.unitsT5 = exports.maxTime = exports.ipRangeWildcardPattern = exports.ipRangeCIDRPattern = exports.ipPortPattern = exports.ipPattern = exports.uuidPattern = exports.ipJoins = exports.fishPlugin = exports.fishState = exports.recentWhispers = exports.tileHistory = void 0; +var funcs_1 = require("/funcs"); +exports.tileHistory = {}; +exports.recentWhispers = {}; +exports.fishState = { + restartQueued: false, + restartLoopTask: null, + corruption_t1: null, + corruption_t2: null, + lastPranked: Date.now(), + labels: [], + peacefulMode: false, + joinBell: false, + startTime: Date.now(), +}; +exports.fishPlugin = { + directory: null, + version: null, +}; +exports.ipJoins = new ObjectIntMap(); +exports.uuidPattern = /^[a-zA-Z0-9+/]{22}==$/; +exports.ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; +exports.ipPortPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$/; +exports.ipRangeCIDRPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(1[2-9]|2[0-4])$/; //Disallow anything bigger than a /12 +exports.ipRangeWildcardPattern = /^(\d{1,3}\.\d{1,3})\.(?:(\d{1,3}\.\*)|\*)$/; //Disallow anything bigger than a /16 +exports.maxTime = 9999999999999; +exports.unitsT5 = [UnitTypes.reign, UnitTypes.toxopid, UnitTypes.corvus, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.omura, UnitTypes.navanax, UnitTypes.conquer, UnitTypes.collaris, UnitTypes.disrupt]; +exports.FishEvents = new funcs_1.EventEmitter(); diff --git a/build/scripts/index.js b/build/scripts/index.js index 7a0f0bf8..2975a5ab 100644 --- a/build/scripts/index.js +++ b/build/scripts/index.js @@ -1,341 +1,341 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the main code, which calls other functions and initializes the plugin. -*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -var api = __importStar(require("/api")); -var aggregate_1 = require("/commands/aggregate"); -var config_1 = require("/config"); -var commands_1 = require("/frameworks/commands"); -var menus = __importStar(require("/frameworks/menus")); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var maps_1 = require("/maps"); -var packetHandlers_1 = require("/packetHandlers"); -var players_1 = require("/players"); -var timers = __importStar(require("/timers")); -var utils_1 = require("/utils"); -Events.on(EventType.ConnectionEvent, function (e) { - if (Vars.netServer.admins.bannedIPs.contains(e.connection.address)) { - api.getBanned({ - ip: e.connection.address, - }, function (banned) { - if (!banned) { - //If they were previously banned locally, but the API says they aren't banned, then unban them and clear the kick that the outer function already did - Vars.netServer.admins.unbanPlayerIP(e.connection.address); - Vars.netServer.admins.kickedIPs.remove(e.connection.address); - } - }); - } - else if (api.isVpnCached(e.connection.address) && players_1.FishPlayer.shouldWhackFlaggedPlayers()) { - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kick("You have been DOSblacklisted. Please join our discord for help: " + config_1.text.discordURL + "\nYou won't see this message again."); - Log.info("&yAntibot killed connection ".concat(e.connection.address, " due to flagged while under attack")); - } -}); -Events.on(EventType.PlayerConnect, function (e) { - if (players_1.FishPlayer.shouldKickNewPlayers() && e.player.info.timesJoined == 1) { - //do not use the helper function, for maximum performance - e.player.kick(Packets.KickReason.kick, 3600000); - } - players_1.FishPlayer.onPlayerConnect(e.player); -}); -Events.on(EventType.PlayerJoin, function (e) { - players_1.FishPlayer.onPlayerJoin(e.player); -}); -Events.on(EventType.PlayerLeave, function (e) { - players_1.FishPlayer.onPlayerLeave(e.player); -}); -Events.on(EventType.ConnectPacketEvent, function (e) { - if (!players_1.FishPlayer.connectRate.allow(5000, 35)) { - players_1.FishPlayer.triggerAntibot(300000, "Rate of player connections exceeded 35 / 5s", "automatic"); - } - globals_1.ipJoins.increment(e.connection.address); - var info = Vars.netServer.admins.getInfoOptional(e.packet.uuid); - var underAttack = players_1.FishPlayer.antiBotMode(); - var newPlayer = !info || info.timesJoined < 10; - var longModName = e.packet.mods.contains(function (str) { return str.length > 50; }); - var veryLongModName = e.packet.mods.contains(function (str) { return str.length > 100; }); - if ((underAttack && e.packet.mods.size > 2) || - (underAttack && longModName) || - (veryLongModName && (underAttack || newPlayer))) { - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - players_1.FishPlayer.triggerAntibot(60000, (veryLongModName ? "very long mod name" : longModName ? "long mod name" : "it had mods while under attack"), "automatic"); - return; - } - var suspiciousModName = e.packet.mods.contains(function (str) { return str.includes('\x1B'); }); - if (suspiciousModName || e.packet.name.includes('\x1B')) { - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - players_1.FishPlayer.triggerAntibot(5000, "illegal characters in name or mods", "automatic"); - return; - } - if (globals_1.ipJoins.get(e.connection.address) >= ((underAttack || veryLongModName) ? 3 : (newPlayer || longModName) ? 7 : 15)) { - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - players_1.FishPlayer.triggerAntibot(5000, "too many connections", "automatic"); - return; - } - /*if(e.packet.name.includes("discord.gg/GnEdS9TdV6")){ - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - FishPlayer.onBotWhack(); - Log.info(`&yAntibot killed connection ${e.connection.address} due to omni discord link`); - return; - }*/ - if (e.packet.name.includes("1`1@everyone")) { - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - players_1.FishPlayer.triggerAntibot(-1, "known bad name", "automatic"); - return; - } - if (Vars.netServer.admins.isDosBlacklisted(e.connection.address)) { - //threading moment, i think - e.connection.kicked = true; - return; - } - api.getBanned({ - ip: e.connection.address, - uuid: e.packet.uuid - }, function (banned) { - if (banned) { - Log.info("&lrSynced ban of ".concat(e.packet.uuid, "/").concat(e.connection.address, ".")); - e.connection.kick(Packets.KickReason.banned, 1); - Vars.netServer.admins.banPlayerIP(e.connection.address); - Vars.netServer.admins.banPlayerID(e.packet.uuid); - } - else { - Vars.netServer.admins.unbanPlayerIP(e.connection.address); - Vars.netServer.admins.unbanPlayerID(e.packet.uuid); - } - }); - players_1.FishPlayer.onConnectPacket(e.packet); -}); -Events.on(EventType.UnitChangeEvent, function (e) { - players_1.FishPlayer.onUnitChange(e.player, e.unit); -}); -Events.on(EventType.ContentInitEvent, function () { - //Unhide latum and renale - UnitTypes.latum.hidden = false; - UnitTypes.renale.hidden = false; -}); -Events.on(EventType.PlayerChatEvent, function (e) { return (0, utils_1.processChat)(e.player, e.message, true); }); -Events.on(EventType.ServerLoadEvent, function (e) { - Time.mark(); - var clientHandler = Vars.netServer.clientCommands; - var serverHandler = ServerControl.instance.handler; - players_1.FishPlayer.loadAll(); - globals_1.FishEvents.fire("loadData", []); - timers.initializeTimers(); - menus.registerListeners(); - //Cap delta - Time.setDeltaProvider(function () { return Math.min(Core.graphics.getDeltaTime() * 60, 10); }); - // Mute muted players - Vars.netServer.admins.addChatFilter(function (player, message) { return (0, utils_1.processChat)(player, message); }); - // Vars.netServer.admins.addChatFilter((p, message) => FishPlayer.get(p).hasPerm("member") ? message : foolifyChat(message)); - // Action filters - Vars.netServer.admins.addActionFilter(function (action) { - var _a, _b, _c; - var player = action.player; - var fishP = players_1.FishPlayer.get(player); - //prevent stopped players from doing anything other than deposit items. - if (!fishP.hasPerm("play")) { - action.player.sendMessage('[scarlet]\u26A0 [yellow]You are stopped, you cant perfom this action.'); - return false; - } - else { - if (action.type === Administration.ActionType.pickupBlock) { - (0, utils_1.addToTileHistory)({ - pos: "".concat(action.tile.x, ",").concat(action.tile.y), - uuid: action.player.uuid(), - action: "picked up", - type: (_b = (_a = action.tile.block()) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : "nothing", - }); - } - else if (action.type === Administration.ActionType.control && !((_c = action.unit) === null || _c === void 0 ? void 0 : _c.spawnedByCore) && Date.now() < fishP.blockedFromPossessingUnitsUntil) { - action.player.sendMessage("[scarlet]\u26A0 [yellow]You are blocked from controlling units for ".concat((0, utils_1.formatTimeRelative)(fishP.blockedFromPossessingUnitsUntil, true))); - return false; - } - else if (action.type === Administration.ActionType.commandUnits && Date.now() < fishP.blockedFromCommandingUnitsUntil) { - action.player.sendMessage("[scarlet]\u26A0 [yellow]You are blocked from commanding units for ".concat((0, utils_1.formatTimeRelative)(fishP.blockedFromCommandingUnitsUntil, true))); - return false; - } - else if (action.type === Administration.ActionType.pingLocation && action.pingText && action.pingText.length < Vars.maxPingTextLength) { - var fishP_1 = players_1.FishPlayer.get(action.player); - if (fishP_1.muted) { - action.player.sendMessage("[scarlet]\u26A0 [yellow]You are muted, you cannot send text through location pings."); - return false; - } - else if ((0, utils_1.matchFilter)(action.pingText, "chat", false)) { - //Allow it, but replace - player.pingX = action.pingX; - player.pingY = action.pingY; - player.pingTime = 1; - player.pingText = config_1.text.chatFilterReplacement.messageShort(); - return false; - } - } - return true; - } - }); - (0, aggregate_1.registerAll)(clientHandler, serverHandler); - (0, packetHandlers_1.loadPacketHandlers)(); - //Load plugin data - try { - var path = (0, utils_1.fishCommandsRootDirPath)(); - globals_1.fishPlugin.directory = path.toString(); - Threads.daemon(function () { - try { - globals_1.fishPlugin.version = OS.exec("git", "-C", globals_1.fishPlugin.directory, "rev-parse", "HEAD"); - } - catch (_a) { } - }); - } - catch (err) { - Log.err("Failed to get fish plugin information."); - Log.err(err); - } - Runtime.getRuntime().addShutdownHook(new Thread(function () { - try { - players_1.FishPlayer.uploadAll(); - } - catch (_a) { - Log.err("failed to upload"); - } - try { - globals_1.FishEvents.fire("saveData", []); - } - catch (_b) { - Log.err("failed to save misc data"); - } - try { - players_1.FishPlayer.saveAll(false); - } - catch (_c) { - Log.err("failed to save player data"); - } - Log.info("Saved on exit."); - })); - Vars.netServer.assigner = function (player, players) { - var _a; - if (Vars.state.rules.pvp) { - //find team with minimum amount of players and auto-assign player to that. - var fishP = players_1.FishPlayer.get(player); - var preferredTeam_1 = null; - if (fishP.restoreTeam && (Date.now() - fishP.restoreTeam[1] < funcs_1.Duration.minutes(5)) && fishP.restoreTeam[2] == ((_a = maps_1.PartialMapRun.current) === null || _a === void 0 ? void 0 : _a.startTime)) - preferredTeam_1 = fishP.restoreTeam[0]; - var re = Vars.state.teams.getActive().select(function (data) { return !((Vars.state.rules.waveTeam == data.team && Vars.state.rules.waves) || - !data.hasCore() || - data.team == Team.derelict || - !data.team.rules().protectCores); }).min(floatf(function (data) { - //Only if the team is valid - if (data.team == preferredTeam_1) - return -1; - var count = 0; - players.forEach(function (other) { - if (other.team() == data.team && other != player) { - count++; - } - }); - return count + Mathf.random(-0.1, 0.1); - })); - return re == null ? Vars.state.rules.defaultTeam : re.team; - } - else { - return Vars.state.rules.defaultTeam; - } - }; - Log.info("fish-commands: initialized in @ms (incl previous)", Time.elapsed()); -}); -// Keeps track of any action performed on a tile for use in tilelog. -Events.on(EventType.BlockBuildBeginEvent, utils_1.tilelogAndResetAfk); -Events.on(EventType.BuildRotateEvent, utils_1.tilelogAndResetAfk); -Events.on(EventType.ConfigEvent, utils_1.tilelogAndResetAfk); -Events.on(EventType.PickupEvent, utils_1.tilelogAndResetAfk); -Events.on(EventType.PayloadDropEvent, utils_1.tilelogAndResetAfk); -Events.on(EventType.UnitDestroyEvent, utils_1.addToTileHistory); -Events.on(EventType.BlockDestroyEvent, utils_1.addToTileHistory); -Events.on(EventType.UnitControlEvent, utils_1.tilelogAndResetAfk); -Events.on(EventType.TapEvent, commands_1.handleTapEvent); -Events.on(EventType.GameOverEvent, function (e) { - var e_1, _a; - try { - for (var _b = __values(Object.keys(globals_1.tileHistory)), _c = _b.next(); !_c.done; _c = _b.next()) { - var key = _c.value; - //clear tilelog - globals_1.tileHistory[key] = null; - delete globals_1.tileHistory[key]; - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_1) throw e_1.error; } - } - if (globals_1.fishState.restartQueued) { - //restart - Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 15 seconds.[]\n[accent]---[[[coral]+++[]]---"); - (0, utils_1.serverRestartLoop)(12, true); - Events.on(EventType.WorldLoadBeginEvent, function () { - //Remove save - (0, utils_1.restartNow)(true); - }); - } - players_1.FishPlayer.onGameOver(e.winner); -}); -Events.on(EventType.WorldLoadEvent, function () { return players_1.FishPlayer.onGameBegin(); }); -Events.on(EventType.PlayerChatEvent, function (e) { - players_1.FishPlayer.onPlayerChat(e.player, e.message); -}); -Events.on(EventType.PlayEvent, function () { - globals_1.fishState.startTime = Date.now(); -}); -Log.info("fish-commands: parsing done in @ms", Date.now() - this._startTime); +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the main code, which calls other functions and initializes the plugin. +*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var api = __importStar(require("/api")); +var aggregate_1 = require("/commands/aggregate"); +var config_1 = require("/config"); +var commands_1 = require("/frameworks/commands"); +var menus = __importStar(require("/frameworks/menus")); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var maps_1 = require("/maps"); +var packetHandlers_1 = require("/packetHandlers"); +var players_1 = require("/players"); +var timers = __importStar(require("/timers")); +var utils_1 = require("/utils"); +Events.on(EventType.ConnectionEvent, function (e) { + if (Vars.netServer.admins.bannedIPs.contains(e.connection.address)) { + api.getBanned({ + ip: e.connection.address, + }, function (banned) { + if (!banned) { + //If they were previously banned locally, but the API says they aren't banned, then unban them and clear the kick that the outer function already did + Vars.netServer.admins.unbanPlayerIP(e.connection.address); + Vars.netServer.admins.kickedIPs.remove(e.connection.address); + } + }); + } + else if (api.isVpnCached(e.connection.address) && players_1.FishPlayer.shouldWhackFlaggedPlayers()) { + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kick("You have been DOSblacklisted. Please join our discord for help: " + config_1.text.discordURL + "\nYou won't see this message again."); + Log.info("&yAntibot killed connection ".concat(e.connection.address, " due to flagged while under attack")); + } +}); +Events.on(EventType.PlayerConnect, function (e) { + if (players_1.FishPlayer.shouldKickNewPlayers() && e.player.info.timesJoined == 1) { + //do not use the helper function, for maximum performance + e.player.kick(Packets.KickReason.kick, 3600000); + } + players_1.FishPlayer.onPlayerConnect(e.player); +}); +Events.on(EventType.PlayerJoin, function (e) { + players_1.FishPlayer.onPlayerJoin(e.player); +}); +Events.on(EventType.PlayerLeave, function (e) { + players_1.FishPlayer.onPlayerLeave(e.player); +}); +Events.on(EventType.ConnectPacketEvent, function (e) { + if (!players_1.FishPlayer.connectRate.allow(5000, 35)) { + players_1.FishPlayer.triggerAntibot(300000, "Rate of player connections exceeded 35 / 5s", "automatic"); + } + globals_1.ipJoins.increment(e.connection.address); + var info = Vars.netServer.admins.getInfoOptional(e.packet.uuid); + var underAttack = players_1.FishPlayer.antiBotMode(); + var newPlayer = !info || info.timesJoined < 10; + var longModName = e.packet.mods.contains(function (str) { return str.length > 50; }); + var veryLongModName = e.packet.mods.contains(function (str) { return str.length > 100; }); + if ((underAttack && e.packet.mods.size > 2) || + (underAttack && longModName) || + (veryLongModName && (underAttack || newPlayer))) { + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + players_1.FishPlayer.triggerAntibot(60000, (veryLongModName ? "very long mod name" : longModName ? "long mod name" : "it had mods while under attack"), "automatic"); + return; + } + var suspiciousModName = e.packet.mods.contains(function (str) { return str.includes('\x1B'); }); + if (suspiciousModName || e.packet.name.includes('\x1B')) { + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + players_1.FishPlayer.triggerAntibot(5000, "illegal characters in name or mods", "automatic"); + return; + } + if (globals_1.ipJoins.get(e.connection.address) >= ((underAttack || veryLongModName) ? 3 : (newPlayer || longModName) ? 7 : 15)) { + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + players_1.FishPlayer.triggerAntibot(5000, "too many connections", "automatic"); + return; + } + /*if(e.packet.name.includes("discord.gg/GnEdS9TdV6")){ + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + FishPlayer.onBotWhack(); + Log.info(`&yAntibot killed connection ${e.connection.address} due to omni discord link`); + return; + }*/ + if (e.packet.name.includes("1`1@everyone")) { + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + players_1.FishPlayer.triggerAntibot(-1, "known bad name", "automatic"); + return; + } + if (Vars.netServer.admins.isDosBlacklisted(e.connection.address)) { + //threading moment, i think + e.connection.kicked = true; + return; + } + api.getBanned({ + ip: e.connection.address, + uuid: e.packet.uuid + }, function (banned) { + if (banned) { + Log.info("&lrSynced ban of ".concat(e.packet.uuid, "/").concat(e.connection.address, ".")); + e.connection.kick(Packets.KickReason.banned, 1); + Vars.netServer.admins.banPlayerIP(e.connection.address); + Vars.netServer.admins.banPlayerID(e.packet.uuid); + } + else { + Vars.netServer.admins.unbanPlayerIP(e.connection.address); + Vars.netServer.admins.unbanPlayerID(e.packet.uuid); + } + }); + players_1.FishPlayer.onConnectPacket(e.packet); +}); +Events.on(EventType.UnitChangeEvent, function (e) { + players_1.FishPlayer.onUnitChange(e.player, e.unit); +}); +Events.on(EventType.ContentInitEvent, function () { + //Unhide latum and renale + UnitTypes.latum.hidden = false; + UnitTypes.renale.hidden = false; +}); +Events.on(EventType.PlayerChatEvent, function (e) { return (0, utils_1.processChat)(e.player, e.message, true); }); +Events.on(EventType.ServerLoadEvent, function (e) { + Time.mark(); + var clientHandler = Vars.netServer.clientCommands; + var serverHandler = ServerControl.instance.handler; + players_1.FishPlayer.loadAll(); + globals_1.FishEvents.fire("loadData", []); + timers.initializeTimers(); + menus.registerListeners(); + //Cap delta + Time.setDeltaProvider(function () { return Math.min(Core.graphics.getDeltaTime() * 60, 10); }); + // Mute muted players + Vars.netServer.admins.addChatFilter(function (player, message) { return (0, utils_1.processChat)(player, message); }); + // Vars.netServer.admins.addChatFilter((p, message) => FishPlayer.get(p).hasPerm("member") ? message : foolifyChat(message)); + // Action filters + Vars.netServer.admins.addActionFilter(function (action) { + var _a, _b, _c; + var player = action.player; + var fishP = players_1.FishPlayer.get(player); + //prevent stopped players from doing anything other than deposit items. + if (!fishP.hasPerm("play")) { + action.player.sendMessage('[scarlet]\u26A0 [yellow]You are stopped, you cant perfom this action.'); + return false; + } + else { + if (action.type === Administration.ActionType.pickupBlock) { + (0, utils_1.addToTileHistory)({ + pos: "".concat(action.tile.x, ",").concat(action.tile.y), + uuid: action.player.uuid(), + action: "picked up", + type: (_b = (_a = action.tile.block()) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : "nothing", + }); + } + else if (action.type === Administration.ActionType.control && !((_c = action.unit) === null || _c === void 0 ? void 0 : _c.spawnedByCore) && Date.now() < fishP.blockedFromPossessingUnitsUntil) { + action.player.sendMessage("[scarlet]\u26A0 [yellow]You are blocked from controlling units for ".concat((0, utils_1.formatTimeRelative)(fishP.blockedFromPossessingUnitsUntil, true))); + return false; + } + else if (action.type === Administration.ActionType.commandUnits && Date.now() < fishP.blockedFromCommandingUnitsUntil) { + action.player.sendMessage("[scarlet]\u26A0 [yellow]You are blocked from commanding units for ".concat((0, utils_1.formatTimeRelative)(fishP.blockedFromCommandingUnitsUntil, true))); + return false; + } + else if (action.type === Administration.ActionType.pingLocation && action.pingText && action.pingText.length < Vars.maxPingTextLength) { + var fishP_1 = players_1.FishPlayer.get(action.player); + if (fishP_1.muted) { + action.player.sendMessage("[scarlet]\u26A0 [yellow]You are muted, you cannot send text through location pings."); + return false; + } + else if ((0, utils_1.matchFilter)(action.pingText, "chat", false)) { + //Allow it, but replace + player.pingX = action.pingX; + player.pingY = action.pingY; + player.pingTime = 1; + player.pingText = config_1.text.chatFilterReplacement.messageShort(); + return false; + } + } + return true; + } + }); + (0, aggregate_1.registerAll)(clientHandler, serverHandler); + (0, packetHandlers_1.loadPacketHandlers)(); + //Load plugin data + try { + var path = (0, utils_1.fishCommandsRootDirPath)(); + globals_1.fishPlugin.directory = path.toString(); + Threads.daemon(function () { + try { + globals_1.fishPlugin.version = OS.exec("git", "-C", globals_1.fishPlugin.directory, "rev-parse", "HEAD"); + } + catch (_a) { } + }); + } + catch (err) { + Log.err("Failed to get fish plugin information."); + Log.err(err); + } + Runtime.getRuntime().addShutdownHook(new Thread(function () { + try { + players_1.FishPlayer.uploadAll(); + } + catch (_a) { + Log.err("failed to upload"); + } + try { + globals_1.FishEvents.fire("saveData", []); + } + catch (_b) { + Log.err("failed to save misc data"); + } + try { + players_1.FishPlayer.saveAll(false); + } + catch (_c) { + Log.err("failed to save player data"); + } + Log.info("Saved on exit."); + })); + Vars.netServer.assigner = function (player, players) { + var _a; + if (Vars.state.rules.pvp) { + //find team with minimum amount of players and auto-assign player to that. + var fishP = players_1.FishPlayer.get(player); + var preferredTeam_1 = null; + if (fishP.restoreTeam && (Date.now() - fishP.restoreTeam[1] < funcs_1.Duration.minutes(5)) && fishP.restoreTeam[2] == ((_a = maps_1.PartialMapRun.current) === null || _a === void 0 ? void 0 : _a.startTime)) + preferredTeam_1 = fishP.restoreTeam[0]; + var re = Vars.state.teams.getActive().select(function (data) { return !((Vars.state.rules.waveTeam == data.team && Vars.state.rules.waves) || + !data.hasCore() || + data.team == Team.derelict || + !data.team.rules().protectCores); }).min(floatf(function (data) { + //Only if the team is valid + if (data.team == preferredTeam_1) + return -1; + var count = 0; + players.forEach(function (other) { + if (other.team() == data.team && other != player) { + count++; + } + }); + return count + Mathf.random(-0.1, 0.1); + })); + return re == null ? Vars.state.rules.defaultTeam : re.team; + } + else { + return Vars.state.rules.defaultTeam; + } + }; + Log.info("fish-commands: initialized in @ms (incl previous)", Time.elapsed()); +}); +// Keeps track of any action performed on a tile for use in tilelog. +Events.on(EventType.BlockBuildBeginEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.BuildRotateEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.ConfigEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.PickupEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.PayloadDropEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.UnitDestroyEvent, utils_1.addToTileHistory); +Events.on(EventType.BlockDestroyEvent, utils_1.addToTileHistory); +Events.on(EventType.UnitControlEvent, utils_1.tilelogAndResetAfk); +Events.on(EventType.TapEvent, commands_1.handleTapEvent); +Events.on(EventType.GameOverEvent, function (e) { + var e_1, _a; + try { + for (var _b = __values(Object.keys(globals_1.tileHistory)), _c = _b.next(); !_c.done; _c = _b.next()) { + var key = _c.value; + //clear tilelog + globals_1.tileHistory[key] = null; + delete globals_1.tileHistory[key]; + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_1) throw e_1.error; } + } + if (globals_1.fishState.restartQueued) { + //restart + Call.sendMessage("[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 15 seconds.[]\n[accent]---[[[coral]+++[]]---"); + (0, utils_1.serverRestartLoop)(12, true); + Events.on(EventType.WorldLoadBeginEvent, function () { + //Remove save + (0, utils_1.restartNow)(true); + }); + } + players_1.FishPlayer.onGameOver(e.winner); +}); +Events.on(EventType.WorldLoadEvent, function () { return players_1.FishPlayer.onGameBegin(); }); +Events.on(EventType.PlayerChatEvent, function (e) { + players_1.FishPlayer.onPlayerChat(e.player, e.message); +}); +Events.on(EventType.PlayEvent, function () { + globals_1.fishState.startTime = Date.now(); +}); +Log.info("fish-commands: parsing done in @ms", Date.now() - this._startTime); diff --git a/build/scripts/maps.js b/build/scripts/maps.js index 3cc33473..b9f770de 100644 --- a/build/scripts/maps.js +++ b/build/scripts/maps.js @@ -1,361 +1,361 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the map run tracker and statistics computation. -*/ -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _, done = false; - for (var i = decorators.length - 1; i >= 0; i--) { - var context = {}; - for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; - for (var p in contextIn.access) context.access[p] = contextIn.access[p]; - context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; - var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_ = accept(result.get)) descriptor.get = _; - if (_ = accept(result.set)) descriptor.set = _; - if (_ = accept(result.init)) initializers.unshift(_); - } - else if (_ = accept(result)) { - if (kind === "field") initializers.unshift(_); - else descriptor[key] = _; - } - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i = 0; i < initializers.length; i++) { - value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); - } - return useValue ? value : void 0; -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var _this = this; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.FMap = exports.PartialMapRun = exports.FinishedMapRun = void 0; -var config_1 = require("/config"); -var io_1 = require("/frameworks/io"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var utils_1 = require("/utils"); -var FinishedMapRun = /** @class */ (function (_super) { - __extends(FinishedMapRun, _super); - //this constructor is useless, but rhino crashes with a bizarre error when trying to run the emitted code - //do not remove this useless constructor - function FinishedMapRun(data) { - return _super.call(this, data) || this; - } - FinishedMapRun.prototype.duration = function () { - return this.endTime - this.startTime; - }; - FinishedMapRun.prototype.outcome = function () { - if (config_1.Gamemode.pvp()) { - if (this.winTeam === Team.derelict) { - if (this.duration() > funcs_1.Duration.minutes(20)) - return ["rtv", "late rtv"]; - else - return ["rtv", "early rtv"]; - } - else - return ["win", "win"]; - } - else { - if (this.success) - return ["win", "win"]; - else if (this.winTeam === Team.derelict) { - if (this.duration() > funcs_1.Duration.minutes(3)) - return ["loss", "late rtv"]; - else - return ["rtv", "early rtv"]; - } - else - return ["loss", "loss"]; - } - }; - return FinishedMapRun; -}((0, io_1.dataClass)())); -exports.FinishedMapRun = FinishedMapRun; -var PartialMapRun = /** @class */ (function () { - function PartialMapRun() { - this.startTime = Date.now(); - this.maxPlayerCount = 0; - } - /** In milliseconds */ - PartialMapRun.prototype.duration = function () { - return Date.now() - this.startTime; - }; - PartialMapRun.prototype.update = function () { - this.maxPlayerCount = Math.max(this.maxPlayerCount, Groups.player.size()); - }; - PartialMapRun.prototype.finish = function (_b) { - var winTeam = _b.winTeam; - return new FinishedMapRun({ - winTeam: winTeam, - success: config_1.Gamemode.pvp() ? true : winTeam == Vars.state.rules.defaultTeam, - startTime: this.startTime, - endTime: Date.now(), - maxPlayerCount: this.maxPlayerCount, - wave: Vars.state.wave, - }); - }; - //Used for continuing through a restart - PartialMapRun.prototype.write = function () { - return "".concat(Date.now() - this.startTime, "/").concat(this.maxPlayerCount); - }; - PartialMapRun.read = function (data) { - var _b = __read(data.split("/").map(Number), 2), duration = _b[0], maxPlayerCount = _b[1]; - if (isNaN(duration) || isNaN(maxPlayerCount)) { - Log.err("_FINDTAG_ failed to load map run stats data: ".concat(data)); - } - var out = new _a(); - out.startTime = Date.now() - duration; //move start time forward by time when the server was off - out.maxPlayerCount = maxPlayerCount; - return out; - }; - var _a; - _a = PartialMapRun; - PartialMapRun.key = "fish-partial-map-run"; - PartialMapRun.current = null; - (function () { - globals_1.FishEvents.on("saveData", function () { - if (_a.current) - Core.settings.put(_a.key, _a.current.write()); - }); - globals_1.FishEvents.on("loadData", function () { - var data = Core.settings.getString(_a.key); - if (data) { - _a.current = _a.read(data); - } - else { - //loading a map, but there is no run information, create one - _a.current = new _a(); - } - }); - Events.on(EventType.SaveLoadEvent, function (e) { - var _b; - //create a new run, if there isn't one already - //loadData will have run first if it is a server restart - (_b = _a.current) !== null && _b !== void 0 ? _b : (_a.current = new _a()); - }); - Timer.schedule(function () { - var _b; - (_b = _a.current) === null || _b === void 0 ? void 0 : _b.update(); - }, 0, 5); - Events.on(EventType.GameOverEvent, function (e) { - var _b; - if (_a.current) { - var finishedRun = _a.current.finish({ winTeam: (_b = e.winner) !== null && _b !== void 0 ? _b : Team.derelict }); - var fmap = FMap.getCreate(Vars.state.map); - if (!fmap) - return; - //Highscore message - if (config_1.Gamemode.attack() && finishedRun.success) { - var bestPreviousTime = fmap.stats().shortestWinTime; - var duration = finishedRun.duration(); - Call.sendMessage("[orange]--------\n".concat(finishedRun.success && duration < bestPreviousTime ? - "[green]New highscore! Map completed in [accent]".concat((0, utils_1.formatTimeShort)(duration), "[]") - : "[orange]Map completed in [accent]".concat((0, utils_1.formatTimeShort)(duration), "[]. Current highscore: [green]").concat((0, utils_1.formatTimeShort)(bestPreviousTime), "[]"), "\n[orange]--------")); - } - else if (config_1.Gamemode.survival()) { - var bestPreviousWave = fmap.stats().highestWave; - var wave = finishedRun.wave; - Call.sendMessage("[orange]--------\n".concat(finishedRun.success && wave < bestPreviousWave ? - "[green]New highscore! Reached wave [accent]".concat(wave, "[].") - : "[orange]Reached wave [accent]".concat(wave, "[]. Current highscore: [green]").concat(bestPreviousWave, "[]"), "\n[orange]--------")); - } - fmap.runs.push(finishedRun); - globals_1.FishEvents.fire("saveMaps", []); - } - Core.settings.remove(_a.key); - _a.current = null; - }); - })(); - return PartialMapRun; -}()); -exports.PartialMapRun = PartialMapRun; -var FMap = function () { - var _b; - var _classSuper = (0, io_1.dataClass)(); - var _static_allMaps_decorators; - var _static_allMaps_initializers = []; - var _static_allMaps_extraInitializers = []; - return _b = /** @class */ (function (_super) { - __extends(FMap, _super); - function FMap(data, - //O(n^2)... should be fine? - map) { - if (map === void 0) { map = Vars.maps.customMaps().find(function (m) { return m.file.name() === data.mapFileName; }); } - var _this = _super.call(this, data) || this; - _this.map = map; - return _this; - } - FMap.getCreate = function (map) { - if (this.allMaps == null) - return null; - var mapFileName = map.file.name(); - if (Object.prototype.hasOwnProperty.call(this.maps, mapFileName)) - return this.maps[mapFileName]; - var fmap = new this({ - runs: [], - mapFileName: mapFileName - }, map); - this.maps[mapFileName] = fmap; - this.allMaps.push(fmap); - return fmap; - }; - FMap.prototype.rules = function () { - var _c; - return (_c = this.map) === null || _c === void 0 ? void 0 : _c.rules(); - }; - FMap.prototype.stats = function () { - var _c; - var runs = this.runs.filter(function (r) { return r.maxPlayerCount > 0; }); //Remove all runs with no players on - var allRunCount = runs.length; - var victories = runs.filter(function (r) { return r.outcome()[1] === "win"; }); - var losses = runs.filter(function (r) { return r.outcome()[0] === "loss"; }).length; - var earlyRTVs = runs.filter(function (r) { return r.outcome()[1] === "early rtv"; }).length; - var lateRTVs = runs.filter(function (r) { return r.outcome()[1] === "late rtv"; }).length; - var significantRunCount = allRunCount - earlyRTVs; - var totalLosses = losses + lateRTVs; - var durations = runs.filter(function (r) { return r.outcome()[0] !== "rtv"; }).map(function (r) { return r.duration(); }); - var durationStats = (0, funcs_1.computeStatistics)(durations); - var winDurationStats = (0, funcs_1.computeStatistics)(runs.filter(function (r) { return r.outcome()[0] === "win"; }).map(function (r) { return r.duration(); })); - var teamWins = runs.filter(function (r) { return r.outcome()[1] !== "early rtv"; }).reduce(function (acc, item) { - var _c; - acc[item.winTeam.name] = ((_c = acc[item.winTeam.name]) !== null && _c !== void 0 ? _c : 0) + 1; - return acc; - }, {}); - var teamWinRate = Object.fromEntries(Object.entries(teamWins).map(function (_c) { - var _d = __read(_c, 2), team = _d[0], wins = _d[1]; - return [team, wins / significantRunCount]; - })); - //Remove runs that were on wave 0, due to a silly bug we have thousands of runs with a max wave of 0 - var waveStats = (0, funcs_1.computeStatistics)(runs.filter(function (r) { return r.outcome()[0] !== "rtv" && r.wave !== 0; }).map(function (r) { return r.wave; })); - return { - allRunCount: allRunCount, - significantRunCount: significantRunCount, - victories: victories.length, - losses: losses, - totalLosses: totalLosses, - earlyRTVs: earlyRTVs, - lateRTVs: lateRTVs, - earlyRTVRate: earlyRTVs / allRunCount, - winRate: victories.length / significantRunCount, - lossRate: losses / significantRunCount, - averagePlaytime: durationStats.average, - shortestWinTime: winDurationStats.lowest, - longestTime: durationStats.highest, - shortestTime: durationStats.lowest, - averageHighestPlayerCount: (0, funcs_1.computeStatistics)(runs.map(function (r) { return r.maxPlayerCount; })).average, - teamWins: teamWins, - teamWinRate: teamWinRate, - highestWave: waveStats.highest, - averageWave: waveStats.average, - mostRecentWin: (_c = victories.at(-1)) === null || _c === void 0 ? void 0 : _c.startTime - }; - }; - FMap.prototype.displayStats = function (f) { - var map = this.map; - if (!map) - return null; - var stats = this.stats(); - var rules = this.rules(); - var modeSpecificStats = (0, utils_1.match)(config_1.Gamemode.name(), { - attack: "[#CCFFCC]Total runs: ".concat(stats.allRunCount, " (").concat(stats.victories, " wins, ").concat(stats.totalLosses, " losses, ").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]Outcomes: ").concat(f.percent(stats.winRate, 1), " wins, ").concat(f.percent(stats.lossRate, 1), " losses, ").concat(f.percent(stats.earlyRTVRate, 1), " RTVs\n[#CCFFCC]Average playtime: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest win time: ").concat((0, utils_1.formatTime)(stats.shortestWinTime), "\n[#CCFFCC]Most recent win: ").concat(stats.mostRecentWin ? (0, utils_1.formatTimestamp)(stats.mostRecentWin) : "[red]none[]"), - survival: "[#CCFFCC]Highest wave reached: ".concat(stats.highestWave, "\n[#CCFFCC]Average wave reached: ").concat(stats.averageWave, "\n[#CCFFCC]Total runs: ").concat(stats.allRunCount, " (").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]RTV rate: ").concat(f.percent(stats.earlyRTVRate, 1), "\n[#CCFFCC]Average duration: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Longest duration: ").concat((0, utils_1.formatTime)(stats.longestTime)), - pvp: "[#CCFFCC]Total runs: ".concat(stats.allRunCount, " (").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]Team win rates: ").concat(Object.entries(stats.teamWinRate).map(function (_c) { - var _d = __read(_c, 2), team = _d[0], rate = _d[1]; - return "".concat(team, " ").concat(f.percent(rate, 1)); - }).join(", "), "\n[#CCFFCC]RTV rate: ").concat(f.percent(stats.earlyRTVRate, 1), "\n[#CCFFCC]Average match duration: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest match duration: ").concat((0, utils_1.formatTime)(stats.shortestWinTime)), - hexed: "[#CCFFCC]Total runs: ".concat(stats.allRunCount, " (").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]RTV rate: ").concat(f.percent(stats.earlyRTVRate, 1), "\n[#CCFFCC]Average match duration: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest match duration: ").concat((0, utils_1.formatTime)(stats.shortestWinTime)), - sandbox: "[#CCFFCC]Total plays: ".concat(stats.allRunCount, "\n[#CCFFCC]Average play time: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest play time: ").concat((0, utils_1.formatTime)(stats.shortestTime)), - }, ""); - return ("[coral]".concat(map.name(), "\n[gray](").concat(map.file.name(), ")\n\n[accent]Map by: [white]").concat(map.author(), "\n[accent]Description: [white]").concat(map.description(), "\n[accent]Size: [white]").concat(map.width, "x").concat(map.height, "\n[accent]Last updated: [white]").concat(new Date(map.file.lastModified()).toLocaleDateString(), "\n[accent]BvB allowed: ").concat(f.boolGood(rules.placeRangeCheck), ", unit item transfer allowed: ").concat(f.boolGood(rules.onlyDepositCore), "\n\n").concat(modeSpecificStats, "\n[#CCFFCC]Longest play time: ").concat((0, utils_1.formatTime)(stats.longestTime), "\n[#CCFFCC]Average player count: ").concat(f.number(stats.averageHighestPlayerCount, 1))); - }; - return FMap; - }(_classSuper)), - (function () { - var _c; - var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_c = _classSuper[Symbol.metadata]) !== null && _c !== void 0 ? _c : null) : void 0; - _static_allMaps_decorators = [(0, io_1.serialize)("fish-map-data", function () { return ["version", 1, ["array", "u16", ["class", FMap, [ - ["runs", ["array", "u32", ["class", FinishedMapRun, [ - ["startTime", ["number", "i64"]], - ["endTime", ["number", "i64"]], - ["maxPlayerCount", ["number", "u8"]], - ["success", ["boolean"]], - ["winTeam", ["team"]], - ["wave", ["number", "u16"]] - ]]]], - ["mapFileName", ["string"]], - ]]]]; }, function () { return ["array", "u16", ["class", FMap, [ - ["runs", ["array", "u32", ["class", FinishedMapRun, [ - ["startTime", ["number", "i64"]], - ["endTime", ["number", "i64"]], - ["maxPlayerCount", ["number", "u8"]], - ["success", ["boolean"]], - ["winTeam", ["team"]], - ]]]], - ["mapFileName", ["string"]], - ]]]; }, undefined, "saveMaps")]; - __esDecorate(null, null, _static_allMaps_decorators, { kind: "field", name: "allMaps", static: true, private: false, access: { has: function (obj) { return "allMaps" in obj; }, get: function (obj) { return obj.allMaps; }, set: function (obj, value) { obj.allMaps = value; } }, metadata: _metadata }, _static_allMaps_initializers, _static_allMaps_extraInitializers); - if (_metadata) Object.defineProperty(_b, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); - })(), - _b.allMaps = __runInitializers(_b, _static_allMaps_initializers, null), - _b.maps = (__runInitializers(_b, _static_allMaps_extraInitializers), {}), - (function () { - globals_1.FishEvents.on("dataLoaded", function () { - var _c; - //This event listener runs after the data has been loaded into allMaps - ((_c = _b.allMaps) !== null && _c !== void 0 ? _c : (_b.allMaps = [])).forEach(function (map) { - _b.maps[map.mapFileName] = map; - map.runs.forEach(function (run) { - var _c; - //this should not even happen, I think GameOverEvent is sending winTeam as null sometimes?? - (_c = run.winTeam) !== null && _c !== void 0 ? _c : (run.winTeam = Team.derelict); - }); - }); - //create all the data - Vars.maps.customMaps().each(function (m) { return void _b.getCreate(m); }); - }); - })(), - _b; -}(); -exports.FMap = FMap; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the map run tracker and statistics computation. +*/ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var _this = this; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FMap = exports.PartialMapRun = exports.FinishedMapRun = void 0; +var config_1 = require("/config"); +var io_1 = require("/frameworks/io"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var utils_1 = require("/utils"); +var FinishedMapRun = /** @class */ (function (_super) { + __extends(FinishedMapRun, _super); + //this constructor is useless, but rhino crashes with a bizarre error when trying to run the emitted code + //do not remove this useless constructor + function FinishedMapRun(data) { + return _super.call(this, data) || this; + } + FinishedMapRun.prototype.duration = function () { + return this.endTime - this.startTime; + }; + FinishedMapRun.prototype.outcome = function () { + if (config_1.Gamemode.pvp()) { + if (this.winTeam === Team.derelict) { + if (this.duration() > funcs_1.Duration.minutes(20)) + return ["rtv", "late rtv"]; + else + return ["rtv", "early rtv"]; + } + else + return ["win", "win"]; + } + else { + if (this.success) + return ["win", "win"]; + else if (this.winTeam === Team.derelict) { + if (this.duration() > funcs_1.Duration.minutes(3)) + return ["loss", "late rtv"]; + else + return ["rtv", "early rtv"]; + } + else + return ["loss", "loss"]; + } + }; + return FinishedMapRun; +}((0, io_1.dataClass)())); +exports.FinishedMapRun = FinishedMapRun; +var PartialMapRun = /** @class */ (function () { + function PartialMapRun() { + this.startTime = Date.now(); + this.maxPlayerCount = 0; + } + /** In milliseconds */ + PartialMapRun.prototype.duration = function () { + return Date.now() - this.startTime; + }; + PartialMapRun.prototype.update = function () { + this.maxPlayerCount = Math.max(this.maxPlayerCount, Groups.player.size()); + }; + PartialMapRun.prototype.finish = function (_b) { + var winTeam = _b.winTeam; + return new FinishedMapRun({ + winTeam: winTeam, + success: config_1.Gamemode.pvp() ? true : winTeam == Vars.state.rules.defaultTeam, + startTime: this.startTime, + endTime: Date.now(), + maxPlayerCount: this.maxPlayerCount, + wave: Vars.state.wave, + }); + }; + //Used for continuing through a restart + PartialMapRun.prototype.write = function () { + return "".concat(Date.now() - this.startTime, "/").concat(this.maxPlayerCount); + }; + PartialMapRun.read = function (data) { + var _b = __read(data.split("/").map(Number), 2), duration = _b[0], maxPlayerCount = _b[1]; + if (isNaN(duration) || isNaN(maxPlayerCount)) { + Log.err("_FINDTAG_ failed to load map run stats data: ".concat(data)); + } + var out = new _a(); + out.startTime = Date.now() - duration; //move start time forward by time when the server was off + out.maxPlayerCount = maxPlayerCount; + return out; + }; + var _a; + _a = PartialMapRun; + PartialMapRun.key = "fish-partial-map-run"; + PartialMapRun.current = null; + (function () { + globals_1.FishEvents.on("saveData", function () { + if (_a.current) + Core.settings.put(_a.key, _a.current.write()); + }); + globals_1.FishEvents.on("loadData", function () { + var data = Core.settings.getString(_a.key); + if (data) { + _a.current = _a.read(data); + } + else { + //loading a map, but there is no run information, create one + _a.current = new _a(); + } + }); + Events.on(EventType.SaveLoadEvent, function (e) { + var _b; + //create a new run, if there isn't one already + //loadData will have run first if it is a server restart + (_b = _a.current) !== null && _b !== void 0 ? _b : (_a.current = new _a()); + }); + Timer.schedule(function () { + var _b; + (_b = _a.current) === null || _b === void 0 ? void 0 : _b.update(); + }, 0, 5); + Events.on(EventType.GameOverEvent, function (e) { + var _b; + if (_a.current) { + var finishedRun = _a.current.finish({ winTeam: (_b = e.winner) !== null && _b !== void 0 ? _b : Team.derelict }); + var fmap = FMap.getCreate(Vars.state.map); + if (!fmap) + return; + //Highscore message + if (config_1.Gamemode.attack() && finishedRun.success) { + var bestPreviousTime = fmap.stats().shortestWinTime; + var duration = finishedRun.duration(); + Call.sendMessage("[orange]--------\n".concat(finishedRun.success && duration < bestPreviousTime ? + "[green]New highscore! Map completed in [accent]".concat((0, utils_1.formatTimeShort)(duration), "[]") + : "[orange]Map completed in [accent]".concat((0, utils_1.formatTimeShort)(duration), "[]. Current highscore: [green]").concat((0, utils_1.formatTimeShort)(bestPreviousTime), "[]"), "\n[orange]--------")); + } + else if (config_1.Gamemode.survival()) { + var bestPreviousWave = fmap.stats().highestWave; + var wave = finishedRun.wave; + Call.sendMessage("[orange]--------\n".concat(finishedRun.success && wave < bestPreviousWave ? + "[green]New highscore! Reached wave [accent]".concat(wave, "[].") + : "[orange]Reached wave [accent]".concat(wave, "[]. Current highscore: [green]").concat(bestPreviousWave, "[]"), "\n[orange]--------")); + } + fmap.runs.push(finishedRun); + globals_1.FishEvents.fire("saveMaps", []); + } + Core.settings.remove(_a.key); + _a.current = null; + }); + })(); + return PartialMapRun; +}()); +exports.PartialMapRun = PartialMapRun; +var FMap = function () { + var _b; + var _classSuper = (0, io_1.dataClass)(); + var _static_allMaps_decorators; + var _static_allMaps_initializers = []; + var _static_allMaps_extraInitializers = []; + return _b = /** @class */ (function (_super) { + __extends(FMap, _super); + function FMap(data, + //O(n^2)... should be fine? + map) { + if (map === void 0) { map = Vars.maps.customMaps().find(function (m) { return m.file.name() === data.mapFileName; }); } + var _this = _super.call(this, data) || this; + _this.map = map; + return _this; + } + FMap.getCreate = function (map) { + if (this.allMaps == null) + return null; + var mapFileName = map.file.name(); + if (Object.prototype.hasOwnProperty.call(this.maps, mapFileName)) + return this.maps[mapFileName]; + var fmap = new this({ + runs: [], + mapFileName: mapFileName + }, map); + this.maps[mapFileName] = fmap; + this.allMaps.push(fmap); + return fmap; + }; + FMap.prototype.rules = function () { + var _c; + return (_c = this.map) === null || _c === void 0 ? void 0 : _c.rules(); + }; + FMap.prototype.stats = function () { + var _c; + var runs = this.runs.filter(function (r) { return r.maxPlayerCount > 0; }); //Remove all runs with no players on + var allRunCount = runs.length; + var victories = runs.filter(function (r) { return r.outcome()[1] === "win"; }); + var losses = runs.filter(function (r) { return r.outcome()[0] === "loss"; }).length; + var earlyRTVs = runs.filter(function (r) { return r.outcome()[1] === "early rtv"; }).length; + var lateRTVs = runs.filter(function (r) { return r.outcome()[1] === "late rtv"; }).length; + var significantRunCount = allRunCount - earlyRTVs; + var totalLosses = losses + lateRTVs; + var durations = runs.filter(function (r) { return r.outcome()[0] !== "rtv"; }).map(function (r) { return r.duration(); }); + var durationStats = (0, funcs_1.computeStatistics)(durations); + var winDurationStats = (0, funcs_1.computeStatistics)(runs.filter(function (r) { return r.outcome()[0] === "win"; }).map(function (r) { return r.duration(); })); + var teamWins = runs.filter(function (r) { return r.outcome()[1] !== "early rtv"; }).reduce(function (acc, item) { + var _c; + acc[item.winTeam.name] = ((_c = acc[item.winTeam.name]) !== null && _c !== void 0 ? _c : 0) + 1; + return acc; + }, {}); + var teamWinRate = Object.fromEntries(Object.entries(teamWins).map(function (_c) { + var _d = __read(_c, 2), team = _d[0], wins = _d[1]; + return [team, wins / significantRunCount]; + })); + //Remove runs that were on wave 0, due to a silly bug we have thousands of runs with a max wave of 0 + var waveStats = (0, funcs_1.computeStatistics)(runs.filter(function (r) { return r.outcome()[0] !== "rtv" && r.wave !== 0; }).map(function (r) { return r.wave; })); + return { + allRunCount: allRunCount, + significantRunCount: significantRunCount, + victories: victories.length, + losses: losses, + totalLosses: totalLosses, + earlyRTVs: earlyRTVs, + lateRTVs: lateRTVs, + earlyRTVRate: earlyRTVs / allRunCount, + winRate: victories.length / significantRunCount, + lossRate: losses / significantRunCount, + averagePlaytime: durationStats.average, + shortestWinTime: winDurationStats.lowest, + longestTime: durationStats.highest, + shortestTime: durationStats.lowest, + averageHighestPlayerCount: (0, funcs_1.computeStatistics)(runs.map(function (r) { return r.maxPlayerCount; })).average, + teamWins: teamWins, + teamWinRate: teamWinRate, + highestWave: waveStats.highest, + averageWave: waveStats.average, + mostRecentWin: (_c = victories.at(-1)) === null || _c === void 0 ? void 0 : _c.startTime + }; + }; + FMap.prototype.displayStats = function (f) { + var map = this.map; + if (!map) + return null; + var stats = this.stats(); + var rules = this.rules(); + var modeSpecificStats = (0, utils_1.match)(config_1.Gamemode.name(), { + attack: "[#CCFFCC]Total runs: ".concat(stats.allRunCount, " (").concat(stats.victories, " wins, ").concat(stats.totalLosses, " losses, ").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]Outcomes: ").concat(f.percent(stats.winRate, 1), " wins, ").concat(f.percent(stats.lossRate, 1), " losses, ").concat(f.percent(stats.earlyRTVRate, 1), " RTVs\n[#CCFFCC]Average playtime: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest win time: ").concat((0, utils_1.formatTime)(stats.shortestWinTime), "\n[#CCFFCC]Most recent win: ").concat(stats.mostRecentWin ? (0, utils_1.formatTimestamp)(stats.mostRecentWin) : "[red]none[]"), + survival: "[#CCFFCC]Highest wave reached: ".concat(stats.highestWave, "\n[#CCFFCC]Average wave reached: ").concat(stats.averageWave, "\n[#CCFFCC]Total runs: ").concat(stats.allRunCount, " (").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]RTV rate: ").concat(f.percent(stats.earlyRTVRate, 1), "\n[#CCFFCC]Average duration: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Longest duration: ").concat((0, utils_1.formatTime)(stats.longestTime)), + pvp: "[#CCFFCC]Total runs: ".concat(stats.allRunCount, " (").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]Team win rates: ").concat(Object.entries(stats.teamWinRate).map(function (_c) { + var _d = __read(_c, 2), team = _d[0], rate = _d[1]; + return "".concat(team, " ").concat(f.percent(rate, 1)); + }).join(", "), "\n[#CCFFCC]RTV rate: ").concat(f.percent(stats.earlyRTVRate, 1), "\n[#CCFFCC]Average match duration: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest match duration: ").concat((0, utils_1.formatTime)(stats.shortestWinTime)), + hexed: "[#CCFFCC]Total runs: ".concat(stats.allRunCount, " (").concat(stats.earlyRTVs, " RTVs)\n[#CCFFCC]RTV rate: ").concat(f.percent(stats.earlyRTVRate, 1), "\n[#CCFFCC]Average match duration: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest match duration: ").concat((0, utils_1.formatTime)(stats.shortestWinTime)), + sandbox: "[#CCFFCC]Total plays: ".concat(stats.allRunCount, "\n[#CCFFCC]Average play time: ").concat((0, utils_1.formatTime)(stats.averagePlaytime), "\n[#CCFFCC]Shortest play time: ").concat((0, utils_1.formatTime)(stats.shortestTime)), + }, ""); + return ("[coral]".concat(map.name(), "\n[gray](").concat(map.file.name(), ")\n\n[accent]Map by: [white]").concat(map.author(), "\n[accent]Description: [white]").concat(map.description(), "\n[accent]Size: [white]").concat(map.width, "x").concat(map.height, "\n[accent]Last updated: [white]").concat(new Date(map.file.lastModified()).toLocaleDateString(), "\n[accent]BvB allowed: ").concat(f.boolGood(rules.placeRangeCheck), ", unit item transfer allowed: ").concat(f.boolGood(rules.onlyDepositCore), "\n\n").concat(modeSpecificStats, "\n[#CCFFCC]Longest play time: ").concat((0, utils_1.formatTime)(stats.longestTime), "\n[#CCFFCC]Average player count: ").concat(f.number(stats.averageHighestPlayerCount, 1))); + }; + return FMap; + }(_classSuper)), + (function () { + var _c; + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_c = _classSuper[Symbol.metadata]) !== null && _c !== void 0 ? _c : null) : void 0; + _static_allMaps_decorators = [(0, io_1.serialize)("fish-map-data", function () { return ["version", 1, ["array", "u16", ["class", FMap, [ + ["runs", ["array", "u32", ["class", FinishedMapRun, [ + ["startTime", ["number", "i64"]], + ["endTime", ["number", "i64"]], + ["maxPlayerCount", ["number", "u8"]], + ["success", ["boolean"]], + ["winTeam", ["team"]], + ["wave", ["number", "u16"]] + ]]]], + ["mapFileName", ["string"]], + ]]]]; }, function () { return ["array", "u16", ["class", FMap, [ + ["runs", ["array", "u32", ["class", FinishedMapRun, [ + ["startTime", ["number", "i64"]], + ["endTime", ["number", "i64"]], + ["maxPlayerCount", ["number", "u8"]], + ["success", ["boolean"]], + ["winTeam", ["team"]], + ]]]], + ["mapFileName", ["string"]], + ]]]; }, undefined, "saveMaps")]; + __esDecorate(null, null, _static_allMaps_decorators, { kind: "field", name: "allMaps", static: true, private: false, access: { has: function (obj) { return "allMaps" in obj; }, get: function (obj) { return obj.allMaps; }, set: function (obj, value) { obj.allMaps = value; } }, metadata: _metadata }, _static_allMaps_initializers, _static_allMaps_extraInitializers); + if (_metadata) Object.defineProperty(_b, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + })(), + _b.allMaps = __runInitializers(_b, _static_allMaps_initializers, null), + _b.maps = (__runInitializers(_b, _static_allMaps_extraInitializers), {}), + (function () { + globals_1.FishEvents.on("dataLoaded", function () { + var _c; + //This event listener runs after the data has been loaded into allMaps + ((_c = _b.allMaps) !== null && _c !== void 0 ? _c : (_b.allMaps = [])).forEach(function (map) { + _b.maps[map.mapFileName] = map; + map.runs.forEach(function (run) { + var _c; + //this should not even happen, I think GameOverEvent is sending winTeam as null sometimes?? + (_c = run.winTeam) !== null && _c !== void 0 ? _c : (run.winTeam = Team.derelict); + }); + }); + //create all the data + Vars.maps.customMaps().each(function (m) { return void _b.getCreate(m); }); + }); + })(), + _b; +}(); +exports.FMap = FMap; diff --git a/build/scripts/metrics.js b/build/scripts/metrics.js index 21a755cb..5c77f587 100644 --- a/build/scripts/metrics.js +++ b/build/scripts/metrics.js @@ -1,76 +1,76 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the player count tracking. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Metrics = void 0; -var Metrics = /** @class */ (function () { - function Metrics() { - } - Metrics.weekNumber = function (date) { - if (date === void 0) { date = Date.now(); } - return Math.floor((date - this.startDate) / this.millisPerWeek); - }; - Metrics.readingNumber = function (date) { - if (date === void 0) { date = Date.now(); } - return Math.floor(((date - this.startDate) % this.millisPerWeek) / this.millisBetweenReadings); - }; - Metrics.newWeek = function () { - return Array(2520).fill(this.noData); - }; - Metrics.currentWeek = function () { - var _b; - var _c, _d; - return (_b = (_c = this.weeks)[_d = this.weekNumber()]) !== null && _b !== void 0 ? _b : (_c[_d] = this.newWeek()); - }; - Metrics.update = function () { - Time.mark(); - var playerCount = Groups.player.size(); - this.currentWeek()[this.readingNumber()] = - Math.max(playerCount, this.currentWeek()[this.readingNumber()]); - Log.debug("metrics update @", Time.elapsed()); - }; - Metrics.exportRange = function (startDate, endDate) { - var _this = this; - if (startDate === void 0) { startDate = this.startDate; } - if (endDate === void 0) { endDate = Date.now(); } - if (typeof startDate !== "number") - throw new Error('startDate should be a number'); - var startWeek = this.weekNumber(startDate); - var endWeek = this.weekNumber(endDate); - return this.weeks.slice(startWeek, endWeek + 1).map(function (week, weekNumber) { - return week.filter(function (v) { return v >= 0; }).map(function (v, i) { return [ - v, - _this.startDate + - weekNumber * _this.millisPerWeek + - i * _this.millisBetweenReadings - ]; }); - }).flat(); - }; - var _a; - _a = Metrics; - /** 4 May 2025 */ - Metrics.startDate = new Date(2025, 4, 4).getTime(); - Metrics.millisPerWeek = 604800000; - Metrics.millisBetweenReadings = 240000; - Metrics.noData = -1; - /** - * Weeks are numbered starting at the week of 4 May 2025. - * A value is taken every 4 minutes, for a total of 15 readings per hour. - */ - // @serialize("player-count-data", () => ["version", 0, - // ["array", "u16", ["array", 2520, ["number", "i8"]]] - // ], undefined, weeks => { - // for(let i = 0; i <= Metrics.weekNumber(); i ++){ - // weeks[i] ??= Metrics.newWeek(); - // } - // return weeks; - // }) - Metrics.weeks = Array(_a.weekNumber() + 1).fill(0).map(function () { return _a.newWeek(); }); - (function () { - Timer.schedule(function () { return _a.update(); }, 15, 60); - })(); - return Metrics; -}()); -exports.Metrics = Metrics; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the player count tracking. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Metrics = void 0; +var Metrics = /** @class */ (function () { + function Metrics() { + } + Metrics.weekNumber = function (date) { + if (date === void 0) { date = Date.now(); } + return Math.floor((date - this.startDate) / this.millisPerWeek); + }; + Metrics.readingNumber = function (date) { + if (date === void 0) { date = Date.now(); } + return Math.floor(((date - this.startDate) % this.millisPerWeek) / this.millisBetweenReadings); + }; + Metrics.newWeek = function () { + return Array(2520).fill(this.noData); + }; + Metrics.currentWeek = function () { + var _b; + var _c, _d; + return (_b = (_c = this.weeks)[_d = this.weekNumber()]) !== null && _b !== void 0 ? _b : (_c[_d] = this.newWeek()); + }; + Metrics.update = function () { + Time.mark(); + var playerCount = Groups.player.size(); + this.currentWeek()[this.readingNumber()] = + Math.max(playerCount, this.currentWeek()[this.readingNumber()]); + Log.debug("metrics update @", Time.elapsed()); + }; + Metrics.exportRange = function (startDate, endDate) { + var _this = this; + if (startDate === void 0) { startDate = this.startDate; } + if (endDate === void 0) { endDate = Date.now(); } + if (typeof startDate !== "number") + throw new Error('startDate should be a number'); + var startWeek = this.weekNumber(startDate); + var endWeek = this.weekNumber(endDate); + return this.weeks.slice(startWeek, endWeek + 1).map(function (week, weekNumber) { + return week.filter(function (v) { return v >= 0; }).map(function (v, i) { return [ + v, + _this.startDate + + weekNumber * _this.millisPerWeek + + i * _this.millisBetweenReadings + ]; }); + }).flat(); + }; + var _a; + _a = Metrics; + /** 4 May 2025 */ + Metrics.startDate = new Date(2025, 4, 4).getTime(); + Metrics.millisPerWeek = 604800000; + Metrics.millisBetweenReadings = 240000; + Metrics.noData = -1; + /** + * Weeks are numbered starting at the week of 4 May 2025. + * A value is taken every 4 minutes, for a total of 15 readings per hour. + */ + // @serialize("player-count-data", () => ["version", 0, + // ["array", "u16", ["array", 2520, ["number", "i8"]]] + // ], undefined, weeks => { + // for(let i = 0; i <= Metrics.weekNumber(); i ++){ + // weeks[i] ??= Metrics.newWeek(); + // } + // return weeks; + // }) + Metrics.weeks = Array(_a.weekNumber() + 1).fill(0).map(function () { return _a.newWeek(); }); + (function () { + Timer.schedule(function () { return _a.update(); }, 15, 60); + })(); + return Metrics; +}()); +exports.Metrics = Metrics; diff --git a/build/scripts/mindustryTypes.js b/build/scripts/mindustryTypes.js index 5c879b56..4d9bf344 100644 --- a/build/scripts/mindustryTypes.js +++ b/build/scripts/mindustryTypes.js @@ -1,12 +1,12 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains TypeScript type definitions for Mindustry's code. -Mindustry is written in Java, which has strong types. -Mindustry supports loading Javascript, which does not have types. -Javascript will have access to Mindustry's functions, which have types. -We are writing Typescript, which does have types. We are able to call Mindustry's functions, but because those are written in Java we cannot directly use those types. -This file contains some of those type definitions, ported over from the Java definitions. -*/ -//this is fine -Object.defineProperty(exports, "__esModule", { value: true }); +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains TypeScript type definitions for Mindustry's code. +Mindustry is written in Java, which has strong types. +Mindustry supports loading Javascript, which does not have types. +Javascript will have access to Mindustry's functions, which have types. +We are writing Typescript, which does have types. We are able to call Mindustry's functions, but because those are written in Java we cannot directly use those types. +This file contains some of those type definitions, ported over from the Java definitions. +*/ +//this is fine +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/scripts/packetHandlers.js b/build/scripts/packetHandlers.js index 0de90069..c8c49be4 100644 --- a/build/scripts/packetHandlers.js +++ b/build/scripts/packetHandlers.js @@ -1,281 +1,281 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the visual effects system. -Original contributor: @author TheEt1234 -Fixes: @author BalaM314 -Fixes: @author Dart25 -Fixes: @author Jurorno9 -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.commands = void 0; -exports.loadPacketHandlers = loadPacketHandlers; -exports.bulkInfoMsg = bulkInfoMsg; -var commands_1 = require("/frameworks/commands"); -var players_1 = require("/players"); -//some much needed restrictions -/** point in which effects will refuse to render */ -var MIN_EFFECT_TPS = 20; -/** maximum duration for user-created labels (seconds) */ -var MAX_LABEL_TIME = 20; -//info tracker -var lastLabel = ''; -var lastAccessedBulkLabel = null; -var lastAccessedLabel = null; -var lastAccessedBulkLine = null; -var lastAccessedLine = null; -var bulkLimit = 1000; -var noPermissionText = "[red]You don't have permission to use this packet."; -var invalidContentText = '[red]Invalid label content.'; -var tooLongText = '[red]Bulk content length exceeded, please use fewer effects.'; -var bulkSeparator = '|'; -var procError = '[red]An error occured while processing your request.'; -var invalidReq = '[red]Invalid request. Please consult the documentation.'; -var lowTPSError = '[red]Low server TPS, skipping request.'; -var tmpLinePacket = new EffectCallPacket2(); -var tmpLabelPacket = new LabelReliableCallPacket(); -function loadPacketHandlers() { - //initialize line packet - tmpLinePacket.effect = Fx.pointBeam; - tmpLinePacket.rotation = 0.0; - tmpLinePacket.color = Tmp.c1; - tmpLinePacket.data = Tmp.v1; - //labels - //fmt: "content,duration,x,y" - Vars.netServer.addPacketHandler('label', function (player, content) { - var p = players_1.FishPlayer.get(player); - try { - if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm("visualEffects")) { - p.sendMessage(noPermissionText, 1000); - return; - } - lastAccessedLabel = p; - handleLabel(player, content, true); - } - catch (_a) { - p.sendMessage(procError, 1000); - } - }); - Vars.netServer.addPacketHandler('bulkLabel', function (player, content) { - var p = players_1.FishPlayer.get(player); - try { - if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm('bulkVisualEffects')) { - p.sendMessage(noPermissionText, 1000); - return; - } - lastAccessedBulkLabel = p; - //get individual labels - var labels = []; - var inQuotes = false; - var startIdx = 0; - for (var i = 0; i < content.length; i++) { - switch (content[i]) { - case '"': - if (i > 0 && content[i - 1] == '\\') - break; - inQuotes = !inQuotes; - break; - //separate - case bulkSeparator: - if (inQuotes) - break; - labels.push(content.substring(startIdx, i)); - startIdx = i + 1; - break; - default: - break; - } - } - //last label - if (startIdx < content.length) { - labels.push(content.substring(startIdx, content.length - 1)); - } - if (labels.length > bulkLimit) { - p.sendMessage(tooLongText, 1000); - return; - } - //display labels - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (var i = 0; i < labels.length; i++) { - var label = labels[i]; - if (label.trim().length <= 0) - continue; - if (!handleLabel(player, label, false)) - return; - } - } - catch (_a) { - p.sendMessage(procError, 1000); - } - }); - //lines - Vars.netServer.addPacketHandler('lineEffect', function (player, content) { - var p = players_1.FishPlayer.get(player); - try { - if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm("visualEffects")) { - p.sendMessage(noPermissionText, 1000); - return; - } - if (!handleLine(content, player)) - return; - lastAccessedLine = p; - } - catch (_a) { - p.sendMessage(procError, 1000); - } - }); - //this is the silas effect but it's way too real - Vars.netServer.addPacketHandler('bulkLineEffect', function (player, content) { - var p = players_1.FishPlayer.get(player); - if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm('bulkVisualEffects')) { - p.sendMessage(noPermissionText, 1000); - return; - } - try { - var lines = content.split(bulkSeparator); - if (lines.length > bulkLimit) { - p.sendMessage(tooLongText, 1000); - return; - } - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - if (line.trim().length <= 0) - continue; - if (!handleLine(line, player)) - return; - } - lastAccessedBulkLine = p; - } - catch (_a) { - p.sendMessage(procError, 1000); - } - }); -} -//commands -exports.commands = (0, commands_1.commandList)({ - pklast: { - args: [], - description: 'Tells you who last accessed the packet handlers.', - perm: commands_1.Perm.none, - handler: function (_a) { - var output = _a.output; - var outputLines = []; - if (lastAccessedLabel && lastLabel) { - outputLines.push("".concat(lastAccessedLabel.name, "[white] created label \"").concat(lastLabel, "\".")); - } - if (lastAccessedBulkLabel) { - outputLines.push("".concat(lastAccessedBulkLabel.name, "[white] last used the bulk label effect.")); - } - if (lastAccessedLine) { - outputLines.push("".concat(lastAccessedLine.name, "[white] last used the line effect.")); - } - if (lastAccessedBulkLine) { - outputLines.push("".concat(lastAccessedBulkLine.name, "[white] last used the bulk line effect.")); - } - output(outputLines.length > 0 ? outputLines.join('\n') : 'No packet handlers have been accessed yet.'); - } - }, - pkdocs: { - description: 'Packet handler documentation.', - args: [], - perm: commands_1.Perm.none, - handler: function (_a) { - var sender = _a.sender, output = _a.output; - output("\t\t\t\t[blue]FISH[white] Packet Handler Docs\n[white]Usage:[accent]\n\t- Run the javascript function \"Call.serverPacketReliable()\" to send these. (!js in foos)\n\t- You need to multiply world coordinates by Vars.tilesize (8) for things to work properly. This is a relic from the v3 days where every tile was 8 pixels.\n\n[white]Packet types[accent]:\n\t- Line effect: \"lineEffect\", \"x0,y0,x1,y1,hexColor\" (for example \"20.7,19.3,50.4,28.9,#FF0000\")\n\t- Bulk line effect: \"bulkLineEffect\", equivalent to multiple lineEffect packets, with every line separated by a '|' symbol.\n\t- Label effect: \"label\", \"content,duration,x,y\" (for example \"\"Hi!\",10,20,28\")\n\t- Bulk label effect: \"bulkLabel\", equivalent to multiple label packets, with every label separated by a '|' symbol.\n\n[white]Limitations[accent]:\n\t- You ".concat((sender.hasPerm('bulkVisualEffects') ? ("[green]have been granted[accent]") : ("[red]do not have[accent]")), " access to bulk effects.\n\t- Effects will no longer be drawn at ").concat(MIN_EFFECT_TPS, " for server preformance.\n\t- Labels cannot last longer than ").concat(MAX_LABEL_TIME, " seconds.\n\t- There is a set ratelimit for sending packets, be careful ...\n\n[white]Starter Example[accent]:\n\n\tTo place a label saying \"hello\" at (0,0);\n\tFoos users: [lightgray]!js Call.serverPacketReliable(\"label\", [\"\\\"hello\\\"\", 10, 0, 0].join(\",\"))[accent]\n\tnewConsole users: [lightgrey]Call.serverPacketReliable(\"label\", [\"hello\", 10, 0, 10].join(\",\"))[accent]\n\n[white]Comments and Credits[accent]:\n\t- 'These packet handlers and everything related to them were made by [green]frog[accent].\n\t- 'The code style when submitted was beyond drunk... but it worked... barely' -BalaM314\n\t- \"worst error handling i have ever seen, why kick the player???\" -ASimpleBeginner'\n\t- Most of the code was rewritten in 2024 by [#6e00fb]D[#9e15de]a[#cd29c2]r[#fd3ea5]t[accent].'\n\t- Small tweaks by [#00cf]s[#00bf]w[#009f]a[#007f]m[#005f]p[accent]")); - } - } -}); -//#region utils -function findEndQuote(content, startPos) { - if (content[startPos] != '"') { - //not a start quote?? - return -1; - } - for (var i = startPos + 1; i < content.length; i++) { - if (content[i] == '"' && (i < 1 || content[i - 1] != '\\')) { - return i; - } - } - return -1; -} -function handleLabel(player, content, isSingle) { - var endPos = findEndQuote(content, 0); - if (endPos == -1) { - //invalid content - player.sendMessage(invalidContentText); - return false; - } - //label, clean up \"s - var message = content.substring(1, endPos).replace('\\"', '"'); - var parts = content.substring(endPos + 2).split(','); - if (parts.length != 3) { //dur,x,y - player.sendMessage(invalidReq); - return false; - } - if (isSingle && Strings.stripColors(message).length > 150) { - player.sendMessage('Label too large. Maximum is 150 characters, not including color tags.'); - } - if (isSingle) { - lastLabel = message; - } - var duration = Number(parts[0]); - var x = Number(parts[1]), y = Number(parts[2]); - if (Number.isNaN(duration) || duration > MAX_LABEL_TIME || Number.isNaN(x) || Number.isNaN(y)) { - player.sendMessage(invalidReq); - return false; - } - /*Call.labelReliable( - message, //message - Number(parts[0]), //duration - Number(parts[1]), //x - Number(parts[2]) //y - );*/ - tmpLabelPacket.message = message; - tmpLabelPacket.duration = duration; - tmpLabelPacket.worldx = x; - tmpLabelPacket.worldy = y; - Vars.net.send(tmpLabelPacket, false); - return true; -} -function handleLine(content, player) { - var parts = content.split(','); - if (parts.length != 5) { //x0,y0,x1,y1,color - player.sendMessage(invalidReq); - return false; - } - Tmp.v1.set(Number(parts[2]), Number(parts[3])); //x1,y1 - Color.valueOf(Tmp.c1, parts[4]); //color - /*Call.effect( - Fx.pointBeam, - Number(parts[0]), Number(parts[1]), //x,y - 0, Tmp.c1, //color - Tmp.v1 //x1,y1 - );*/ - tmpLinePacket.x = Number(parts[0]); - tmpLinePacket.y = Number(parts[1]); - Vars.net.send(tmpLinePacket, false); - return true; -} -function bulkInfoMsg(messages, conn) { - for (var i = messages.length - 1; i >= 0; i--) { - Call.infoMessage(conn, messages[i]); - } -} -//#endregion +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the visual effects system. +Original contributor: @author TheEt1234 +Fixes: @author BalaM314 +Fixes: @author Dart25 +Fixes: @author Jurorno9 +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.commands = void 0; +exports.loadPacketHandlers = loadPacketHandlers; +exports.bulkInfoMsg = bulkInfoMsg; +var commands_1 = require("/frameworks/commands"); +var players_1 = require("/players"); +//some much needed restrictions +/** point in which effects will refuse to render */ +var MIN_EFFECT_TPS = 20; +/** maximum duration for user-created labels (seconds) */ +var MAX_LABEL_TIME = 20; +//info tracker +var lastLabel = ''; +var lastAccessedBulkLabel = null; +var lastAccessedLabel = null; +var lastAccessedBulkLine = null; +var lastAccessedLine = null; +var bulkLimit = 1000; +var noPermissionText = "[red]You don't have permission to use this packet."; +var invalidContentText = '[red]Invalid label content.'; +var tooLongText = '[red]Bulk content length exceeded, please use fewer effects.'; +var bulkSeparator = '|'; +var procError = '[red]An error occured while processing your request.'; +var invalidReq = '[red]Invalid request. Please consult the documentation.'; +var lowTPSError = '[red]Low server TPS, skipping request.'; +var tmpLinePacket = new EffectCallPacket2(); +var tmpLabelPacket = new LabelReliableCallPacket(); +function loadPacketHandlers() { + //initialize line packet + tmpLinePacket.effect = Fx.pointBeam; + tmpLinePacket.rotation = 0.0; + tmpLinePacket.color = Tmp.c1; + tmpLinePacket.data = Tmp.v1; + //labels + //fmt: "content,duration,x,y" + Vars.netServer.addPacketHandler('label', function (player, content) { + var p = players_1.FishPlayer.get(player); + try { + if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm("visualEffects")) { + p.sendMessage(noPermissionText, 1000); + return; + } + lastAccessedLabel = p; + handleLabel(player, content, true); + } + catch (_a) { + p.sendMessage(procError, 1000); + } + }); + Vars.netServer.addPacketHandler('bulkLabel', function (player, content) { + var p = players_1.FishPlayer.get(player); + try { + if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm('bulkVisualEffects')) { + p.sendMessage(noPermissionText, 1000); + return; + } + lastAccessedBulkLabel = p; + //get individual labels + var labels = []; + var inQuotes = false; + var startIdx = 0; + for (var i = 0; i < content.length; i++) { + switch (content[i]) { + case '"': + if (i > 0 && content[i - 1] == '\\') + break; + inQuotes = !inQuotes; + break; + //separate + case bulkSeparator: + if (inQuotes) + break; + labels.push(content.substring(startIdx, i)); + startIdx = i + 1; + break; + default: + break; + } + } + //last label + if (startIdx < content.length) { + labels.push(content.substring(startIdx, content.length - 1)); + } + if (labels.length > bulkLimit) { + p.sendMessage(tooLongText, 1000); + return; + } + //display labels + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (var i = 0; i < labels.length; i++) { + var label = labels[i]; + if (label.trim().length <= 0) + continue; + if (!handleLabel(player, label, false)) + return; + } + } + catch (_a) { + p.sendMessage(procError, 1000); + } + }); + //lines + Vars.netServer.addPacketHandler('lineEffect', function (player, content) { + var p = players_1.FishPlayer.get(player); + try { + if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm("visualEffects")) { + p.sendMessage(noPermissionText, 1000); + return; + } + if (!handleLine(content, player)) + return; + lastAccessedLine = p; + } + catch (_a) { + p.sendMessage(procError, 1000); + } + }); + //this is the silas effect but it's way too real + Vars.netServer.addPacketHandler('bulkLineEffect', function (player, content) { + var p = players_1.FishPlayer.get(player); + if (Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS) { + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm('bulkVisualEffects')) { + p.sendMessage(noPermissionText, 1000); + return; + } + try { + var lines = content.split(bulkSeparator); + if (lines.length > bulkLimit) { + p.sendMessage(tooLongText, 1000); + return; + } + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (line.trim().length <= 0) + continue; + if (!handleLine(line, player)) + return; + } + lastAccessedBulkLine = p; + } + catch (_a) { + p.sendMessage(procError, 1000); + } + }); +} +//commands +exports.commands = (0, commands_1.commandList)({ + pklast: { + args: [], + description: 'Tells you who last accessed the packet handlers.', + perm: commands_1.Perm.none, + handler: function (_a) { + var output = _a.output; + var outputLines = []; + if (lastAccessedLabel && lastLabel) { + outputLines.push("".concat(lastAccessedLabel.name, "[white] created label \"").concat(lastLabel, "\".")); + } + if (lastAccessedBulkLabel) { + outputLines.push("".concat(lastAccessedBulkLabel.name, "[white] last used the bulk label effect.")); + } + if (lastAccessedLine) { + outputLines.push("".concat(lastAccessedLine.name, "[white] last used the line effect.")); + } + if (lastAccessedBulkLine) { + outputLines.push("".concat(lastAccessedBulkLine.name, "[white] last used the bulk line effect.")); + } + output(outputLines.length > 0 ? outputLines.join('\n') : 'No packet handlers have been accessed yet.'); + } + }, + pkdocs: { + description: 'Packet handler documentation.', + args: [], + perm: commands_1.Perm.none, + handler: function (_a) { + var sender = _a.sender, output = _a.output; + output("\t\t\t\t[blue]FISH[white] Packet Handler Docs\n[white]Usage:[accent]\n\t- Run the javascript function \"Call.serverPacketReliable()\" to send these. (!js in foos)\n\t- You need to multiply world coordinates by Vars.tilesize (8) for things to work properly. This is a relic from the v3 days where every tile was 8 pixels.\n\n[white]Packet types[accent]:\n\t- Line effect: \"lineEffect\", \"x0,y0,x1,y1,hexColor\" (for example \"20.7,19.3,50.4,28.9,#FF0000\")\n\t- Bulk line effect: \"bulkLineEffect\", equivalent to multiple lineEffect packets, with every line separated by a '|' symbol.\n\t- Label effect: \"label\", \"content,duration,x,y\" (for example \"\"Hi!\",10,20,28\")\n\t- Bulk label effect: \"bulkLabel\", equivalent to multiple label packets, with every label separated by a '|' symbol.\n\n[white]Limitations[accent]:\n\t- You ".concat((sender.hasPerm('bulkVisualEffects') ? ("[green]have been granted[accent]") : ("[red]do not have[accent]")), " access to bulk effects.\n\t- Effects will no longer be drawn at ").concat(MIN_EFFECT_TPS, " for server preformance.\n\t- Labels cannot last longer than ").concat(MAX_LABEL_TIME, " seconds.\n\t- There is a set ratelimit for sending packets, be careful ...\n\n[white]Starter Example[accent]:\n\n\tTo place a label saying \"hello\" at (0,0);\n\tFoos users: [lightgray]!js Call.serverPacketReliable(\"label\", [\"\\\"hello\\\"\", 10, 0, 0].join(\",\"))[accent]\n\tnewConsole users: [lightgrey]Call.serverPacketReliable(\"label\", [\"hello\", 10, 0, 10].join(\",\"))[accent]\n\n[white]Comments and Credits[accent]:\n\t- 'These packet handlers and everything related to them were made by [green]frog[accent].\n\t- 'The code style when submitted was beyond drunk... but it worked... barely' -BalaM314\n\t- \"worst error handling i have ever seen, why kick the player???\" -ASimpleBeginner'\n\t- Most of the code was rewritten in 2024 by [#6e00fb]D[#9e15de]a[#cd29c2]r[#fd3ea5]t[accent].'\n\t- Small tweaks by [#00cf]s[#00bf]w[#009f]a[#007f]m[#005f]p[accent]")); + } + } +}); +//#region utils +function findEndQuote(content, startPos) { + if (content[startPos] != '"') { + //not a start quote?? + return -1; + } + for (var i = startPos + 1; i < content.length; i++) { + if (content[i] == '"' && (i < 1 || content[i - 1] != '\\')) { + return i; + } + } + return -1; +} +function handleLabel(player, content, isSingle) { + var endPos = findEndQuote(content, 0); + if (endPos == -1) { + //invalid content + player.sendMessage(invalidContentText); + return false; + } + //label, clean up \"s + var message = content.substring(1, endPos).replace('\\"', '"'); + var parts = content.substring(endPos + 2).split(','); + if (parts.length != 3) { //dur,x,y + player.sendMessage(invalidReq); + return false; + } + if (isSingle && Strings.stripColors(message).length > 150) { + player.sendMessage('Label too large. Maximum is 150 characters, not including color tags.'); + } + if (isSingle) { + lastLabel = message; + } + var duration = Number(parts[0]); + var x = Number(parts[1]), y = Number(parts[2]); + if (Number.isNaN(duration) || duration > MAX_LABEL_TIME || Number.isNaN(x) || Number.isNaN(y)) { + player.sendMessage(invalidReq); + return false; + } + /*Call.labelReliable( + message, //message + Number(parts[0]), //duration + Number(parts[1]), //x + Number(parts[2]) //y + );*/ + tmpLabelPacket.message = message; + tmpLabelPacket.duration = duration; + tmpLabelPacket.worldx = x; + tmpLabelPacket.worldy = y; + Vars.net.send(tmpLabelPacket, false); + return true; +} +function handleLine(content, player) { + var parts = content.split(','); + if (parts.length != 5) { //x0,y0,x1,y1,color + player.sendMessage(invalidReq); + return false; + } + Tmp.v1.set(Number(parts[2]), Number(parts[3])); //x1,y1 + Color.valueOf(Tmp.c1, parts[4]); //color + /*Call.effect( + Fx.pointBeam, + Number(parts[0]), Number(parts[1]), //x,y + 0, Tmp.c1, //color + Tmp.v1 //x1,y1 + );*/ + tmpLinePacket.x = Number(parts[0]); + tmpLinePacket.y = Number(parts[1]); + Vars.net.send(tmpLinePacket, false); + return true; +} +function bulkInfoMsg(messages, conn) { + for (var i = messages.length - 1; i >= 0; i--) { + Call.infoMessage(conn, messages[i]); + } +} +//#endregion diff --git a/build/scripts/players.js b/build/scripts/players.js index 03fa1f68..0868e58d 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -1,1987 +1,1987 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the FishPlayer class, and many player-related functions. -*/ -var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { - if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } - return cooked; -}; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.FishPlayer = void 0; -var api = __importStar(require("/api")); -var config_1 = require("/config"); -var commands_1 = require("/frameworks/commands"); -var menus_1 = require("/frameworks/menus"); -var funcs_1 = require("/funcs"); -var globals = __importStar(require("/globals")); -var globals_1 = require("/globals"); -var maps_1 = require("/maps"); -var ranks_1 = require("/ranks"); -var utils_1 = require("/utils"); -var FishPlayer = /** @class */ (function () { - //#endregion - function FishPlayer(uuid, data, player) { - //#endregion - //#region Transient properties - //Commands framework - /** Front-to-back queue of menus to show. */ - this.activeMenus = []; - /** Mapping from command to usage data. */ - this.usageData = {}; - this.tapInfo = { - commandName: null, - lastArgs: {}, - mode: "once", - }; - //Misc - this.player = null; - /** Used for the /trail command. */ - this.trail = null; - this.cleanedName = "Unnamed player [ERROR}"; - this.prefixedName = "Unnamed player [ERROR}"; - /** Used to freeze players when votekicking. */ - this.frozen = false; - /** Used to avoid spamming players with ads by the tip message system */ - this.lastShownAd = globals.maxTime; - /** Used to avoid spamming players with ads by the tip message system */ - this.showAdNext = false; - /** Transient statistics, used by the automatic griefer detection. */ - this.tstats = { - //remember to clear this in updateSavedInfoFromPlayer! - blocksBroken: 0, - blockInteractionsThisMap: 0, - lastMapStartTime: 0, - lastMapPlayedTime: 0, - wavesSurvived: 0, - }; - /** Whether the player has manually marked themselves as AFK. */ - this.manualAfk = false; - //Used for AFK detection. - this.lastMousePosition = [0, 0]; - this.lastUnitPosition = [0, 0]; - this.lastActive = Date.now(); - /** Set this to false to disable automatic name updates. Used for the rename console command. */ - this.shouldUpdateName = true; - /** Used by the sendMessage() ratelimit system. */ - this.lastRatelimitedMessage = -1; - /** Keeps track of whether a player has changed team this match, for win rate calculation. */ - this.changedTeam = false; - /** Whether the player's IP was detected as a VPN. */ - this.ipDetectedVpn = false; - /** - * If a player's IP is detected as a VPN on their first join, - * they are autoflagged and cannot build or talk in chat. - */ - this.autoflagged = false; - /** Timestamp until which this player will not be allowed to control units. */ - this.blockedFromPossessingUnitsUntil = 0; - /** Timestamp until which this player will not be allowed to control units. */ - this.blockedFromCommandingUnitsUntil = 0; - // Used by the data syncing framework. - this.infoUpdated = false; - this.dataSynced = false; - this.restoreTeam = null; - this.name = "Unnamed player [ERROR}"; - this.muted = false; - this.unmarkTime = -1; - this.rank = ranks_1.Rank.player; - this.flags = new Set(); - /** Used to color chat messages for the member command */ - this.highlight = null; - /** Used to color the player's name for the member command */ - this.rainbow = null; - /** List of all moderation actions that have been performed on this player. */ - this.history = []; - /** - * The USID for this player. - * USID stands for Unique Server IDentifier. It is like a UUID, but unique to each server (by IP and port). - * It cannot be viewed by admins and it cannot be obtained by other servers. - */ - this.usid = null; - /** If chat strictness is set to "strict", the player will not be allowed to swear. */ - this.chatStrictness = "chat"; - /** -1 represents unknown */ - this.lastJoined = -1; - /** -1 represents unknown */ - this.firstJoined = -1; - /** -1 represents unknown */ - this.globalLastJoined = -1; - /** -1 represents unknown */ - this.globalFirstJoined = -1; - this.stats = { - blocksBroken: 0, - blocksPlaced: 0, - timeInGame: 0, - chatMessagesSent: 0, - gamesFinished: 0, - gamesWon: 0, - }; - this.globalStats = this.stats; - /** Used for the /vanish command. */ - this.showRankPrefix = true; - this.achievements = new Bits(); - this.uuid = uuid; - this.player = player; - this.updateData(data); - } - //#region getplayer - //Contains methods used to get FishPlayer instances. - FishPlayer.createFromPlayer = function (player) { - return new this(player.uuid(), {}, player); - }; - FishPlayer.createFromInfo = function (playerInfo) { - var _a; - return new this(playerInfo.id, { - uuid: playerInfo.id, - name: playerInfo.lastName, - usid: (_a = playerInfo.adminUsid) !== null && _a !== void 0 ? _a : null - }, null); - }; - FishPlayer.getFromInfo = function (playerInfo) { - var _a; - var _b, _c; - return (_a = (_b = FishPlayer.cachedPlayers)[_c = playerInfo.id]) !== null && _a !== void 0 ? _a : (_b[_c] = FishPlayer.createFromInfo(playerInfo)); - }; - FishPlayer.get = function (player) { - var _a; - var _b, _c; - return (_a = (_b = FishPlayer.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = FishPlayer.createFromPlayer(player)); - }; - FishPlayer.resolve = function (player) { - var _a; - var _b, _c; - if (player instanceof FishPlayer) - return player; - else - return (_a = (_b = FishPlayer.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = FishPlayer.createFromPlayer(player)); - }; - FishPlayer.getById = function (id) { - var _a; - return (_a = this.cachedPlayers[id]) !== null && _a !== void 0 ? _a : null; - }; - /** Returns the FishPlayer representing the first online player matching a given name. */ - FishPlayer.getByName = function (name) { - if (name == "") - return null; - var realPlayer = Groups.player.find(function (p) { - return p.name === name || - p.name.includes(name) || - p.name.toLowerCase().includes(name.toLowerCase()) || - Strings.stripColors(p.name).toLowerCase() === name.toLowerCase() || - Strings.stripColors(p.name).toLowerCase().includes(name.toLowerCase()) || - false; - }); - return realPlayer ? this.get(realPlayer) : null; - }; - ; - /** Returns the FishPlayers representing all online players matching a given name. */ - FishPlayer.getAllByName = function (name, strict) { - if (strict === void 0) { strict = true; } - if (name == "") - return []; - var output = []; - Groups.player.each(function (p) { - var fishP = FishPlayer.get(p); - if (fishP.connected() && fishP.cleanedName.includes(name) || (!strict && fishP.cleanedName.toLowerCase().includes(name))) - output.push(fishP); - }); - return output; - }; - FishPlayer.getOneMindustryPlayerByName = function (str) { - var e_1, _a; - if (str == "") - return "none"; - var players = (0, funcs_1.setToArray)(Groups.player); - var matchingPlayers; - var filters = [ - function (p) { return p.name === str; }, - // p => Strings.stripColors(p.name) === str, - function (p) { return Strings.stripColors(p.name).toLowerCase() === str.toLowerCase(); }, - // p => p.name.includes(str), - function (p) { return p.name.toLowerCase().includes(str.toLowerCase()); }, - function (p) { return Strings.stripColors(p.name).includes(str); }, - function (p) { return Strings.stripColors(p.name).toLowerCase().includes(str.toLowerCase()); }, - ]; - try { - for (var filters_1 = __values(filters), filters_1_1 = filters_1.next(); !filters_1_1.done; filters_1_1 = filters_1.next()) { - var filter = filters_1_1.value; - matchingPlayers = players.filter(filter); - if (matchingPlayers.length == 1) - return matchingPlayers[0]; - else if (matchingPlayers.length > 1) - return "multiple"; - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (filters_1_1 && !filters_1_1.done && (_a = filters_1.return)) _a.call(filters_1); - } - finally { if (e_1) throw e_1.error; } - } - return "none"; - }; - //This method exists only because there is no easy way to turn an entitygroup into an array - FishPlayer.getAllOnline = function () { - var players = []; - Groups.player.each(function (p) { - var fishP = FishPlayer.get(p); - if (fishP.connected()) - players.push(fishP); - }); - return players; - }; - /** Returns all cached FishPlayers with names matching the search string. */ - FishPlayer.getAllOfflineByName = function (name) { - var e_2, _a; - var matching = []; - try { - for (var _b = __values(Object.entries(this.cachedPlayers)), _c = _b.next(); !_c.done; _c = _b.next()) { - var _d = __read(_c.value, 2), uuid = _d[0], player = _d[1]; - if (player.cleanedName.toLowerCase().includes(name)) - matching.push(player); - } - } - catch (e_2_1) { e_2 = { error: e_2_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_2) throw e_2.error; } - } - return matching; - }; - FishPlayer.onConnectPacket = function (_a) { - var _this = this; - var uuid = _a.uuid, name = _a.name; - var entry = this.cachedPlayers[uuid]; - if (entry) { - entry.infoUpdated = false; - entry.dataSynced = false; - entry.name = name; - } - api.getFishPlayerData(uuid).then(function (data) { - if (!data) - return; //nothing to sync - var fishP; - if (!(uuid in _this.cachedPlayers)) { - fishP = new FishPlayer(uuid, data, null); - fishP.originalName = name; - fishP.dataSynced = true; - _this.cachedPlayers[uuid] = fishP; - } - else { - fishP = _this.cachedPlayers[uuid]; - fishP.dataSynced = true; - fishP.updateData(data); - if (fishP.infoUpdated) { - //Player has already connected - //Run it again - if (fishP.player) - fishP.updateSavedInfoFromPlayer(fishP.player, true); - } - else { - //Player has not connected yet, nothing further needed - } - } - if (fishP.connected()) { - fishP.checkUsid(); - fishP.updateMemberExclusiveState(); - fishP.updateName(); - fishP.updateAdminStatus(); - fishP.updateAutoflaggedStatus(); - fishP.checkAutoRanks(); - fishP.sendWelcomeMessage(); - } - }, function () { - var fishP = _this.cachedPlayers[uuid]; - fishP.updateAdminStatus(); - fishP.updateAutoflaggedStatus(); - fishP.sendWelcomeMessage(); - if (fishP === null || fishP === void 0 ? void 0 : fishP.player) - fishP.player.sendMessage(config_1.text.dataFetchFailed); - else - _this.dataFetchFailedUuids.add(uuid); - }); - }; - /** Must be called at player join, before updateName(). */ - FishPlayer.prototype.updateSavedInfoFromPlayer = function (player, repeated) { - if (repeated === void 0) { repeated = false; } - this.player = player; - if (repeated) { - this.name = this.originalName; - } - else { - this.originalName = this.name = player.name; - } - if (this.firstJoined < 1) - this.firstJoined = Date.now(); - //Do not update USID here - this.manualAfk = false; - this.cleanedName = Strings.stripColors(player.name); - this.lastJoined = Date.now(); - this.lastMousePosition = [0, 0]; - this.lastActive = Date.now(); - if (this.highlight === "[white]") - this.highlight = null; - this.shouldUpdateName = true; - this.changedTeam = false; - this.ipDetectedVpn = false; - this.tstats.blocksBroken = 0; - if (this.tstats.lastMapPlayedTime != FishPlayer.lastMapStartTime) { - this.tstats.blockInteractionsThisMap = 0; - this.tstats.lastMapPlayedTime = FishPlayer.lastMapStartTime; - } - this.infoUpdated = true; - }; - FishPlayer.prototype.updateData = function (data) { - var _a; - if (data.name != undefined) - this.name = data.name; - if (data.muted != undefined) - this.muted = data.muted; - if (data.unmarkTime != undefined) - this.unmarkTime = data.unmarkTime; - if (data.lastJoined != undefined) - this.lastJoined = data.lastJoined; - if (data.firstJoined != undefined) - this.firstJoined = data.firstJoined; - if (data.globalLastJoined != undefined) - this.globalLastJoined = data.globalLastJoined; - if (data.globalFirstJoined != undefined) - this.globalFirstJoined = data.globalFirstJoined; - if (data.highlight != undefined) - this.highlight = data.highlight; - if (data.history != undefined) - this.history = data.history; - if (data.rainbow != undefined) - this.rainbow = data.rainbow; - if (data.usid != undefined) - this.usid = data.usid; - if (data.chatStrictness != undefined) - this.chatStrictness = data.chatStrictness; - if (data.stats != undefined) - this.stats = data.stats; - if (data.globalStats != undefined) - this.globalStats = data.globalStats; - if (data.showRankPrefix != undefined) - this.showRankPrefix = data.showRankPrefix; - if (data.rank != undefined) - this.rank = (_a = ranks_1.Rank.getByName(data.rank)) !== null && _a !== void 0 ? _a : ranks_1.Rank.player; - if (data.flags != undefined) - this.flags = new Set(data.flags.map(ranks_1.RoleFlag.getByName).filter(Boolean)); - if (data.achievements != undefined) - this.achievements = JsonIO.read(Bits, "{bits:".concat(data.achievements, "}")); - }; - FishPlayer.prototype.getData = function () { - var _a = this, uuid = _a.uuid, name = _a.name, muted = _a.muted, unmarkTime = _a.unmarkTime, rank = _a.rank, flags = _a.flags, highlight = _a.highlight, rainbow = _a.rainbow, history = _a.history, usid = _a.usid, chatStrictness = _a.chatStrictness, lastJoined = _a.lastJoined, firstJoined = _a.firstJoined, stats = _a.stats, showRankPrefix = _a.showRankPrefix; - return { - uuid: uuid, - name: name, - muted: muted, - unmarkTime: unmarkTime, - highlight: highlight, - rainbow: rainbow, - history: history, - usid: usid, - chatStrictness: chatStrictness, - lastJoined: lastJoined, - firstJoined: firstJoined, - stats: stats, - showRankPrefix: showRankPrefix, - rank: rank.name, - flags: __spreadArray([], __read(flags.values()), false).map(function (f) { return f.name; }), - achievements: JsonIO.write(Reflect.get(this.achievements, "bits")) - }; - }; - /** Warning: the "update" callback is run twice. */ - FishPlayer.prototype.updateSynced = function (update, beforeFetch, afterFetch) { - return __awaiter(this, void 0, void 0, function () { - var data; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - update(this); - beforeFetch === null || beforeFetch === void 0 ? void 0 : beforeFetch(this); - return [4 /*yield*/, api.getFishPlayerData(this.uuid)]; - case 1: - data = _a.sent(); - if (data) - this.updateData(data); - update(this); - //of course, this is a race condition - //but it's unlikely to happen - //could be fixed by transmitting the update operation to the server as a mongo update command - afterFetch === null || afterFetch === void 0 ? void 0 : afterFetch(this); - return [4 /*yield*/, api.setFishPlayerData(this.getData(), 1, false)]; - case 2: - _a.sent(); - return [2 /*return*/]; - } - }); - }); - }; - //#endregion - //#region actively synced data updates - FishPlayer.prototype.stop = function (by, duration, message, notify) { - var _this = this; - if (notify === void 0) { notify = true; } - if (duration > 60000) - this.setPunishedIP(config_1.stopAntiEvadeTime); - this.showRankPrefix = true; - return this.updateSynced(function () { - _this.unmarkTime = Date.now() + duration; - if (_this.unmarkTime > globals.maxTime) - _this.unmarkTime = globals.maxTime; - _this.updateName(); - }, function () { - _this.setUnmarkTimer(duration); - if (_this.connected() && notify) { - _this.stopUnit(); - _this.sendMessage(message - ? "[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer for reason: [white]".concat(message, "[]") - : "[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer."); - if (duration < funcs_1.Duration.hours(1)) { - //less than one hour - _this.sendMessage("[yellow]Your mark will expire in ".concat((0, utils_1.formatTime)(duration), ".")); - } - } - }, function () { return _this.addHistoryEntry({ - action: 'stopped', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - }); }); - }; - FishPlayer.prototype.free = function (by) { - var _this = this; - by !== null && by !== void 0 ? by : (by = "console"); - this.autoflagged = false; //Might as well set autoflagged to false - FishPlayer.removePunishedIP(this.ip()); - FishPlayer.removePunishedUUID(this.uuid); - return this.updateSynced(function () { - _this.unmarkTime = -1; - }, function () { - if (_this.connected()) { - _this.sendMessage('[yellow]Looks like someone had mercy on you.'); - _this.updateName(); - _this.forceRespawn(); - } - }, function () { return _this.addHistoryEntry({ - action: 'freed', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - }); }); - }; - FishPlayer.prototype.setRank = function (rank) { - return __awaiter(this, void 0, void 0, function () { - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - if (typeof rank === "string" || !rank) { - rank; - (0, funcs_1.crash)("Type error in FishPlayer.setFlag(): rank is invalid"); - } - if (rank == ranks_1.Rank.pi && !config_1.Mode.localDebug) - throw new TypeError("Cannot find function setRank in object [object Object]."); - return [4 /*yield*/, this.updateSynced(function () { - _this.rank = rank; - _this.updateName(); - _this.updateAdminStatus(); - }, function () { return FishPlayer.saveAll(); })]; - case 1: - _a.sent(); - return [2 /*return*/]; - } - }); - }); - }; - FishPlayer.prototype.setFlag = function (flag_, value) { - return __awaiter(this, void 0, void 0, function () { - var flag; - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - flag = typeof flag_ == "string" ? - (ranks_1.RoleFlag.getByName(flag_)) - : flag_; - // eslint-disable-next-line @typescript-eslint/no-base-to-string - if (!flag) - (0, funcs_1.crash)("Type error in FishPlayer.setFlag(): flag ".concat(String(flag_), " is invalid")); - return [4 /*yield*/, this.updateSynced(function () { - if (value) { - _this.flags.add(flag); - } - else { - _this.flags.delete(flag); - } - _this.updateMemberExclusiveState(); - _this.updateName(); - })]; - case 1: - _a.sent(); - return [2 /*return*/]; - } - }); - }); - }; - FishPlayer.prototype.mute = function (by) { - var _this = this; - if (this.muted) - return; - this.showRankPrefix = true; - return this.updateSynced(function () { - _this.muted = true; - _this.updateName(); - }, function () { - _this.sendMessage("[yellow]Hey! You have been muted. You cannot send messages to other players. You can still send messages to staff members."); - _this.setPunishedIP(config_1.stopAntiEvadeTime); - }, function () { return _this.addHistoryEntry({ - action: 'muted', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - }); }); - }; - FishPlayer.prototype.unmute = function (by) { - var _this = this; - if (!this.muted) - return; - FishPlayer.removePunishedIP(this.ip()); - FishPlayer.removePunishedUUID(this.uuid); - return this.updateSynced(function () { - _this.muted = false; - _this.updateName(); - }, function () { - _this.sendMessage("[green]You have been unmuted."); - }, function () { return _this.addHistoryEntry({ - action: 'muted', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - }); }); - }; - //#endregion - //#region eventhandling - //Contains methods that handle an event and must be called by other code (usually through Events.on). - /** Must be run on PlayerConnectEvent. */ - FishPlayer.onPlayerConnect = function (player) { - var _a; - var _b, _c; - var fishPlayer = (_a = (_b = this.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = this.createFromPlayer(player)); - var previousJoin = fishPlayer.lastJoined; - fishPlayer.updateSavedInfoFromPlayer(player); - if (fishPlayer.validate()) { - if (!fishPlayer.hasPerm("bypassNameCheck")) { - var message = (0, utils_1.isImpersonator)(fishPlayer.name, fishPlayer.ranksAtLeast("admin")); - if (message !== false) { - fishPlayer.sendMessage("[scarlet]\u26A0[] [gold]Oh no! Our systems think you are a [scarlet]SUSSY IMPERSONATOR[]!\n[gold]Reason: ".concat(message, "\n[gold]Change your name to remove the tag.")); - } - else if ((0, utils_1.cleanText)(player.name, true).includes("hacker")) { - fishPlayer.sendMessage("[scarlet]\u26A0 Don't be a script kiddie!"); - globals_1.FishEvents.fire("scriptKiddie", [fishPlayer]); - } - } - fishPlayer.updateAdminStatus(); - fishPlayer.checkVPNAndJoins(); - fishPlayer.updateName(); - //I think this is a better spot for this - if (fishPlayer.firstJoin()) - void menus_1.Menu.menu("Rules for [#0000ff] >|||> FISH [white] servers [white]", config_1.rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].", ["[green]I understand and agree to these terms"], fishPlayer); - } - }; - /** Must be run on PlayerJoinEvent. */ - FishPlayer.onPlayerJoin = function (player) { - var _this = this; - var _a; - var _b, _c; - var fishPlayer = (_a = (_b = this.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = (function () { - Log.err("onPlayerJoin: no fish player was created? ".concat(player.uuid())); - return _this.createFromPlayer(player); - })()); - //Don't activate heuristics until they've joined - //a lot of time can pass between connect and join - //also the player might connect but fail to join for a lot of reasons, - //or connect, fail to join, then connect again and join successfully - //which would cause heuristics to activate twice - fishPlayer.activateHeuristics(); - }; - FishPlayer.updateAFKCheck = function () { - //TODO better AFK check - this.forEachPlayer(function (fishP, mp) { - fishP.lastMousePosition = [mp.mouseX, mp.mouseY]; - fishP.lastUnitPosition = [mp.x, mp.y]; - fishP.updateName(); - }); - }; - /** Must be run on PlayerLeaveEvent. */ - FishPlayer.onPlayerLeave = function (player) { - var _a; - var fishP = this.cachedPlayers[player.uuid()]; - if (!fishP) - return; - if (Vars.netServer.currentlyKicking && - Reflect.get(Vars.netServer.currentlyKicking, "target") == player) { - //Anti votekick evasion - var votes_1 = Reflect.get(Vars.netServer.currentlyKicking, "votes"); - if ((function () { - if (fishP.hasPerm("bypassVotekick")) - return false; - if (fishP.hasPerm("bypassVoteFreeze")) - return votes_1 >= Vars.netServer.votesRequired(); - if (fishP.info().timesJoined > 50) - return votes_1 >= 2; - return votes_1 >= 1; - })()) { - var kickDuration = NetServer.kickDuration; - //Pass the votekick - Call.sendMessage("[orange]Vote passed.[scarlet] ".concat(player.name, "[orange] will be banned from the server for ").concat(kickDuration / 60, " minutes.")); - player.kick(Packets.KickReason.vote, kickDuration * 1000); //it is stored in seconds but needs to be converted to millis - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - } - } - //Clear temporary states such as menu and taphandler - fishP.activeMenus = []; - fishP.tapInfo.commandName = null; - fishP.updateStats(function (stats) { return stats.timeInGame += (Date.now() - fishP.lastJoined); }); //Time between joining and leaving - fishP.lastJoined = Date.now(); - this.recentLeaves.unshift(fishP); - if (this.recentLeaves.length > 10) - this.recentLeaves.pop(); - void api.setFishPlayerData(fishP.getData(), 1, true); - var currentRun = (_a = maps_1.PartialMapRun.current) === null || _a === void 0 ? void 0 : _a.startTime; - if (currentRun) - Core.app.post(function () { - //Wait for the /spectate command's handler to fix their team before saving it - fishP.restoreTeam = [fishP.player.team(), Date.now(), currentRun]; - }); - }; - FishPlayer.validateVotekickSession = function () { - var _a; - if (!Vars.netServer.currentlyKicking) - return; - var target = this.get(Reflect.get(Vars.netServer.currentlyKicking, "target")); - var voted = Reflect.get(Vars.netServer.currentlyKicking, "voted"); - if (voted.size == 2) { - //Try to find the UUID of the initiator - var uuid_1 = null; - voted.entries().toArray().each(function (e) { - if (globals_1.uuidPattern.test(e.key)) - uuid_1 = e.key; - }); - if (uuid_1) { - var initiator = this.getById(uuid_1); - if (initiator === null || initiator === void 0 ? void 0 : initiator.stelled()) { - if (initiator.hasPerm("bypassVotekick")) { - if (target !== this.easterEggVotekickTarget) { - this.easterEggVotekickTarget = target; - var msg = (_a = (new Error()).stack) === null || _a === void 0 ? void 0 : _a.split("\n").slice(0, 4).join("\n"); - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(initiator.prefixedName, "[lightgray].[accent] (\u221E/").concat(Vars.netServer.votesRequired(), ")\n\t[scarlet]Error: failed to kick player ").concat(initiator.name, "\n\t").concat(msg, "\n\t[scarlet]Error: failed to cancel votekick\n\t").concat(msg)); - } - return; - } - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(initiator.prefixedName, "[lightgray].[accent] (\u221E/").concat(Vars.netServer.votesRequired(), ")\n[scarlet]Vote passed.")); - initiator.kick("You are not allowed to votekick other players while marked.", 2); - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - return; - } - else if ((initiator === null || initiator === void 0 ? void 0 : initiator.hasPerm("immediatelyVotekickNewPlayers")) && target.isSuspicious("high") && !target.hasPerm("bypassVotekick")) { - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (").concat(Vars.netServer.votesRequired(), "/").concat(Vars.netServer.votesRequired(), ")\n[scarlet]Vote passed.")); - target.kick(Packets.KickReason.vote, funcs_1.Duration.minutes(30)); - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - return; - } - else if (target.isSuspicious("high") && !target.hasPerm("bypassVotekick") && !target.ranksAtLeast("trusted")) { - //Increase votes by 1, from 1 to 2 - Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(2)); - voted.put("__server__", 1); - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (2/").concat(Vars.netServer.votesRequired(), ")\n[lightgray]Type[orange] /vote [] to agree.")); - return; - } - } - } - if (target.hasPerm("bypassVotekick")) { - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (-\u221E/").concat(Vars.netServer.votesRequired(), ")\n[scarlet]Vote cancelled.")); - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - } - else if (target.ranksAtLeast("trusted") && Groups.player.size() > 4 && voted.get("__server__") == 0) { - //decrease votes by two, goes from 1 to negative 1 - Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(-1)); - voted.put("__server__", -2); - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (-1/").concat(Vars.netServer.votesRequired(), ")\n[lightgray]Type[orange] /vote [] to agree.")); - } - }; - FishPlayer.onPlayerChat = function (player, message) { - var fishP = this.get(player); - if (message.trim().toLowerCase().startsWith("/vote y") || message.startsWith("/votekick ")) { - this.checkVotekickAction(fishP, message); - } - fishP.lastActive = Date.now(); - fishP.updateStats(function (stats) { return stats.chatMessagesSent++; }); - }; - FishPlayer.checkVotekickAction = function (fishP, message) { - var e_3, _a, e_4, _b, e_5, _c; - var _d, _e; - var sus = fishP.suspicionLevel(); - var timeSinceJoin = Date.now() - fishP.lastJoined; - var target; - if (message.startsWith("/votekick")) { - var id = (_d = message.split(" ")[1]) === null || _d === void 0 ? void 0 : _d.split("#")[1]; - target = Groups.player.getByID(Number(id)); - if (!target) - return; //invalid votekick command, harmless - } - else { //TODO these "harmless" actions could be indications of a malfunctioning vkbot and should be logged if they repeat a lot (eg more than 5 times per minute) - if (!Vars.netServer.currentlyKicking) - return; //nobody to votekick, harmless - target = Reflect.get(Vars.netServer.currentlyKicking, "target"); - } - var targetSusLevel = FishPlayer.get(target).suspicionLevel(); - //Evaluate if this action should be blocked - if (sus <= 1) - return; - var reason = undefined; - if (!this.votekickActionRate.allow(108000, 8)) - reason = "Exceeded 8 votekick actions in the last 2 minutes"; - else if (sus == 3 && this.lastVKActions.find(function (a) { return Date.now() - a.time < 10000 && a.playerSusLevel == 3; }) && timeSinceJoin < 6000) - reason = "Performed votekick within 6 seconds of joining and there was a recent suspicious vote"; - else if (sus == 3 && timeSinceJoin < 80000 && this.lastVKActions.find(function (a) { return a.player == fishP; }) && targetSusLevel <= 1) - reason = "Two votekick actions within 80 seconds of joining and the target is not suspicious"; - else if (sus >= 2 && this.lastVKActions.filter(function (a) { return a.playerSusLevel == 3 && Date.now() - a.time < 33000; }).length >= 3) - reason = "More than 3 recent votekick actions by suspicious players"; - else if (sus >= 2 && this.lastVKActions.filter(function (a) { return a.playerSusLevel >= 2; }).length >= 6 && this.lastVKActions.filter(function (a) { return a.player == fishP; }).length >= 3) - reason = "More than 6 slightly suspicious votekick actions within the past 20 minutes and this player has already performed 3 of them"; - if (reason != undefined) { - //Should we ban everyone? - var suspiciousActions = this.lastVKActions.filter(function (action) { - return (action.playerSusLevel == 3 || (action.targetSusLevel <= 2 && action.playerSusLevel >= 2) || action.player == fishP) && Date.now() - action.time < 78000; - }); - if (suspiciousActions.length >= 3) { - //Ban everyone - var playersToBan = suspiciousActions.map(function (a) { return a.player; }).reduce(function (map, p) { - var _a; - map.set(p, ((_a = map.get(p)) !== null && _a !== void 0 ? _a : 0) + 1); - return map; - }, new Map()); - //Only ban players that appeared in the list twice or are high suslevel - var admins = Vars.netServer.admins; - try { - for (var playersToBan_1 = __values(playersToBan), playersToBan_1_1 = playersToBan_1.next(); !playersToBan_1_1.done; playersToBan_1_1 = playersToBan_1.next()) { - var _f = __read(playersToBan_1_1.value, 2), p = _f[0], times = _f[1]; - if (p.suspicionLevel() == 3 || p.suspicionLevel() == 2 && times > 1) { - admins.banPlayerID(p.uuid); - admins.banPlayerIP(p.ip()); - api.ban({ ip: p.ip(), uuid: p.uuid }); - (0, utils_1.logHTrip)(p, "votekick abuse", (p == fishP ? "Player banned automatically" : "Player banned automatically based on previous activity") + - ". Trigger reason: ".concat(reason)); - } - } - } - catch (e_3_1) { e_3 = { error: e_3_1 }; } - finally { - try { - if (playersToBan_1_1 && !playersToBan_1_1.done && (_a = playersToBan_1.return)) _a.call(playersToBan_1); - } - finally { if (e_3) throw e_3.error; } - } - (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked automatically for suspected votekick abuse."); }); - //Pardon most of the votekick targets (the ones that weren't voted on by a non-sus player) - var candidatePardons = new Set(FishPlayer.lastVKActions.map(function (a) { return a.target; })); - try { - for (var _g = __values(FishPlayer.lastVKActions), _h = _g.next(); !_h.done; _h = _g.next()) { - var action = _h.value; - if (action.playerSusLevel <= 1) - candidatePardons.delete(action.target); - } - } - catch (e_4_1) { e_4 = { error: e_4_1 }; } - finally { - try { - if (_h && !_h.done && (_b = _g.return)) _b.call(_g); - } - finally { if (e_4) throw e_4.error; } - } - var playersToPardon = __spreadArray([], __read(candidatePardons), false).map(FishPlayer.get); - try { - //Don't pardon players with suslevel 3 - for (var playersToPardon_1 = __values(playersToPardon), playersToPardon_1_1 = playersToPardon_1.next(); !playersToPardon_1_1.done; playersToPardon_1_1 = playersToPardon_1.next()) { - var p = playersToPardon_1_1.value; - if (!p.isSuspicious("high")) { - p.info().lastKicked = 0; - admins.kickedIPs.remove(p.ip()); - Log.info("Pardoned player @ (@/@)", p.name, p.uuid, p.ip()); - (0, utils_1.logAction)("pardoned", "automod", p, "kicked by suspected votekick bot"); - } - } - } - catch (e_5_1) { e_5 = { error: e_5_1 }; } - finally { - try { - if (playersToPardon_1_1 && !playersToPardon_1_1.done && (_c = playersToPardon_1.return)) _c.call(playersToPardon_1); - } - finally { if (e_5) throw e_5.error; } - } - } - else { - //Just kick the player - (0, utils_1.logHTrip)(fishP, "votekick abuse", "sus=".concat(sus)); - fishP.kick("You have been kicked [accent]automatically[] due to suspicious behavior. Please wait [accent]35[] seconds before rejoining.", 30000); - Call.sendMessage("[scarlet]Player [yellow]".concat(fishP.prefixedName, "[scarlet] was kicked due to suspected votekick abuse.")); - //If this message is going to start a votekick, cancel it - if (message.startsWith("/votekick") && Vars.netServer.currentlyKicking == null) - Core.app.post(function () { - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.name, "[lightgray].[accent] (-\u221E/").concat(Vars.netServer.votesRequired(), ")\n\t\t[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.")); - if (Vars.netServer.currentlyKicking) - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - }); - //If there is an ongoing votekick and the initiator is suspicious, cancel that - else if (((_e = FishPlayer.lastVKActions.slice().reverse().find(function (a) { return a.type == "start"; })) === null || _e === void 0 ? void 0 : _e.playerSusLevel) == 3) { - Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.name, "[lightgray].[accent] (-\u221E/").concat(Vars.netServer.votesRequired(), ")\n\t\t[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.")); - if (Vars.netServer.currentlyKicking) - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - } - //Otherwise, revoke the vote - else - Core.app.post(function () { - if (Vars.netServer.currentlyKicking) { - var votes = Reflect.get(Vars.netServer.currentlyKicking, "votes") - 1; - Reflect.set(Vars.netServer.currentlyKicking, "votes", votes); - var voted = Reflect.get(Vars.netServer.currentlyKicking, "voted"); - voted.put(fishP.uuid, 0); - voted.put(fishP.ip(), 0); - Call.sendMessage("[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff."); - } - }); - } - } - //Update state to catch future actions - this.lastVKActions.push({ - player: fishP, - playerSusLevel: sus, - target: target, - targetSusLevel: targetSusLevel, - time: Date.now(), - type: message.startsWith("/votekick") ? "start" : "vote y", - reason: message.startsWith("/votekick") ? message.split(" ").slice(2).join(" ") : undefined - }); - this.lastVKActions = this.lastVKActions.filter(function (a) { return Date.now() - a.time < funcs_1.Duration.minutes(10); }); - }; - FishPlayer.onPlayerCommand = function (player, command, unjoinedRawArgs) { - if (command == "msg" && unjoinedRawArgs[1] == "Please do not use that logic, as it is attem83 logic and is bad to use. For more information please read www.mindustry.dev/attem") - return; //Attemwarfare message, not sent by the player - player.lastActive = Date.now(); - }; - FishPlayer.onGameOver = function (winningTeam) { - var _this = this; - globals_1.FishEvents.fire("gameOver", [winningTeam]); - this.forEachPlayer(function (fishPlayer) { - //Clear temporary states such as menu and taphandler - fishPlayer.activeMenus = []; - fishPlayer.tapInfo.commandName = null; - //Update stats - if (!_this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict) { - fishPlayer.updateStats(function (stats) { return stats.gamesFinished++; }); - if (fishPlayer.changedTeam) { - fishPlayer.sendMessage("Refusing to update stats due to a team change."); - } - else { - if (fishPlayer.team() == winningTeam) - fishPlayer.updateStats(function (stats) { return stats.gamesWon++; }); - } - } - fishPlayer.changedTeam = false; - fishPlayer.tstats.wavesSurvived = 0; - fishPlayer.tstats.blockInteractionsThisMap = 0; - }); - }; - FishPlayer.ignoreGameover = function (callback) { - this.ignoreGameOver = true; - callback(); - this.ignoreGameOver = false; - }; - FishPlayer.onGameBegin = function () { - var startTime = Date.now(); - FishPlayer.lastMapStartTime = startTime; - //wait 7 seconds for players to join - Timer.schedule(function () { return FishPlayer.forEachPlayer(function (p) { return p.tstats.lastMapStartTime = startTime; }); }, 7); - }; - /** Must be run on UnitChangeEvent. */ - FishPlayer.onUnitChange = function (player, unit) { - if (unit === null || unit === void 0 ? void 0 : unit.spawnedByCore) - this.onRespawn(player); - }; - FishPlayer.onRespawn = function (player) { - var fishP = this.get(player); - if (fishP.stelled()) - fishP.stopUnit(); - }; - FishPlayer.forEachPlayer = function (func) { - var _this = this; - Groups.player.each(function (player) { - if (player == null) { - Log.err(".FINDTAG. Groups.player.each() returned a null player???"); - return; - } - var fishP = _this.get(player); - func(fishP, player); - }); - }; - FishPlayer.mapPlayers = function (func) { - var _this = this; - var out = []; - Groups.player.each(function (player) { - if (player == null) { - Log.err(".FINDTAG. Groups.player.each() returned a null player???"); - return; - } - out.push(func(_this.get(player))); - }); - return out; - }; - FishPlayer.prototype.updateMemberExclusiveState = function () { - if (!this.hasPerm("member")) { - this.highlight = null; - this.rainbow = null; - } - }; - /** Updates the mindustry player's name, using the prefixes of the current rank and role flags. */ - FishPlayer.prototype.updateName = function () { - var e_6, _a; - var _b; - if (!this.connected() || !this.shouldUpdateName) - return; //No player, no need to update - var name = (_b = this.originalName) !== null && _b !== void 0 ? _b : this.name; - if (this.marked()) - this.showRankPrefix = true; - var prefix = ''; - if (!this.hasPerm("bypassNameCheck") && (0, utils_1.isImpersonator)(name, this.ranksAtLeast("admin"))) - prefix += "[scarlet]SUSSY IMPOSTOR[]"; - if (this.marked()) - prefix += config_1.prefixes.marked; - else if (this.autoflagged) - prefix += config_1.prefixes.flagged; - if (this.muted) - prefix += config_1.prefixes.muted; - if (this.afk()) - prefix += "[orange]\uE876 AFK \uE876 | [white]"; - if (this.showRankPrefix) { - try { - for (var _c = __values(this.flags), _d = _c.next(); !_d.done; _d = _c.next()) { - var flag = _d.value; - prefix += flag.prefix; - } - } - catch (e_6_1) { e_6 = { error: e_6_1 }; } - finally { - try { - if (_d && !_d.done && (_a = _c.return)) _a.call(_c); - } - finally { if (e_6) throw e_6.error; } - } - prefix += this.rank.prefix; - } - if (prefix.length > 0 && !prefix.endsWith(" ")) - prefix += " "; - var replacedName; - if ((0, utils_1.cleanText)(name, true).includes("hacker")) { - //"Don't be a script kiddie" - //-LiveOverflow, 2015 - if (/h.*a.*c.*k.*[3e].*r/i.test(name)) { //try to only replace the part that contains "hacker" if it can be found with a simple regex - replacedName = name.replace(/h.*a.*c.*k.*[3e].*r/gi, "[brown]script kiddie[]"); - } - else { - replacedName = "[brown]script kiddie"; - } - } - else if (this.name.endsWith("[") && !this.name.endsWith("[[")) { - replacedName = name + "["; - } - else - replacedName = name; - this.player.name = this.prefixedName = prefix + replacedName; - }; - FishPlayer.prototype.updateAdminStatus = function () { - if (!this.connected()) - return; - if (this.hasPerm("admin")) { - Vars.netServer.admins.adminPlayer(this.uuid, this.player.usid()); - this.player.admin = true; - } - else { - Vars.netServer.admins.unAdminPlayer(this.uuid); - this.player.admin = false; - } - }; - FishPlayer.prototype.updateAutoflaggedStatus = function () { - if (this.ranksAtLeast("active")) { - this.autoflagged = false; - } - }; - FishPlayer.prototype.checkAntiEvasion = function () { - var e_7, _a; - var _b, _c; - FishPlayer.updatePunishedIPs(); - try { - for (var _d = __values(FishPlayer.punishedIPs), _e = _d.next(); !_e.done; _e = _d.next()) { - var _f = __read(_e.value, 2), ip = _f[0], uuid = _f[1]; - if (ip == this.ip() && uuid != this.uuid && !this.ranksAtLeast("mod")) { - api.sendModerationMessage("Automatically banned player `".concat(this.cleanedName, "` (`").concat(this.uuid, "`/`").concat(this.ip(), "`) for suspected punishment evasion.\nPreviously used UUID `").concat(uuid, "`(").concat((_b = Vars.netServer.admins.getInfoOptional(uuid)) === null || _b === void 0 ? void 0 : _b.plainLastName(), "), currently using UUID `").concat(this.uuid, "` from the same IP address.")); - Log.warn("&yAutomatically banned player &b".concat(this.cleanedName, "&y (&b").concat(this.uuid, "&y/&b").concat(this.ip(), "&y) for suspected punishment evasion.\n&yPreviously used UUID &b").concat(uuid, "&y(&b").concat((_c = Vars.netServer.admins.getInfoOptional(uuid)) === null || _c === void 0 ? void 0 : _c.plainLastName(), "&y), currently using UUID &b").concat(this.uuid, "&y from the same IP address.")); - FishPlayer.messageStaff("[yellow]Automatically banned player [cyan]".concat(this.cleanedName, "[] for suspected punishment evasion.")); - Vars.netServer.admins.banPlayerIP(ip); - api.ban({ ip: ip, uuid: uuid }); - this.kick(Packets.KickReason.banned); - return false; - } - } - } - catch (e_7_1) { e_7 = { error: e_7_1 }; } - finally { - try { - if (_e && !_e.done && (_a = _d.return)) _a.call(_d); - } - finally { if (e_7) throw e_7.error; } - } - return true; - }; - FishPlayer.updatePunishedIPs = function () { - for (var i = 0; i < this.punishedIPs.length; i++) { - if (this.punishedIPs[i][2] < Date.now()) { - this.punishedIPs.splice(i, 1); - } - } - }; - FishPlayer.prototype.checkVPNAndJoins = function () { - var _this = this; - var ip = this.ip(); - var info = this.info(); - api.isVpn(ip, function (isVpn) { - if (isVpn) { - Log.warn("IP ".concat(ip, " was flagged as VPN. Flag rate: ").concat(FishPlayer.stats.numIpsFlagged, "/").concat(FishPlayer.stats.numIpsChecked, " (").concat(100 * FishPlayer.stats.numIpsFlagged / FishPlayer.stats.numIpsChecked, "%)")); - _this.ipDetectedVpn = true; - if (!FishPlayer.autoflagRate.allow(30000, 5)) { - FishPlayer.triggerAntibot(funcs_1.Duration.minutes(3), "rate of flagged IPs exceeded 5 / 30s", "automatic"); - return; - } - if ((info.timesJoined <= 1 || (FishPlayer.autoflagRate.occurences > 3 && info.timesJoined <= 10)) //is this smart? - && !_this.ranksAtLeast("active") - && FishPlayer.punishedIPs.length > 0) { - _this.autoflagged = true; - _this.stopUnit(); - _this.updateName(); - if (FishPlayer.shouldWhackFlaggedPlayers()) { - FishPlayer.whackFlaggedPlayers(); //calls whack all flagged players - } - else { - (0, utils_1.logAction)("autoflagged", "AntiVPN", _this); - api.sendStaffMessage("Autoflagged player ".concat(_this.name, "[cyan] for suspected vpn!"), "AntiVPN", true); - FishPlayer.messageStaff("[yellow]WARNING:[scarlet] player [cyan]\"".concat(_this.name, "[cyan]\"[yellow] is new (").concat(info.timesJoined - 1, " joins) and using a vpn. They have been automatically stopped and muted. Unless there is an ongoing griefer raid, they are most likely innocent. Free them with /free.")); - Log.warn("Player ".concat(_this.name, " (").concat(_this.uuid, ") was autoflagged.")); - void menus_1.Menu.buttons(_this, "[gold]Welcome to Fish Community!", "[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on."), [[ - { data: "Close", text: "Close" }, - { data: "Discord", text: config_1.FColor.discord("Discord") }, - ]]).then(function (option) { - if (option == "Discord") { - Call.openURI(_this.con, config_1.text.discordURL); - } - }); - _this.sendMessage("[gold]Welcome to Fish Community!\n[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on.")); - } - } - else if (info.timesJoined < 5) { - FishPlayer.messageStaff("[yellow]WARNING:[scarlet] player [cyan]\"".concat(_this.name, "[cyan]\"[yellow] is new (").concat(info.timesJoined - 1, " joins) and using a vpn.")); - } - } - else { - if (info.timesJoined == 1) { - FishPlayer.messageTrusted("[yellow]Player \"".concat(_this.cleanedName, "\" is on first join.")); - } - } - if (info.timesJoined == 1) { - var message = "&lrNew player joined: &c".concat(_this.cleanedName, "&lr (&c").concat(_this.uuid, "&lr/&c").concat(ip, "&lr)"); - //Add BEL, this causes an audible noise - if (globals.fishState.joinBell) - message += '\x07'; - Log.info(message); - } - }, function (err) { - Log.err("Error while checking for VPN status of ip ".concat(ip, "!")); - Log.err(err); - }); - }; - FishPlayer.prototype.validate = function () { - return this.checkName() && this.checkUsid() && this.checkAntiEvasion(); - }; - /** Checks if this player's name is allowed. */ - FishPlayer.prototype.checkName = function () { - if ((0, utils_1.matchFilter)(this.name, "name")) { - this.kick("[scarlet]\"".concat(this.name, "[scarlet]\" is not an allowed name because it contains a banned word.\n\nIf you are unable to change it, please download Mindustry from Steam or itch.io."), 1); - } - else if (Strings.stripColors(this.name.replace(/[\u3164]/g, "")).trim().length == 0) { - this.kick("[scarlet]\"".concat((0, funcs_1.escapeStringColorsClient)(this.name), "[scarlet]\" is not an allowed name because it is empty. Please change it."), 1); - } - else { - return true; - } - return false; - }; - /** Checks if this player's USID is correct. */ - FishPlayer.prototype.checkUsid = function () { - var storedUSID = this.usid; - var usidMissing = storedUSID == null || !storedUSID; - var receivedUSID = this.player.usid(); - if (this.hasPerm("usidCheck")) { - if (usidMissing) { - if (this.hasPerm("mod")) { - //Staff missing USID, don't let them in - Log.err("&rUSID missing for privileged player &c\"".concat(this.cleanedName, "\"&r: no stored usid, cannot authenticate.\nRun &lgsetusid ").concat(this.uuid, " ").concat(receivedUSID, "&fr if you have verified this connection attempt.")); - this.kick("Authorization failure! Please ask a staff member with Console Access to approve this connection.", 1); - FishPlayer.lastAuthKicked = this; - return false; - } - else { - Log.info("Acquired USID for player &c\"".concat(this.cleanedName, "\"&fr: &c\"").concat(receivedUSID, "\"&fr")); - } - } - else { - if (receivedUSID != storedUSID) { - Log.err("&rUSID mismatch for player &c\"".concat(this.cleanedName, "\"&r: stored usid is &c").concat(storedUSID, "&r, but they tried to connect with usid &c").concat(receivedUSID, "&r\nRun &lgsetusid ").concat(this.uuid, " ").concat(receivedUSID, "&fr if you have verified this connection attempt.")); - this.kick("Authorization failure!", 1); - FishPlayer.lastAuthKicked = this; - return false; - } - } - } - else { - if (!usidMissing && receivedUSID != storedUSID) { - Log.err("&rUSID mismatch for player &c\"".concat(this.cleanedName, "\"&r: stored usid is &c").concat(storedUSID, "&r, but they tried to connect with usid &c").concat(receivedUSID, "&r")); - } - } - this.usid = receivedUSID; - return true; - }; - FishPlayer.prototype.displayTrail = function () { - if (this.trail) - Call.effect(Fx[this.trail.type], this.player.x, this.player.y, 0, this.trail.color); - }; - FishPlayer.prototype.sendWelcomeMessage = function () { - var _this = this; - var appealLine = "To appeal, ".concat(config_1.FColor.discord(templateObject_3 || (templateObject_3 = __makeTemplateObject(["join our discord"], ["join our discord"]))), " with ").concat(config_1.FColor.discord(templateObject_4 || (templateObject_4 = __makeTemplateObject(["/discord"], ["/discord"]))), ", or ask a ").concat(ranks_1.Rank.mod.color, "staff member[] in-game."); - if (FishPlayer.dataFetchFailedUuids.has(this.uuid)) { - this.sendMessage(config_1.text.dataFetchFailed); - FishPlayer.dataFetchFailedUuids.delete(this.uuid); - } - if (this.marked()) - this.sendMessage("[gold]Hello there! You are currently [scarlet]marked as a griefer[]. You cannot do anything in-game while marked.\n".concat(appealLine, "\nYour mark will expire automatically ").concat(this.unmarkTime == globals.maxTime ? "in [red]never[]" : "[green]".concat((0, utils_1.formatTimeRelative)(this.unmarkTime), "[]"), ".\nWe apologize for the inconvenience.")); - else if (this.muted) - this.sendMessage("[gold]Hello there! You are currently [red]muted[]. You can still play normally, but cannot send chat messages to other non-staff players while muted.\n".concat(appealLine, "\nWe apologize for the inconvenience.")); - else if (this.autoflagged) - this.sendMessage("[gold]Hello there! You are currently [red]flagged as suspicious[]. You cannot do anything in-game.\n".concat(appealLine, "\nWe apologize for the inconvenience.")); - else if (!this.showRankPrefix) - this.sendMessage("[gold]Hello there! Your rank prefix is currently hidden. You can show it again by running [white]/vanish[]."); - else { - this.sendMessage(config_1.text.welcomeMessage()); - //show tips - var showAd = false; - if (Date.now() - this.lastShownAd > funcs_1.Duration.days(1)) { - this.lastShownAd = Date.now(); - this.showAdNext = true; - } - else if (this.lastShownAd == globals.maxTime) { - //this is the first time they joined, show ad the next time they join - this.showAdNext = true; - this.lastShownAd = Date.now(); - } - else if (this.showAdNext) { - this.showAdNext = false; - showAd = true; - } - var messagePool = showAd ? config_1.tips.ads : (config_1.Mode.isChristmas && Math.random() > 0.6) ? config_1.tips.christmas : config_1.tips.normal; - var messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; - var message_1 = showAd ? "[gold]".concat(messageText, "[]") : "[gold]Tip: ".concat(messageText, "[]"); - //Delay sending the message so it doesn't get lost in the spam of messages that usually occurs when you join - Timer.schedule(function () { return _this.sendMessage(message_1); }, 3); - } - }; - FishPlayer.prototype.checkAutoRanks = function () { - var e_8, _a; - var _this = this; - if (this.stelled()) - return; - var _loop_1 = function (rankToAssign) { - if (!this_1.ranksAtLeast(rankToAssign) && rankToAssign.autoRankData) { - if (this_1.joinsAtLeast(rankToAssign.autoRankData.joins) && - this_1.globalStats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && - this_1.globalStats.timeInGame >= rankToAssign.autoRankData.playtime && - this_1.globalStats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && - (Date.now() - this_1.globalFirstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin) { - void this_1.setRank(rankToAssign).then(function () { - return _this.sendMessage("You have been automatically promoted to rank ".concat(rankToAssign.coloredName(), "!")); - }); - } - } - }; - var this_1 = this; - try { - for (var _b = __values(ranks_1.Rank.autoRanks), _c = _b.next(); !_c.done; _c = _b.next()) { - var rankToAssign = _c.value; - _loop_1(rankToAssign); - } - } - catch (e_8_1) { e_8 = { error: e_8_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_8) throw e_8.error; } - } - }; - //#endregion - //#region I/O - FishPlayer.read = function (version, fishPlayerData, player) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j; - switch (version) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - (0, funcs_1.crash)("Version ".concat(version, " is not longer supported, this should not be possible")); - break; - case 10: { - var uuid = (_a = fishPlayerData.readString(2)) !== null && _a !== void 0 ? _a : (0, funcs_1.crash)("Failed to deserialize FishPlayer: UUID was null."); - var fishP = new this(uuid, { - name: (_b = fishPlayerData.readString(2)) !== null && _b !== void 0 ? _b : "Unnamed player [ERROR]", - muted: (function () { - var muted = fishPlayerData.readBool(); - void fishPlayerData.readBool(); //discard the stored data for autoflagged - return muted; - })(), - unmarkTime: fishPlayerData.readNumber(13), - highlight: fishPlayerData.readString(2), - history: fishPlayerData.readArray(function (str) { - var _a, _b; - return ({ - action: (_a = str.readString(2)) !== null && _a !== void 0 ? _a : "null", - by: (_b = str.readString(2)) !== null && _b !== void 0 ? _b : "null", - time: str.readNumber(15) - }); - }), - rainbow: (function (n) { return n == 0 ? null : { speed: n }; })(fishPlayerData.readNumber(2)), - rank: (_c = fishPlayerData.readString(2)) !== null && _c !== void 0 ? _c : "", - flags: fishPlayerData.readArray(function (str) { return str.readString(2); }, 2).filter(function (s) { return s != null; }), - usid: fishPlayerData.readString(2), - chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), - lastJoined: fishPlayerData.readNumber(15), - firstJoined: fishPlayerData.readNumber(15), - stats: { - blocksBroken: fishPlayerData.readNumber(10), - blocksPlaced: fishPlayerData.readNumber(10), - timeInGame: fishPlayerData.readNumber(15), - chatMessagesSent: fishPlayerData.readNumber(7), - gamesFinished: fishPlayerData.readNumber(5), - gamesWon: fishPlayerData.readNumber(5), - }, - showRankPrefix: fishPlayerData.readBool(), - }, player); - fishPlayerData.readNumber(1); //discard pollResponse - return fishP; - } - case 11: { - var uuid = (_d = fishPlayerData.readString(2)) !== null && _d !== void 0 ? _d : (0, funcs_1.crash)("Failed to deserialize FishPlayer: UUID was null."); - return new this(uuid, { - name: (_e = fishPlayerData.readString(2)) !== null && _e !== void 0 ? _e : "Unnamed player [ERROR]", - muted: (function () { - var muted = fishPlayerData.readBool(); - void fishPlayerData.readBool(); //discard the stored data for autoflagged - return muted; - })(), - unmarkTime: fishPlayerData.readNumber(13), - highlight: fishPlayerData.readString(2), - history: fishPlayerData.readArray(function (str) { - var _a, _b; - return ({ - action: (_a = str.readString(2)) !== null && _a !== void 0 ? _a : "null", - by: (_b = str.readString(2)) !== null && _b !== void 0 ? _b : "null", - time: str.readNumber(15) - }); - }), - rainbow: (function (n) { return n == 0 ? null : { speed: n }; })(fishPlayerData.readNumber(2)), - rank: (_f = fishPlayerData.readString(2)) !== null && _f !== void 0 ? _f : "", - flags: fishPlayerData.readArray(function (str) { return str.readString(2); }, 2).filter(function (s) { return s != null; }), - usid: fishPlayerData.readString(2), - chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), - lastJoined: fishPlayerData.readNumber(15), - firstJoined: fishPlayerData.readNumber(15), - stats: { - blocksBroken: fishPlayerData.readNumber(10), - blocksPlaced: fishPlayerData.readNumber(10), - timeInGame: fishPlayerData.readNumber(15), - chatMessagesSent: fishPlayerData.readNumber(7), - gamesFinished: fishPlayerData.readNumber(5), - gamesWon: fishPlayerData.readNumber(5), - }, - showRankPrefix: fishPlayerData.readBool(), - }, player); - } - case 12: { - var uuid = (_g = fishPlayerData.readString(2)) !== null && _g !== void 0 ? _g : (0, funcs_1.crash)("Failed to deserialize FishPlayer: UUID was null."); - return new this(uuid, { - name: (_h = fishPlayerData.readString(2)) !== null && _h !== void 0 ? _h : "Unnamed player [ERROR]", - muted: fishPlayerData.readBool(), - unmarkTime: fishPlayerData.readNumber(13), - highlight: fishPlayerData.readString(2), - history: fishPlayerData.readArray(function (str) { - var _a, _b; - return ({ - action: (_a = str.readString(2)) !== null && _a !== void 0 ? _a : "null", - by: (_b = str.readString(2)) !== null && _b !== void 0 ? _b : "null", - time: str.readNumber(15) - }); - }), - rainbow: (function (n) { return n == 0 ? null : { speed: n }; })(fishPlayerData.readNumber(2)), - rank: (_j = fishPlayerData.readString(2)) !== null && _j !== void 0 ? _j : "", - flags: fishPlayerData.readArray(function (str) { return str.readString(2); }, 2).filter(function (s) { return s != null; }), - usid: fishPlayerData.readString(2), - chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), - lastJoined: fishPlayerData.readNumber(15), - firstJoined: fishPlayerData.readNumber(15), - stats: { - blocksBroken: fishPlayerData.readNumber(10), - blocksPlaced: fishPlayerData.readNumber(10), - timeInGame: fishPlayerData.readNumber(15), - chatMessagesSent: fishPlayerData.readNumber(7), - gamesFinished: fishPlayerData.readNumber(5), - gamesWon: fishPlayerData.readNumber(5), - }, - showRankPrefix: fishPlayerData.readBool(), - }, player); - } - default: (0, funcs_1.crash)("Unknown save version ".concat(version)); - } - }; - FishPlayer.prototype.write = function (out) { - var _a, _b; - if (typeof this.unmarkTime === "string") - this.unmarkTime = 0; - out.writeString(this.uuid, 2); - out.writeString(this.name, 2, true); - out.writeBool(this.muted); - out.writeNumber(this.unmarkTime, 13); // this will stop working in 2286! https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs#Year_2286 - out.writeString(this.highlight, 2, true); - out.writeArray(this.history.slice(-5), function (i, str) { - str.writeString(i.action, 2); - str.writeString(i.by.slice(0, 98), 2, true); - str.writeNumber(i.time, 15); - }); - out.writeNumber((_b = (_a = this.rainbow) === null || _a === void 0 ? void 0 : _a.speed) !== null && _b !== void 0 ? _b : 0, 2); - out.writeString(this.rank.name, 2); - out.writeArray(Array.from(this.flags), function (f, str) { return str.writeString(f.name, 2); }, 2); - out.writeString(this.usid, 2); - out.writeEnumString(this.chatStrictness, ["chat", "strict"]); - out.writeNumber(this.lastJoined, 15); - out.writeNumber(this.firstJoined, 15); - out.writeNumber(this.stats.blocksBroken, 10, true); - out.writeNumber(this.stats.blocksPlaced, 10, true); - out.writeNumber(this.stats.timeInGame, 15, true); - out.writeNumber(this.stats.chatMessagesSent, 7, true); - out.writeNumber(this.stats.gamesFinished, 5, true); - out.writeNumber(this.stats.gamesWon, 5, true); - out.writeBool(this.showRankPrefix); - }; - /** Saves cached FishPlayers to JSON in Core.settings. */ - FishPlayer.saveAll = function (forceSaveSettings) { - if (forceSaveSettings === void 0) { forceSaveSettings = true; } - var out = new funcs_1.StringIO(); - out.writeNumber(this.saveVersion, 2); - out.writeArray(Object.entries(this.cachedPlayers).filter(function (_a) { - var _b = __read(_a, 2), uuid = _b[0], fishP = _b[1]; - return fishP.shouldCache(); - }), function (_a) { - var _b = __read(_a, 2), uuid = _b[0], player = _b[1]; - return player.write(out); - }, 6); - var string = out.string; - var numKeys = Math.ceil(string.length / this.chunkSize); - Core.settings.put('fish-subkeys', Packages.java.lang.Integer(numKeys)); - for (var i = 1; i <= numKeys; i++) { - Core.settings.put("fish-playerdata-part-".concat(i), string.slice(0, this.chunkSize)); - string = string.slice(this.chunkSize); - } - if (forceSaveSettings) - Core.settings.manualSave(); - }; - FishPlayer.prototype.shouldCache = function () { - return this.ranksAtLeast("mod"); - }; - FishPlayer.uploadAll = function () { - FishPlayer.forEachPlayer(function (fishP) { - return void api.setFishPlayerData(fishP.getData(), 1, true); - }); - }; - /** Does not include stats */ - FishPlayer.prototype.hasData = function () { - return (this.rank != ranks_1.Rank.player) || this.muted || (this.flags.size > 0) || this.chatStrictness != "chat"; - }; - FishPlayer.getFishPlayersString = function () { - if (Core.settings.has("fish-subkeys")) { - var subkeys = Core.settings.get("fish-subkeys", 1); - var string = ""; - for (var i = 1; i <= subkeys; i++) { - string += Core.settings.get("fish-playerdata-part-".concat(i), ""); - } - return string; - } - else { - return Core.settings.get("fish", ""); - } - }; - /** Loads cached FishPlayers from JSON in Core.settings. */ - FishPlayer.loadAll = function (string) { - var _this = this; - if (string === void 0) { string = this.getFishPlayersString(); } - try { - if (string == "") - return; //If it's empty, don't try to load anything - var out = new funcs_1.StringIO(string); - var version_1 = out.readNumber(2); - var players = out.readArray(function (str) { return FishPlayer.read(version_1, str, null); }, 6); - out.expectEOF(); - players.forEach(function (p) { return _this.cachedPlayers[p.uuid] = p; }); - } - catch (err) { - Log.err("[CRITICAL] FAILED TO LOAD CACHED FISH PLAYER DATA"); - Log.err((0, funcs_1.parseError)(err)); - Log.err("============================="); - Log.err(string); - Log.err("============================="); - } - }; - //#endregion - //#region antibot - FishPlayer.antiBotMode = function () { - return Date.now() < this.antibotExpires; - }; - FishPlayer.shouldKickNewPlayers = function () { - return false; - }; - FishPlayer.shouldWhackFlaggedPlayers = function () { - return Date.now() < this.antibotExpires; - }; - FishPlayer.whackFlaggedPlayers = function () { - this.forEachPlayer(function (p) { - if (p.autoflagged) { - Vars.netServer.admins.blacklistDos(p.ip()); - Log.info("&yAntibot killed connection ".concat(p.ip(), " due to flagged while under attack")); - p.player.kick(Packets.KickReason.banned, 10000000); - } - }); - }; - FishPlayer.triggerAntibot = function (duration, reason, category) { - if (category == "automatic") { - //Ping reports based on - if (Date.now() - this.antibotExpires > funcs_1.Duration.hours(1)) - api.sendModerationMessage("!!! ".concat(config_1.text.reportsPing, " Possible ongoing bot attack in **").concat(config_1.Gamemode.name(), "** Reason: ").concat((0, funcs_1.escapeTextDiscord)(reason))); - else if (Date.now() - this.antibotExpires > funcs_1.Duration.minutes(10)) - api.sendModerationMessage("!!! Possible ongoing bot attack in **".concat(config_1.Gamemode.name(), "** Reason: ").concat((0, funcs_1.escapeTextDiscord)(reason))); - } - if (Date.now() > this.antibotExpires || reason != this.lastAntibotReason) - Log.info("&yAntibot triggered: ".concat((0, funcs_1.escapeStringColorsServer)(reason))); - this.antibotExpires = Math.max(this.antibotExpires, Date.now() + duration); - this.lastAntibotReason = reason; - if (this.shouldWhackFlaggedPlayers()) - this.whackFlaggedPlayers(); - }; - FishPlayer.messageStaff = function (arg1, arg2) { - var message = arg2 ? "[gray]<[cyan]staff[gray]>[white]".concat(arg1, "[green]: [cyan]").concat(arg2) : arg1; - var messageReceived = false; - Groups.player.each(function (pl) { - var fishP = FishPlayer.get(pl); - if (fishP.hasPerm("mod")) { - pl.sendMessage(message); - messageReceived = true; - } - }); - return messageReceived; - }; - FishPlayer.messageTrusted = function (arg1, arg2) { - var message = arg2 ? "[gray]<[".concat(ranks_1.Rank.trusted.color, "]trusted[gray]>[white]").concat(arg1, "[green]: [cyan]").concat(arg2) : arg1; - FishPlayer.forEachPlayer(function (fishP) { - if (fishP.ranksAtLeast("trusted")) - fishP.sendMessage(message); - }); - }; - FishPlayer.messageMuted = function (arg1, arg2) { - var message = arg2 ? "[gray]<[red]muted[gray]>[white]".concat(arg1, "[coral]: [lightgray]").concat(arg2) : arg1; - var messageReceived = false; - Groups.player.each(function (pl) { - var fishP = FishPlayer.get(pl); - if (fishP.hasPerm("seeMutedMessages")) { - pl.sendMessage(message); - messageReceived = true; - } - }); - return messageReceived; - }; - FishPlayer.messageAllExcept = function (exclude, message) { - FishPlayer.forEachPlayer(function (fishP) { - if (fishP !== exclude) - fishP.sendMessage(message); - }); - }; - FishPlayer.messageAllWithPerm = function (perm, message) { - if (perm) { - FishPlayer.forEachPlayer(function (fishP) { - if (fishP.hasPerm(perm)) - fishP.sendMessage(message); - }); - } - else { - Call.sendMessage(message); - } - }; - FishPlayer.prototype.position = function () { - return "(".concat(Math.floor(this.player.x / 8), ", ").concat(Math.floor(this.player.y / 8), ")"); - }; - FishPlayer.prototype.connected = function () { - return this.player != null && !this.con.hasDisconnected; - }; - FishPlayer.prototype.voteWeight = function () { - //TODO vote weighting based on rank and joins - return 1; - }; - /** - * @returns whether a player can perform a moderation action on another player. - * @param disallowSameRank If false, then the action is also allowed on players of same rank. - * @param minimumLevel Permission required to ever be able to perform this moderation action. Default: mod. - */ - FishPlayer.prototype.canModerate = function (player, disallowSameRank, minimumLevel, allowSelfIfUnauthorized) { - if (disallowSameRank === void 0) { disallowSameRank = true; } - if (minimumLevel === void 0) { minimumLevel = "mod"; } - if (allowSelfIfUnauthorized === void 0) { allowSelfIfUnauthorized = false; } - if (player == this && allowSelfIfUnauthorized) - return true; - if (!this.hasPerm(minimumLevel)) - return; //players below mod rank have no moderation permissions and cannot moderate anybody, except themselves - if (player == this) - return true; - if (disallowSameRank) - return this.rank.level > player.rank.level; - else - return this.rank.level >= player.rank.level; - }; - FishPlayer.prototype.ranksAtLeast = function (rank) { - if (typeof rank == "string") - rank = ranks_1.Rank.getByName(rank); - return this.rank.level >= rank.level; - }; - FishPlayer.prototype.hasPerm = function (perm) { - return commands_1.Perm[perm].check(this); - }; - FishPlayer.prototype.unit = function (unit) { - if (unit) - return this.player.unit(unit); - else - return this.player.unit(); - }; - FishPlayer.prototype.team = function () { - return this.player.team(); - }; - FishPlayer.prototype.setTeam = function (team) { - var oldTeam = this.player.team(); - this.player.team(team); - globals.FishEvents.fire("playerTeamChange", [this, oldTeam]); - }; - Object.defineProperty(FishPlayer.prototype, "con", { - get: function () { - var _a; - return (_a = this.player) === null || _a === void 0 ? void 0 : _a.con; - }, - enumerable: false, - configurable: true - }); - FishPlayer.prototype.ip = function () { - if (this.connected()) - return this.player.con.address; - else - return this.info().lastIP; - }; - FishPlayer.prototype.info = function () { - return Vars.netServer.admins.getInfo(this.uuid); - }; - /** - * Sends this player a chat message. - * @param ratelimit Time in milliseconds before sending another ratelimited message. - */ - FishPlayer.prototype.sendMessage = function (message, ratelimit) { - var _a; - if (ratelimit === void 0) { ratelimit = 0; } - if (Date.now() - this.lastRatelimitedMessage >= ratelimit) { - (_a = this.player) === null || _a === void 0 ? void 0 : _a.sendMessage(message); - this.lastRatelimitedMessage = Date.now(); - } - }; - FishPlayer.prototype.hasFlag = function (flagName) { - var flag = ranks_1.RoleFlag.getByName(flagName); - if (flag) - return this.flags.has(flag); - else - return false; - }; - FishPlayer.prototype.forceRespawn = function () { - this.player.clearUnit(); - this.player.checkSpawn(); - }; - FishPlayer.prototype.getUsageData = function (command) { - var _a; - var _b; - return (_a = (_b = this.usageData)[command]) !== null && _a !== void 0 ? _a : (_b[command] = { - lastUsed: -1, - lastUsedSuccessfully: -1, - tapLastUsed: -1, - tapLastUsedSuccessfully: -1, - }); - }; - FishPlayer.prototype.immutable = function () { - return this.name == "\x5b\x23\x33\x31\x34\x31\x46\x46\x5d\x42\x61\x6c\x61\x4d\x5b\x23\x33\x31\x46\x46\x34\x31\x5d\x33\x31\x34" && this.rank == ranks_1.Rank.pi; - }; - FishPlayer.prototype.firstJoin = function () { - return this.info().timesJoined == 1; - }; - FishPlayer.prototype.joinsAtLeast = function (amount) { - return this.info().timesJoined >= amount; - }; - FishPlayer.prototype.joinsLessThan = function (amount) { - return this.info().timesJoined < amount; - }; - /** - * 3 for first join or less than 2 minutes in game - * 2 for relatively new players - * 1 for players who we're fairly certain are not griefers (10 joins, 150 chat messages, 2 hours ingame) - * 0 for active ranked players - */ - FishPlayer.prototype.suspicionLevel = function () { - if (this.ranksAtLeast("active") || this.stats.chatMessagesSent > 2000) - return 0; - if (this.info().timesJoined == 1 && this.stats.timeInGame <= funcs_1.Duration.hours(1) || - this.info().timesJoined == 2 && this.stats.timeInGame < funcs_1.Duration.minutes(8) || - this.stats.timeInGame < 120000) - return 3; - if ((+(this.info().timesJoined > 40) + - +(this.info().timesJoined > 10) + - +(this.stats.blocksBroken > 1000 && this.stats.blocksPlaced > 2000) + - +(this.stats.chatMessagesSent > 150) + - +(this.stats.timeInGame > funcs_1.Duration.hours(2)) + - +(this.stats.timeInGame > funcs_1.Duration.hours(5))) < 3) - return 2; - return 1; - }; - FishPlayer.prototype.isSuspicious = function (level) { - var num = this.suspicionLevel(); - switch (level) { - case "high": return num >= 3; - case "medium": return num >= 2; - case "low": return num >= 1; - } - }; - FishPlayer.prototype.updateStats = function (func) { - func(this.stats); - func(this.globalStats); - }; - /** - * Returns a score between 0 and 1, as an estimate of the player's skill level. - * Defaults to 0.2 (guessing that the best trusted players can beat 5 noobs) - */ - FishPlayer.prototype.teamBalanceScore = function () { - var _this = this; - /** A number between 0 and 0.7 */ - var score = (function () { - if (_this.stats.gamesFinished < 10) - return 0.2; - })(); - }; - //#endregion - //#region moderation - /** Records a moderation action taken on a player. */ - FishPlayer.prototype.addHistoryEntry = function (entry) { - this.history.push(entry); - }; - FishPlayer.addPlayerHistory = function (id, entry) { - var _a; - (_a = this.getById(id)) === null || _a === void 0 ? void 0 : _a.addHistoryEntry(entry); - }; - FishPlayer.prototype.marked = function () { - return this.unmarkTime > Date.now(); - }; - FishPlayer.prototype.afk = function () { - return Date.now() - this.lastActive > 60000 || this.manualAfk; - }; - FishPlayer.prototype.stelled = function () { - return this.marked() || this.autoflagged; - }; - FishPlayer.prototype.setUnmarkTimer = function (duration) { - var _this = this; - var oldUnmarkTime = this.unmarkTime; - Timer.schedule(function () { - if (_this.unmarkTime === oldUnmarkTime && _this.connected()) { - //Only run the code if the unmark time hasn't changed - _this.forceRespawn(); - _this.updateName(); - _this.sendMessage("[yellow]Your mark has automatically expired."); - } - }, duration / 1000); - }; - FishPlayer.prototype.kick = function (reason, duration) { - var _a; - if (reason === void 0) { reason = Packets.KickReason.kick; } - if (duration === void 0) { duration = 30000; } - (_a = this.player) === null || _a === void 0 ? void 0 : _a.kick(reason, duration); - }; - FishPlayer.prototype.setPunishedIP = function (duration) { - FishPlayer.punishedIPs.push([this.ip(), this.uuid, Date.now() + duration]); - }; - FishPlayer.removePunishedIP = function (target) { - var ipIndex; - if ((ipIndex = FishPlayer.punishedIPs.findIndex(function (_a) { - var _b = __read(_a, 1), ip = _b[0]; - return ip == target; - })) != -1) { - FishPlayer.punishedIPs.splice(ipIndex, 1); - return true; - } - else - return false; - }; - FishPlayer.removePunishedUUID = function (target) { - var uuidIndex; - if ((uuidIndex = FishPlayer.punishedIPs.findIndex(function (_a) { - var _b = __read(_a, 2), uuid = _b[1]; - return uuid == target; - })) != -1) { - FishPlayer.punishedIPs.splice(uuidIndex, 1); - return true; - } - else - return false; - }; - FishPlayer.prototype.trollName = function (name) { - this.shouldUpdateName = false; - this.player.name = name; - }; - FishPlayer.prototype.freeze = function () { - this.frozen = true; - this.sendMessage("You have been temporarily frozen."); - }; - FishPlayer.prototype.unfreeze = function () { - this.frozen = false; - }; - /** Sets the unmark time but doesn't stop the player's unit or send them a message. */ - FishPlayer.prototype.updateStopTime = function (duration) { - var _this = this; - return this.updateSynced(function () { - var time = Math.min(Date.now() + duration, globals.maxTime); - _this.unmarkTime = time; - _this.updateName(); - }, function () { return _this.setUnmarkTimer(duration); }); - }; - FishPlayer.prototype.stopUnit = function () { - var unit = this.unit(); - if (this.connected() && unit) { - if (unit.spawnedByCore) { - unit.type = UnitTypes.stell; - unit.health = UnitTypes.stell.health; - unit.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); - } - else { - this.forceRespawn(); - //This will cause FishPlayer.onRespawn to run, calling this function again, but then the player will be in a core unit, which can be safely stell'd - } - } - }; - //#endregion - //#region heuristics - FishPlayer.prototype.activateHeuristics = function () { - var _this = this; - if (config_1.Gamemode.hexed() || config_1.Gamemode.sandbox()) - return; - //Blocks broken check - if (this.joinsLessThan(5)) { - var tripped_1 = false; - Timer.schedule(function () { - if (_this.connected() && !tripped_1) { - if (_this.tstats.blocksBroken > config_1.heuristics.blocksBrokenAfterJoin) { - tripped_1 = true; - (0, utils_1.logHTrip)(_this, "blocks broken after join", "".concat(_this.tstats.blocksBroken, "/").concat(config_1.heuristics.blocksBrokenAfterJoin)); - void _this.stop("automod", globals.maxTime, "Automatic stop due to suspicious activity"); - FishPlayer.messageAllExcept(_this, "[yellow]Player ".concat(_this.cleanedName, " has been stopped automatically due to suspected griefing.\nPlease look at ").concat(_this.position(), " and see if they were actually griefing. If they were not, please inform a staff member.")); - } - } - }, 0, 1, this.firstJoin() ? 30 : this.joinsLessThan(3) ? 25 : 15); - } - }; - //#region Static constants - /** Save version used for serialized FishPlayers. */ - FishPlayer.saveVersion = 12; - /** Maximum chunk size used when writing FishPlayer data to Core.settings. */ - FishPlayer.chunkSize = 50000; - //#endregion - //#region Static transients - /** Stores all currently loaded FishPlayer objects. */ - FishPlayer.cachedPlayers = {}; - FishPlayer.stats = { - numIpsChecked: 0, - numIpsFlagged: 0, - numIpsErrored: 0, - }; - /** The last player that was kicked due to a USID mismatch. */ - FishPlayer.lastAuthKicked = null; - /** - * List of IPs that were recently punished. - * If a new account joins from one of these IPs, - * we assume they are trying to evade the punishment - * and the IP gets banned. - */ - FishPlayer.punishedIPs = []; - FishPlayer.lastMapStartTime = 0; - /** Stores the 10 most recent players that left. */ - FishPlayer.recentLeaves = []; - //Used for the antibot. Some of these values are reset by timers. - FishPlayer.antibotExpires = -1; - FishPlayer.lastAntibotReason = ""; - FishPlayer.autoflagRate = new Ratekeeper(); - FishPlayer.connectRate = new Ratekeeper(); - FishPlayer.votekickActionRate = new Ratekeeper(); - FishPlayer.lastVKActions = []; - FishPlayer.search = (0, funcs_1.search)(function (p, str) { return p.uuid === str; }, function (p, str) { return p.player.id.toString() === str; }, function (p, str) { return p.name.toLowerCase() === str.toLowerCase(); }, - // (p, str) => p.cleanedName === str, - function (p, str) { return p.cleanedName.toLowerCase() === str.toLowerCase(); }, function (p, str) { return p.name.toLowerCase().includes(str.toLowerCase()); }, - // (p, str) => p.cleanedName.includes(str), - function (p, str) { return p.cleanedName.toLowerCase().includes(str.toLowerCase()); }); - //#endregion - //#region datasync - //Please see docs/data-management.md for a description of the update syncing algorithm. - FishPlayer.dataFetchFailedUuids = new Set(); - FishPlayer.easterEggVotekickTarget = null; - FishPlayer.ignoreGameOver = false; - return FishPlayer; -}()); -exports.FishPlayer = FishPlayer; -//TODO convert all the unnecessary event handlers to simple calls to Events.on -Events.on(EventType.WaveEvent, function () { return FishPlayer.forEachPlayer(function (p) { return p.tstats.wavesSurvived++; }); }); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the FishPlayer class, and many player-related functions. +*/ +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FishPlayer = void 0; +var api = __importStar(require("/api")); +var config_1 = require("/config"); +var commands_1 = require("/frameworks/commands"); +var menus_1 = require("/frameworks/menus"); +var funcs_1 = require("/funcs"); +var globals = __importStar(require("/globals")); +var globals_1 = require("/globals"); +var maps_1 = require("/maps"); +var ranks_1 = require("/ranks"); +var utils_1 = require("/utils"); +var FishPlayer = /** @class */ (function () { + //#endregion + function FishPlayer(uuid, data, player) { + //#endregion + //#region Transient properties + //Commands framework + /** Front-to-back queue of menus to show. */ + this.activeMenus = []; + /** Mapping from command to usage data. */ + this.usageData = {}; + this.tapInfo = { + commandName: null, + lastArgs: {}, + mode: "once", + }; + //Misc + this.player = null; + /** Used for the /trail command. */ + this.trail = null; + this.cleanedName = "Unnamed player [ERROR}"; + this.prefixedName = "Unnamed player [ERROR}"; + /** Used to freeze players when votekicking. */ + this.frozen = false; + /** Used to avoid spamming players with ads by the tip message system */ + this.lastShownAd = globals.maxTime; + /** Used to avoid spamming players with ads by the tip message system */ + this.showAdNext = false; + /** Transient statistics, used by the automatic griefer detection. */ + this.tstats = { + //remember to clear this in updateSavedInfoFromPlayer! + blocksBroken: 0, + blockInteractionsThisMap: 0, + lastMapStartTime: 0, + lastMapPlayedTime: 0, + wavesSurvived: 0, + }; + /** Whether the player has manually marked themselves as AFK. */ + this.manualAfk = false; + //Used for AFK detection. + this.lastMousePosition = [0, 0]; + this.lastUnitPosition = [0, 0]; + this.lastActive = Date.now(); + /** Set this to false to disable automatic name updates. Used for the rename console command. */ + this.shouldUpdateName = true; + /** Used by the sendMessage() ratelimit system. */ + this.lastRatelimitedMessage = -1; + /** Keeps track of whether a player has changed team this match, for win rate calculation. */ + this.changedTeam = false; + /** Whether the player's IP was detected as a VPN. */ + this.ipDetectedVpn = false; + /** + * If a player's IP is detected as a VPN on their first join, + * they are autoflagged and cannot build or talk in chat. + */ + this.autoflagged = false; + /** Timestamp until which this player will not be allowed to control units. */ + this.blockedFromPossessingUnitsUntil = 0; + /** Timestamp until which this player will not be allowed to control units. */ + this.blockedFromCommandingUnitsUntil = 0; + // Used by the data syncing framework. + this.infoUpdated = false; + this.dataSynced = false; + this.restoreTeam = null; + this.name = "Unnamed player [ERROR}"; + this.muted = false; + this.unmarkTime = -1; + this.rank = ranks_1.Rank.player; + this.flags = new Set(); + /** Used to color chat messages for the member command */ + this.highlight = null; + /** Used to color the player's name for the member command */ + this.rainbow = null; + /** List of all moderation actions that have been performed on this player. */ + this.history = []; + /** + * The USID for this player. + * USID stands for Unique Server IDentifier. It is like a UUID, but unique to each server (by IP and port). + * It cannot be viewed by admins and it cannot be obtained by other servers. + */ + this.usid = null; + /** If chat strictness is set to "strict", the player will not be allowed to swear. */ + this.chatStrictness = "chat"; + /** -1 represents unknown */ + this.lastJoined = -1; + /** -1 represents unknown */ + this.firstJoined = -1; + /** -1 represents unknown */ + this.globalLastJoined = -1; + /** -1 represents unknown */ + this.globalFirstJoined = -1; + this.stats = { + blocksBroken: 0, + blocksPlaced: 0, + timeInGame: 0, + chatMessagesSent: 0, + gamesFinished: 0, + gamesWon: 0, + }; + this.globalStats = this.stats; + /** Used for the /vanish command. */ + this.showRankPrefix = true; + this.achievements = new Bits(); + this.uuid = uuid; + this.player = player; + this.updateData(data); + } + //#region getplayer + //Contains methods used to get FishPlayer instances. + FishPlayer.createFromPlayer = function (player) { + return new this(player.uuid(), {}, player); + }; + FishPlayer.createFromInfo = function (playerInfo) { + var _a; + return new this(playerInfo.id, { + uuid: playerInfo.id, + name: playerInfo.lastName, + usid: (_a = playerInfo.adminUsid) !== null && _a !== void 0 ? _a : null + }, null); + }; + FishPlayer.getFromInfo = function (playerInfo) { + var _a; + var _b, _c; + return (_a = (_b = FishPlayer.cachedPlayers)[_c = playerInfo.id]) !== null && _a !== void 0 ? _a : (_b[_c] = FishPlayer.createFromInfo(playerInfo)); + }; + FishPlayer.get = function (player) { + var _a; + var _b, _c; + return (_a = (_b = FishPlayer.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = FishPlayer.createFromPlayer(player)); + }; + FishPlayer.resolve = function (player) { + var _a; + var _b, _c; + if (player instanceof FishPlayer) + return player; + else + return (_a = (_b = FishPlayer.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = FishPlayer.createFromPlayer(player)); + }; + FishPlayer.getById = function (id) { + var _a; + return (_a = this.cachedPlayers[id]) !== null && _a !== void 0 ? _a : null; + }; + /** Returns the FishPlayer representing the first online player matching a given name. */ + FishPlayer.getByName = function (name) { + if (name == "") + return null; + var realPlayer = Groups.player.find(function (p) { + return p.name === name || + p.name.includes(name) || + p.name.toLowerCase().includes(name.toLowerCase()) || + Strings.stripColors(p.name).toLowerCase() === name.toLowerCase() || + Strings.stripColors(p.name).toLowerCase().includes(name.toLowerCase()) || + false; + }); + return realPlayer ? this.get(realPlayer) : null; + }; + ; + /** Returns the FishPlayers representing all online players matching a given name. */ + FishPlayer.getAllByName = function (name, strict) { + if (strict === void 0) { strict = true; } + if (name == "") + return []; + var output = []; + Groups.player.each(function (p) { + var fishP = FishPlayer.get(p); + if (fishP.connected() && fishP.cleanedName.includes(name) || (!strict && fishP.cleanedName.toLowerCase().includes(name))) + output.push(fishP); + }); + return output; + }; + FishPlayer.getOneMindustryPlayerByName = function (str) { + var e_1, _a; + if (str == "") + return "none"; + var players = (0, funcs_1.setToArray)(Groups.player); + var matchingPlayers; + var filters = [ + function (p) { return p.name === str; }, + // p => Strings.stripColors(p.name) === str, + function (p) { return Strings.stripColors(p.name).toLowerCase() === str.toLowerCase(); }, + // p => p.name.includes(str), + function (p) { return p.name.toLowerCase().includes(str.toLowerCase()); }, + function (p) { return Strings.stripColors(p.name).includes(str); }, + function (p) { return Strings.stripColors(p.name).toLowerCase().includes(str.toLowerCase()); }, + ]; + try { + for (var filters_1 = __values(filters), filters_1_1 = filters_1.next(); !filters_1_1.done; filters_1_1 = filters_1.next()) { + var filter = filters_1_1.value; + matchingPlayers = players.filter(filter); + if (matchingPlayers.length == 1) + return matchingPlayers[0]; + else if (matchingPlayers.length > 1) + return "multiple"; + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (filters_1_1 && !filters_1_1.done && (_a = filters_1.return)) _a.call(filters_1); + } + finally { if (e_1) throw e_1.error; } + } + return "none"; + }; + //This method exists only because there is no easy way to turn an entitygroup into an array + FishPlayer.getAllOnline = function () { + var players = []; + Groups.player.each(function (p) { + var fishP = FishPlayer.get(p); + if (fishP.connected()) + players.push(fishP); + }); + return players; + }; + /** Returns all cached FishPlayers with names matching the search string. */ + FishPlayer.getAllOfflineByName = function (name) { + var e_2, _a; + var matching = []; + try { + for (var _b = __values(Object.entries(this.cachedPlayers)), _c = _b.next(); !_c.done; _c = _b.next()) { + var _d = __read(_c.value, 2), uuid = _d[0], player = _d[1]; + if (player.cleanedName.toLowerCase().includes(name)) + matching.push(player); + } + } + catch (e_2_1) { e_2 = { error: e_2_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_2) throw e_2.error; } + } + return matching; + }; + FishPlayer.onConnectPacket = function (_a) { + var _this = this; + var uuid = _a.uuid, name = _a.name; + var entry = this.cachedPlayers[uuid]; + if (entry) { + entry.infoUpdated = false; + entry.dataSynced = false; + entry.name = name; + } + api.getFishPlayerData(uuid).then(function (data) { + if (!data) + return; //nothing to sync + var fishP; + if (!(uuid in _this.cachedPlayers)) { + fishP = new FishPlayer(uuid, data, null); + fishP.originalName = name; + fishP.dataSynced = true; + _this.cachedPlayers[uuid] = fishP; + } + else { + fishP = _this.cachedPlayers[uuid]; + fishP.dataSynced = true; + fishP.updateData(data); + if (fishP.infoUpdated) { + //Player has already connected + //Run it again + if (fishP.player) + fishP.updateSavedInfoFromPlayer(fishP.player, true); + } + else { + //Player has not connected yet, nothing further needed + } + } + if (fishP.connected()) { + fishP.checkUsid(); + fishP.updateMemberExclusiveState(); + fishP.updateName(); + fishP.updateAdminStatus(); + fishP.updateAutoflaggedStatus(); + fishP.checkAutoRanks(); + fishP.sendWelcomeMessage(); + } + }, function () { + var fishP = _this.cachedPlayers[uuid]; + fishP.updateAdminStatus(); + fishP.updateAutoflaggedStatus(); + fishP.sendWelcomeMessage(); + if (fishP === null || fishP === void 0 ? void 0 : fishP.player) + fishP.player.sendMessage(config_1.text.dataFetchFailed); + else + _this.dataFetchFailedUuids.add(uuid); + }); + }; + /** Must be called at player join, before updateName(). */ + FishPlayer.prototype.updateSavedInfoFromPlayer = function (player, repeated) { + if (repeated === void 0) { repeated = false; } + this.player = player; + if (repeated) { + this.name = this.originalName; + } + else { + this.originalName = this.name = player.name; + } + if (this.firstJoined < 1) + this.firstJoined = Date.now(); + //Do not update USID here + this.manualAfk = false; + this.cleanedName = Strings.stripColors(player.name); + this.lastJoined = Date.now(); + this.lastMousePosition = [0, 0]; + this.lastActive = Date.now(); + if (this.highlight === "[white]") + this.highlight = null; + this.shouldUpdateName = true; + this.changedTeam = false; + this.ipDetectedVpn = false; + this.tstats.blocksBroken = 0; + if (this.tstats.lastMapPlayedTime != FishPlayer.lastMapStartTime) { + this.tstats.blockInteractionsThisMap = 0; + this.tstats.lastMapPlayedTime = FishPlayer.lastMapStartTime; + } + this.infoUpdated = true; + }; + FishPlayer.prototype.updateData = function (data) { + var _a; + if (data.name != undefined) + this.name = data.name; + if (data.muted != undefined) + this.muted = data.muted; + if (data.unmarkTime != undefined) + this.unmarkTime = data.unmarkTime; + if (data.lastJoined != undefined) + this.lastJoined = data.lastJoined; + if (data.firstJoined != undefined) + this.firstJoined = data.firstJoined; + if (data.globalLastJoined != undefined) + this.globalLastJoined = data.globalLastJoined; + if (data.globalFirstJoined != undefined) + this.globalFirstJoined = data.globalFirstJoined; + if (data.highlight != undefined) + this.highlight = data.highlight; + if (data.history != undefined) + this.history = data.history; + if (data.rainbow != undefined) + this.rainbow = data.rainbow; + if (data.usid != undefined) + this.usid = data.usid; + if (data.chatStrictness != undefined) + this.chatStrictness = data.chatStrictness; + if (data.stats != undefined) + this.stats = data.stats; + if (data.globalStats != undefined) + this.globalStats = data.globalStats; + if (data.showRankPrefix != undefined) + this.showRankPrefix = data.showRankPrefix; + if (data.rank != undefined) + this.rank = (_a = ranks_1.Rank.getByName(data.rank)) !== null && _a !== void 0 ? _a : ranks_1.Rank.player; + if (data.flags != undefined) + this.flags = new Set(data.flags.map(ranks_1.RoleFlag.getByName).filter(Boolean)); + if (data.achievements != undefined) + this.achievements = JsonIO.read(Bits, "{bits:".concat(data.achievements, "}")); + }; + FishPlayer.prototype.getData = function () { + var _a = this, uuid = _a.uuid, name = _a.name, muted = _a.muted, unmarkTime = _a.unmarkTime, rank = _a.rank, flags = _a.flags, highlight = _a.highlight, rainbow = _a.rainbow, history = _a.history, usid = _a.usid, chatStrictness = _a.chatStrictness, lastJoined = _a.lastJoined, firstJoined = _a.firstJoined, stats = _a.stats, showRankPrefix = _a.showRankPrefix; + return { + uuid: uuid, + name: name, + muted: muted, + unmarkTime: unmarkTime, + highlight: highlight, + rainbow: rainbow, + history: history, + usid: usid, + chatStrictness: chatStrictness, + lastJoined: lastJoined, + firstJoined: firstJoined, + stats: stats, + showRankPrefix: showRankPrefix, + rank: rank.name, + flags: __spreadArray([], __read(flags.values()), false).map(function (f) { return f.name; }), + achievements: JsonIO.write(Reflect.get(this.achievements, "bits")) + }; + }; + /** Warning: the "update" callback is run twice. */ + FishPlayer.prototype.updateSynced = function (update, beforeFetch, afterFetch) { + return __awaiter(this, void 0, void 0, function () { + var data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + update(this); + beforeFetch === null || beforeFetch === void 0 ? void 0 : beforeFetch(this); + return [4 /*yield*/, api.getFishPlayerData(this.uuid)]; + case 1: + data = _a.sent(); + if (data) + this.updateData(data); + update(this); + //of course, this is a race condition + //but it's unlikely to happen + //could be fixed by transmitting the update operation to the server as a mongo update command + afterFetch === null || afterFetch === void 0 ? void 0 : afterFetch(this); + return [4 /*yield*/, api.setFishPlayerData(this.getData(), 1, false)]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + //#endregion + //#region actively synced data updates + FishPlayer.prototype.stop = function (by, duration, message, notify) { + var _this = this; + if (notify === void 0) { notify = true; } + if (duration > 60000) + this.setPunishedIP(config_1.stopAntiEvadeTime); + this.showRankPrefix = true; + return this.updateSynced(function () { + _this.unmarkTime = Date.now() + duration; + if (_this.unmarkTime > globals.maxTime) + _this.unmarkTime = globals.maxTime; + _this.updateName(); + }, function () { + _this.setUnmarkTimer(duration); + if (_this.connected() && notify) { + _this.stopUnit(); + _this.sendMessage(message + ? "[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer for reason: [white]".concat(message, "[]") + : "[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer."); + if (duration < funcs_1.Duration.hours(1)) { + //less than one hour + _this.sendMessage("[yellow]Your mark will expire in ".concat((0, utils_1.formatTime)(duration), ".")); + } + } + }, function () { return _this.addHistoryEntry({ + action: 'stopped', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + }); }); + }; + FishPlayer.prototype.free = function (by) { + var _this = this; + by !== null && by !== void 0 ? by : (by = "console"); + this.autoflagged = false; //Might as well set autoflagged to false + FishPlayer.removePunishedIP(this.ip()); + FishPlayer.removePunishedUUID(this.uuid); + return this.updateSynced(function () { + _this.unmarkTime = -1; + }, function () { + if (_this.connected()) { + _this.sendMessage('[yellow]Looks like someone had mercy on you.'); + _this.updateName(); + _this.forceRespawn(); + } + }, function () { return _this.addHistoryEntry({ + action: 'freed', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + }); }); + }; + FishPlayer.prototype.setRank = function (rank) { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (typeof rank === "string" || !rank) { + rank; + (0, funcs_1.crash)("Type error in FishPlayer.setFlag(): rank is invalid"); + } + if (rank == ranks_1.Rank.pi && !config_1.Mode.localDebug) + throw new TypeError("Cannot find function setRank in object [object Object]."); + return [4 /*yield*/, this.updateSynced(function () { + _this.rank = rank; + _this.updateName(); + _this.updateAdminStatus(); + }, function () { return FishPlayer.saveAll(); })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + FishPlayer.prototype.setFlag = function (flag_, value) { + return __awaiter(this, void 0, void 0, function () { + var flag; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + flag = typeof flag_ == "string" ? + (ranks_1.RoleFlag.getByName(flag_)) + : flag_; + // eslint-disable-next-line @typescript-eslint/no-base-to-string + if (!flag) + (0, funcs_1.crash)("Type error in FishPlayer.setFlag(): flag ".concat(String(flag_), " is invalid")); + return [4 /*yield*/, this.updateSynced(function () { + if (value) { + _this.flags.add(flag); + } + else { + _this.flags.delete(flag); + } + _this.updateMemberExclusiveState(); + _this.updateName(); + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + FishPlayer.prototype.mute = function (by) { + var _this = this; + if (this.muted) + return; + this.showRankPrefix = true; + return this.updateSynced(function () { + _this.muted = true; + _this.updateName(); + }, function () { + _this.sendMessage("[yellow]Hey! You have been muted. You cannot send messages to other players. You can still send messages to staff members."); + _this.setPunishedIP(config_1.stopAntiEvadeTime); + }, function () { return _this.addHistoryEntry({ + action: 'muted', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + }); }); + }; + FishPlayer.prototype.unmute = function (by) { + var _this = this; + if (!this.muted) + return; + FishPlayer.removePunishedIP(this.ip()); + FishPlayer.removePunishedUUID(this.uuid); + return this.updateSynced(function () { + _this.muted = false; + _this.updateName(); + }, function () { + _this.sendMessage("[green]You have been unmuted."); + }, function () { return _this.addHistoryEntry({ + action: 'muted', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + }); }); + }; + //#endregion + //#region eventhandling + //Contains methods that handle an event and must be called by other code (usually through Events.on). + /** Must be run on PlayerConnectEvent. */ + FishPlayer.onPlayerConnect = function (player) { + var _a; + var _b, _c; + var fishPlayer = (_a = (_b = this.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = this.createFromPlayer(player)); + var previousJoin = fishPlayer.lastJoined; + fishPlayer.updateSavedInfoFromPlayer(player); + if (fishPlayer.validate()) { + if (!fishPlayer.hasPerm("bypassNameCheck")) { + var message = (0, utils_1.isImpersonator)(fishPlayer.name, fishPlayer.ranksAtLeast("admin")); + if (message !== false) { + fishPlayer.sendMessage("[scarlet]\u26A0[] [gold]Oh no! Our systems think you are a [scarlet]SUSSY IMPERSONATOR[]!\n[gold]Reason: ".concat(message, "\n[gold]Change your name to remove the tag.")); + } + else if ((0, utils_1.cleanText)(player.name, true).includes("hacker")) { + fishPlayer.sendMessage("[scarlet]\u26A0 Don't be a script kiddie!"); + globals_1.FishEvents.fire("scriptKiddie", [fishPlayer]); + } + } + fishPlayer.updateAdminStatus(); + fishPlayer.checkVPNAndJoins(); + fishPlayer.updateName(); + //I think this is a better spot for this + if (fishPlayer.firstJoin()) + void menus_1.Menu.menu("Rules for [#0000ff] >|||> FISH [white] servers [white]", config_1.rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].", ["[green]I understand and agree to these terms"], fishPlayer); + } + }; + /** Must be run on PlayerJoinEvent. */ + FishPlayer.onPlayerJoin = function (player) { + var _this = this; + var _a; + var _b, _c; + var fishPlayer = (_a = (_b = this.cachedPlayers)[_c = player.uuid()]) !== null && _a !== void 0 ? _a : (_b[_c] = (function () { + Log.err("onPlayerJoin: no fish player was created? ".concat(player.uuid())); + return _this.createFromPlayer(player); + })()); + //Don't activate heuristics until they've joined + //a lot of time can pass between connect and join + //also the player might connect but fail to join for a lot of reasons, + //or connect, fail to join, then connect again and join successfully + //which would cause heuristics to activate twice + fishPlayer.activateHeuristics(); + }; + FishPlayer.updateAFKCheck = function () { + //TODO better AFK check + this.forEachPlayer(function (fishP, mp) { + fishP.lastMousePosition = [mp.mouseX, mp.mouseY]; + fishP.lastUnitPosition = [mp.x, mp.y]; + fishP.updateName(); + }); + }; + /** Must be run on PlayerLeaveEvent. */ + FishPlayer.onPlayerLeave = function (player) { + var _a; + var fishP = this.cachedPlayers[player.uuid()]; + if (!fishP) + return; + if (Vars.netServer.currentlyKicking && + Reflect.get(Vars.netServer.currentlyKicking, "target") == player) { + //Anti votekick evasion + var votes_1 = Reflect.get(Vars.netServer.currentlyKicking, "votes"); + if ((function () { + if (fishP.hasPerm("bypassVotekick")) + return false; + if (fishP.hasPerm("bypassVoteFreeze")) + return votes_1 >= Vars.netServer.votesRequired(); + if (fishP.info().timesJoined > 50) + return votes_1 >= 2; + return votes_1 >= 1; + })()) { + var kickDuration = NetServer.kickDuration; + //Pass the votekick + Call.sendMessage("[orange]Vote passed.[scarlet] ".concat(player.name, "[orange] will be banned from the server for ").concat(kickDuration / 60, " minutes.")); + player.kick(Packets.KickReason.vote, kickDuration * 1000); //it is stored in seconds but needs to be converted to millis + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + } + } + //Clear temporary states such as menu and taphandler + fishP.activeMenus = []; + fishP.tapInfo.commandName = null; + fishP.updateStats(function (stats) { return stats.timeInGame += (Date.now() - fishP.lastJoined); }); //Time between joining and leaving + fishP.lastJoined = Date.now(); + this.recentLeaves.unshift(fishP); + if (this.recentLeaves.length > 10) + this.recentLeaves.pop(); + void api.setFishPlayerData(fishP.getData(), 1, true); + var currentRun = (_a = maps_1.PartialMapRun.current) === null || _a === void 0 ? void 0 : _a.startTime; + if (currentRun) + Core.app.post(function () { + //Wait for the /spectate command's handler to fix their team before saving it + fishP.restoreTeam = [fishP.player.team(), Date.now(), currentRun]; + }); + }; + FishPlayer.validateVotekickSession = function () { + var _a; + if (!Vars.netServer.currentlyKicking) + return; + var target = this.get(Reflect.get(Vars.netServer.currentlyKicking, "target")); + var voted = Reflect.get(Vars.netServer.currentlyKicking, "voted"); + if (voted.size == 2) { + //Try to find the UUID of the initiator + var uuid_1 = null; + voted.entries().toArray().each(function (e) { + if (globals_1.uuidPattern.test(e.key)) + uuid_1 = e.key; + }); + if (uuid_1) { + var initiator = this.getById(uuid_1); + if (initiator === null || initiator === void 0 ? void 0 : initiator.stelled()) { + if (initiator.hasPerm("bypassVotekick")) { + if (target !== this.easterEggVotekickTarget) { + this.easterEggVotekickTarget = target; + var msg = (_a = (new Error()).stack) === null || _a === void 0 ? void 0 : _a.split("\n").slice(0, 4).join("\n"); + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(initiator.prefixedName, "[lightgray].[accent] (\u221E/").concat(Vars.netServer.votesRequired(), ")\n\t[scarlet]Error: failed to kick player ").concat(initiator.name, "\n\t").concat(msg, "\n\t[scarlet]Error: failed to cancel votekick\n\t").concat(msg)); + } + return; + } + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(initiator.prefixedName, "[lightgray].[accent] (\u221E/").concat(Vars.netServer.votesRequired(), ")\n[scarlet]Vote passed.")); + initiator.kick("You are not allowed to votekick other players while marked.", 2); + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + return; + } + else if ((initiator === null || initiator === void 0 ? void 0 : initiator.hasPerm("immediatelyVotekickNewPlayers")) && target.isSuspicious("high") && !target.hasPerm("bypassVotekick")) { + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (").concat(Vars.netServer.votesRequired(), "/").concat(Vars.netServer.votesRequired(), ")\n[scarlet]Vote passed.")); + target.kick(Packets.KickReason.vote, funcs_1.Duration.minutes(30)); + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + return; + } + else if (target.isSuspicious("high") && !target.hasPerm("bypassVotekick") && !target.ranksAtLeast("trusted")) { + //Increase votes by 1, from 1 to 2 + Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(2)); + voted.put("__server__", 1); + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (2/").concat(Vars.netServer.votesRequired(), ")\n[lightgray]Type[orange] /vote [] to agree.")); + return; + } + } + } + if (target.hasPerm("bypassVotekick")) { + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (-\u221E/").concat(Vars.netServer.votesRequired(), ")\n[scarlet]Vote cancelled.")); + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + } + else if (target.ranksAtLeast("trusted") && Groups.player.size() > 4 && voted.get("__server__") == 0) { + //decrease votes by two, goes from 1 to negative 1 + Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(-1)); + voted.put("__server__", -2); + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.prefixedName, "[lightgray].[accent] (-1/").concat(Vars.netServer.votesRequired(), ")\n[lightgray]Type[orange] /vote [] to agree.")); + } + }; + FishPlayer.onPlayerChat = function (player, message) { + var fishP = this.get(player); + if (message.trim().toLowerCase().startsWith("/vote y") || message.startsWith("/votekick ")) { + this.checkVotekickAction(fishP, message); + } + fishP.lastActive = Date.now(); + fishP.updateStats(function (stats) { return stats.chatMessagesSent++; }); + }; + FishPlayer.checkVotekickAction = function (fishP, message) { + var e_3, _a, e_4, _b, e_5, _c; + var _d, _e; + var sus = fishP.suspicionLevel(); + var timeSinceJoin = Date.now() - fishP.lastJoined; + var target; + if (message.startsWith("/votekick")) { + var id = (_d = message.split(" ")[1]) === null || _d === void 0 ? void 0 : _d.split("#")[1]; + target = Groups.player.getByID(Number(id)); + if (!target) + return; //invalid votekick command, harmless + } + else { //TODO these "harmless" actions could be indications of a malfunctioning vkbot and should be logged if they repeat a lot (eg more than 5 times per minute) + if (!Vars.netServer.currentlyKicking) + return; //nobody to votekick, harmless + target = Reflect.get(Vars.netServer.currentlyKicking, "target"); + } + var targetSusLevel = FishPlayer.get(target).suspicionLevel(); + //Evaluate if this action should be blocked + if (sus <= 1) + return; + var reason = undefined; + if (!this.votekickActionRate.allow(108000, 8)) + reason = "Exceeded 8 votekick actions in the last 2 minutes"; + else if (sus == 3 && this.lastVKActions.find(function (a) { return Date.now() - a.time < 10000 && a.playerSusLevel == 3; }) && timeSinceJoin < 6000) + reason = "Performed votekick within 6 seconds of joining and there was a recent suspicious vote"; + else if (sus == 3 && timeSinceJoin < 80000 && this.lastVKActions.find(function (a) { return a.player == fishP; }) && targetSusLevel <= 1) + reason = "Two votekick actions within 80 seconds of joining and the target is not suspicious"; + else if (sus >= 2 && this.lastVKActions.filter(function (a) { return a.playerSusLevel == 3 && Date.now() - a.time < 33000; }).length >= 3) + reason = "More than 3 recent votekick actions by suspicious players"; + else if (sus >= 2 && this.lastVKActions.filter(function (a) { return a.playerSusLevel >= 2; }).length >= 6 && this.lastVKActions.filter(function (a) { return a.player == fishP; }).length >= 3) + reason = "More than 6 slightly suspicious votekick actions within the past 20 minutes and this player has already performed 3 of them"; + if (reason != undefined) { + //Should we ban everyone? + var suspiciousActions = this.lastVKActions.filter(function (action) { + return (action.playerSusLevel == 3 || (action.targetSusLevel <= 2 && action.playerSusLevel >= 2) || action.player == fishP) && Date.now() - action.time < 78000; + }); + if (suspiciousActions.length >= 3) { + //Ban everyone + var playersToBan = suspiciousActions.map(function (a) { return a.player; }).reduce(function (map, p) { + var _a; + map.set(p, ((_a = map.get(p)) !== null && _a !== void 0 ? _a : 0) + 1); + return map; + }, new Map()); + //Only ban players that appeared in the list twice or are high suslevel + var admins = Vars.netServer.admins; + try { + for (var playersToBan_1 = __values(playersToBan), playersToBan_1_1 = playersToBan_1.next(); !playersToBan_1_1.done; playersToBan_1_1 = playersToBan_1.next()) { + var _f = __read(playersToBan_1_1.value, 2), p = _f[0], times = _f[1]; + if (p.suspicionLevel() == 3 || p.suspicionLevel() == 2 && times > 1) { + admins.banPlayerID(p.uuid); + admins.banPlayerIP(p.ip()); + api.ban({ ip: p.ip(), uuid: p.uuid }); + (0, utils_1.logHTrip)(p, "votekick abuse", (p == fishP ? "Player banned automatically" : "Player banned automatically based on previous activity") + + ". Trigger reason: ".concat(reason)); + } + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (playersToBan_1_1 && !playersToBan_1_1.done && (_a = playersToBan_1.return)) _a.call(playersToBan_1); + } + finally { if (e_3) throw e_3.error; } + } + (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked automatically for suspected votekick abuse."); }); + //Pardon most of the votekick targets (the ones that weren't voted on by a non-sus player) + var candidatePardons = new Set(FishPlayer.lastVKActions.map(function (a) { return a.target; })); + try { + for (var _g = __values(FishPlayer.lastVKActions), _h = _g.next(); !_h.done; _h = _g.next()) { + var action = _h.value; + if (action.playerSusLevel <= 1) + candidatePardons.delete(action.target); + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (_h && !_h.done && (_b = _g.return)) _b.call(_g); + } + finally { if (e_4) throw e_4.error; } + } + var playersToPardon = __spreadArray([], __read(candidatePardons), false).map(FishPlayer.get); + try { + //Don't pardon players with suslevel 3 + for (var playersToPardon_1 = __values(playersToPardon), playersToPardon_1_1 = playersToPardon_1.next(); !playersToPardon_1_1.done; playersToPardon_1_1 = playersToPardon_1.next()) { + var p = playersToPardon_1_1.value; + if (!p.isSuspicious("high")) { + p.info().lastKicked = 0; + admins.kickedIPs.remove(p.ip()); + Log.info("Pardoned player @ (@/@)", p.name, p.uuid, p.ip()); + (0, utils_1.logAction)("pardoned", "automod", p, "kicked by suspected votekick bot"); + } + } + } + catch (e_5_1) { e_5 = { error: e_5_1 }; } + finally { + try { + if (playersToPardon_1_1 && !playersToPardon_1_1.done && (_c = playersToPardon_1.return)) _c.call(playersToPardon_1); + } + finally { if (e_5) throw e_5.error; } + } + } + else { + //Just kick the player + (0, utils_1.logHTrip)(fishP, "votekick abuse", "sus=".concat(sus)); + fishP.kick("You have been kicked [accent]automatically[] due to suspicious behavior. Please wait [accent]35[] seconds before rejoining.", 30000); + Call.sendMessage("[scarlet]Player [yellow]".concat(fishP.prefixedName, "[scarlet] was kicked due to suspected votekick abuse.")); + //If this message is going to start a votekick, cancel it + if (message.startsWith("/votekick") && Vars.netServer.currentlyKicking == null) + Core.app.post(function () { + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.name, "[lightgray].[accent] (-\u221E/").concat(Vars.netServer.votesRequired(), ")\n\t\t[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.")); + if (Vars.netServer.currentlyKicking) + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + }); + //If there is an ongoing votekick and the initiator is suspicious, cancel that + else if (((_e = FishPlayer.lastVKActions.slice().reverse().find(function (a) { return a.type == "start"; })) === null || _e === void 0 ? void 0 : _e.playerSusLevel) == 3) { + Call.sendMessage("[scarlet]Server[lightgray] has voted on kicking[orange] ".concat(target.name, "[lightgray].[accent] (-\u221E/").concat(Vars.netServer.votesRequired(), ")\n\t\t[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.")); + if (Vars.netServer.currentlyKicking) + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + } + //Otherwise, revoke the vote + else + Core.app.post(function () { + if (Vars.netServer.currentlyKicking) { + var votes = Reflect.get(Vars.netServer.currentlyKicking, "votes") - 1; + Reflect.set(Vars.netServer.currentlyKicking, "votes", votes); + var voted = Reflect.get(Vars.netServer.currentlyKicking, "voted"); + voted.put(fishP.uuid, 0); + voted.put(fishP.ip(), 0); + Call.sendMessage("[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff."); + } + }); + } + } + //Update state to catch future actions + this.lastVKActions.push({ + player: fishP, + playerSusLevel: sus, + target: target, + targetSusLevel: targetSusLevel, + time: Date.now(), + type: message.startsWith("/votekick") ? "start" : "vote y", + reason: message.startsWith("/votekick") ? message.split(" ").slice(2).join(" ") : undefined + }); + this.lastVKActions = this.lastVKActions.filter(function (a) { return Date.now() - a.time < funcs_1.Duration.minutes(10); }); + }; + FishPlayer.onPlayerCommand = function (player, command, unjoinedRawArgs) { + if (command == "msg" && unjoinedRawArgs[1] == "Please do not use that logic, as it is attem83 logic and is bad to use. For more information please read www.mindustry.dev/attem") + return; //Attemwarfare message, not sent by the player + player.lastActive = Date.now(); + }; + FishPlayer.onGameOver = function (winningTeam) { + var _this = this; + globals_1.FishEvents.fire("gameOver", [winningTeam]); + this.forEachPlayer(function (fishPlayer) { + //Clear temporary states such as menu and taphandler + fishPlayer.activeMenus = []; + fishPlayer.tapInfo.commandName = null; + //Update stats + if (!_this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict) { + fishPlayer.updateStats(function (stats) { return stats.gamesFinished++; }); + if (fishPlayer.changedTeam) { + fishPlayer.sendMessage("Refusing to update stats due to a team change."); + } + else { + if (fishPlayer.team() == winningTeam) + fishPlayer.updateStats(function (stats) { return stats.gamesWon++; }); + } + } + fishPlayer.changedTeam = false; + fishPlayer.tstats.wavesSurvived = 0; + fishPlayer.tstats.blockInteractionsThisMap = 0; + }); + }; + FishPlayer.ignoreGameover = function (callback) { + this.ignoreGameOver = true; + callback(); + this.ignoreGameOver = false; + }; + FishPlayer.onGameBegin = function () { + var startTime = Date.now(); + FishPlayer.lastMapStartTime = startTime; + //wait 7 seconds for players to join + Timer.schedule(function () { return FishPlayer.forEachPlayer(function (p) { return p.tstats.lastMapStartTime = startTime; }); }, 7); + }; + /** Must be run on UnitChangeEvent. */ + FishPlayer.onUnitChange = function (player, unit) { + if (unit === null || unit === void 0 ? void 0 : unit.spawnedByCore) + this.onRespawn(player); + }; + FishPlayer.onRespawn = function (player) { + var fishP = this.get(player); + if (fishP.stelled()) + fishP.stopUnit(); + }; + FishPlayer.forEachPlayer = function (func) { + var _this = this; + Groups.player.each(function (player) { + if (player == null) { + Log.err(".FINDTAG. Groups.player.each() returned a null player???"); + return; + } + var fishP = _this.get(player); + func(fishP, player); + }); + }; + FishPlayer.mapPlayers = function (func) { + var _this = this; + var out = []; + Groups.player.each(function (player) { + if (player == null) { + Log.err(".FINDTAG. Groups.player.each() returned a null player???"); + return; + } + out.push(func(_this.get(player))); + }); + return out; + }; + FishPlayer.prototype.updateMemberExclusiveState = function () { + if (!this.hasPerm("member")) { + this.highlight = null; + this.rainbow = null; + } + }; + /** Updates the mindustry player's name, using the prefixes of the current rank and role flags. */ + FishPlayer.prototype.updateName = function () { + var e_6, _a; + var _b; + if (!this.connected() || !this.shouldUpdateName) + return; //No player, no need to update + var name = (_b = this.originalName) !== null && _b !== void 0 ? _b : this.name; + if (this.marked()) + this.showRankPrefix = true; + var prefix = ''; + if (!this.hasPerm("bypassNameCheck") && (0, utils_1.isImpersonator)(name, this.ranksAtLeast("admin"))) + prefix += "[scarlet]SUSSY IMPOSTOR[]"; + if (this.marked()) + prefix += config_1.prefixes.marked; + else if (this.autoflagged) + prefix += config_1.prefixes.flagged; + if (this.muted) + prefix += config_1.prefixes.muted; + if (this.afk()) + prefix += "[orange]\uE876 AFK \uE876 | [white]"; + if (this.showRankPrefix) { + try { + for (var _c = __values(this.flags), _d = _c.next(); !_d.done; _d = _c.next()) { + var flag = _d.value; + prefix += flag.prefix; + } + } + catch (e_6_1) { e_6 = { error: e_6_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_6) throw e_6.error; } + } + prefix += this.rank.prefix; + } + if (prefix.length > 0 && !prefix.endsWith(" ")) + prefix += " "; + var replacedName; + if ((0, utils_1.cleanText)(name, true).includes("hacker")) { + //"Don't be a script kiddie" + //-LiveOverflow, 2015 + if (/h.*a.*c.*k.*[3e].*r/i.test(name)) { //try to only replace the part that contains "hacker" if it can be found with a simple regex + replacedName = name.replace(/h.*a.*c.*k.*[3e].*r/gi, "[brown]script kiddie[]"); + } + else { + replacedName = "[brown]script kiddie"; + } + } + else if (this.name.endsWith("[") && !this.name.endsWith("[[")) { + replacedName = name + "["; + } + else + replacedName = name; + this.player.name = this.prefixedName = prefix + replacedName; + }; + FishPlayer.prototype.updateAdminStatus = function () { + if (!this.connected()) + return; + if (this.hasPerm("admin")) { + Vars.netServer.admins.adminPlayer(this.uuid, this.player.usid()); + this.player.admin = true; + } + else { + Vars.netServer.admins.unAdminPlayer(this.uuid); + this.player.admin = false; + } + }; + FishPlayer.prototype.updateAutoflaggedStatus = function () { + if (this.ranksAtLeast("active")) { + this.autoflagged = false; + } + }; + FishPlayer.prototype.checkAntiEvasion = function () { + var e_7, _a; + var _b, _c; + FishPlayer.updatePunishedIPs(); + try { + for (var _d = __values(FishPlayer.punishedIPs), _e = _d.next(); !_e.done; _e = _d.next()) { + var _f = __read(_e.value, 2), ip = _f[0], uuid = _f[1]; + if (ip == this.ip() && uuid != this.uuid && !this.ranksAtLeast("mod")) { + api.sendModerationMessage("Automatically banned player `".concat(this.cleanedName, "` (`").concat(this.uuid, "`/`").concat(this.ip(), "`) for suspected punishment evasion.\nPreviously used UUID `").concat(uuid, "`(").concat((_b = Vars.netServer.admins.getInfoOptional(uuid)) === null || _b === void 0 ? void 0 : _b.plainLastName(), "), currently using UUID `").concat(this.uuid, "` from the same IP address.")); + Log.warn("&yAutomatically banned player &b".concat(this.cleanedName, "&y (&b").concat(this.uuid, "&y/&b").concat(this.ip(), "&y) for suspected punishment evasion.\n&yPreviously used UUID &b").concat(uuid, "&y(&b").concat((_c = Vars.netServer.admins.getInfoOptional(uuid)) === null || _c === void 0 ? void 0 : _c.plainLastName(), "&y), currently using UUID &b").concat(this.uuid, "&y from the same IP address.")); + FishPlayer.messageStaff("[yellow]Automatically banned player [cyan]".concat(this.cleanedName, "[] for suspected punishment evasion.")); + Vars.netServer.admins.banPlayerIP(ip); + api.ban({ ip: ip, uuid: uuid }); + this.kick(Packets.KickReason.banned); + return false; + } + } + } + catch (e_7_1) { e_7 = { error: e_7_1 }; } + finally { + try { + if (_e && !_e.done && (_a = _d.return)) _a.call(_d); + } + finally { if (e_7) throw e_7.error; } + } + return true; + }; + FishPlayer.updatePunishedIPs = function () { + for (var i = 0; i < this.punishedIPs.length; i++) { + if (this.punishedIPs[i][2] < Date.now()) { + this.punishedIPs.splice(i, 1); + } + } + }; + FishPlayer.prototype.checkVPNAndJoins = function () { + var _this = this; + var ip = this.ip(); + var info = this.info(); + api.isVpn(ip, function (isVpn) { + if (isVpn) { + Log.warn("IP ".concat(ip, " was flagged as VPN. Flag rate: ").concat(FishPlayer.stats.numIpsFlagged, "/").concat(FishPlayer.stats.numIpsChecked, " (").concat(100 * FishPlayer.stats.numIpsFlagged / FishPlayer.stats.numIpsChecked, "%)")); + _this.ipDetectedVpn = true; + if (!FishPlayer.autoflagRate.allow(30000, 5)) { + FishPlayer.triggerAntibot(funcs_1.Duration.minutes(3), "rate of flagged IPs exceeded 5 / 30s", "automatic"); + return; + } + if ((info.timesJoined <= 1 || (FishPlayer.autoflagRate.occurences > 3 && info.timesJoined <= 10)) //is this smart? + && !_this.ranksAtLeast("active") + && FishPlayer.punishedIPs.length > 0) { + _this.autoflagged = true; + _this.stopUnit(); + _this.updateName(); + if (FishPlayer.shouldWhackFlaggedPlayers()) { + FishPlayer.whackFlaggedPlayers(); //calls whack all flagged players + } + else { + (0, utils_1.logAction)("autoflagged", "AntiVPN", _this); + api.sendStaffMessage("Autoflagged player ".concat(_this.name, "[cyan] for suspected vpn!"), "AntiVPN", true); + FishPlayer.messageStaff("[yellow]WARNING:[scarlet] player [cyan]\"".concat(_this.name, "[cyan]\"[yellow] is new (").concat(info.timesJoined - 1, " joins) and using a vpn. They have been automatically stopped and muted. Unless there is an ongoing griefer raid, they are most likely innocent. Free them with /free.")); + Log.warn("Player ".concat(_this.name, " (").concat(_this.uuid, ") was autoflagged.")); + void menus_1.Menu.buttons(_this, "[gold]Welcome to Fish Community!", "[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on."), [[ + { data: "Close", text: "Close" }, + { data: "Discord", text: config_1.FColor.discord("Discord") }, + ]]).then(function (option) { + if (option == "Discord") { + Call.openURI(_this.con, config_1.text.discordURL); + } + }); + _this.sendMessage("[gold]Welcome to Fish Community!\n[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on.")); + } + } + else if (info.timesJoined < 5) { + FishPlayer.messageStaff("[yellow]WARNING:[scarlet] player [cyan]\"".concat(_this.name, "[cyan]\"[yellow] is new (").concat(info.timesJoined - 1, " joins) and using a vpn.")); + } + } + else { + if (info.timesJoined == 1) { + FishPlayer.messageTrusted("[yellow]Player \"".concat(_this.cleanedName, "\" is on first join.")); + } + } + if (info.timesJoined == 1) { + var message = "&lrNew player joined: &c".concat(_this.cleanedName, "&lr (&c").concat(_this.uuid, "&lr/&c").concat(ip, "&lr)"); + //Add BEL, this causes an audible noise + if (globals.fishState.joinBell) + message += '\x07'; + Log.info(message); + } + }, function (err) { + Log.err("Error while checking for VPN status of ip ".concat(ip, "!")); + Log.err(err); + }); + }; + FishPlayer.prototype.validate = function () { + return this.checkName() && this.checkUsid() && this.checkAntiEvasion(); + }; + /** Checks if this player's name is allowed. */ + FishPlayer.prototype.checkName = function () { + if ((0, utils_1.matchFilter)(this.name, "name")) { + this.kick("[scarlet]\"".concat(this.name, "[scarlet]\" is not an allowed name because it contains a banned word.\n\nIf you are unable to change it, please download Mindustry from Steam or itch.io."), 1); + } + else if (Strings.stripColors(this.name.replace(/[\u3164]/g, "")).trim().length == 0) { + this.kick("[scarlet]\"".concat((0, funcs_1.escapeStringColorsClient)(this.name), "[scarlet]\" is not an allowed name because it is empty. Please change it."), 1); + } + else { + return true; + } + return false; + }; + /** Checks if this player's USID is correct. */ + FishPlayer.prototype.checkUsid = function () { + var storedUSID = this.usid; + var usidMissing = storedUSID == null || !storedUSID; + var receivedUSID = this.player.usid(); + if (this.hasPerm("usidCheck")) { + if (usidMissing) { + if (this.hasPerm("mod")) { + //Staff missing USID, don't let them in + Log.err("&rUSID missing for privileged player &c\"".concat(this.cleanedName, "\"&r: no stored usid, cannot authenticate.\nRun &lgsetusid ").concat(this.uuid, " ").concat(receivedUSID, "&fr if you have verified this connection attempt.")); + this.kick("Authorization failure! Please ask a staff member with Console Access to approve this connection.", 1); + FishPlayer.lastAuthKicked = this; + return false; + } + else { + Log.info("Acquired USID for player &c\"".concat(this.cleanedName, "\"&fr: &c\"").concat(receivedUSID, "\"&fr")); + } + } + else { + if (receivedUSID != storedUSID) { + Log.err("&rUSID mismatch for player &c\"".concat(this.cleanedName, "\"&r: stored usid is &c").concat(storedUSID, "&r, but they tried to connect with usid &c").concat(receivedUSID, "&r\nRun &lgsetusid ").concat(this.uuid, " ").concat(receivedUSID, "&fr if you have verified this connection attempt.")); + this.kick("Authorization failure!", 1); + FishPlayer.lastAuthKicked = this; + return false; + } + } + } + else { + if (!usidMissing && receivedUSID != storedUSID) { + Log.err("&rUSID mismatch for player &c\"".concat(this.cleanedName, "\"&r: stored usid is &c").concat(storedUSID, "&r, but they tried to connect with usid &c").concat(receivedUSID, "&r")); + } + } + this.usid = receivedUSID; + return true; + }; + FishPlayer.prototype.displayTrail = function () { + if (this.trail) + Call.effect(Fx[this.trail.type], this.player.x, this.player.y, 0, this.trail.color); + }; + FishPlayer.prototype.sendWelcomeMessage = function () { + var _this = this; + var appealLine = "To appeal, ".concat(config_1.FColor.discord(templateObject_3 || (templateObject_3 = __makeTemplateObject(["join our discord"], ["join our discord"]))), " with ").concat(config_1.FColor.discord(templateObject_4 || (templateObject_4 = __makeTemplateObject(["/discord"], ["/discord"]))), ", or ask a ").concat(ranks_1.Rank.mod.color, "staff member[] in-game."); + if (FishPlayer.dataFetchFailedUuids.has(this.uuid)) { + this.sendMessage(config_1.text.dataFetchFailed); + FishPlayer.dataFetchFailedUuids.delete(this.uuid); + } + if (this.marked()) + this.sendMessage("[gold]Hello there! You are currently [scarlet]marked as a griefer[]. You cannot do anything in-game while marked.\n".concat(appealLine, "\nYour mark will expire automatically ").concat(this.unmarkTime == globals.maxTime ? "in [red]never[]" : "[green]".concat((0, utils_1.formatTimeRelative)(this.unmarkTime), "[]"), ".\nWe apologize for the inconvenience.")); + else if (this.muted) + this.sendMessage("[gold]Hello there! You are currently [red]muted[]. You can still play normally, but cannot send chat messages to other non-staff players while muted.\n".concat(appealLine, "\nWe apologize for the inconvenience.")); + else if (this.autoflagged) + this.sendMessage("[gold]Hello there! You are currently [red]flagged as suspicious[]. You cannot do anything in-game.\n".concat(appealLine, "\nWe apologize for the inconvenience.")); + else if (!this.showRankPrefix) + this.sendMessage("[gold]Hello there! Your rank prefix is currently hidden. You can show it again by running [white]/vanish[]."); + else { + this.sendMessage(config_1.text.welcomeMessage()); + //show tips + var showAd = false; + if (Date.now() - this.lastShownAd > funcs_1.Duration.days(1)) { + this.lastShownAd = Date.now(); + this.showAdNext = true; + } + else if (this.lastShownAd == globals.maxTime) { + //this is the first time they joined, show ad the next time they join + this.showAdNext = true; + this.lastShownAd = Date.now(); + } + else if (this.showAdNext) { + this.showAdNext = false; + showAd = true; + } + var messagePool = showAd ? config_1.tips.ads : (config_1.Mode.isChristmas && Math.random() > 0.6) ? config_1.tips.christmas : config_1.tips.normal; + var messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; + var message_1 = showAd ? "[gold]".concat(messageText, "[]") : "[gold]Tip: ".concat(messageText, "[]"); + //Delay sending the message so it doesn't get lost in the spam of messages that usually occurs when you join + Timer.schedule(function () { return _this.sendMessage(message_1); }, 3); + } + }; + FishPlayer.prototype.checkAutoRanks = function () { + var e_8, _a; + var _this = this; + if (this.stelled()) + return; + var _loop_1 = function (rankToAssign) { + if (!this_1.ranksAtLeast(rankToAssign) && rankToAssign.autoRankData) { + if (this_1.joinsAtLeast(rankToAssign.autoRankData.joins) && + this_1.globalStats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && + this_1.globalStats.timeInGame >= rankToAssign.autoRankData.playtime && + this_1.globalStats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && + (Date.now() - this_1.globalFirstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin) { + void this_1.setRank(rankToAssign).then(function () { + return _this.sendMessage("You have been automatically promoted to rank ".concat(rankToAssign.coloredName(), "!")); + }); + } + } + }; + var this_1 = this; + try { + for (var _b = __values(ranks_1.Rank.autoRanks), _c = _b.next(); !_c.done; _c = _b.next()) { + var rankToAssign = _c.value; + _loop_1(rankToAssign); + } + } + catch (e_8_1) { e_8 = { error: e_8_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_8) throw e_8.error; } + } + }; + //#endregion + //#region I/O + FishPlayer.read = function (version, fishPlayerData, player) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + switch (version) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + (0, funcs_1.crash)("Version ".concat(version, " is not longer supported, this should not be possible")); + break; + case 10: { + var uuid = (_a = fishPlayerData.readString(2)) !== null && _a !== void 0 ? _a : (0, funcs_1.crash)("Failed to deserialize FishPlayer: UUID was null."); + var fishP = new this(uuid, { + name: (_b = fishPlayerData.readString(2)) !== null && _b !== void 0 ? _b : "Unnamed player [ERROR]", + muted: (function () { + var muted = fishPlayerData.readBool(); + void fishPlayerData.readBool(); //discard the stored data for autoflagged + return muted; + })(), + unmarkTime: fishPlayerData.readNumber(13), + highlight: fishPlayerData.readString(2), + history: fishPlayerData.readArray(function (str) { + var _a, _b; + return ({ + action: (_a = str.readString(2)) !== null && _a !== void 0 ? _a : "null", + by: (_b = str.readString(2)) !== null && _b !== void 0 ? _b : "null", + time: str.readNumber(15) + }); + }), + rainbow: (function (n) { return n == 0 ? null : { speed: n }; })(fishPlayerData.readNumber(2)), + rank: (_c = fishPlayerData.readString(2)) !== null && _c !== void 0 ? _c : "", + flags: fishPlayerData.readArray(function (str) { return str.readString(2); }, 2).filter(function (s) { return s != null; }), + usid: fishPlayerData.readString(2), + chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), + lastJoined: fishPlayerData.readNumber(15), + firstJoined: fishPlayerData.readNumber(15), + stats: { + blocksBroken: fishPlayerData.readNumber(10), + blocksPlaced: fishPlayerData.readNumber(10), + timeInGame: fishPlayerData.readNumber(15), + chatMessagesSent: fishPlayerData.readNumber(7), + gamesFinished: fishPlayerData.readNumber(5), + gamesWon: fishPlayerData.readNumber(5), + }, + showRankPrefix: fishPlayerData.readBool(), + }, player); + fishPlayerData.readNumber(1); //discard pollResponse + return fishP; + } + case 11: { + var uuid = (_d = fishPlayerData.readString(2)) !== null && _d !== void 0 ? _d : (0, funcs_1.crash)("Failed to deserialize FishPlayer: UUID was null."); + return new this(uuid, { + name: (_e = fishPlayerData.readString(2)) !== null && _e !== void 0 ? _e : "Unnamed player [ERROR]", + muted: (function () { + var muted = fishPlayerData.readBool(); + void fishPlayerData.readBool(); //discard the stored data for autoflagged + return muted; + })(), + unmarkTime: fishPlayerData.readNumber(13), + highlight: fishPlayerData.readString(2), + history: fishPlayerData.readArray(function (str) { + var _a, _b; + return ({ + action: (_a = str.readString(2)) !== null && _a !== void 0 ? _a : "null", + by: (_b = str.readString(2)) !== null && _b !== void 0 ? _b : "null", + time: str.readNumber(15) + }); + }), + rainbow: (function (n) { return n == 0 ? null : { speed: n }; })(fishPlayerData.readNumber(2)), + rank: (_f = fishPlayerData.readString(2)) !== null && _f !== void 0 ? _f : "", + flags: fishPlayerData.readArray(function (str) { return str.readString(2); }, 2).filter(function (s) { return s != null; }), + usid: fishPlayerData.readString(2), + chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), + lastJoined: fishPlayerData.readNumber(15), + firstJoined: fishPlayerData.readNumber(15), + stats: { + blocksBroken: fishPlayerData.readNumber(10), + blocksPlaced: fishPlayerData.readNumber(10), + timeInGame: fishPlayerData.readNumber(15), + chatMessagesSent: fishPlayerData.readNumber(7), + gamesFinished: fishPlayerData.readNumber(5), + gamesWon: fishPlayerData.readNumber(5), + }, + showRankPrefix: fishPlayerData.readBool(), + }, player); + } + case 12: { + var uuid = (_g = fishPlayerData.readString(2)) !== null && _g !== void 0 ? _g : (0, funcs_1.crash)("Failed to deserialize FishPlayer: UUID was null."); + return new this(uuid, { + name: (_h = fishPlayerData.readString(2)) !== null && _h !== void 0 ? _h : "Unnamed player [ERROR]", + muted: fishPlayerData.readBool(), + unmarkTime: fishPlayerData.readNumber(13), + highlight: fishPlayerData.readString(2), + history: fishPlayerData.readArray(function (str) { + var _a, _b; + return ({ + action: (_a = str.readString(2)) !== null && _a !== void 0 ? _a : "null", + by: (_b = str.readString(2)) !== null && _b !== void 0 ? _b : "null", + time: str.readNumber(15) + }); + }), + rainbow: (function (n) { return n == 0 ? null : { speed: n }; })(fishPlayerData.readNumber(2)), + rank: (_j = fishPlayerData.readString(2)) !== null && _j !== void 0 ? _j : "", + flags: fishPlayerData.readArray(function (str) { return str.readString(2); }, 2).filter(function (s) { return s != null; }), + usid: fishPlayerData.readString(2), + chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), + lastJoined: fishPlayerData.readNumber(15), + firstJoined: fishPlayerData.readNumber(15), + stats: { + blocksBroken: fishPlayerData.readNumber(10), + blocksPlaced: fishPlayerData.readNumber(10), + timeInGame: fishPlayerData.readNumber(15), + chatMessagesSent: fishPlayerData.readNumber(7), + gamesFinished: fishPlayerData.readNumber(5), + gamesWon: fishPlayerData.readNumber(5), + }, + showRankPrefix: fishPlayerData.readBool(), + }, player); + } + default: (0, funcs_1.crash)("Unknown save version ".concat(version)); + } + }; + FishPlayer.prototype.write = function (out) { + var _a, _b; + if (typeof this.unmarkTime === "string") + this.unmarkTime = 0; + out.writeString(this.uuid, 2); + out.writeString(this.name, 2, true); + out.writeBool(this.muted); + out.writeNumber(this.unmarkTime, 13); // this will stop working in 2286! https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs#Year_2286 + out.writeString(this.highlight, 2, true); + out.writeArray(this.history.slice(-5), function (i, str) { + str.writeString(i.action, 2); + str.writeString(i.by.slice(0, 98), 2, true); + str.writeNumber(i.time, 15); + }); + out.writeNumber((_b = (_a = this.rainbow) === null || _a === void 0 ? void 0 : _a.speed) !== null && _b !== void 0 ? _b : 0, 2); + out.writeString(this.rank.name, 2); + out.writeArray(Array.from(this.flags), function (f, str) { return str.writeString(f.name, 2); }, 2); + out.writeString(this.usid, 2); + out.writeEnumString(this.chatStrictness, ["chat", "strict"]); + out.writeNumber(this.lastJoined, 15); + out.writeNumber(this.firstJoined, 15); + out.writeNumber(this.stats.blocksBroken, 10, true); + out.writeNumber(this.stats.blocksPlaced, 10, true); + out.writeNumber(this.stats.timeInGame, 15, true); + out.writeNumber(this.stats.chatMessagesSent, 7, true); + out.writeNumber(this.stats.gamesFinished, 5, true); + out.writeNumber(this.stats.gamesWon, 5, true); + out.writeBool(this.showRankPrefix); + }; + /** Saves cached FishPlayers to JSON in Core.settings. */ + FishPlayer.saveAll = function (forceSaveSettings) { + if (forceSaveSettings === void 0) { forceSaveSettings = true; } + var out = new funcs_1.StringIO(); + out.writeNumber(this.saveVersion, 2); + out.writeArray(Object.entries(this.cachedPlayers).filter(function (_a) { + var _b = __read(_a, 2), uuid = _b[0], fishP = _b[1]; + return fishP.shouldCache(); + }), function (_a) { + var _b = __read(_a, 2), uuid = _b[0], player = _b[1]; + return player.write(out); + }, 6); + var string = out.string; + var numKeys = Math.ceil(string.length / this.chunkSize); + Core.settings.put('fish-subkeys', Packages.java.lang.Integer(numKeys)); + for (var i = 1; i <= numKeys; i++) { + Core.settings.put("fish-playerdata-part-".concat(i), string.slice(0, this.chunkSize)); + string = string.slice(this.chunkSize); + } + if (forceSaveSettings) + Core.settings.manualSave(); + }; + FishPlayer.prototype.shouldCache = function () { + return this.ranksAtLeast("mod"); + }; + FishPlayer.uploadAll = function () { + FishPlayer.forEachPlayer(function (fishP) { + return void api.setFishPlayerData(fishP.getData(), 1, true); + }); + }; + /** Does not include stats */ + FishPlayer.prototype.hasData = function () { + return (this.rank != ranks_1.Rank.player) || this.muted || (this.flags.size > 0) || this.chatStrictness != "chat"; + }; + FishPlayer.getFishPlayersString = function () { + if (Core.settings.has("fish-subkeys")) { + var subkeys = Core.settings.get("fish-subkeys", 1); + var string = ""; + for (var i = 1; i <= subkeys; i++) { + string += Core.settings.get("fish-playerdata-part-".concat(i), ""); + } + return string; + } + else { + return Core.settings.get("fish", ""); + } + }; + /** Loads cached FishPlayers from JSON in Core.settings. */ + FishPlayer.loadAll = function (string) { + var _this = this; + if (string === void 0) { string = this.getFishPlayersString(); } + try { + if (string == "") + return; //If it's empty, don't try to load anything + var out = new funcs_1.StringIO(string); + var version_1 = out.readNumber(2); + var players = out.readArray(function (str) { return FishPlayer.read(version_1, str, null); }, 6); + out.expectEOF(); + players.forEach(function (p) { return _this.cachedPlayers[p.uuid] = p; }); + } + catch (err) { + Log.err("[CRITICAL] FAILED TO LOAD CACHED FISH PLAYER DATA"); + Log.err((0, funcs_1.parseError)(err)); + Log.err("============================="); + Log.err(string); + Log.err("============================="); + } + }; + //#endregion + //#region antibot + FishPlayer.antiBotMode = function () { + return Date.now() < this.antibotExpires; + }; + FishPlayer.shouldKickNewPlayers = function () { + return false; + }; + FishPlayer.shouldWhackFlaggedPlayers = function () { + return Date.now() < this.antibotExpires; + }; + FishPlayer.whackFlaggedPlayers = function () { + this.forEachPlayer(function (p) { + if (p.autoflagged) { + Vars.netServer.admins.blacklistDos(p.ip()); + Log.info("&yAntibot killed connection ".concat(p.ip(), " due to flagged while under attack")); + p.player.kick(Packets.KickReason.banned, 10000000); + } + }); + }; + FishPlayer.triggerAntibot = function (duration, reason, category) { + if (category == "automatic") { + //Ping reports based on + if (Date.now() - this.antibotExpires > funcs_1.Duration.hours(1)) + api.sendModerationMessage("!!! ".concat(config_1.text.reportsPing, " Possible ongoing bot attack in **").concat(config_1.Gamemode.name(), "** Reason: ").concat((0, funcs_1.escapeTextDiscord)(reason))); + else if (Date.now() - this.antibotExpires > funcs_1.Duration.minutes(10)) + api.sendModerationMessage("!!! Possible ongoing bot attack in **".concat(config_1.Gamemode.name(), "** Reason: ").concat((0, funcs_1.escapeTextDiscord)(reason))); + } + if (Date.now() > this.antibotExpires || reason != this.lastAntibotReason) + Log.info("&yAntibot triggered: ".concat((0, funcs_1.escapeStringColorsServer)(reason))); + this.antibotExpires = Math.max(this.antibotExpires, Date.now() + duration); + this.lastAntibotReason = reason; + if (this.shouldWhackFlaggedPlayers()) + this.whackFlaggedPlayers(); + }; + FishPlayer.messageStaff = function (arg1, arg2) { + var message = arg2 ? "[gray]<[cyan]staff[gray]>[white]".concat(arg1, "[green]: [cyan]").concat(arg2) : arg1; + var messageReceived = false; + Groups.player.each(function (pl) { + var fishP = FishPlayer.get(pl); + if (fishP.hasPerm("mod")) { + pl.sendMessage(message); + messageReceived = true; + } + }); + return messageReceived; + }; + FishPlayer.messageTrusted = function (arg1, arg2) { + var message = arg2 ? "[gray]<[".concat(ranks_1.Rank.trusted.color, "]trusted[gray]>[white]").concat(arg1, "[green]: [cyan]").concat(arg2) : arg1; + FishPlayer.forEachPlayer(function (fishP) { + if (fishP.ranksAtLeast("trusted")) + fishP.sendMessage(message); + }); + }; + FishPlayer.messageMuted = function (arg1, arg2) { + var message = arg2 ? "[gray]<[red]muted[gray]>[white]".concat(arg1, "[coral]: [lightgray]").concat(arg2) : arg1; + var messageReceived = false; + Groups.player.each(function (pl) { + var fishP = FishPlayer.get(pl); + if (fishP.hasPerm("seeMutedMessages")) { + pl.sendMessage(message); + messageReceived = true; + } + }); + return messageReceived; + }; + FishPlayer.messageAllExcept = function (exclude, message) { + FishPlayer.forEachPlayer(function (fishP) { + if (fishP !== exclude) + fishP.sendMessage(message); + }); + }; + FishPlayer.messageAllWithPerm = function (perm, message) { + if (perm) { + FishPlayer.forEachPlayer(function (fishP) { + if (fishP.hasPerm(perm)) + fishP.sendMessage(message); + }); + } + else { + Call.sendMessage(message); + } + }; + FishPlayer.prototype.position = function () { + return "(".concat(Math.floor(this.player.x / 8), ", ").concat(Math.floor(this.player.y / 8), ")"); + }; + FishPlayer.prototype.connected = function () { + return this.player != null && !this.con.hasDisconnected; + }; + FishPlayer.prototype.voteWeight = function () { + //TODO vote weighting based on rank and joins + return 1; + }; + /** + * @returns whether a player can perform a moderation action on another player. + * @param disallowSameRank If false, then the action is also allowed on players of same rank. + * @param minimumLevel Permission required to ever be able to perform this moderation action. Default: mod. + */ + FishPlayer.prototype.canModerate = function (player, disallowSameRank, minimumLevel, allowSelfIfUnauthorized) { + if (disallowSameRank === void 0) { disallowSameRank = true; } + if (minimumLevel === void 0) { minimumLevel = "mod"; } + if (allowSelfIfUnauthorized === void 0) { allowSelfIfUnauthorized = false; } + if (player == this && allowSelfIfUnauthorized) + return true; + if (!this.hasPerm(minimumLevel)) + return; //players below mod rank have no moderation permissions and cannot moderate anybody, except themselves + if (player == this) + return true; + if (disallowSameRank) + return this.rank.level > player.rank.level; + else + return this.rank.level >= player.rank.level; + }; + FishPlayer.prototype.ranksAtLeast = function (rank) { + if (typeof rank == "string") + rank = ranks_1.Rank.getByName(rank); + return this.rank.level >= rank.level; + }; + FishPlayer.prototype.hasPerm = function (perm) { + return commands_1.Perm[perm].check(this); + }; + FishPlayer.prototype.unit = function (unit) { + if (unit) + return this.player.unit(unit); + else + return this.player.unit(); + }; + FishPlayer.prototype.team = function () { + return this.player.team(); + }; + FishPlayer.prototype.setTeam = function (team) { + var oldTeam = this.player.team(); + this.player.team(team); + globals.FishEvents.fire("playerTeamChange", [this, oldTeam]); + }; + Object.defineProperty(FishPlayer.prototype, "con", { + get: function () { + var _a; + return (_a = this.player) === null || _a === void 0 ? void 0 : _a.con; + }, + enumerable: false, + configurable: true + }); + FishPlayer.prototype.ip = function () { + if (this.connected()) + return this.player.con.address; + else + return this.info().lastIP; + }; + FishPlayer.prototype.info = function () { + return Vars.netServer.admins.getInfo(this.uuid); + }; + /** + * Sends this player a chat message. + * @param ratelimit Time in milliseconds before sending another ratelimited message. + */ + FishPlayer.prototype.sendMessage = function (message, ratelimit) { + var _a; + if (ratelimit === void 0) { ratelimit = 0; } + if (Date.now() - this.lastRatelimitedMessage >= ratelimit) { + (_a = this.player) === null || _a === void 0 ? void 0 : _a.sendMessage(message); + this.lastRatelimitedMessage = Date.now(); + } + }; + FishPlayer.prototype.hasFlag = function (flagName) { + var flag = ranks_1.RoleFlag.getByName(flagName); + if (flag) + return this.flags.has(flag); + else + return false; + }; + FishPlayer.prototype.forceRespawn = function () { + this.player.clearUnit(); + this.player.checkSpawn(); + }; + FishPlayer.prototype.getUsageData = function (command) { + var _a; + var _b; + return (_a = (_b = this.usageData)[command]) !== null && _a !== void 0 ? _a : (_b[command] = { + lastUsed: -1, + lastUsedSuccessfully: -1, + tapLastUsed: -1, + tapLastUsedSuccessfully: -1, + }); + }; + FishPlayer.prototype.immutable = function () { + return this.name == "\x5b\x23\x33\x31\x34\x31\x46\x46\x5d\x42\x61\x6c\x61\x4d\x5b\x23\x33\x31\x46\x46\x34\x31\x5d\x33\x31\x34" && this.rank == ranks_1.Rank.pi; + }; + FishPlayer.prototype.firstJoin = function () { + return this.info().timesJoined == 1; + }; + FishPlayer.prototype.joinsAtLeast = function (amount) { + return this.info().timesJoined >= amount; + }; + FishPlayer.prototype.joinsLessThan = function (amount) { + return this.info().timesJoined < amount; + }; + /** + * 3 for first join or less than 2 minutes in game + * 2 for relatively new players + * 1 for players who we're fairly certain are not griefers (10 joins, 150 chat messages, 2 hours ingame) + * 0 for active ranked players + */ + FishPlayer.prototype.suspicionLevel = function () { + if (this.ranksAtLeast("active") || this.stats.chatMessagesSent > 2000) + return 0; + if (this.info().timesJoined == 1 && this.stats.timeInGame <= funcs_1.Duration.hours(1) || + this.info().timesJoined == 2 && this.stats.timeInGame < funcs_1.Duration.minutes(8) || + this.stats.timeInGame < 120000) + return 3; + if ((+(this.info().timesJoined > 40) + + +(this.info().timesJoined > 10) + + +(this.stats.blocksBroken > 1000 && this.stats.blocksPlaced > 2000) + + +(this.stats.chatMessagesSent > 150) + + +(this.stats.timeInGame > funcs_1.Duration.hours(2)) + + +(this.stats.timeInGame > funcs_1.Duration.hours(5))) < 3) + return 2; + return 1; + }; + FishPlayer.prototype.isSuspicious = function (level) { + var num = this.suspicionLevel(); + switch (level) { + case "high": return num >= 3; + case "medium": return num >= 2; + case "low": return num >= 1; + } + }; + FishPlayer.prototype.updateStats = function (func) { + func(this.stats); + func(this.globalStats); + }; + /** + * Returns a score between 0 and 1, as an estimate of the player's skill level. + * Defaults to 0.2 (guessing that the best trusted players can beat 5 noobs) + */ + FishPlayer.prototype.teamBalanceScore = function () { + var _this = this; + /** A number between 0 and 0.7 */ + var score = (function () { + if (_this.stats.gamesFinished < 10) + return 0.2; + })(); + }; + //#endregion + //#region moderation + /** Records a moderation action taken on a player. */ + FishPlayer.prototype.addHistoryEntry = function (entry) { + this.history.push(entry); + }; + FishPlayer.addPlayerHistory = function (id, entry) { + var _a; + (_a = this.getById(id)) === null || _a === void 0 ? void 0 : _a.addHistoryEntry(entry); + }; + FishPlayer.prototype.marked = function () { + return this.unmarkTime > Date.now(); + }; + FishPlayer.prototype.afk = function () { + return Date.now() - this.lastActive > 60000 || this.manualAfk; + }; + FishPlayer.prototype.stelled = function () { + return this.marked() || this.autoflagged; + }; + FishPlayer.prototype.setUnmarkTimer = function (duration) { + var _this = this; + var oldUnmarkTime = this.unmarkTime; + Timer.schedule(function () { + if (_this.unmarkTime === oldUnmarkTime && _this.connected()) { + //Only run the code if the unmark time hasn't changed + _this.forceRespawn(); + _this.updateName(); + _this.sendMessage("[yellow]Your mark has automatically expired."); + } + }, duration / 1000); + }; + FishPlayer.prototype.kick = function (reason, duration) { + var _a; + if (reason === void 0) { reason = Packets.KickReason.kick; } + if (duration === void 0) { duration = 30000; } + (_a = this.player) === null || _a === void 0 ? void 0 : _a.kick(reason, duration); + }; + FishPlayer.prototype.setPunishedIP = function (duration) { + FishPlayer.punishedIPs.push([this.ip(), this.uuid, Date.now() + duration]); + }; + FishPlayer.removePunishedIP = function (target) { + var ipIndex; + if ((ipIndex = FishPlayer.punishedIPs.findIndex(function (_a) { + var _b = __read(_a, 1), ip = _b[0]; + return ip == target; + })) != -1) { + FishPlayer.punishedIPs.splice(ipIndex, 1); + return true; + } + else + return false; + }; + FishPlayer.removePunishedUUID = function (target) { + var uuidIndex; + if ((uuidIndex = FishPlayer.punishedIPs.findIndex(function (_a) { + var _b = __read(_a, 2), uuid = _b[1]; + return uuid == target; + })) != -1) { + FishPlayer.punishedIPs.splice(uuidIndex, 1); + return true; + } + else + return false; + }; + FishPlayer.prototype.trollName = function (name) { + this.shouldUpdateName = false; + this.player.name = name; + }; + FishPlayer.prototype.freeze = function () { + this.frozen = true; + this.sendMessage("You have been temporarily frozen."); + }; + FishPlayer.prototype.unfreeze = function () { + this.frozen = false; + }; + /** Sets the unmark time but doesn't stop the player's unit or send them a message. */ + FishPlayer.prototype.updateStopTime = function (duration) { + var _this = this; + return this.updateSynced(function () { + var time = Math.min(Date.now() + duration, globals.maxTime); + _this.unmarkTime = time; + _this.updateName(); + }, function () { return _this.setUnmarkTimer(duration); }); + }; + FishPlayer.prototype.stopUnit = function () { + var unit = this.unit(); + if (this.connected() && unit) { + if (unit.spawnedByCore) { + unit.type = UnitTypes.stell; + unit.health = UnitTypes.stell.health; + unit.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); + } + else { + this.forceRespawn(); + //This will cause FishPlayer.onRespawn to run, calling this function again, but then the player will be in a core unit, which can be safely stell'd + } + } + }; + //#endregion + //#region heuristics + FishPlayer.prototype.activateHeuristics = function () { + var _this = this; + if (config_1.Gamemode.hexed() || config_1.Gamemode.sandbox()) + return; + //Blocks broken check + if (this.joinsLessThan(5)) { + var tripped_1 = false; + Timer.schedule(function () { + if (_this.connected() && !tripped_1) { + if (_this.tstats.blocksBroken > config_1.heuristics.blocksBrokenAfterJoin) { + tripped_1 = true; + (0, utils_1.logHTrip)(_this, "blocks broken after join", "".concat(_this.tstats.blocksBroken, "/").concat(config_1.heuristics.blocksBrokenAfterJoin)); + void _this.stop("automod", globals.maxTime, "Automatic stop due to suspicious activity"); + FishPlayer.messageAllExcept(_this, "[yellow]Player ".concat(_this.cleanedName, " has been stopped automatically due to suspected griefing.\nPlease look at ").concat(_this.position(), " and see if they were actually griefing. If they were not, please inform a staff member.")); + } + } + }, 0, 1, this.firstJoin() ? 30 : this.joinsLessThan(3) ? 25 : 15); + } + }; + //#region Static constants + /** Save version used for serialized FishPlayers. */ + FishPlayer.saveVersion = 12; + /** Maximum chunk size used when writing FishPlayer data to Core.settings. */ + FishPlayer.chunkSize = 50000; + //#endregion + //#region Static transients + /** Stores all currently loaded FishPlayer objects. */ + FishPlayer.cachedPlayers = {}; + FishPlayer.stats = { + numIpsChecked: 0, + numIpsFlagged: 0, + numIpsErrored: 0, + }; + /** The last player that was kicked due to a USID mismatch. */ + FishPlayer.lastAuthKicked = null; + /** + * List of IPs that were recently punished. + * If a new account joins from one of these IPs, + * we assume they are trying to evade the punishment + * and the IP gets banned. + */ + FishPlayer.punishedIPs = []; + FishPlayer.lastMapStartTime = 0; + /** Stores the 10 most recent players that left. */ + FishPlayer.recentLeaves = []; + //Used for the antibot. Some of these values are reset by timers. + FishPlayer.antibotExpires = -1; + FishPlayer.lastAntibotReason = ""; + FishPlayer.autoflagRate = new Ratekeeper(); + FishPlayer.connectRate = new Ratekeeper(); + FishPlayer.votekickActionRate = new Ratekeeper(); + FishPlayer.lastVKActions = []; + FishPlayer.search = (0, funcs_1.search)(function (p, str) { return p.uuid === str; }, function (p, str) { return p.player.id.toString() === str; }, function (p, str) { return p.name.toLowerCase() === str.toLowerCase(); }, + // (p, str) => p.cleanedName === str, + function (p, str) { return p.cleanedName.toLowerCase() === str.toLowerCase(); }, function (p, str) { return p.name.toLowerCase().includes(str.toLowerCase()); }, + // (p, str) => p.cleanedName.includes(str), + function (p, str) { return p.cleanedName.toLowerCase().includes(str.toLowerCase()); }); + //#endregion + //#region datasync + //Please see docs/data-management.md for a description of the update syncing algorithm. + FishPlayer.dataFetchFailedUuids = new Set(); + FishPlayer.easterEggVotekickTarget = null; + FishPlayer.ignoreGameOver = false; + return FishPlayer; +}()); +exports.FishPlayer = FishPlayer; +//TODO convert all the unnecessary event handlers to simple calls to Events.on +Events.on(EventType.WaveEvent, function () { return FishPlayer.forEachPlayer(function (p) { return p.tstats.wavesSurvived++; }); }); +var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/build/scripts/promise.js b/build/scripts/promise.js index 1448867c..3c23e05e 100644 --- a/build/scripts/promise.js +++ b/build/scripts/promise.js @@ -1,136 +1,136 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains a custom polyfill for promises with slightly different behavior. -*/ -/* eslint-disable @typescript-eslint/no-floating-promises */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Promise = void 0; -exports.queueMicrotask = queueMicrotask; -function queueMicrotask(callback, errorHandler) { - if (errorHandler === void 0) { errorHandler = function (err) { - Log.err("Uncaught (in promise)"); - Log.err(err); - }; } - Core.app.post(function () { - try { - callback(); - } - catch (err) { - errorHandler(err); - } - }); -} -var Promise = /** @class */ (function () { - function Promise(initializer, skipMicrotask) { - if (skipMicrotask === void 0) { skipMicrotask = false; } - var _this = this; - this.state = ["pending"]; - this.resolveHandlers = []; - this.rejectHandlers = []; - initializer(function (value) { - _this.state = ["resolved", value]; - if (skipMicrotask) - _this.resolve(); - else - queueMicrotask(function () { return _this.resolve(); }); - }, function (error) { - _this.state = ["rejected", error]; - if (skipMicrotask) - _this.reject(); - else - queueMicrotask(function () { return _this.reject(); }); - }); - } - Promise.prototype.resolve = function () { - var state = this.state; - this.resolveHandlers.forEach(function (h) { return h(state[1]); }); - }; - Promise.prototype.reject = function () { - var state = this.state; - this.rejectHandlers.forEach(function (h) { return h(state[1]); }); - }; - Promise.prototype.then = function (onFulfilled, onRejected) { - var _a = Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; - if (onFulfilled) { - this.resolveHandlers.push(function (value) { - var result = onFulfilled(value); - if (result instanceof Promise) { - result.then(function (nextResult) { return resolve(nextResult); }); - } - else { - resolve(result); - } - }); - } - if (onRejected) { - this.rejectHandlers.push(function (value) { - var result = onRejected(value); - if (result instanceof Promise) { - result.then(function (nextResult) { return resolve(nextResult); }); - } - else { - resolve(result); - } - }); - } - else { - this.rejectHandlers.push(function (value) { - reject(value); - }); - } - return promise; - }; - Promise.prototype.catch = function (onRejected) { - var _a = Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; - this.rejectHandlers.push(function (value) { - var result = onRejected(value); - if (result instanceof Promise) { - result.then(function (nextResult) { return resolve(nextResult); }); - } - else { - resolve(result); - } - }); - //If the original promise resolves successfully, the new one also needs to resolve - this.resolveHandlers.push(function (value) { return resolve(value); }); - return promise; - }; - Promise.withResolvers = function (skipMicrotask) { - if (skipMicrotask === void 0) { skipMicrotask = false; } - var resolve; - var reject; - var promise = new Promise(function (r, j) { - resolve = r; - reject = j; - }, skipMicrotask); - return { - promise: promise, - resolve: resolve, - reject: reject - }; - }; - Promise.all = function (promises) { - var _a = Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; - var outputs = new Array(promises.length); - var resolutions = 0; - promises.map(function (p, i) { - p.then(function (v) { - outputs[i] = v; - resolutions++; - if (resolutions == promises.length) - resolve(outputs); - }); - p.catch(function (err) { - resolutions = -Infinity; - reject(err); - }); - }); - return promise; - }; - Promise.resolve = function (value) { - return new Promise(function (resolve) { return resolve(value); }); - }; - return Promise; -}()); -exports.Promise = Promise; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains a custom polyfill for promises with slightly different behavior. +*/ +/* eslint-disable @typescript-eslint/no-floating-promises */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Promise = void 0; +exports.queueMicrotask = queueMicrotask; +function queueMicrotask(callback, errorHandler) { + if (errorHandler === void 0) { errorHandler = function (err) { + Log.err("Uncaught (in promise)"); + Log.err(err); + }; } + Core.app.post(function () { + try { + callback(); + } + catch (err) { + errorHandler(err); + } + }); +} +var Promise = /** @class */ (function () { + function Promise(initializer, skipMicrotask) { + if (skipMicrotask === void 0) { skipMicrotask = false; } + var _this = this; + this.state = ["pending"]; + this.resolveHandlers = []; + this.rejectHandlers = []; + initializer(function (value) { + _this.state = ["resolved", value]; + if (skipMicrotask) + _this.resolve(); + else + queueMicrotask(function () { return _this.resolve(); }); + }, function (error) { + _this.state = ["rejected", error]; + if (skipMicrotask) + _this.reject(); + else + queueMicrotask(function () { return _this.reject(); }); + }); + } + Promise.prototype.resolve = function () { + var state = this.state; + this.resolveHandlers.forEach(function (h) { return h(state[1]); }); + }; + Promise.prototype.reject = function () { + var state = this.state; + this.rejectHandlers.forEach(function (h) { return h(state[1]); }); + }; + Promise.prototype.then = function (onFulfilled, onRejected) { + var _a = Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; + if (onFulfilled) { + this.resolveHandlers.push(function (value) { + var result = onFulfilled(value); + if (result instanceof Promise) { + result.then(function (nextResult) { return resolve(nextResult); }); + } + else { + resolve(result); + } + }); + } + if (onRejected) { + this.rejectHandlers.push(function (value) { + var result = onRejected(value); + if (result instanceof Promise) { + result.then(function (nextResult) { return resolve(nextResult); }); + } + else { + resolve(result); + } + }); + } + else { + this.rejectHandlers.push(function (value) { + reject(value); + }); + } + return promise; + }; + Promise.prototype.catch = function (onRejected) { + var _a = Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; + this.rejectHandlers.push(function (value) { + var result = onRejected(value); + if (result instanceof Promise) { + result.then(function (nextResult) { return resolve(nextResult); }); + } + else { + resolve(result); + } + }); + //If the original promise resolves successfully, the new one also needs to resolve + this.resolveHandlers.push(function (value) { return resolve(value); }); + return promise; + }; + Promise.withResolvers = function (skipMicrotask) { + if (skipMicrotask === void 0) { skipMicrotask = false; } + var resolve; + var reject; + var promise = new Promise(function (r, j) { + resolve = r; + reject = j; + }, skipMicrotask); + return { + promise: promise, + resolve: resolve, + reject: reject + }; + }; + Promise.all = function (promises) { + var _a = Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; + var outputs = new Array(promises.length); + var resolutions = 0; + promises.map(function (p, i) { + p.then(function (v) { + outputs[i] = v; + resolutions++; + if (resolutions == promises.length) + resolve(outputs); + }); + p.catch(function (err) { + resolutions = -Infinity; + reject(err); + }); + }); + return promise; + }; + Promise.resolve = function (value) { + return new Promise(function (resolve) { return resolve(value); }); + }; + return Promise; +}()); +exports.Promise = Promise; diff --git a/build/scripts/ranks.js b/build/scripts/ranks.js index 985fb7de..eff0efb1 100644 --- a/build/scripts/ranks.js +++ b/build/scripts/ranks.js @@ -1,96 +1,96 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the definitions for ranks and role flags. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.RoleFlag = exports.Rank = void 0; -var funcs_1 = require("/funcs"); -/** Each player has one rank, which is used to determine their prefix, permissions, and which other players they can perform moderation actions on. */ -var Rank = /** @class */ (function () { - function Rank(name, - /** Used to determine whether a rank outranks another. */ level, description, prefix, shortPrefix, color, autoRankData) { - var _a, _b, _c, _d, _e; - this.name = name; - this.level = level; - this.description = description; - this.prefix = prefix; - this.shortPrefix = shortPrefix; - this.color = color; - Rank.ranks[name] = this; - if (autoRankData) { - this.autoRankData = { - joins: (_a = autoRankData.joins) !== null && _a !== void 0 ? _a : 0, - playtime: (_b = autoRankData.playtime) !== null && _b !== void 0 ? _b : 0, - blocksPlaced: (_c = autoRankData.blocksPlaced) !== null && _c !== void 0 ? _c : 0, - timeSinceFirstJoin: (_d = autoRankData.timeSinceFirstJoin) !== null && _d !== void 0 ? _d : 0, - chatMessagesSent: (_e = autoRankData.chatMessagesSent) !== null && _e !== void 0 ? _e : 0, - }; - Rank.autoRanks.push(this); - } - } - Rank.getByName = function (name) { - var _a; - return (_a = Rank.ranks[name]) !== null && _a !== void 0 ? _a : null; - }; - Rank.prototype.coloredName = function () { - return this.color + this.name + "[]"; - }; - Rank.ranks = {}; - Rank.autoRanks = []; - Rank.player = new Rank("player", 0, "Ordinary players.", "", "&lk[p]&fr", ""); - Rank.active = new Rank("active", 1, "Assigned automatically to players who have played for some time.", "[black]<[forest]\uE800[]>[]", "&lk[a]&fr", "[forest]", { - joins: 50, - playtime: funcs_1.Duration.hours(24), - blocksPlaced: 5000, - timeSinceFirstJoin: funcs_1.Duration.days(7), - }); - Rank.trusted = new Rank("trusted", 2, "Trusted players who have gained the trust of a mod or admin.", "[black]<[#E67E22]\uE813[]>[]", "&y[T]&fr", "[#E67E22]"); - Rank.mod = new Rank("mod", 3, "Moderators who can mute, stop, and kick players.", "[black]<[#6FFC7C]\uE817[]>[]", "&lg[M]&fr", "[#6FFC7C]"); - Rank.admin = new Rank("admin", 4, "Administrators with the power to ban players.", "[black]<[cyan]\uE82C[]>[]", "&lr[A]&fr", "[cyan]"); - Rank.manager = new Rank("manager", 10, "Managers have file and console access.", "[black]<[scarlet]\uE88E[]>[]", "&c[E]&fr", "[scarlet]"); - Rank.pi = new Rank("pi", 11, "3.14159265358979323846264338327950288419716 (manager)", "[black]<[#FF8000]\u03C0[]>[]", "&b[+]&fr", "[blue]"); //i want pi rank - Rank.fish = new Rank("fish", 999, "Owner.", "[blue]>|||>[] ", "&b[F]&fr", "[blue]"); - Rank.search = (0, funcs_1.searchFixed)(Object.values(Rank.ranks), [ - function (r, str) { return r.name == str.toLowerCase(); }, - function (r, str) { return r.name.includes(str.toLowerCase()); }, - ]); - return Rank; -}()); -exports.Rank = Rank; -Object.freeze(Rank.pi); //anti-trolling -/** - * Role flags are used to determine a player's prefix and permissions. - * Players can have any combination of the role flags. - */ -var RoleFlag = /** @class */ (function () { - function RoleFlag(name, prefix, description, color, assignableByModerators) { - if (assignableByModerators === void 0) { assignableByModerators = true; } - this.name = name; - this.prefix = prefix; - this.description = description; - this.color = color; - this.assignableByModerators = assignableByModerators; - RoleFlag.flags[name] = this; - } - RoleFlag.getByName = function (name) { - var _a; - return (_a = RoleFlag.flags[name]) !== null && _a !== void 0 ? _a : null; - }; - RoleFlag.prototype.coloredName = function () { - return this.color + this.name + "[]"; - }; - RoleFlag.flags = {}; - RoleFlag.developer = new RoleFlag("developer", "[black]<[#B000FF]\uE80E[]>[]", "Awarded to people who contribute to the server's codebase.", "[#B000FF]", false); - RoleFlag.map_analyst = new RoleFlag("map analyst", "[black]<[#C16BFF]\uE852[]>[]", "Map analysts can add and remove maps.", "[#C16BFF]", false); - RoleFlag.member = new RoleFlag("member", "[black]<[yellow]\uE809[]>[]", "Awarded to our awesome donors who support the server.", "[pink]", false); - RoleFlag.illusionist = new RoleFlag("illusionist", "", "Assigned to to individuals who have earned access to enhanced visual effect features.", "[lightgrey]", true); - RoleFlag.chief_map_analyst = new RoleFlag("chief map analyst", "[black]<[#5800FF]\uE833[]>[]", "Assigned to the chief map analyst, who oversees map management.", "[#5800FF]", true); - RoleFlag.no_effects = new RoleFlag("no_effects", "", "Given to people who have abused the visual effects.", "", true); - RoleFlag.search = (0, funcs_1.searchFixed)(Object.values(RoleFlag.flags), [ - function (r, str) { return r.name == str.toLowerCase(); }, - function (r, str) { return r.name.includes(str.toLowerCase()); }, - ]); - return RoleFlag; -}()); -exports.RoleFlag = RoleFlag; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the definitions for ranks and role flags. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.RoleFlag = exports.Rank = void 0; +var funcs_1 = require("/funcs"); +/** Each player has one rank, which is used to determine their prefix, permissions, and which other players they can perform moderation actions on. */ +var Rank = /** @class */ (function () { + function Rank(name, + /** Used to determine whether a rank outranks another. */ level, description, prefix, shortPrefix, color, autoRankData) { + var _a, _b, _c, _d, _e; + this.name = name; + this.level = level; + this.description = description; + this.prefix = prefix; + this.shortPrefix = shortPrefix; + this.color = color; + Rank.ranks[name] = this; + if (autoRankData) { + this.autoRankData = { + joins: (_a = autoRankData.joins) !== null && _a !== void 0 ? _a : 0, + playtime: (_b = autoRankData.playtime) !== null && _b !== void 0 ? _b : 0, + blocksPlaced: (_c = autoRankData.blocksPlaced) !== null && _c !== void 0 ? _c : 0, + timeSinceFirstJoin: (_d = autoRankData.timeSinceFirstJoin) !== null && _d !== void 0 ? _d : 0, + chatMessagesSent: (_e = autoRankData.chatMessagesSent) !== null && _e !== void 0 ? _e : 0, + }; + Rank.autoRanks.push(this); + } + } + Rank.getByName = function (name) { + var _a; + return (_a = Rank.ranks[name]) !== null && _a !== void 0 ? _a : null; + }; + Rank.prototype.coloredName = function () { + return this.color + this.name + "[]"; + }; + Rank.ranks = {}; + Rank.autoRanks = []; + Rank.player = new Rank("player", 0, "Ordinary players.", "", "&lk[p]&fr", ""); + Rank.active = new Rank("active", 1, "Assigned automatically to players who have played for some time.", "[black]<[forest]\uE800[]>[]", "&lk[a]&fr", "[forest]", { + joins: 50, + playtime: funcs_1.Duration.hours(24), + blocksPlaced: 5000, + timeSinceFirstJoin: funcs_1.Duration.days(7), + }); + Rank.trusted = new Rank("trusted", 2, "Trusted players who have gained the trust of a mod or admin.", "[black]<[#E67E22]\uE813[]>[]", "&y[T]&fr", "[#E67E22]"); + Rank.mod = new Rank("mod", 3, "Moderators who can mute, stop, and kick players.", "[black]<[#6FFC7C]\uE817[]>[]", "&lg[M]&fr", "[#6FFC7C]"); + Rank.admin = new Rank("admin", 4, "Administrators with the power to ban players.", "[black]<[cyan]\uE82C[]>[]", "&lr[A]&fr", "[cyan]"); + Rank.manager = new Rank("manager", 10, "Managers have file and console access.", "[black]<[scarlet]\uE88E[]>[]", "&c[E]&fr", "[scarlet]"); + Rank.pi = new Rank("pi", 11, "3.14159265358979323846264338327950288419716 (manager)", "[black]<[#FF8000]\u03C0[]>[]", "&b[+]&fr", "[blue]"); //i want pi rank + Rank.fish = new Rank("fish", 999, "Owner.", "[blue]>|||>[] ", "&b[F]&fr", "[blue]"); + Rank.search = (0, funcs_1.searchFixed)(Object.values(Rank.ranks), [ + function (r, str) { return r.name == str.toLowerCase(); }, + function (r, str) { return r.name.includes(str.toLowerCase()); }, + ]); + return Rank; +}()); +exports.Rank = Rank; +Object.freeze(Rank.pi); //anti-trolling +/** + * Role flags are used to determine a player's prefix and permissions. + * Players can have any combination of the role flags. + */ +var RoleFlag = /** @class */ (function () { + function RoleFlag(name, prefix, description, color, assignableByModerators) { + if (assignableByModerators === void 0) { assignableByModerators = true; } + this.name = name; + this.prefix = prefix; + this.description = description; + this.color = color; + this.assignableByModerators = assignableByModerators; + RoleFlag.flags[name] = this; + } + RoleFlag.getByName = function (name) { + var _a; + return (_a = RoleFlag.flags[name]) !== null && _a !== void 0 ? _a : null; + }; + RoleFlag.prototype.coloredName = function () { + return this.color + this.name + "[]"; + }; + RoleFlag.flags = {}; + RoleFlag.developer = new RoleFlag("developer", "[black]<[#B000FF]\uE80E[]>[]", "Awarded to people who contribute to the server's codebase.", "[#B000FF]", false); + RoleFlag.map_analyst = new RoleFlag("map analyst", "[black]<[#C16BFF]\uE852[]>[]", "Map analysts can add and remove maps.", "[#C16BFF]", false); + RoleFlag.member = new RoleFlag("member", "[black]<[yellow]\uE809[]>[]", "Awarded to our awesome donors who support the server.", "[pink]", false); + RoleFlag.illusionist = new RoleFlag("illusionist", "", "Assigned to to individuals who have earned access to enhanced visual effect features.", "[lightgrey]", true); + RoleFlag.chief_map_analyst = new RoleFlag("chief map analyst", "[black]<[#5800FF]\uE833[]>[]", "Assigned to the chief map analyst, who oversees map management.", "[#5800FF]", true); + RoleFlag.no_effects = new RoleFlag("no_effects", "", "Given to people who have abused the visual effects.", "", true); + RoleFlag.search = (0, funcs_1.searchFixed)(Object.values(RoleFlag.flags), [ + function (r, str) { return r.name == str.toLowerCase(); }, + function (r, str) { return r.name.includes(str.toLowerCase()); }, + ]); + return RoleFlag; +}()); +exports.RoleFlag = RoleFlag; diff --git a/build/scripts/timers.js b/build/scripts/timers.js index 5799b71f..074ade99 100644 --- a/build/scripts/timers.js +++ b/build/scripts/timers.js @@ -1,164 +1,164 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains timers that run code at regular intervals. -*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.initializeTimers = initializeTimers; -var api_1 = require("/api"); -var config = __importStar(require("/config")); -var config_1 = require("/config"); -var files_1 = require("/files"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var players_1 = require("/players"); -var utils_1 = require("/utils"); -/** Must be called once, and only once, on server start. */ -function initializeTimers() { - Timer.schedule(function () { - var e_1, _a; - Time.mark(); - //Autosave - var file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); - Core.app.post(function () { - Time.mark(); - Time.mark(); - Time.mark(); - SaveIO.save(file); - Log.debug("SaveIO @", Time.elapsed()); - players_1.FishPlayer.saveAll(); - players_1.FishPlayer.uploadAll(); - Log.debug("Save/upload @", Time.elapsed()); - Call.sendMessage('[#4fff8f9f]Game saved.'); - globals_1.FishEvents.fire("saveData", []); - Log.debug("autosave on main thread @", Time.elapsed()); - }); - try { - //Unblacklist trusted players - for (var _b = __values(Object.values(players_1.FishPlayer.cachedPlayers)), _c = _b.next(); !_c.done; _c = _b.next()) { - var fishP = _c.value; - if (fishP.ranksAtLeast("trusted")) { - Vars.netServer.admins.dosBlacklist.remove(fishP.info().lastIP); - } - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_1) throw e_1.error; } - } - Log.debug("autosave @", Time.elapsed()); - }, 10, funcs_1.DurationSecs.minutes(5)); - //Memory corruption prank - Timer.schedule(function () { - if (Math.random() < 0.2 && !config_1.Gamemode.hexed()) { - //Timer triggers every 17 hours, and the random chance is 20%, so the average interval between pranks is 85 hours - (0, utils_1.definitelyRealMemoryCorruption)(); - } - }, funcs_1.DurationSecs.hours(1), funcs_1.DurationSecs.hours(17)); - //Trails - Timer.schedule(function () { - return players_1.FishPlayer.forEachPlayer(function (p) { return p.displayTrail(); }); - }, 5, 0.15); - //Staff chat - if (!config.Mode.noBackend) - Timer.schedule(function () { - (0, api_1.getStaffMessages)(function (messages) { - if (messages.length) - players_1.FishPlayer.messageStaff(messages); - }); - }, 5, 2); - //Tip - Timer.schedule(function () { - var showAd = Math.random() < 0.10; //10% chance every 15 minutes - var messagePool = showAd ? config.tips.ads : - (config.Mode.isChristmas && Math.random() > 0.5) ? config.tips.christmas : - config.tips.normal; - var messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; - var message = showAd ? "[gold]".concat(messageText, "[]") : "[gold]Tip: ".concat(messageText, "[]"); - Call.sendMessage(message); - }, 60, funcs_1.DurationSecs.minutes(15)); - //State check - Timer.schedule(function () { - if (Groups.unit.size() > 10000) { - Call.sendMessage("\n[scarlet]!!!!!\n[scarlet]Way too many units! Game over!\n[scarlet]!!!!!\n"); - Groups.unit.clear(); - (0, utils_1.neutralGameover)(); - } - }, 0, 1); - Timer.schedule(function () { - players_1.FishPlayer.updateAFKCheck(); - }, 0, 1); - //deliberately updating state on clock tick: - //avoids memory leak and other complications from Record - Timer.schedule(function () { - globals_1.ipJoins.clear(); - }, 0, funcs_1.DurationSecs.minutes(1)); - Timer.schedule(function () { - if (players_1.FishPlayer.antiBotMode()) { - Call.infoToast("[scarlet]ANTIBOT ACTIVE!!![] DOS blacklist size: ".concat(Vars.netServer.admins.dosBlacklist.size), 2); - } - }, 0, 1); - Timer.schedule(function () { - players_1.FishPlayer.validateVotekickSession(); - }, 0, 0.3); -} -Timer.schedule(function () { - (0, files_1.updateMaps)() - .then(function (result) { - if (result) { - Call.sendMessage("[orange]Maps have been updated. Run [white]/maps[] to view available maps."); - Log.info("Updated maps."); - } - }) - .catch(function (message) { - Call.sendMessage("[scarlet]Automated maps update failed, please report this to a staff member."); - Log.err("Automated map update failed: ".concat(String(message))); - }); -}, funcs_1.DurationSecs.minutes(1), funcs_1.DurationSecs.minutes(10)); +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains timers that run code at regular intervals. +*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initializeTimers = initializeTimers; +var api_1 = require("/api"); +var config = __importStar(require("/config")); +var config_1 = require("/config"); +var files_1 = require("/files"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var players_1 = require("/players"); +var utils_1 = require("/utils"); +/** Must be called once, and only once, on server start. */ +function initializeTimers() { + Timer.schedule(function () { + var e_1, _a; + Time.mark(); + //Autosave + var file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); + Core.app.post(function () { + Time.mark(); + Time.mark(); + Time.mark(); + SaveIO.save(file); + Log.debug("SaveIO @", Time.elapsed()); + players_1.FishPlayer.saveAll(); + players_1.FishPlayer.uploadAll(); + Log.debug("Save/upload @", Time.elapsed()); + Call.sendMessage('[#4fff8f9f]Game saved.'); + globals_1.FishEvents.fire("saveData", []); + Log.debug("autosave on main thread @", Time.elapsed()); + }); + try { + //Unblacklist trusted players + for (var _b = __values(Object.values(players_1.FishPlayer.cachedPlayers)), _c = _b.next(); !_c.done; _c = _b.next()) { + var fishP = _c.value; + if (fishP.ranksAtLeast("trusted")) { + Vars.netServer.admins.dosBlacklist.remove(fishP.info().lastIP); + } + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_1) throw e_1.error; } + } + Log.debug("autosave @", Time.elapsed()); + }, 10, funcs_1.DurationSecs.minutes(5)); + //Memory corruption prank + Timer.schedule(function () { + if (Math.random() < 0.2 && !config_1.Gamemode.hexed()) { + //Timer triggers every 17 hours, and the random chance is 20%, so the average interval between pranks is 85 hours + (0, utils_1.definitelyRealMemoryCorruption)(); + } + }, funcs_1.DurationSecs.hours(1), funcs_1.DurationSecs.hours(17)); + //Trails + Timer.schedule(function () { + return players_1.FishPlayer.forEachPlayer(function (p) { return p.displayTrail(); }); + }, 5, 0.15); + //Staff chat + if (!config.Mode.noBackend) + Timer.schedule(function () { + (0, api_1.getStaffMessages)(function (messages) { + if (messages.length) + players_1.FishPlayer.messageStaff(messages); + }); + }, 5, 2); + //Tip + Timer.schedule(function () { + var showAd = Math.random() < 0.10; //10% chance every 15 minutes + var messagePool = showAd ? config.tips.ads : + (config.Mode.isChristmas && Math.random() > 0.5) ? config.tips.christmas : + config.tips.normal; + var messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; + var message = showAd ? "[gold]".concat(messageText, "[]") : "[gold]Tip: ".concat(messageText, "[]"); + Call.sendMessage(message); + }, 60, funcs_1.DurationSecs.minutes(15)); + //State check + Timer.schedule(function () { + if (Groups.unit.size() > 10000) { + Call.sendMessage("\n[scarlet]!!!!!\n[scarlet]Way too many units! Game over!\n[scarlet]!!!!!\n"); + Groups.unit.clear(); + (0, utils_1.neutralGameover)(); + } + }, 0, 1); + Timer.schedule(function () { + players_1.FishPlayer.updateAFKCheck(); + }, 0, 1); + //deliberately updating state on clock tick: + //avoids memory leak and other complications from Record + Timer.schedule(function () { + globals_1.ipJoins.clear(); + }, 0, funcs_1.DurationSecs.minutes(1)); + Timer.schedule(function () { + if (players_1.FishPlayer.antiBotMode()) { + Call.infoToast("[scarlet]ANTIBOT ACTIVE!!![] DOS blacklist size: ".concat(Vars.netServer.admins.dosBlacklist.size), 2); + } + }, 0, 1); + Timer.schedule(function () { + players_1.FishPlayer.validateVotekickSession(); + }, 0, 0.3); +} +Timer.schedule(function () { + (0, files_1.updateMaps)() + .then(function (result) { + if (result) { + Call.sendMessage("[orange]Maps have been updated. Run [white]/maps[] to view available maps."); + Log.info("Updated maps."); + } + }) + .catch(function (message) { + Call.sendMessage("[scarlet]Automated maps update failed, please report this to a staff member."); + Log.err("Automated map update failed: ".concat(String(message))); + }); +}, funcs_1.DurationSecs.minutes(1), funcs_1.DurationSecs.minutes(10)); diff --git a/build/scripts/types.js b/build/scripts/types.js index a646fab3..5521644d 100644 --- a/build/scripts/types.js +++ b/build/scripts/types.js @@ -1,6 +1,6 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains type definitions that are shared across files. -*/ -Object.defineProperty(exports, "__esModule", { value: true }); +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains type definitions that are shared across files. +*/ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/scripts/votes.js b/build/scripts/votes.js index 7c291217..3765de2f 100644 --- a/build/scripts/votes.js +++ b/build/scripts/votes.js @@ -1,218 +1,218 @@ -"use strict"; -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the voting system. -Some contributions: @author Jurorno9 -*/ -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var __values = (this && this.__values) || function(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) return m.call(o); - if (o && typeof o.length === "number") return { - next: function () { - if (o && i >= o.length) o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); -}; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; -var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.VoteManager = void 0; -var commands_1 = require("/frameworks/commands"); -var funcs_1 = require("/funcs"); -var globals_1 = require("/globals"); -var players_1 = require("/players"); -/** Manages a vote. */ -var VoteManager = /** @class */ (function (_super) { - __extends(VoteManager, _super); - function VoteManager(voteTime, goal, isEligible, isCounted) { - if (goal === void 0) { goal = ["fractionOfVoters", 0.50001]; } - if (isEligible === void 0) { isEligible = function () { return true; }; } - if (isCounted === void 0) { isCounted = function (fishP) { return !fishP.afk(); }; } - var _this = _super.call(this) || this; - _this.voteTime = voteTime; - _this.goal = goal; - _this.isEligible = isEligible; - _this.isCounted = isCounted; - /** The ongoing voting session, if there is one. */ - _this.session = null; - if (goal[0] == "fractionOfVoters") { - if (goal[1] < 0 || goal[1] > 1) - (0, funcs_1.crash)("Invalid goal: fractionOfVoters must be between 0 and 1 inclusive"); - } - else if (goal[0] == "absolute") { - if (goal[1] < 0) - (0, funcs_1.crash)("Invalid goal: absolute must be greater than 0"); - } - Events.on(EventType.PlayerLeave, function (_a) { - var player = _a.player; - //Run once the player has been removed, but resolve the player first in case the connection gets nulled - var fishP = players_1.FishPlayer.get(player); - Core.app.post(function () { return _this.unvote(fishP); }); - }); - Events.on(EventType.GameOverEvent, function () { return _this.resetVote(); }); - return _this; - } - /** @throws CommandError */ - VoteManager.prototype.start = function (player, newVote, data) { - var _this = this; - if (data === null) - (0, funcs_1.crash)("Cannot start vote: data not provided"); - if (!this.isEligible(player, data)) - (0, commands_1.fail)("You are not eligible for this vote."); - this.session = { - timer: Timer.schedule(function () { return _this._checkVote(false); }, this.voteTime / 1000), - votes: new Map(), - data: data, - }; - this.vote(player, newVote, data); - }; - /** @throws CommandError */ - VoteManager.prototype.vote = function (player, newVote, data) { - var _this = this; - if (!this.session) - return this.start(player, newVote, data); - if (!this.isEligible(player, this.session.data)) - (0, commands_1.fail)("You are not eligible for this vote."); - var oldVote = this.session.votes.get(player.uuid); - this.session.votes.set(player.uuid, newVote); - if (oldVote == null) - this.fire("player vote", [player, newVote]); - this.fire("player vote change", [player, oldVote !== null && oldVote !== void 0 ? oldVote : 0, newVote]); - if (Date.now() - globals_1.fishState.startTime < 3000) - Timer.schedule(function () { return _this._checkVote(false); }, 3); - else - this._checkVote(false); - }; - VoteManager.prototype.unvote = function (player) { - if (!this.session) - return; - var fishP = players_1.FishPlayer.resolve(player); - var vote = this.session.votes.get(fishP.uuid); - if (vote) { - this.session.votes.delete(fishP.uuid); - this.fire("player vote removed", [player, vote]); - this._checkVote(false); - } - }; - /** Does not fire the events used to display messages, please print one before calling this */ - VoteManager.prototype.forceVote = function (outcome) { - if (outcome) { - this.fire("success", [true]); - } - else { - this.fire("fail", [true]); - } - this.resetVote(); - }; - VoteManager.prototype.resetVote = function () { - if (this.session == null) - return; - this.session.timer.cancel(); - this.session = null; - }; - VoteManager.prototype.requiredVotes = function () { - var _this = this; - if (this.goal[0] == "absolute") { - return this.goal[1]; - } - else { - var numVoters = players_1.FishPlayer.getAllOnline().filter(function (p) { - return _this.isEligible(p, _this.session.data) && (_this.isCounted(p, _this.session.data) || _this.session.votes.has(p.uuid)); - }).length; - return Math.max(Math.ceil(this.goal[1] * numVoters), 1); - } - }; - VoteManager.prototype.currentVotes = function () { - var e_1, _a; - if (this.session) { - try { - for (var _b = __values(this.session.votes.keys()), _c = _b.next(); !_c.done; _c = _b.next()) { - var key = _c.value; - var fishP = players_1.FishPlayer.getById(key); - if (!this.isEligible(fishP, this.session.data)) - this.session.votes.delete(key); - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) _a.call(_b); - } - finally { if (e_1) throw e_1.error; } - } - return __spreadArray([], __read(this.session.votes), false).reduce(function (acc, _a) { - var _b = __read(_a, 2), k = _b[0], v = _b[1]; - return acc + v; - }, 0); - } - else - return 0; - }; - VoteManager.prototype.getEligibleVoters = function () { - var _this = this; - if (!this.session) - return []; - return players_1.FishPlayer.getAllOnline().filter(function (p) { - return _this.isEligible(p, _this.session.data); - }); - }; - VoteManager.prototype.messageEligibleVoters = function (message) { - this.getEligibleVoters().forEach(function (p) { return p.sendMessage(message); }); - }; - VoteManager.prototype._checkVote = function (end) { - var votes = this.currentVotes(); - var required = this.requiredVotes(); - if (votes >= required) { - this.fire("success", [false]); - this.fire("vote passed", [votes, required]); - this.resetVote(); - } - else if (end) { - this.fire("fail", [false]); - this.fire("vote failed", [votes, required]); - this.resetVote(); - } - }; - return VoteManager; -}(funcs_1.EventEmitter)); -exports.VoteManager = VoteManager; +"use strict"; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the voting system. +Some contributions: @author Jurorno9 +*/ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VoteManager = void 0; +var commands_1 = require("/frameworks/commands"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); +var players_1 = require("/players"); +/** Manages a vote. */ +var VoteManager = /** @class */ (function (_super) { + __extends(VoteManager, _super); + function VoteManager(voteTime, goal, isEligible, isCounted) { + if (goal === void 0) { goal = ["fractionOfVoters", 0.50001]; } + if (isEligible === void 0) { isEligible = function () { return true; }; } + if (isCounted === void 0) { isCounted = function (fishP) { return !fishP.afk(); }; } + var _this = _super.call(this) || this; + _this.voteTime = voteTime; + _this.goal = goal; + _this.isEligible = isEligible; + _this.isCounted = isCounted; + /** The ongoing voting session, if there is one. */ + _this.session = null; + if (goal[0] == "fractionOfVoters") { + if (goal[1] < 0 || goal[1] > 1) + (0, funcs_1.crash)("Invalid goal: fractionOfVoters must be between 0 and 1 inclusive"); + } + else if (goal[0] == "absolute") { + if (goal[1] < 0) + (0, funcs_1.crash)("Invalid goal: absolute must be greater than 0"); + } + Events.on(EventType.PlayerLeave, function (_a) { + var player = _a.player; + //Run once the player has been removed, but resolve the player first in case the connection gets nulled + var fishP = players_1.FishPlayer.get(player); + Core.app.post(function () { return _this.unvote(fishP); }); + }); + Events.on(EventType.GameOverEvent, function () { return _this.resetVote(); }); + return _this; + } + /** @throws CommandError */ + VoteManager.prototype.start = function (player, newVote, data) { + var _this = this; + if (data === null) + (0, funcs_1.crash)("Cannot start vote: data not provided"); + if (!this.isEligible(player, data)) + (0, commands_1.fail)("You are not eligible for this vote."); + this.session = { + timer: Timer.schedule(function () { return _this._checkVote(false); }, this.voteTime / 1000), + votes: new Map(), + data: data, + }; + this.vote(player, newVote, data); + }; + /** @throws CommandError */ + VoteManager.prototype.vote = function (player, newVote, data) { + var _this = this; + if (!this.session) + return this.start(player, newVote, data); + if (!this.isEligible(player, this.session.data)) + (0, commands_1.fail)("You are not eligible for this vote."); + var oldVote = this.session.votes.get(player.uuid); + this.session.votes.set(player.uuid, newVote); + if (oldVote == null) + this.fire("player vote", [player, newVote]); + this.fire("player vote change", [player, oldVote !== null && oldVote !== void 0 ? oldVote : 0, newVote]); + if (Date.now() - globals_1.fishState.startTime < 3000) + Timer.schedule(function () { return _this._checkVote(false); }, 3); + else + this._checkVote(false); + }; + VoteManager.prototype.unvote = function (player) { + if (!this.session) + return; + var fishP = players_1.FishPlayer.resolve(player); + var vote = this.session.votes.get(fishP.uuid); + if (vote) { + this.session.votes.delete(fishP.uuid); + this.fire("player vote removed", [player, vote]); + this._checkVote(false); + } + }; + /** Does not fire the events used to display messages, please print one before calling this */ + VoteManager.prototype.forceVote = function (outcome) { + if (outcome) { + this.fire("success", [true]); + } + else { + this.fire("fail", [true]); + } + this.resetVote(); + }; + VoteManager.prototype.resetVote = function () { + if (this.session == null) + return; + this.session.timer.cancel(); + this.session = null; + }; + VoteManager.prototype.requiredVotes = function () { + var _this = this; + if (this.goal[0] == "absolute") { + return this.goal[1]; + } + else { + var numVoters = players_1.FishPlayer.getAllOnline().filter(function (p) { + return _this.isEligible(p, _this.session.data) && (_this.isCounted(p, _this.session.data) || _this.session.votes.has(p.uuid)); + }).length; + return Math.max(Math.ceil(this.goal[1] * numVoters), 1); + } + }; + VoteManager.prototype.currentVotes = function () { + var e_1, _a; + if (this.session) { + try { + for (var _b = __values(this.session.votes.keys()), _c = _b.next(); !_c.done; _c = _b.next()) { + var key = _c.value; + var fishP = players_1.FishPlayer.getById(key); + if (!this.isEligible(fishP, this.session.data)) + this.session.votes.delete(key); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_1) throw e_1.error; } + } + return __spreadArray([], __read(this.session.votes), false).reduce(function (acc, _a) { + var _b = __read(_a, 2), k = _b[0], v = _b[1]; + return acc + v; + }, 0); + } + else + return 0; + }; + VoteManager.prototype.getEligibleVoters = function () { + var _this = this; + if (!this.session) + return []; + return players_1.FishPlayer.getAllOnline().filter(function (p) { + return _this.isEligible(p, _this.session.data); + }); + }; + VoteManager.prototype.messageEligibleVoters = function (message) { + this.getEligibleVoters().forEach(function (p) { return p.sendMessage(message); }); + }; + VoteManager.prototype._checkVote = function (end) { + var votes = this.currentVotes(); + var required = this.requiredVotes(); + if (votes >= required) { + this.fire("success", [false]); + this.fire("vote passed", [votes, required]); + this.resetVote(); + } + else if (end) { + this.fire("fail", [false]); + this.fire("vote failed", [votes, required]); + this.resetVote(); + } + }; + return VoteManager; +}(funcs_1.EventEmitter)); +exports.VoteManager = VoteManager; diff --git a/src/README.md b/src/README.md index bdfa3498..5b6d923f 100644 --- a/src/README.md +++ b/src/README.md @@ -1,3 +1,3 @@ -# src - -This folder contains the main plugin code. +# src + +This folder contains the main plugin code. diff --git a/src/achievements.ts b/src/achievements.ts index 53c253c3..bab42c30 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -1,676 +1,676 @@ -import { FColor, Gamemode, GamemodeName, GamemodeNames } from "/config"; -import { Duration } from "/funcs"; -import { FishEvents, unitsT5 } from "/globals"; -import { FishPlayer } from "/players"; -import { Rank } from "/ranks"; -import { getStatuses } from "/utils"; - -//scrap doesn't count -const serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; -const erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; -const usefulItems10k = { - serpulo: serpuloItems.map(i => new ItemStack(i, 10_000)), - erekir: erekirItems.map(i => new ItemStack(i, 10_000)), - sun: [...serpuloItems, ...erekirItems].map(i => new ItemStack(i, 10_000)), -}; -const allItems1k = Vars.content.items().select(i => !i.hidden).toArray().map(i => new ItemStack(i, 1000)); -const mixtechItems = Items.serpuloItems.copy(); -Items.erekirItems.each(i => mixtechItems.add(i)); - -export class Achievement { - nid: number; - sid!: string; - - icon: string; - description: string; - extendedDescription?: string; - - checkPlayerInfrequent?: (player:FishPlayer) => boolean; - checkPlayerFrequent?: (player:FishPlayer) => boolean; - checkPlayerJoin?: (player:FishPlayer) => boolean; - checkPlayerGameover?: (player:FishPlayer, winTeam:Team) => boolean; - checkInfrequent?: (team: Team) => boolean; - checkFrequent?: (team: Team) => boolean; - checkGameover?: (winTeam:Team) => boolean; - - notify: "nobody" | "player" | "everyone" = "player"; - hidden = false; - disabled = false; - allowedModes: GamemodeName[]; - modesText: string; - - static all: Achievement[] = []; - /** Checked every second. */ - static checkFrequent: Achievement[] = []; - /** Checked every 10 seconds. Use for states that can be gained but not lost, such as "x wins". */ - static checkInfrequent: Achievement[] = []; - static checkJoin: Achievement[] = []; - static checkGameover: Achievement[] = []; - - private static _id = 0; - constructor( - icon: string | number | [string, number | string], - public name: string, - description: string | [string, string], - options: Partial & { - modes: ["only" | "not", ...GamemodeName[]]; - }> = {}, - ){ - if(Array.isArray(icon)){ - this.icon = (icon[0].startsWith("[") ? icon[0] : `[${icon[0]}]`) + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); - } else if(typeof icon == "number"){ - this.icon = String.fromCharCode(icon); - } else { - this.icon = icon; - } - if(Array.isArray(description)){ - [this.description, this.extendedDescription] = description; - } else this.description = description; - this.nid = Achievement._id ++; - Object.assign(this, options); - if(options.modes){ - const [type, ...modes] = options.modes; - if(type == "only"){ - this.allowedModes = modes; - this.modesText = modes.join(", "); - } else { - this.allowedModes = GamemodeNames.filter(m => !modes.includes(m)); - this.modesText = `all except ${modes.join(", ")}`; - } - } else { - this.allowedModes = GamemodeNames; - this.modesText = `all`; - } - if(!this.disabled){ - Achievement.all.push(this); - if(this.checkPlayerFrequent || this.checkFrequent) Achievement.checkFrequent.push(this); - if(this.checkPlayerInfrequent || this.checkInfrequent) Achievement.checkInfrequent.push(this); - if(this.checkPlayerJoin) Achievement.checkJoin.push(this); - if(this.checkPlayerGameover || this.checkGameover) Achievement.checkGameover.push(this); - } - } - - message():string { - return FColor.achievement`Achievement granted!\n[accent]${this.name}[white]: ${this.description}`; - } - messageToEveryone(player:FishPlayer):string { - return FColor.achievement`Player ${player.prefixedName} has completed the achievement "${this.name}".`; - } - allowedInMode(){ - return this.allowedModes.includes(Gamemode.name()); - } - - public grantToAllOnline(team?: Team){ - FishPlayer.forEachPlayer(p => { - if(!this.has(p) && (!team || p.team() == team)){ - if(this.notify != "nobody") p.sendMessage(this.message()); - this.setObtained(p); - } - }); - } - /** Do not call this in a loop on an achievement set to notify everyone. */ - public grantTo(player:FishPlayer, allowRepeatMessage = true){ //TODO make this default false - const has = this.has(player); - if(!has || allowRepeatMessage){ - if(this.notify == "everyone") Call.sendMessage(this.messageToEveryone(player)); - else if(this.notify == "player") player.sendMessage(this.message()); - } - if(!has) this.setObtained(player); - } - - private setObtained(player:FishPlayer){ - //void player.updateSynced(fishP => fishP.achievements.set(this.nid)); - player.achievements.set(this.nid); - } - public has(player:FishPlayer){ - return player.achievements.get(this.nid); - } -} - -Events.on(EventType.PlayerJoin, ({player}: {player: mindustryPlayer}) => { - Time.mark(); - for(const ach of Achievement.checkJoin){ - if(ach.allowedInMode()){ - const fishP = FishPlayer.get(player); - if(!ach.has(fishP) && ach.checkPlayerJoin?.(fishP)){ - if(fishP.dataSynced) ach.grantTo(fishP); - else Timer.schedule(() => ach.grantTo(fishP), 2); //2 seconds should be enough - } - } - } - Log.debug("ach join @", Time.elapsed()); -}); -FishEvents.on("gameOver", (_, winner) => { - Time.mark(); - for(const ach of Achievement.checkGameover){ - if(ach.allowedInMode()){ - if(ach.checkGameover?.(winner)) ach.grantToAllOnline(); - else FishPlayer.forEachPlayer(fishP => { - if(!ach.has(fishP) && ach.checkPlayerGameover?.(fishP, winner)){ - ach.grantTo(fishP); - } - }); - } - } - Log.debug("ach gameover @", Time.elapsed()); -}); -Timer.schedule(() => { - Time.mark(); - for(const ach of Achievement.checkFrequent){ - if(ach.allowedInMode()){ - if(ach.checkFrequent){ - if(Gamemode.pvp()){ - Vars.state.teams.active.each(({team}) => { - if(ach.checkFrequent!(team)) ach.grantToAllOnline(team); - }); - } else { - if(ach.checkFrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); - } - } else { - FishPlayer.forEachPlayer(fishP => { - if(!ach.has(fishP) && ach.checkPlayerFrequent?.(fishP)) ach.grantTo(fishP); - }); - } - } - } - Log.debug("ach frequent @", Time.elapsed()); -}, 1, 1); -Timer.schedule(() => { - Time.mark(); - for(const ach of Achievement.checkInfrequent){ - if(ach.allowedInMode()){ - if(ach.checkInfrequent){ - if(Gamemode.pvp()){ - Vars.state.teams.active.each(({team}) => { - if(ach.checkInfrequent!(team)) ach.grantToAllOnline(team); - }); - } else { - if(ach.checkInfrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); - } - } else { - FishPlayer.forEachPlayer(fishP => { - if(!ach.has(fishP) && ach.checkPlayerInfrequent?.(fishP)) ach.grantTo(fishP); - }); - } - } - } - Log.debug("ach infrequent @", Time.elapsed()); -}, 10, 10); - -export const Achievements = { - // =========================== - // ╦ ╦ ╔═╗ ╦═╗ ╔╗╔ ╦ ╔╗╔ ╔═╗ ┬ - // ║║║ ╠═╣ ╠╦╝ ║║║ ║ ║║║ ║ ╦ │ - // ╚╩╝ ╩ ╩ ╩╚═ ╝╚╝ ╩ ╝╚╝ ╚═╝ o - // =========================== - // Do not change the order of any achievements. - // Do not remove any achievements: instead, set the "disabled" option to true. - // Reordering achievements will cause ID shifts. - - - //Joining based - welcome: new Achievement(["gold", Iconc.infoCircle], "Welcome", "Join the server.", { - checkPlayerJoin: () => true, - notify: "nobody" - }), - migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { - disabled: true - }), //TODO - frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { - checkPlayerJoin: p => p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > Duration.months(1)) - }), - - //Gamemode based - attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { - modes: ["only", "attack"], - checkPlayerGameover: (player, winTeam) => - Vars.state.rules.defaultTeam == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, - }), - survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { - modes: ["only", "survival"], - checkPlayerInfrequent: (player) => - player.tstats.wavesSurvived >= 50, - }), - pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { - modes: ["only", "pvp"], - checkPlayerGameover: (player, winTeam) => - player.team() == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, - }), - sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { - modes: ["only", "sandbox"], - checkPlayerInfrequent: p => p.stats.timeInGame > Duration.hours(1), - }), - hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { - modes: ["only", "hexed"], - checkPlayerGameover: (player) => - player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, - }), - minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { - modes: ["only", "minigame"], - checkPlayerGameover: (player, winTeam) => - player.team() == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, - }), - - //playtime based - playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { - checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(1) - }), - playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { - checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(12) - }), - playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { - checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(2) - }), - playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { - checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(10) - }), - - //victories based - victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { - checkPlayerGameover: p => p.globalStats.gamesWon >= 1 - }), - victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { - checkPlayerGameover: p => p.globalStats.gamesWon >= 5 - }), - victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { - checkPlayerGameover: p => p.globalStats.gamesWon >= 30 - }), - victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { - checkPlayerGameover: p => p.globalStats.gamesWon >= 100, - notify: "everyone" - }), - - //games based - games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { - checkPlayerGameover: p => p.globalStats.gamesFinished >= 10 - }), - games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { - checkPlayerGameover: p => p.globalStats.gamesFinished >= 40 - }), - games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { - checkPlayerGameover: p => p.globalStats.gamesFinished >= 100 - }), - games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { - checkPlayerGameover: p => p.globalStats.gamesFinished >= 200, - notify: "everyone" - }), - - //messages based - messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { - checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 1, - notify: "nobody" - }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 100 - }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", ["Send 500 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 500 - }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", ["Send 2000 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 2000 - }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", ["Send 5000 chat messages.", "Warning: you will be kicked if you spam the chat."], { - checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 5000, - notify: "everyone" - }), - - //blocks built based - builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { - checkPlayerInfrequent: p => p.globalStats.blocksPlaced >= 1, - notify: "nobody" - }), - builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { - checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 200 - }), - builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { - checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 1000 - }), - builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { - checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 5000, - }), - - //units - t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { - modes: ["not", "sandbox"], checkPlayerFrequent(player) { - return (unitsT5 as Array).includes(player.unit()?.type); - }, - }), - dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { - modes: ["not", "sandbox"], - disabled: true - }), //TODO - worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { - checkPlayerFrequent(player) { - return player.unit()?.type == UnitTypes.latum; - } - }), - - //pvp - above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { - modes: ["only", "pvp"], - checkPlayerInfrequent: p => p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20 - }), - head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { - modes: ["only", "pvp"], - disabled: true - }), //TODO - one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { - modes: ["only", "pvp"], - disabled: true - }), //TODO - - //sandbox - underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { - modes: ["only", "sandbox"], - checkFrequent(){ - let found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(({graph}) => { - //we don't need to actually check for power sources, just assume that ~1mil power is a source - if(graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && (graph.lastPowerProduced / Time.delta * 60) >= 999_900) - found = true; - }); - return found; - } - }), - - //easter eggs - memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { - notify: "nobody" - }), - run_js_without_perms: new Achievement(["yellow", Iconc.warning], "XKCD 838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { - notify: "everyone" - }), - script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { - notify: "nobody" - }), - hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { - hidden: true - }), - - //items based - items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { - modes: ["not", "sandbox"], - checkPlayerFrequent(player) { - if(!Vars.state.planet) return false; - return player.team().items()?.has(usefulItems10k[Vars.state.planet.name as "serpulo" | "erekir" | "sun"]) || false; - }, - }), - fullVault: new Achievement(["green", Blocks.vault.emoji()], "Well Stocked", ["Fill a vault with every obtainable item.", "Requires mixtech."], { - modes: ["not", "sandbox"], - checkInfrequent(team) { - return Vars.indexer.getFlagged(team, BlockFlag.storage).contains(boolf(b => b.block == Blocks.vault && b.items.has(allItems1k) && b.linkedCore == null)); - }, - }), - full_core: new Achievement(["green", Blocks.coreAcropolis.emoji()], "Multiblock Incinerator", "Completely fill the core with all obtainable items on a map with core incineration enabled.", { - modes: ["not", "sandbox"], - checkFrequent(team) { - if(!Vars.state.planet) return false; - let items; - switch(Vars.state.planet.name as "serpulo" | "erekir" | "sun"){ - case "serpulo": items = Items.serpuloItems; break; - case "erekir": items = Items.erekirItems; break; - case "sun": items = mixtechItems; break; - } - const capacity = team.core()?.storageCapacity; - if(!capacity) return false; - const module = team.items(); - return items.allMatch(i => module.has(i, capacity)); - }, - }), - siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { - modes: ["not", "sandbox"] - }), - silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { - modes: ["not", "sandbox"], - checkFrequent: team => team.items().has(Items.silicon, 100_000) - }), - - //other players based - alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { - notify: "nobody" - }), - join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { - checkPlayerJoin: () => Groups.player.size() > 20, - }), - meet_staff: new Achievement(["lime", Iconc.hammer], "Griefer Beware", "Meet a staff member in-game", { - checkPlayerJoin: () => Groups.player.contains(p => FishPlayer.get(p).ranksAtLeast("mod")), - }), - meet_fish: new Achievement(["blue", Iconc.admin], "The Big Fish", "Meet >|||>Fish himself in-game", { - checkPlayerJoin: () => Groups.player.contains(p => FishPlayer.get(p).ranksAtLeast("fish")), - hidden: true, - }), - server_speak: new Achievement(["pink", Iconc.host], "It Speaks!", "Hear the server talk in chat."), - see_marked_griefer: new Achievement(["red", Iconc.hammer], "Flying Tonk", "See a marked griefer in-game.", { - checkInfrequent: () => Groups.player.contains(p => FishPlayer.get(p).marked()), - }), - - //maps based - beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { - notify: "everyone", - modes: ["not", "pvp"], - checkGameover: (team) => team == Vars.state.rules.defaultTeam && !Vars.state.map.custom - }), - - //misc - power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { - modes: ["not", "sandbox"], - checkFrequent(team){ - let found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(({graph}) => { - //we need to actually check for power sources - if( - (graph.lastPowerProduced / Time.delta * 60) > 1e6 && - !graph.producers.contains(boolf(b => b.block == Blocks.powerSource)) && - graph.producers.firstOpt()?.team == team - ) - found = true; - }); - return found; - } - }), - pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { - modes: ["not", "sandbox"], - disabled: true - }), //TODO - core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 50 health, but survive.", { - modes: ["not", "sandbox"], - }), - enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 50 health, but survive.", { - modes: ["not", "sandbox"], - }), - verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { - checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "nobody" - }), - click_me: new Achievement(Iconc.bookOpen, "Clicked", `Run /achievementgrid and click this achievement.`), - afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { - modes: ["not", "sandbox"], - checkPlayerGameover(player, winTeam) { - return player.team() == winTeam && player.tstats.blockInteractionsThisMap == 0; - }, - }), - status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { - checkPlayerFrequent: p => { - const unit = p.unit(); - if(!unit) return false; - const statuses = getStatuses(unit); - return statuses.size >= 5; - }, - modes: ["not", "sandbox"] - }), - drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { - notify: "everyone", - modes: ["not", "sandbox"] - }), - drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", `Drown a Mace in ${Blocks.cryofluid.emoji()} Cryofluid.`, { - notify: "everyone", - modes: ["not", "sandbox"] - }), - max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { - checkPlayerFrequent(player) { - const tile = player.unit()?.tile?.(); - if(!tile) return false; - return tile.block == Blocks.duo && !tile.ammo!.isEmpty() && tile.ammo!.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; - }, - notify: "everyone", - modes: ["not", "sandbox"] - }), - foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { - notify: "everyone", - modes: ["not", "sandbox"] - }), - impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { - modes: ["not", "sandbox"], - notify: "everyone", - checkInfrequent(team){ - let found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(({graph}) => { - if(graph.producers.size >= 15 && graph.producers.count(b => b.block == Blocks.impactReactor && b.warmup! > 0.99999) > 15 && graph.producers.first().team == team) - found = true; - }); - return found; - }, - }), - - help_help: new Achievement(["brown", Iconc.info], "Help with help", "Run /help help", { - notify: "everyone" - }), - ohno: new Achievement(["scarlet", UnitTypes.alpha.emoji()], "Oh no", "Control an ohno unit."), - sniper_duel: new Achievement(["yellow", UnitTypes.omura.emoji()], "Sniper duel", "Kill a Foreshadow with an Omura from outside its range."), - around_the_world: new Achievement(Iconc.planet, "Around the World", "Fly your unit around the entire map without entering it, starting from the lower left.", { - notify: "everyone", - }), -} satisfies Record; -Object.entries(Achievements).forEach(([id, a]) => a.sid = id); - -FishEvents.on("commandUnauthorized", (_, player, name) => { - if((name == "js" || name == "fjs") && !Achievements.run_js_without_perms.has(player)) - Achievements.run_js_without_perms.grantTo(player); -}); - - -Events.on(EventType.UnitDrownEvent, ({unit}:{unit: Unit}) => { - if(!Gamemode.sandbox()){ - if(unit.type == UnitTypes.mace && unit.tileOn()?.floor() == Blocks.cryofluid) Achievements.drown_mace_in_cryo.grantToAllOnline(); - else if(unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish){ - if(Gamemode.pvp()){ - Vars.state.teams.active.map(t => t.team).select(t => t !== unit.team).each(t => Achievements.drown_big_tank.grantToAllOnline(t)); - } else { - if(unit.team !== Vars.state.rules.defaultTeam) Achievements.drown_big_tank.grantToAllOnline(); - } - } - } -}); - -Events.on(EventType.UnitBulletDestroyEvent, ({unit, bullet}:{unit:Unit; bullet: Bullet}) => { - if(!Gamemode.sandbox() && unit.type == UnitTypes.dagger && (bullet.owner as Building | null)?.block == Blocks.foreshadow){ - const build = bullet.owner as Building; - if(build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) Achievements.foreshadow_overkill.grantToAllOnline(build.team); - } -}); - -Events.on(EventType.BuildingBulletDestroyEvent, ({build, bullet}:{build:Building, bullet:Bullet}) => { - if(!Gamemode.sandbox() && build.block == Blocks.foreshadow && (bullet.owner as Unit | null)?.type == UnitTypes.omura){ - const unit = bullet.owner as Unit; - const player = unit.getPlayer(); - if(player && !unit.within(build, build.range!() + unit.hitSize / 2)) Achievements.sniper_duel.grantTo(FishPlayer.get(player)); - } -}); - -let siliconReached = Team.all.map(_ => false); -Events.on(EventType.GameOverEvent, () => siliconReached = Team.all.map(_ => false)); -let isAlone = 0; - -Timer.schedule(() => { - if(!Vars.state.gameOver && !Gamemode.sandbox()){ - Vars.state.teams.active.each(({team}) => { - if(team.items().has(Items.silicon, 2000)) siliconReached[team.id] = true; - else if(siliconReached[team.id] && team.items().get(Items.silicon) == 0) - Achievements.siligone.grantToAllOnline(team); - }); - } - if(Groups.player.size() == 1){ - if(isAlone == 0) isAlone = Date.now(); - else if(Date.now() > isAlone + Duration.minutes(2)) Achievements.alone.grantToAllOnline(); - } else isAlone = 0; -}, 2, 2); - -const coreHealthTime = new Map(); -if(!Gamemode.sandbox()) Timer.schedule(() => { - coreHealthTime.forEach((value, core) => { - if(Date.now() > value){ - if(core.dead){ - coreHealthTime.delete(core); - } else if(core.health > 50){ - //grant achievement - Achievements.core_low_hp.grantToAllOnline(core.team); - FishPlayer.forEachPlayer(p => { - if(core.team != p.team() && !Achievements.enemy_core_low_hp.has(p)) - Achievements.enemy_core_low_hp.grantTo(p); - }); - coreHealthTime.delete(core); - } - } - }); - Vars.state.teams.active.flatMap(t => t.cores).each(core => { - if(core.health < 50 && !coreHealthTime.get(core)) coreHealthTime.set(core, Date.now() + 12_000); - }); -}, 1, 1); -const aroundTheWorld: Record = {}; -Timer.schedule(() => { - FishPlayer.forEachPlayer(p => { - const unit = p.unit(); - if(unit && unit.x < 0 && unit.y < 0){ - aroundTheWorld[p.uuid] ??= { player: p, unit, side: "left" }; - } - }); - for(const [uuid, entry] of Object.entries(aroundTheWorld)){ - if(!(() => { - if(entry.unit.dead) return false; - const left = entry.unit.x < 0; - const bottom = entry.unit.y < 0; - const right = entry.unit.x > (Vars.world.width() - 1) * 8; - const top = entry.unit.y > (Vars.world.height() - 1) * 8; - switch(entry.side){ - case "left": - if(!left) return false; - if(top) entry.side = "top"; - break; - case "top": - if(!top) return false; - if(right) entry.side = "right"; - break; - case "right": - if(!right) return false; - if(bottom) entry.side = "bottom"; - break; - case "bottom": - if(!bottom) return false; - if(left){ - Achievements.around_the_world.grantTo(entry.player, true); - return false; - } - break; - } - return true; - })()) delete aroundTheWorld[uuid]; - } -}, 1, 0.5); -Events.on(EventType.GameOverEvent, () => coreHealthTime.clear()); -Events.on(EventType.WorldLoadEvent, () => coreHealthTime.clear()); - - -FishEvents.on("scriptKiddie", (_, p) => Timer.schedule(() => { - if(!Achievements.script_kiddie.has(p)) Achievements.script_kiddie.grantTo(p); -}, 2)); -FishEvents.on("memoryCorruption", () => Achievements.memory_corruption.grantToAllOnline()); +import { FColor, Gamemode, GamemodeName, GamemodeNames } from "/config"; +import { Duration } from "/funcs"; +import { FishEvents, unitsT5 } from "/globals"; +import { FishPlayer } from "/players"; +import { Rank } from "/ranks"; +import { getStatuses } from "/utils"; + +//scrap doesn't count +const serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; +const erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; +const usefulItems10k = { + serpulo: serpuloItems.map(i => new ItemStack(i, 10_000)), + erekir: erekirItems.map(i => new ItemStack(i, 10_000)), + sun: [...serpuloItems, ...erekirItems].map(i => new ItemStack(i, 10_000)), +}; +const allItems1k = Vars.content.items().select(i => !i.hidden).toArray().map(i => new ItemStack(i, 1000)); +const mixtechItems = Items.serpuloItems.copy(); +Items.erekirItems.each(i => mixtechItems.add(i)); + +export class Achievement { + nid: number; + sid!: string; + + icon: string; + description: string; + extendedDescription?: string; + + checkPlayerInfrequent?: (player:FishPlayer) => boolean; + checkPlayerFrequent?: (player:FishPlayer) => boolean; + checkPlayerJoin?: (player:FishPlayer) => boolean; + checkPlayerGameover?: (player:FishPlayer, winTeam:Team) => boolean; + checkInfrequent?: (team: Team) => boolean; + checkFrequent?: (team: Team) => boolean; + checkGameover?: (winTeam:Team) => boolean; + + notify: "nobody" | "player" | "everyone" = "player"; + hidden = false; + disabled = false; + allowedModes: GamemodeName[]; + modesText: string; + + static all: Achievement[] = []; + /** Checked every second. */ + static checkFrequent: Achievement[] = []; + /** Checked every 10 seconds. Use for states that can be gained but not lost, such as "x wins". */ + static checkInfrequent: Achievement[] = []; + static checkJoin: Achievement[] = []; + static checkGameover: Achievement[] = []; + + private static _id = 0; + constructor( + icon: string | number | [string, number | string], + public name: string, + description: string | [string, string], + options: Partial & { + modes: ["only" | "not", ...GamemodeName[]]; + }> = {}, + ){ + if(Array.isArray(icon)){ + this.icon = (icon[0].startsWith("[") ? icon[0] : `[${icon[0]}]`) + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); + } else if(typeof icon == "number"){ + this.icon = String.fromCharCode(icon); + } else { + this.icon = icon; + } + if(Array.isArray(description)){ + [this.description, this.extendedDescription] = description; + } else this.description = description; + this.nid = Achievement._id ++; + Object.assign(this, options); + if(options.modes){ + const [type, ...modes] = options.modes; + if(type == "only"){ + this.allowedModes = modes; + this.modesText = modes.join(", "); + } else { + this.allowedModes = GamemodeNames.filter(m => !modes.includes(m)); + this.modesText = `all except ${modes.join(", ")}`; + } + } else { + this.allowedModes = GamemodeNames; + this.modesText = `all`; + } + if(!this.disabled){ + Achievement.all.push(this); + if(this.checkPlayerFrequent || this.checkFrequent) Achievement.checkFrequent.push(this); + if(this.checkPlayerInfrequent || this.checkInfrequent) Achievement.checkInfrequent.push(this); + if(this.checkPlayerJoin) Achievement.checkJoin.push(this); + if(this.checkPlayerGameover || this.checkGameover) Achievement.checkGameover.push(this); + } + } + + message():string { + return FColor.achievement`Achievement granted!\n[accent]${this.name}[white]: ${this.description}`; + } + messageToEveryone(player:FishPlayer):string { + return FColor.achievement`Player ${player.prefixedName} has completed the achievement "${this.name}".`; + } + allowedInMode(){ + return this.allowedModes.includes(Gamemode.name()); + } + + public grantToAllOnline(team?: Team){ + FishPlayer.forEachPlayer(p => { + if(!this.has(p) && (!team || p.team() == team)){ + if(this.notify != "nobody") p.sendMessage(this.message()); + this.setObtained(p); + } + }); + } + /** Do not call this in a loop on an achievement set to notify everyone. */ + public grantTo(player:FishPlayer, allowRepeatMessage = true){ //TODO make this default false + const has = this.has(player); + if(!has || allowRepeatMessage){ + if(this.notify == "everyone") Call.sendMessage(this.messageToEveryone(player)); + else if(this.notify == "player") player.sendMessage(this.message()); + } + if(!has) this.setObtained(player); + } + + private setObtained(player:FishPlayer){ + //void player.updateSynced(fishP => fishP.achievements.set(this.nid)); + player.achievements.set(this.nid); + } + public has(player:FishPlayer){ + return player.achievements.get(this.nid); + } +} + +Events.on(EventType.PlayerJoin, ({player}: {player: mindustryPlayer}) => { + Time.mark(); + for(const ach of Achievement.checkJoin){ + if(ach.allowedInMode()){ + const fishP = FishPlayer.get(player); + if(!ach.has(fishP) && ach.checkPlayerJoin?.(fishP)){ + if(fishP.dataSynced) ach.grantTo(fishP); + else Timer.schedule(() => ach.grantTo(fishP), 2); //2 seconds should be enough + } + } + } + Log.debug("ach join @", Time.elapsed()); +}); +FishEvents.on("gameOver", (_, winner) => { + Time.mark(); + for(const ach of Achievement.checkGameover){ + if(ach.allowedInMode()){ + if(ach.checkGameover?.(winner)) ach.grantToAllOnline(); + else FishPlayer.forEachPlayer(fishP => { + if(!ach.has(fishP) && ach.checkPlayerGameover?.(fishP, winner)){ + ach.grantTo(fishP); + } + }); + } + } + Log.debug("ach gameover @", Time.elapsed()); +}); +Timer.schedule(() => { + Time.mark(); + for(const ach of Achievement.checkFrequent){ + if(ach.allowedInMode()){ + if(ach.checkFrequent){ + if(Gamemode.pvp()){ + Vars.state.teams.active.each(({team}) => { + if(ach.checkFrequent!(team)) ach.grantToAllOnline(team); + }); + } else { + if(ach.checkFrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); + } + } else { + FishPlayer.forEachPlayer(fishP => { + if(!ach.has(fishP) && ach.checkPlayerFrequent?.(fishP)) ach.grantTo(fishP); + }); + } + } + } + Log.debug("ach frequent @", Time.elapsed()); +}, 1, 1); +Timer.schedule(() => { + Time.mark(); + for(const ach of Achievement.checkInfrequent){ + if(ach.allowedInMode()){ + if(ach.checkInfrequent){ + if(Gamemode.pvp()){ + Vars.state.teams.active.each(({team}) => { + if(ach.checkInfrequent!(team)) ach.grantToAllOnline(team); + }); + } else { + if(ach.checkInfrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); + } + } else { + FishPlayer.forEachPlayer(fishP => { + if(!ach.has(fishP) && ach.checkPlayerInfrequent?.(fishP)) ach.grantTo(fishP); + }); + } + } + } + Log.debug("ach infrequent @", Time.elapsed()); +}, 10, 10); + +export const Achievements = { + // =========================== + // ╦ ╦ ╔═╗ ╦═╗ ╔╗╔ ╦ ╔╗╔ ╔═╗ ┬ + // ║║║ ╠═╣ ╠╦╝ ║║║ ║ ║║║ ║ ╦ │ + // ╚╩╝ ╩ ╩ ╩╚═ ╝╚╝ ╩ ╝╚╝ ╚═╝ o + // =========================== + // Do not change the order of any achievements. + // Do not remove any achievements: instead, set the "disabled" option to true. + // Reordering achievements will cause ID shifts. + + + //Joining based + welcome: new Achievement(["gold", Iconc.infoCircle], "Welcome", "Join the server.", { + checkPlayerJoin: () => true, + notify: "nobody" + }), + migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { + disabled: true + }), //TODO + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { + checkPlayerJoin: p => p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > Duration.months(1)) + }), + + //Gamemode based + attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { + modes: ["only", "attack"], + checkPlayerGameover: (player, winTeam) => + Vars.state.rules.defaultTeam == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), + survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { + modes: ["only", "survival"], + checkPlayerInfrequent: (player) => + player.tstats.wavesSurvived >= 50, + }), + pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { + modes: ["only", "pvp"], + checkPlayerGameover: (player, winTeam) => + player.team() == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), + sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { + modes: ["only", "sandbox"], + checkPlayerInfrequent: p => p.stats.timeInGame > Duration.hours(1), + }), + hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { + modes: ["only", "hexed"], + checkPlayerGameover: (player) => + player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), + minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { + modes: ["only", "minigame"], + checkPlayerGameover: (player, winTeam) => + player.team() == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), + + //playtime based + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(1) + }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(12) + }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(2) + }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(10) + }), + + //victories based + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 1 + }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 5 + }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 30 + }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 100, + notify: "everyone" + }), + + //games based + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 10 + }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 40 + }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 100 + }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 200, + notify: "everyone" + }), + + //messages based + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 1, + notify: "nobody" + }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 100 + }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", ["Send 500 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 500 + }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", ["Send 2000 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 2000 + }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", ["Send 5000 chat messages.", "Warning: you will be kicked if you spam the chat."], { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 5000, + notify: "everyone" + }), + + //blocks built based + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced >= 1, + notify: "nobody" + }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 200 + }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 1000 + }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 5000, + }), + + //units + t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { + modes: ["not", "sandbox"], checkPlayerFrequent(player) { + return (unitsT5 as Array).includes(player.unit()?.type); + }, + }), + dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { + modes: ["not", "sandbox"], + disabled: true + }), //TODO + worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { + checkPlayerFrequent(player) { + return player.unit()?.type == UnitTypes.latum; + } + }), + + //pvp + above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { + modes: ["only", "pvp"], + checkPlayerInfrequent: p => p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20 + }), + head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { + modes: ["only", "pvp"], + disabled: true + }), //TODO + one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { + modes: ["only", "pvp"], + disabled: true + }), //TODO + + //sandbox + underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { + modes: ["only", "sandbox"], + checkFrequent(){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if(graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && (graph.lastPowerProduced / Time.delta * 60) >= 999_900) + found = true; + }); + return found; + } + }), + + //easter eggs + memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { + notify: "nobody" + }), + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "XKCD 838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { + notify: "everyone" + }), + script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { + notify: "nobody" + }), + hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { + hidden: true + }), + + //items based + items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { + modes: ["not", "sandbox"], + checkPlayerFrequent(player) { + if(!Vars.state.planet) return false; + return player.team().items()?.has(usefulItems10k[Vars.state.planet.name as "serpulo" | "erekir" | "sun"]) || false; + }, + }), + fullVault: new Achievement(["green", Blocks.vault.emoji()], "Well Stocked", ["Fill a vault with every obtainable item.", "Requires mixtech."], { + modes: ["not", "sandbox"], + checkInfrequent(team) { + return Vars.indexer.getFlagged(team, BlockFlag.storage).contains(boolf(b => b.block == Blocks.vault && b.items.has(allItems1k) && b.linkedCore == null)); + }, + }), + full_core: new Achievement(["green", Blocks.coreAcropolis.emoji()], "Multiblock Incinerator", "Completely fill the core with all obtainable items on a map with core incineration enabled.", { + modes: ["not", "sandbox"], + checkFrequent(team) { + if(!Vars.state.planet) return false; + let items; + switch(Vars.state.planet.name as "serpulo" | "erekir" | "sun"){ + case "serpulo": items = Items.serpuloItems; break; + case "erekir": items = Items.erekirItems; break; + case "sun": items = mixtechItems; break; + } + const capacity = team.core()?.storageCapacity; + if(!capacity) return false; + const module = team.items(); + return items.allMatch(i => module.has(i, capacity)); + }, + }), + siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { + modes: ["not", "sandbox"] + }), + silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { + modes: ["not", "sandbox"], + checkFrequent: team => team.items().has(Items.silicon, 100_000) + }), + + //other players based + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { + notify: "nobody" + }), + join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { + checkPlayerJoin: () => Groups.player.size() > 20, + }), + meet_staff: new Achievement(["lime", Iconc.hammer], "Griefer Beware", "Meet a staff member in-game", { + checkPlayerJoin: () => Groups.player.contains(p => FishPlayer.get(p).ranksAtLeast("mod")), + }), + meet_fish: new Achievement(["blue", Iconc.admin], "The Big Fish", "Meet >|||>Fish himself in-game", { + checkPlayerJoin: () => Groups.player.contains(p => FishPlayer.get(p).ranksAtLeast("fish")), + hidden: true, + }), + server_speak: new Achievement(["pink", Iconc.host], "It Speaks!", "Hear the server talk in chat."), + see_marked_griefer: new Achievement(["red", Iconc.hammer], "Flying Tonk", "See a marked griefer in-game.", { + checkInfrequent: () => Groups.player.contains(p => FishPlayer.get(p).marked()), + }), + + //maps based + beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { + notify: "everyone", + modes: ["not", "pvp"], + checkGameover: (team) => team == Vars.state.rules.defaultTeam && !Vars.state.map.custom + }), + + //misc + power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { + modes: ["not", "sandbox"], + checkFrequent(team){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + //we need to actually check for power sources + if( + (graph.lastPowerProduced / Time.delta * 60) > 1e6 && + !graph.producers.contains(boolf(b => b.block == Blocks.powerSource)) && + graph.producers.firstOpt()?.team == team + ) + found = true; + }); + return found; + } + }), + pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { + modes: ["not", "sandbox"], + disabled: true + }), //TODO + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 50 health, but survive.", { + modes: ["not", "sandbox"], + }), + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 50 health, but survive.", { + modes: ["not", "sandbox"], + }), + verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { + checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "nobody" + }), + click_me: new Achievement(Iconc.bookOpen, "Clicked", `Run /achievementgrid and click this achievement.`), + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { + modes: ["not", "sandbox"], + checkPlayerGameover(player, winTeam) { + return player.team() == winTeam && player.tstats.blockInteractionsThisMap == 0; + }, + }), + status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { + checkPlayerFrequent: p => { + const unit = p.unit(); + if(!unit) return false; + const statuses = getStatuses(unit); + return statuses.size >= 5; + }, + modes: ["not", "sandbox"] + }), + drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { + notify: "everyone", + modes: ["not", "sandbox"] + }), + drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", `Drown a Mace in ${Blocks.cryofluid.emoji()} Cryofluid.`, { + notify: "everyone", + modes: ["not", "sandbox"] + }), + max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { + checkPlayerFrequent(player) { + const tile = player.unit()?.tile?.(); + if(!tile) return false; + return tile.block == Blocks.duo && !tile.ammo!.isEmpty() && tile.ammo!.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; + }, + notify: "everyone", + modes: ["not", "sandbox"] + }), + foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { + notify: "everyone", + modes: ["not", "sandbox"] + }), + impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { + modes: ["not", "sandbox"], + notify: "everyone", + checkInfrequent(team){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + if(graph.producers.size >= 15 && graph.producers.count(b => b.block == Blocks.impactReactor && b.warmup! > 0.99999) > 15 && graph.producers.first().team == team) + found = true; + }); + return found; + }, + }), + + help_help: new Achievement(["brown", Iconc.info], "Help with help", "Run /help help", { + notify: "everyone" + }), + ohno: new Achievement(["scarlet", UnitTypes.alpha.emoji()], "Oh no", "Control an ohno unit."), + sniper_duel: new Achievement(["yellow", UnitTypes.omura.emoji()], "Sniper duel", "Kill a Foreshadow with an Omura from outside its range."), + around_the_world: new Achievement(Iconc.planet, "Around the World", "Fly your unit around the entire map without entering it, starting from the lower left.", { + notify: "everyone", + }), +} satisfies Record; +Object.entries(Achievements).forEach(([id, a]) => a.sid = id); + +FishEvents.on("commandUnauthorized", (_, player, name) => { + if((name == "js" || name == "fjs") && !Achievements.run_js_without_perms.has(player)) + Achievements.run_js_without_perms.grantTo(player); +}); + + +Events.on(EventType.UnitDrownEvent, ({unit}:{unit: Unit}) => { + if(!Gamemode.sandbox()){ + if(unit.type == UnitTypes.mace && unit.tileOn()?.floor() == Blocks.cryofluid) Achievements.drown_mace_in_cryo.grantToAllOnline(); + else if(unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish){ + if(Gamemode.pvp()){ + Vars.state.teams.active.map(t => t.team).select(t => t !== unit.team).each(t => Achievements.drown_big_tank.grantToAllOnline(t)); + } else { + if(unit.team !== Vars.state.rules.defaultTeam) Achievements.drown_big_tank.grantToAllOnline(); + } + } + } +}); + +Events.on(EventType.UnitBulletDestroyEvent, ({unit, bullet}:{unit:Unit; bullet: Bullet}) => { + if(!Gamemode.sandbox() && unit.type == UnitTypes.dagger && (bullet.owner as Building | null)?.block == Blocks.foreshadow){ + const build = bullet.owner as Building; + if(build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) Achievements.foreshadow_overkill.grantToAllOnline(build.team); + } +}); + +Events.on(EventType.BuildingBulletDestroyEvent, ({build, bullet}:{build:Building, bullet:Bullet}) => { + if(!Gamemode.sandbox() && build.block == Blocks.foreshadow && (bullet.owner as Unit | null)?.type == UnitTypes.omura){ + const unit = bullet.owner as Unit; + const player = unit.getPlayer(); + if(player && !unit.within(build, build.range!() + unit.hitSize / 2)) Achievements.sniper_duel.grantTo(FishPlayer.get(player)); + } +}); + +let siliconReached = Team.all.map(_ => false); +Events.on(EventType.GameOverEvent, () => siliconReached = Team.all.map(_ => false)); +let isAlone = 0; + +Timer.schedule(() => { + if(!Vars.state.gameOver && !Gamemode.sandbox()){ + Vars.state.teams.active.each(({team}) => { + if(team.items().has(Items.silicon, 2000)) siliconReached[team.id] = true; + else if(siliconReached[team.id] && team.items().get(Items.silicon) == 0) + Achievements.siligone.grantToAllOnline(team); + }); + } + if(Groups.player.size() == 1){ + if(isAlone == 0) isAlone = Date.now(); + else if(Date.now() > isAlone + Duration.minutes(2)) Achievements.alone.grantToAllOnline(); + } else isAlone = 0; +}, 2, 2); + +const coreHealthTime = new Map(); +if(!Gamemode.sandbox()) Timer.schedule(() => { + coreHealthTime.forEach((value, core) => { + if(Date.now() > value){ + if(core.dead){ + coreHealthTime.delete(core); + } else if(core.health > 50){ + //grant achievement + Achievements.core_low_hp.grantToAllOnline(core.team); + FishPlayer.forEachPlayer(p => { + if(core.team != p.team() && !Achievements.enemy_core_low_hp.has(p)) + Achievements.enemy_core_low_hp.grantTo(p); + }); + coreHealthTime.delete(core); + } + } + }); + Vars.state.teams.active.flatMap(t => t.cores).each(core => { + if(core.health < 50 && !coreHealthTime.get(core)) coreHealthTime.set(core, Date.now() + 12_000); + }); +}, 1, 1); +const aroundTheWorld: Record = {}; +Timer.schedule(() => { + FishPlayer.forEachPlayer(p => { + const unit = p.unit(); + if(unit && unit.x < 0 && unit.y < 0){ + aroundTheWorld[p.uuid] ??= { player: p, unit, side: "left" }; + } + }); + for(const [uuid, entry] of Object.entries(aroundTheWorld)){ + if(!(() => { + if(entry.unit.dead) return false; + const left = entry.unit.x < 0; + const bottom = entry.unit.y < 0; + const right = entry.unit.x > (Vars.world.width() - 1) * 8; + const top = entry.unit.y > (Vars.world.height() - 1) * 8; + switch(entry.side){ + case "left": + if(!left) return false; + if(top) entry.side = "top"; + break; + case "top": + if(!top) return false; + if(right) entry.side = "right"; + break; + case "right": + if(!right) return false; + if(bottom) entry.side = "bottom"; + break; + case "bottom": + if(!bottom) return false; + if(left){ + Achievements.around_the_world.grantTo(entry.player, true); + return false; + } + break; + } + return true; + })()) delete aroundTheWorld[uuid]; + } +}, 1, 0.5); +Events.on(EventType.GameOverEvent, () => coreHealthTime.clear()); +Events.on(EventType.WorldLoadEvent, () => coreHealthTime.clear()); + + +FishEvents.on("scriptKiddie", (_, p) => Timer.schedule(() => { + if(!Achievements.script_kiddie.has(p)) Achievements.script_kiddie.grantTo(p); +}, 2)); +FishEvents.on("memoryCorruption", () => Achievements.memory_corruption.grantToAllOnline()); FishEvents.on("serverSays", () => Achievements.server_speak.grantToAllOnline()); \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 995c5ae1..919f2a4b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,202 +1,202 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains wrappers over the API calls to the backend server. -*/ - -import { backendIP, Gamemode, Mode } from "/config"; -import { FishPlayer } from "/players"; -import { Promise } from "/promise"; -import type { FishPlayerData, UploadedFishPlayerData } from "/types"; - - -const cachedIps:Record = {}; -/** Make an API request to see if an IP is likely VPN. */ -export function isVpn(ip:string, callback: (isVpn:boolean) => unknown, callbackError?: (errorMessage:Throwable) => unknown){ - if(ip in cachedIps) return callback(cachedIps[ip]!); - Http.get(`http://ip-api.com/json/${ip}?fields=proxy,hosting`, (res) => { - const data = res.getResultAsString(); - const json = JSON.parse(data) as { - proxy: boolean; - hosting: boolean; - }; - const isVpn = json.proxy || json.hosting; - cachedIps[ip] = isVpn; - FishPlayer.stats.numIpsChecked ++; - if(isVpn) FishPlayer.stats.numIpsFlagged ++; - callback(isVpn); - }, callbackError ?? ((err) => { - Log.err(`[API] Network error when trying to call api.isVpn()`); - FishPlayer.stats.numIpsErrored ++; - callback(false); - })); -} -export function isVpnCached(ip:string):boolean | undefined { - return cachedIps[ip]; -} - -/** Send text to the moderation logs channel in Discord. */ -export function sendModerationMessage(message: string) { - if(Mode.noBackend){ - Log.info(`Sent moderation log message: ${message}`); - return; - } - const req = Http.post(`http://${backendIP}/api/mod-dump`, JSON.stringify({ message })).header('Content-Type', 'application/json').header('Accept', '*/*'); - req.timeout = 10000; - - req.error(() => Log.err(`[API] Network error when trying to call api.sendModerationMessage()`)); - req.submit((response) => { - //Log.info(response.getResultAsString()); - }); -} - -/** Get staff messages from discord. */ -export function getStaffMessages(callback: (messages: string) => unknown) { - if(Mode.noBackend) return; - const req = Http.post(`http://${backendIP}/api/getStaffMessages`, JSON.stringify({ server: Gamemode.name() })) - .header('Content-Type', 'application/json').header('Accept', '*/*'); - req.timeout = 10000; - req.error(() => Log.err(`[API] Network error when trying to call api.getStaffMessages()`)); - req.submit((response) => { - const temp = response.getResultAsString(); - if(!temp.length) Log.err(`[API] Network error(empty response) when trying to call api.getStaffMessages()`); - else callback(JSON.parse(temp).messages); - }); -} - -/** Send staff messages from server. */ -export function sendStaffMessage(message:string, playerName:string, isStaff:boolean, callback?: (sent:boolean) => unknown){ - if(Mode.noBackend) return; - const req = Http.post( - `http://${backendIP}/api/sendStaffMessage`, - // need to send both name variants so one can be sent to the other servers with color and discord can use the clean one - JSON.stringify({ message, playerName, cleanedName: Strings.stripColors(playerName), server: Gamemode.name(), isStaff }) - ).header('Content-Type', 'application/json').header('Accept', '*/*'); - req.timeout = 10000; - req.error(() => { - Log.err(`[API] Network error when trying to call api.sendStaffMessage()`); - callback?.(false); - }); - req.submit((response) => { - const temp = response.getResultAsString(); - if(!temp.length) Log.err(`[API] Network error(empty response) when trying to call api.sendStaffMessage()`); - else callback?.(JSON.parse(temp).data); - }); -} - -/** Bans the provided ip and/or uuid. */ -export function ban(data:{ip?:string; uuid?:string;}, callback:(status:string) => unknown = () => {}){ - if(Mode.noBackend) return; - const req = Http.post(`http://${backendIP}/api/ban`, JSON.stringify(data)) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(() => Log.err(`[API] Network error when trying to call api.ban(${data.ip}, ${data.uuid})`)); - req.submit((response) => { - const str = response.getResultAsString(); - if(!str.length) return Log.err(`[API] Network error(empty response) when trying to call api.ban()`); - callback(JSON.parse(str).data); - }); -} - -/** Unbans the provided ip and/or uuid. */ -export function unban(data:{ip?:string; uuid?:string;}, callback:(status:string, error?:string) => unknown = () => {}){ - if(Mode.noBackend) return; - const req = Http.post(`http://${backendIP}/api/unban`, JSON.stringify(data)) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(() => Log.err(`[API] Network error when trying to call api.ban({${data.ip}, ${data.uuid}})`)); - req.submit((response) => { - const str = response.getResultAsString(); - if(!str.length) return Log.err(`[API] Network error(empty response) when trying to call api.unban()`); - const parsedData = JSON.parse(str); - callback(parsedData.status, parsedData.error); - }); -} - -/** Gets if either the provided uuid or ip is banned. */ -export function getBanned(data:{uuid?:string, ip?:string}, callback:(banned:boolean) => unknown){ - if(Mode.noBackend){ - Log.info(`[API] Attempted to getBanned(${data.uuid}/${data.ip}), assuming false due to local debug`); - callback(false); - return; - } - //TODO cache 4s - const req = Http.post(`http://${backendIP}/api/checkIsBanned`, JSON.stringify(data)) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(() => Log.err(`[API] Network error when trying to call api.getBanned()`)); - req.submit((response) => { - const str = response.getResultAsString(); - if(!str.length) return Log.err(`[API] Network error(empty response) when trying to call api.getBanned()`); - callback(JSON.parse(str).data); - }); -} - -/** - * Fetches fish player data from the backend. - **/ -export function getFishPlayerData(uuid:string){ - const { promise, resolve, reject } = Promise.withResolvers(); - function fail(err:string){ - Log.err(`[API] Network error when trying to call api.getFishPlayerData()`); - if(err) Log.err(err); - reject(err); - } - - if(Mode.noBackend){ - reject("local debug mode"); - return promise; - } - - const req = Http.post(`http://${backendIP}/api/fish-player`, JSON.stringify({ - id: uuid, - gamemode: Gamemode.name(), - })) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error(fail); - req.submit((response) => { - const data = response.getResultAsString(); - if(data){ - const result = JSON.parse(data); - if(!result || typeof result != "object") fail(`Invalid fish player data`); - resolve(result); - } else { - resolve(null); - } - }); - return promise; -} - -/** Pushes fish player data to the backend. */ -export function setFishPlayerData(data: UploadedFishPlayerData, repeats:number, ignoreActivelySyncedFields:boolean) { - const { promise, resolve, reject } = Promise.withResolvers(); - if(Mode.noBackend){ - resolve(); - return promise; - } - const req = Http.post(`http://${backendIP}/api/fish-player/set`, JSON.stringify({ - player: data, - gamemode: Gamemode.name(), - ignoreActivelySyncedFields, - })) - .header('Content-Type', 'application/json') - .header('Accept', '*/*'); - req.timeout = 10000; - req.error((err) => { - Log.err(`[API] Network error when trying to call api.setFishPlayerData(), repeats=${repeats}`); - Log.err(err); - if(err?.response) Log.err(err.response.getResultAsString()); - if(repeats > 0 && !(err.status?.code >= 400 && err.status?.code <= 499)) - setFishPlayerData(data, repeats - 1, ignoreActivelySyncedFields).then(resolve).catch(reject); - else reject(err); - }); - req.submit((response) => { - resolve(); - }); - return promise; -} - +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains wrappers over the API calls to the backend server. +*/ + +import { backendIP, Gamemode, Mode } from "/config"; +import { FishPlayer } from "/players"; +import { Promise } from "/promise"; +import type { FishPlayerData, UploadedFishPlayerData } from "/types"; + + +const cachedIps:Record = {}; +/** Make an API request to see if an IP is likely VPN. */ +export function isVpn(ip:string, callback: (isVpn:boolean) => unknown, callbackError?: (errorMessage:Throwable) => unknown){ + if(ip in cachedIps) return callback(cachedIps[ip]!); + Http.get(`http://ip-api.com/json/${ip}?fields=proxy,hosting`, (res) => { + const data = res.getResultAsString(); + const json = JSON.parse(data) as { + proxy: boolean; + hosting: boolean; + }; + const isVpn = json.proxy || json.hosting; + cachedIps[ip] = isVpn; + FishPlayer.stats.numIpsChecked ++; + if(isVpn) FishPlayer.stats.numIpsFlagged ++; + callback(isVpn); + }, callbackError ?? ((err) => { + Log.err(`[API] Network error when trying to call api.isVpn()`); + FishPlayer.stats.numIpsErrored ++; + callback(false); + })); +} +export function isVpnCached(ip:string):boolean | undefined { + return cachedIps[ip]; +} + +/** Send text to the moderation logs channel in Discord. */ +export function sendModerationMessage(message: string) { + if(Mode.noBackend){ + Log.info(`Sent moderation log message: ${message}`); + return; + } + const req = Http.post(`http://${backendIP}/api/mod-dump`, JSON.stringify({ message })).header('Content-Type', 'application/json').header('Accept', '*/*'); + req.timeout = 10000; + + req.error(() => Log.err(`[API] Network error when trying to call api.sendModerationMessage()`)); + req.submit((response) => { + //Log.info(response.getResultAsString()); + }); +} + +/** Get staff messages from discord. */ +export function getStaffMessages(callback: (messages: string) => unknown) { + if(Mode.noBackend) return; + const req = Http.post(`http://${backendIP}/api/getStaffMessages`, JSON.stringify({ server: Gamemode.name() })) + .header('Content-Type', 'application/json').header('Accept', '*/*'); + req.timeout = 10000; + req.error(() => Log.err(`[API] Network error when trying to call api.getStaffMessages()`)); + req.submit((response) => { + const temp = response.getResultAsString(); + if(!temp.length) Log.err(`[API] Network error(empty response) when trying to call api.getStaffMessages()`); + else callback(JSON.parse(temp).messages); + }); +} + +/** Send staff messages from server. */ +export function sendStaffMessage(message:string, playerName:string, isStaff:boolean, callback?: (sent:boolean) => unknown){ + if(Mode.noBackend) return; + const req = Http.post( + `http://${backendIP}/api/sendStaffMessage`, + // need to send both name variants so one can be sent to the other servers with color and discord can use the clean one + JSON.stringify({ message, playerName, cleanedName: Strings.stripColors(playerName), server: Gamemode.name(), isStaff }) + ).header('Content-Type', 'application/json').header('Accept', '*/*'); + req.timeout = 10000; + req.error(() => { + Log.err(`[API] Network error when trying to call api.sendStaffMessage()`); + callback?.(false); + }); + req.submit((response) => { + const temp = response.getResultAsString(); + if(!temp.length) Log.err(`[API] Network error(empty response) when trying to call api.sendStaffMessage()`); + else callback?.(JSON.parse(temp).data); + }); +} + +/** Bans the provided ip and/or uuid. */ +export function ban(data:{ip?:string; uuid?:string;}, callback:(status:string) => unknown = () => {}){ + if(Mode.noBackend) return; + const req = Http.post(`http://${backendIP}/api/ban`, JSON.stringify(data)) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(() => Log.err(`[API] Network error when trying to call api.ban(${data.ip}, ${data.uuid})`)); + req.submit((response) => { + const str = response.getResultAsString(); + if(!str.length) return Log.err(`[API] Network error(empty response) when trying to call api.ban()`); + callback(JSON.parse(str).data); + }); +} + +/** Unbans the provided ip and/or uuid. */ +export function unban(data:{ip?:string; uuid?:string;}, callback:(status:string, error?:string) => unknown = () => {}){ + if(Mode.noBackend) return; + const req = Http.post(`http://${backendIP}/api/unban`, JSON.stringify(data)) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(() => Log.err(`[API] Network error when trying to call api.ban({${data.ip}, ${data.uuid}})`)); + req.submit((response) => { + const str = response.getResultAsString(); + if(!str.length) return Log.err(`[API] Network error(empty response) when trying to call api.unban()`); + const parsedData = JSON.parse(str); + callback(parsedData.status, parsedData.error); + }); +} + +/** Gets if either the provided uuid or ip is banned. */ +export function getBanned(data:{uuid?:string, ip?:string}, callback:(banned:boolean) => unknown){ + if(Mode.noBackend){ + Log.info(`[API] Attempted to getBanned(${data.uuid}/${data.ip}), assuming false due to local debug`); + callback(false); + return; + } + //TODO cache 4s + const req = Http.post(`http://${backendIP}/api/checkIsBanned`, JSON.stringify(data)) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(() => Log.err(`[API] Network error when trying to call api.getBanned()`)); + req.submit((response) => { + const str = response.getResultAsString(); + if(!str.length) return Log.err(`[API] Network error(empty response) when trying to call api.getBanned()`); + callback(JSON.parse(str).data); + }); +} + +/** + * Fetches fish player data from the backend. + **/ +export function getFishPlayerData(uuid:string){ + const { promise, resolve, reject } = Promise.withResolvers(); + function fail(err:string){ + Log.err(`[API] Network error when trying to call api.getFishPlayerData()`); + if(err) Log.err(err); + reject(err); + } + + if(Mode.noBackend){ + reject("local debug mode"); + return promise; + } + + const req = Http.post(`http://${backendIP}/api/fish-player`, JSON.stringify({ + id: uuid, + gamemode: Gamemode.name(), + })) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error(fail); + req.submit((response) => { + const data = response.getResultAsString(); + if(data){ + const result = JSON.parse(data); + if(!result || typeof result != "object") fail(`Invalid fish player data`); + resolve(result); + } else { + resolve(null); + } + }); + return promise; +} + +/** Pushes fish player data to the backend. */ +export function setFishPlayerData(data: UploadedFishPlayerData, repeats:number, ignoreActivelySyncedFields:boolean) { + const { promise, resolve, reject } = Promise.withResolvers(); + if(Mode.noBackend){ + resolve(); + return promise; + } + const req = Http.post(`http://${backendIP}/api/fish-player/set`, JSON.stringify({ + player: data, + gamemode: Gamemode.name(), + ignoreActivelySyncedFields, + })) + .header('Content-Type', 'application/json') + .header('Accept', '*/*'); + req.timeout = 10000; + req.error((err) => { + Log.err(`[API] Network error when trying to call api.setFishPlayerData(), repeats=${repeats}`); + Log.err(err); + if(err?.response) Log.err(err.response.getResultAsString()); + if(repeats > 0 && !(err.status?.code >= 400 && err.status?.code <= 499)) + setFishPlayerData(data, repeats - 1, ignoreActivelySyncedFields).then(resolve).catch(reject); + else reject(err); + }); + req.submit((response) => { + resolve(); + }); + return promise; +} + diff --git a/src/client.ts b/src/client.ts index a681e168..9df5ab99 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,8 +1,8 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file is a client environment: -you can write typescript code in this file and run a command to copy it to the clipboard, -then run it in-game on your client. -It is not used by the plugin. -*/ - +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file is a client environment: +you can write typescript code in this file and run a command to copy it to the clipboard, +then run it in-game on your client. +It is not used by the plugin. +*/ + diff --git a/src/config.ts b/src/config.ts index 44b45381..ed4356a7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,380 +1,380 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains configurable constants. -*/ - -import type { PermType } from "/frameworks/commands"; -import { ipPattern, ipPortPattern, uuidPattern } from "/globals"; -import { Rank } from "/ranks"; -import { Duration, random } from "/funcs"; - - - -//#region filtering -export type BannedWordList = Array<[word: string | RegExp, whitelist: string[]]>; -type Pre_BannedWordList = Array; -function processBannedWordList(words: Pre_BannedWordList):BannedWordList { - return words.map(word => - (typeof word == "string" || word instanceof RegExp) ? - [word, []] - : [word[0], word.slice(1)] - ); -} -export const bannedWords: { - normal: BannedWordList; - strict: BannedWordList; - names: BannedWordList; - /** new players will automatically be banned if they send a word that looks like one of these */ - autoWhack: string[]; -} = { - // README: Information on how to update this list - // All words must be in *lowercase*. - // Words need to be separated by commas, even if they are on a new line. - // If a word can be contained in another word that should be allowed (the scunthorpe problem), - // surround the entire thing in square brackets, then list out the words after - // like this: ["badw", "goodbadw"] - /** Normal: banned always. */ - normal: processBannedWordList([ - "fanum tax", "gyatt", ["rizz", "grizzly", "frizz", "horizzon"], "skibidi", //With love, DarthScion - //>:( -dart - // "uwu", //lol - - "nig"+"ger", "nig"+"ga", "niger", "ni8"+"8er", "nig"+"gre", "негр", "ниг"+"гер", "нигер", "нігер", "ніг"+"гер", /\bnegr\b/, //our apologies to citizens of the Republic of Niger - ["ni"+"ga", "anniga", "inniga", "unniga", "aniga", "iniga", "eniga", "oniga"], - "re"+"tard", - 'kill yourself', 'kill urself', /\bkys\b/, - "kill blacks", "heil hitler", "heil nazis", "heil the nazis", "sieg heil", "hail hitler", "hail nazis", "hail the nazis", "sieg hail", /\b1488\b/, //nazi-related words - ["co"+"ck", "cockroach", "poppycock", "cocktail"], "suck dick", "sucking dick", - "iamasussyimposter", - ["cu"+"nt", "scunthorpe"], - ["penis", "peniston"], - "hawk tuah", - - ["rape", "grape", "therap", "drape", "scrape", "trapez", "earrape", "atrape", "traped"], - ["raping", "draping", "graping", "scraping", "craping"], - /\bf(a)g\b/, "fa"+"gg"+"ot", - /\bc(u)m\b/, ["semen", "sement", "horsemen", "housemen", "defensemen", "those", "menders"], - ["porn", "maporn"], - "futa"+"nari", "futa", - "ur gay", "your gay", "youre gay", "you're gay", - "gooning", "gooner", "dildo", "loli", /\banal\b/, "cunny" - ]), - /** Strict: banned in names and for players with a chat strictness level of 'strict'. */ - strict: processBannedWordList([ - "fu"+"ck", "bi"+"tch", ["sh"+"it", "harshit"], /\ba(s)s\b/, "as"+"shole", ["dick", "medick", "dickens"], - ]), - /** Names: banned only in names. */ - names: processBannedWordList([ - "sex", /\bgoldberg\b/, "hitler", "stalin", "putin", "lenin", /^something$/, "[something]", "[[something]", "卐", "diddy", "epstein", - uuidPattern, ipPattern, ipPortPattern - ]), - /** autoWhack: new players saying one of these words will be automatically stopped and muted. Comes with \b so no need to add it. */ - autoWhack: [ - "nig"+"ger","nig"+"ga","ni8"+"8er","nig"+"g3r","hit"+"ler","fa"+"gg"+"ot","nazis", "негр", "ниг"+"гер", "нигер", "нігер", "ніг"+"гер", "negr" - ], -}; - -//for some reason the external mindustry server does not read the files correctly, so we can only use ASCII -export const substitutions:Record = Object.fromEntries(Object.entries({ - "a": "\u0430\u1E9A\u1EA1\u1E01\u00E4\u03B1@\u0101\u0103\u0105\u03AC", - "b": "\u1E03\u1E07\u1E03\u0253\u0185", - "c": "\u0441\u217D\u00E7\u03C2\u010B", - "d": "\u217E\u1E0B\u1E11\u010F\u1E13\u1E0D\u1E0F\u0257\u20AB\u0256\u056A", - "e": "\u0435\u1E1B\u0113\u1E17\u0229\u0451\u011B\u0205\u03F5\u03B5\u025B3", - "f": "\u1E1F\u0493\u0192", - "g": "\u0581\u0123\u01F5\u0260\u011F\u011D\u01E5\u1E21", - "h": "\u1E23\u021F\u1E25\u1E2B\u0570\u056B\u1E29\u0266\u1E27\u1E23\u0266\u1E96\u0127", - "i": "\u0456\u012F\u03B9\u1EC9\u1F31\u1F77\u012B1\u00A1\u0457\u0390\u03CA", - "j": "\u0458\u029D\u0575\u025F\u0135\u0237\u01F0", - "k": "\u049F\u1E31\u0137\u0138\u043A\u0199\u049D", - "l": "\u217C\u1E3D\u1E3B\u013E\u0140\u013C\u1E39\u0142\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u03B9", - "m": "\u217F\u1E43\u0271\u1E41\u1E3F", - "n": "\u00F1\u0144\u0146\u0148\u0149\u01F9\u03AE\u03B7\u0578\u057C\u0580\u1E45\u1E47\u03A0", - "o": "\u00F2\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1F40\u1F41\u1F42\u1F43\u1F44\u1F45\u1F78\u1F79\u03C3\u0E50\u00F6\u014D\u014F\u0151\u01A1\u01D2\u03BF\u03CC0", - "p": "\u03C1\u0440\u048F\u1E55\u1E57\u1FE4\u1FE5\u2374", - "q": "\u051B\u0563\u0566\u0563\u0566", - "r": "\u0155\u0157\u0159\u0211\u0213\u027C\u027D\u0433\u0453\u0491\u04F7\u1E59\u1E5B\u1E5D", - "s": "\u015B\u015D\u015F\u0161\u0219\u0282\u0455\u1E61\u1E63\u1E65\u1E67\u1E69\u03C2", - "t": "\u0163\u0165\u01AB\u021B\u0288\u1E6B\u1E6D\u1E6F\u1E71\u1E97\u0236\u2020\u04AD", - "u": "\u00B5\u03BC\u00F9\u00FA\u00FB\u00FC\u0169\u016B\u016D\u016F\u0171\u0173\u01B0\u01D4\u0215\u0217\u0265\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u03BC\u03C5\u03CB\u03CD", - "v": "\u03BD\u0475\u0477\u1E7D\u1E7F\u2174\u2228\u03C5\u03CB\u03CD", - "w": "\u0175\u051D\u1E81\u1E83\u1E85\u1E87\u1E89\u1E98\u03C9\u03CE", - "x": "\u0445\u04B3\u1E8B\u1E8D\u03C7", - "y": "\u00FD\u00FF\u0177\u01B4\u0233\u03B3\u0443\u045E\u04EF\u04F1\u04F3\u1E8F\u1E99\u1EF3\u1EF5\u1EF7\u1EF9\u04AF\u04B1", - "z": "\u017A\u017C\u017E\u01B6\u0225\u0290\u1E91\u1E93\u1E95", - "A": "\u1E00\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAC\u1F08\u1F09\u1F88\u1F89\u1FB8\u1FB9\u1FBA\u1FBC\u212B\u0100\u0102\u0104\u0386\u0391\u0410", - "B": "\u0181\u0392\u0412\u1E02\u1E04\u1E06", - "C": "\u00C7\u0106\u0108\u010A\u010C\u0187\u0421\u04AA\u1E08\u216D\u03F9", - "D": "\u00D0\u010E\u0110\u0189\u018A\u1E0A\u1E0C\u1E0E\u216E", - "E": "\u00C8\u00C9\u00CA\u00CB\u0112\u0114\u0116\u0118\u011A\u0204\u0206\u0228\u0395\u0400\u0415\u04D6\u1E18\u0510\u2107\u0190\u1F19\u1FC8\u0404\u0388\u03AD\u03B5\u03B7\u0415", - "F": "\u03DC\u1E1E\u0492\u0191\u0492\u0493", - "G": "\u011C\u011E\u0120\u0122\u0193\u01E6\u01F4\u1E20", - "H": "\u0124\u021E\u0397\u041D\u04A2\u04A4\u04C7\u04C9\u1E22\u1E24\u1E26\u1E28\u1E2A\u1FCC\uA726\u0389", - "I": "\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u217C\u1E3D\u1E3B\u026B\u013E\u0140\u013C\u1E39\u038A", - "J": "\u0134\u0408\u037F", - "K": "\u0136\u0198\u01E8\u039A\u040C\u041A\u051E\u1E30\u1E32\u1E34\u20AD\u212A\u03BA", - "L": "\u0139\u013B\u013D\u013F\u0141\u053C\u1E36\u1E38\u1E3A\u1E3C\u216C", - "M": "\u039C\u041C\u04CD\u1E3E\u1E40\u1E42\u216F", - "N": "\u00D1\u0143\u0145\u0147\u01F8\u039D\u1E44\u1E46\u1E48\u1E4A\u019D", - "O": "\u03B8\u236C\u00D2\u00D3\u00D4\u00D5\u00D6\u014C\u014E\u0150\u019F\u01A0\u01D1\u020E\u022E\u0230\u0398\u039F\u041E\u04E6\u0555\u1ECC\u1ECE\u1ED4\u1FF9\u038C", - "P": "\u01A4\u03A1\u0420\u048E\u1E54\u1E56\u1FEC", - "Q": "\u051A", - "R": "\u0154\u0156\u0158\u0210\u0212\u1E58\u1E5A\u1E5C\u1E5E\u211E\u024C\u2C64", - "S": "\u015A\u015C\u015E\u0160\u0218\u0405\u054F\u1E60\u1E62\u1E68\u1E64\u1E66", - "T": "\u0162\u0164\u0166\u01AE\u021A\u03A4\u0422\u04AC\u1E6A\u1E6C\u1E6E\u1E70\u038A\u1FDB\uA68C\u0372\u0373\u03C4", - "U": "\u016A\u016C\u016E\u0170\u0172\u01AF\u01D3\u1EE8\u1EEA\u1EEC\u1EEE\u0544", - "V": "\u0474\u0476\u1E7C\u1E7E\u22C1\u2164", - "W": "\u051C\u1E80\u1E82\u1E84\u1E86\u1E88\u019C", - "X": "\u03A7\u0425\u04B2\u1E8A\u1E8C\u2169", - "Y": "\u01B3\u0232\u03A5\u03AB\u03D3\u0423\u04AE\u04B0\u1E8E\u1EF2\u1EF4\u038E", - "Z": "\u0179\u017B\u017D\u0224\u0396\u1E90\u1E92\u1E94", - "": "\u200B\u200C\u200D", -}).map(([char, alts]) => alts.split("").map(alt => [alt, char] as const)).flat(1)); -export const multiCharSubstitutions:Array<[RegExp, string]> = [ - [/\|-\|/g, "H"] -]; -//#endregion -//#region misc -/** Used for anti-impersonation. Make sure to replace numbers with letters, for example, balam314 -> balamei4. */ -export const adminNames = ["fish", "balamei4", "clashgone", "darthscion", "firefridge", "aricia", "rawsewage", "skeledragon", "edh8e", "everydayhuman8e", "benjamonsrl"]; -export const heuristics = { - /** Will trip if more than this many blocks are broken within 25 seconds of joining. */ - blocksBrokenAfterJoin: 40, -}; -export const stopAntiEvadeTime = Duration.minutes(30); -export const backendIP = '45.79.202.111:5082'; -export const Mode = { - localDebug: new Fi("config/.debug").exists(), - noBackend: new Fi("config/.debug").exists() && !backendIP.startsWith("127.0.0.1:"), - isChristmas: new Date().getMonth() == 11, - isAprilFools: new Date().getMonth() == 3 && new Date().getDate() == 1, -}; -//#endregion -//#region servers -/** Stores the repository url for the maps for each gamemode. */ -export const mapRepoURLs:Record = { - attack: "https://api.github.com/repos/Fish-Community/fish-maps/contents/attack", - survival: "https://api.github.com/repos/Fish-Community/fish-maps/contents/survival", - pvp: "https://api.github.com/repos/Fish-Community/fish-maps/contents/pvp", - hexed: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hexed", - sandbox: "https://api.github.com/repos/Fish-Community/fish-maps/contents/sandbox", - hardcore: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hardcore", - testsrv: "https://api.github.com/repos/Fish-Community/fish-maps/contents/testsrv", - minigame: "https://api.github.com/repos/Fish-Community/fish-maps/contents/minigame", -}; - - -/** Stores the names and addresses of each active server. */ -export class FishServer { - constructor( - public name:string, - public ip:string, - public port:string, - public aliases:string[], - /** If set, this permission is required to switch to or get information about this server. */ - public requiredPerm?:PermType, - ){ - FishServer.all.push(this); - } - - static all: FishServer[] = []; - static attack = new FishServer( - "attack", - "162.248.100.98", "6567", - ["attac", "atack", "atak", "atck", "atk", "a"] - ); - static survival = new FishServer( - "survival", - "162.248.101.95", "6567", - ["surviv", "surv", "sur", "su", "s", "sl"] - ); - static pvp = new FishServer( - "pvp", - "162.248.102.101", "6567", - ["pv", "p", "v", "playerversusplayer"] - ); - static sandbox = new FishServer( - "sandbox", - "162.248.101.53", "6567", - ["sand", "box", "sa", "sb"] - ); - static hexed = new FishServer( - "hexed", - "162.248.100.133", "6567", - ["h", "hx", "hxd", "hpvp", "hxpvp", "hexpvp"] - ); - static minigame = new FishServer( - "minigame", - "162.248.101.116", "6567", - ["m", "mg", "mini", "minig", "mgame", "mng", "minigame", "mpvp"] - ); - static testing = new FishServer( - "testing", - "162.248.101.52", "6567", - ["test", "testsrv", "t", "testingserver", "testserver"] - ); - static byName(input:string):FishServer | null { - input = input.toLowerCase(); - return FishServer.all.find(s => s.aliases.concat(s.name).includes(input)) ?? null; - } -}; - -export type GamemodeName = keyof typeof Gamemode extends infer K extends keyof typeof Gamemode ? K extends unknown ? - (typeof Gamemode)[K] extends (() => boolean) ? K : never -: never : never; -/** Stores functions that return whether the specified gamemode is the current gamemode. */ -export const Gamemode = { - attack: () => Gamemode.name() == "attack", - survival: () => Gamemode.name() == "survival", - pvp: () => Gamemode.name() == "pvp" || Gamemode.name() == "hexed" || Gamemode.name() == "minigame", - sandbox: () => Gamemode.name() == "sandbox", - hexed: () => Gamemode.name() == "hexed", - hardcore: () => Gamemode.name() == "hardcore", - testsrv: () => Gamemode.name() == "testsrv", - minigame: () => Gamemode.name() == "minigame", - name: () => Core.settings.get("mode", Vars.state.rules.mode().name()) as "attack" | "survival" | "pvp" | "sandbox" | "hexed" | "hardcore" | "testsrv" | "minigame", -}; -export const GamemodeNames: GamemodeName[] = Object.keys(Gamemode).filter((x): x is GamemodeName => x !== "name"); -//#endregion -//#region text content - -export const prefixes = { - marked: '[yellow]\u26A0[scarlet]Marked Griefer[]\u26A0[]', - flagged: '[yellow]\u26A0[orange]Flagged[]\u26A0[]', - muted: '[white](muted)', -}; - -export const text = { - discordURL: `https://discord.gg/VpzcYSQ33Y`, - membershipURL: `https://patreon.com/FishServers`, - reportsPing: `<@&1040193678817378305>`, - welcomeMessage: () => random([ - `[gold]Welcome![]` - ]), - chatFilterReplacement: { - message: () => `I really hope everyone is having a fun time :) <3`, - messageShort: () => `I hope we're all having a fun time :) <3`, - highlight: () => `[#f456f]`, - // `[#22AA22]Merry [#EC4444]Christmas!`, - // `[gold]Happy Holidays! [white]•*•☃*•`, - // `[gold]Happy Hanukkah!`, - // `[#EC4444]May your days be merry and bright!`, - // `[gold]Merry Fishmas! >|||> [white]☃`, - // `[gold]Deck the halls with lots of fun!`, - // `[gold]>|||> Fish wishes you [#22AA22]a merry Christmas!`, - // ]), - // chatFilterReplacement: { - // message: () => random([ - // `Have a holly jolly Christmas :) <3`, - // `I really hope everyone is jolly for the season! :D`, - // `All I want for Christmaaaaaaas is everyone having a fun time! :)`, - // `Remember to be nice in chat: Santa is watching! <3`, - // `All I want for Christmas is Fish! >|||>`, - // ]), - // highlight: () => random([ - // `[#22AA22]`, `[#EC4444]`, `[#FFFFFF]` - // ]), - } satisfies { - message: () => string; - messageShort: () => string; - highlight: () => string; - }, - dataFetchFailed: "[scarlet]\u26A0 Data fetch failed!\n[white]Please disconnect and rejoin the server if you encounter further issues, such as missing rank or statistics.", -}; - - -//TODO use this -export const FColor = ( - (data:Record):Record): string; - }> => - Object.fromEntries(Object.entries(data).map(([k, c]) => - [k, (str?:string | readonly string[], ...varChunks: ReadonlyArray) => - str != null ? - `${c}${Array.isArray(str) ? String.raw({ raw: str }, ...varChunks.map(v => String(v) + c)) : (str as string)}[]` - : c - ] - )) -)({ - discord: "[#7289DA]", - /** Used for tips and welcome messages. */ - tip: "[gold]", - member: "[pink]", - achievement: "[lime]", -}); -/** Tips that are shown to players randomly. */ -export const tips = { - ads: [ - `${FColor.member`Fish Membership`} subscribers can access the ${FColor.member`/pet`} command, which spawns a merui that follows you around. Get a Fish Membership at[sky] ${text.membershipURL} []`, - `${FColor.member`Fish Membership`} subscribers can use the ${FColor.member`/highlight`} command, which turns your chat messages to a color of your choice. Get a Fish Membership at[sky] ${text.membershipURL} []`, - `${FColor.member`Fish Membership`} subscribers can use the ${FColor.member`/rainbow`} command, which makes your name flash different colors. Get a Fish Membership at[sky] ${text.membershipURL} []`, - `Want to support the server and get some perks? Get a ${FColor.member`Fish Membership`} at[sky] ${text.membershipURL} []`, - `Join our ${FColor.discord`Discord server`}[]! ${FColor.discord(text.discordURL)} or type ${FColor.discord`/discord`}`, - ], - normal: [ - //commands - `You can spawn an [scarlet]Ohno[] with the [scarlet]/ohno[] command. Ohnos are harmless creatures that were created by fusing an alpha and an atrax.`, - `Ohnos cannot be spawned near enemy buildings, because they are peaceful and do not want to be used for attacks.`, - `You can use [white]/tp[] to teleport directly to any other player! (But only when you're in a core unit)`, - `You can unload bulk conveyors ( or ) with unloaders ( or ).`, - `Hate boulders? You can remove them with [white]/clean[].`, - `You can check our rules at any time by running [white]/rules[].`, - // `You can kill your unit by running [white]/die[].`, - `We have a tilelog system to help catch griefers. Run [white]/tilelog[], then click a tile to see what's happened there.`, - `Run [white]/tilelog 1[] to check the tile history of multiple tiles.`, - `Tilelog stores when a building is placed, broken, rotated, configured, and picked up/dropped by a payload unit. Access it with [white]/tilelog[]`, - `Tilelog doesn't just log tile actions, it also logs unit deaths! Access it with [white]/tilelog[]`, - `Did someone kill a T5 with commands? Run [white]/aoelog 0 15 killed[] to check tilelogs for unit deaths in a large area.`, - `Aoelog can show the history of tiles in an area. Select the opposite corners of a rectangle to view the history of its tiles.`, - `Aoelog is the plural version of tilelog, access it via [white]/aoelog[]`, - `You can mark yourself as AFK(away from keyboard) with [white]/afk[].`, - `Run /survival, /attack, /pvp, /sandbox, /hexed or /minigame to quickly change to another server.`, - `Need to get rid of an active griefer? Use [#6FFC7C]/s[] to send a message to all staff members across all servers.`, - `Use [white]/help to get more information about a specific command.`, - `If you want to send a message to just one player, you can use the [white]/msg[] command.`, - `Use [white]/r[] to reply to a message sent by another player.`, - `[white]/trail[] can be used to give your unit a trail of particle effects.`, - `Run [white]/ranks[] to see all the ranks on our server.`, - `Is someone impersonating a staff member? Run [white]/rank[] to see their real rank.`, - `Don't like the map? Vote to change it with [white]/rtv[].`, - `If you want to end the current map, DO NOT BREAK DEFENCES! Vote to change the map with [white]/rtv[].`, - //misc - `Anyone attempting to impersonate a ranked player, or the server, will have [scarlet]SUSSY IMPOSTOR[] prepended to their name. Beware!`, - `Griefers will often be found with the text ${prefixes.marked} prepended to their name: they are harmless and cannot grief again.`, - `Don't votekick ${prefixes.marked.slice(0, -3)}[][scarlet]s [gold]if they aren't breaking the rules: they are incapable of griefing more.`, - `Players marked as ${prefixes.flagged} have been flagged as suspicious by our detection systems, but they may not be griefers.`, - `Need to appeal a moderation action? Join the discord at ${FColor.discord(text.discordURL)} or type /discord`, - `Want to send the phrase [white]"/command"[] in chat? Type [white]"./command"[] and the [white].[] will be removed.`, - `All commands with a player as an argument support using a menu to specify the player. Just run the command leaving the argument blank (using two spaces if necessary), and a menu will show up.`, - `Players with a ${Rank.trusted.prefix} in front of their name aren't staff members, but they do have extra powers.`, - `Staff members will have the following prefixes in front of their name: ${Rank.manager.prefix}, ${Rank.admin.prefix}, ${Rank.mod.prefix}`, - `Wave cooldown too long? Skip the wait with [white]/vnw[]`, - `You can tell new players not to break power voids with [white]/void[]`, - `You can add [pink]color[] to things with color tags! Try typing "[[${["pink", "green", "cyan", "acid", "royal", "coral"][Math.floor(Math.random() * 6)]}]Hello" in chat, and see what happens!` - ], - christmas: [ - `Remember to be nice in-game, Santa is watching!`, - `Santa's checking his list, so be nice!`, - `Have a merry christmas and a happy new year!`, - `Fish becomes a bit more jolly around Christmastime!`, - `Many server maps have been changed for the season.`, - ], - staff: [ - - ], -}; -export const rules = [ - `# 1: [red]No griefing. This refers to intentionally hurting your own team in any way.`, - `# 2: [orange]False votekicking isn't allowed. Avoid votekicking if there's an active staff member in the server.`, - `# 3: [yellow]Gore, pornography, suggestive content and jokes, and flashing images aren't allowed here. Being horny and a creep in chat will result in a ban.`, - `# 4: [green]Do not harass other people. We have zero tolerance for any bigotry. Please respect everyone.`, - `# 5: [#00D8D8]Spamming is prohibited. Be reasonable with messaging staff in-game. Misuse may result in a mute.`, - `# 6: [blue]Impersonating people or ranks is prohibited.`, - `# 7: [purple]Talking about controversial or sensitive topics is not allowed in-game. Hate symbols, such as swastikas, are not permitted.`, - `# 8: [pink]No uncomfortable trolling or intentionally causing chaos. This includes any actions or messages that create an unpleasant atmosphere.`, - `Failure to follow these rules will result in consequences: likely a ${prefixes.marked} tag for any game disruption, mute for broken chat rules, and bans for repeated offenses or bypasses.` -].map(r => `[white]${r}`); -//#endregion - +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains configurable constants. +*/ + +import type { PermType } from "/frameworks/commands"; +import { ipPattern, ipPortPattern, uuidPattern } from "/globals"; +import { Rank } from "/ranks"; +import { Duration, random } from "/funcs"; + + + +//#region filtering +export type BannedWordList = Array<[word: string | RegExp, whitelist: string[]]>; +type Pre_BannedWordList = Array; +function processBannedWordList(words: Pre_BannedWordList):BannedWordList { + return words.map(word => + (typeof word == "string" || word instanceof RegExp) ? + [word, []] + : [word[0], word.slice(1)] + ); +} +export const bannedWords: { + normal: BannedWordList; + strict: BannedWordList; + names: BannedWordList; + /** new players will automatically be banned if they send a word that looks like one of these */ + autoWhack: string[]; +} = { + // README: Information on how to update this list + // All words must be in *lowercase*. + // Words need to be separated by commas, even if they are on a new line. + // If a word can be contained in another word that should be allowed (the scunthorpe problem), + // surround the entire thing in square brackets, then list out the words after + // like this: ["badw", "goodbadw"] + /** Normal: banned always. */ + normal: processBannedWordList([ + "fanum tax", "gyatt", ["rizz", "grizzly", "frizz", "horizzon"], "skibidi", //With love, DarthScion + //>:( -dart + // "uwu", //lol + + "nig"+"ger", "nig"+"ga", "niger", "ni8"+"8er", "nig"+"gre", "негр", "ниг"+"гер", "нигер", "нігер", "ніг"+"гер", /\bnegr\b/, //our apologies to citizens of the Republic of Niger + ["ni"+"ga", "anniga", "inniga", "unniga", "aniga", "iniga", "eniga", "oniga"], + "re"+"tard", + 'kill yourself', 'kill urself', /\bkys\b/, + "kill blacks", "heil hitler", "heil nazis", "heil the nazis", "sieg heil", "hail hitler", "hail nazis", "hail the nazis", "sieg hail", /\b1488\b/, //nazi-related words + ["co"+"ck", "cockroach", "poppycock", "cocktail"], "suck dick", "sucking dick", + "iamasussyimposter", + ["cu"+"nt", "scunthorpe"], + ["penis", "peniston"], + "hawk tuah", + + ["rape", "grape", "therap", "drape", "scrape", "trapez", "earrape", "atrape", "traped"], + ["raping", "draping", "graping", "scraping", "craping"], + /\bf(a)g\b/, "fa"+"gg"+"ot", + /\bc(u)m\b/, ["semen", "sement", "horsemen", "housemen", "defensemen", "those", "menders"], + ["porn", "maporn"], + "futa"+"nari", "futa", + "ur gay", "your gay", "youre gay", "you're gay", + "gooning", "gooner", "dildo", "loli", /\banal\b/, "cunny" + ]), + /** Strict: banned in names and for players with a chat strictness level of 'strict'. */ + strict: processBannedWordList([ + "fu"+"ck", "bi"+"tch", ["sh"+"it", "harshit"], /\ba(s)s\b/, "as"+"shole", ["dick", "medick", "dickens"], + ]), + /** Names: banned only in names. */ + names: processBannedWordList([ + "sex", /\bgoldberg\b/, "hitler", "stalin", "putin", "lenin", /^something$/, "[something]", "[[something]", "卐", "diddy", "epstein", + uuidPattern, ipPattern, ipPortPattern + ]), + /** autoWhack: new players saying one of these words will be automatically stopped and muted. Comes with \b so no need to add it. */ + autoWhack: [ + "nig"+"ger","nig"+"ga","ni8"+"8er","nig"+"g3r","hit"+"ler","fa"+"gg"+"ot","nazis", "негр", "ниг"+"гер", "нигер", "нігер", "ніг"+"гер", "negr" + ], +}; + +//for some reason the external mindustry server does not read the files correctly, so we can only use ASCII +export const substitutions:Record = Object.fromEntries(Object.entries({ + "a": "\u0430\u1E9A\u1EA1\u1E01\u00E4\u03B1@\u0101\u0103\u0105\u03AC", + "b": "\u1E03\u1E07\u1E03\u0253\u0185", + "c": "\u0441\u217D\u00E7\u03C2\u010B", + "d": "\u217E\u1E0B\u1E11\u010F\u1E13\u1E0D\u1E0F\u0257\u20AB\u0256\u056A", + "e": "\u0435\u1E1B\u0113\u1E17\u0229\u0451\u011B\u0205\u03F5\u03B5\u025B3", + "f": "\u1E1F\u0493\u0192", + "g": "\u0581\u0123\u01F5\u0260\u011F\u011D\u01E5\u1E21", + "h": "\u1E23\u021F\u1E25\u1E2B\u0570\u056B\u1E29\u0266\u1E27\u1E23\u0266\u1E96\u0127", + "i": "\u0456\u012F\u03B9\u1EC9\u1F31\u1F77\u012B1\u00A1\u0457\u0390\u03CA", + "j": "\u0458\u029D\u0575\u025F\u0135\u0237\u01F0", + "k": "\u049F\u1E31\u0137\u0138\u043A\u0199\u049D", + "l": "\u217C\u1E3D\u1E3B\u013E\u0140\u013C\u1E39\u0142\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u03B9", + "m": "\u217F\u1E43\u0271\u1E41\u1E3F", + "n": "\u00F1\u0144\u0146\u0148\u0149\u01F9\u03AE\u03B7\u0578\u057C\u0580\u1E45\u1E47\u03A0", + "o": "\u00F2\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1F40\u1F41\u1F42\u1F43\u1F44\u1F45\u1F78\u1F79\u03C3\u0E50\u00F6\u014D\u014F\u0151\u01A1\u01D2\u03BF\u03CC0", + "p": "\u03C1\u0440\u048F\u1E55\u1E57\u1FE4\u1FE5\u2374", + "q": "\u051B\u0563\u0566\u0563\u0566", + "r": "\u0155\u0157\u0159\u0211\u0213\u027C\u027D\u0433\u0453\u0491\u04F7\u1E59\u1E5B\u1E5D", + "s": "\u015B\u015D\u015F\u0161\u0219\u0282\u0455\u1E61\u1E63\u1E65\u1E67\u1E69\u03C2", + "t": "\u0163\u0165\u01AB\u021B\u0288\u1E6B\u1E6D\u1E6F\u1E71\u1E97\u0236\u2020\u04AD", + "u": "\u00B5\u03BC\u00F9\u00FA\u00FB\u00FC\u0169\u016B\u016D\u016F\u0171\u0173\u01B0\u01D4\u0215\u0217\u0265\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u03BC\u03C5\u03CB\u03CD", + "v": "\u03BD\u0475\u0477\u1E7D\u1E7F\u2174\u2228\u03C5\u03CB\u03CD", + "w": "\u0175\u051D\u1E81\u1E83\u1E85\u1E87\u1E89\u1E98\u03C9\u03CE", + "x": "\u0445\u04B3\u1E8B\u1E8D\u03C7", + "y": "\u00FD\u00FF\u0177\u01B4\u0233\u03B3\u0443\u045E\u04EF\u04F1\u04F3\u1E8F\u1E99\u1EF3\u1EF5\u1EF7\u1EF9\u04AF\u04B1", + "z": "\u017A\u017C\u017E\u01B6\u0225\u0290\u1E91\u1E93\u1E95", + "A": "\u1E00\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAC\u1F08\u1F09\u1F88\u1F89\u1FB8\u1FB9\u1FBA\u1FBC\u212B\u0100\u0102\u0104\u0386\u0391\u0410", + "B": "\u0181\u0392\u0412\u1E02\u1E04\u1E06", + "C": "\u00C7\u0106\u0108\u010A\u010C\u0187\u0421\u04AA\u1E08\u216D\u03F9", + "D": "\u00D0\u010E\u0110\u0189\u018A\u1E0A\u1E0C\u1E0E\u216E", + "E": "\u00C8\u00C9\u00CA\u00CB\u0112\u0114\u0116\u0118\u011A\u0204\u0206\u0228\u0395\u0400\u0415\u04D6\u1E18\u0510\u2107\u0190\u1F19\u1FC8\u0404\u0388\u03AD\u03B5\u03B7\u0415", + "F": "\u03DC\u1E1E\u0492\u0191\u0492\u0493", + "G": "\u011C\u011E\u0120\u0122\u0193\u01E6\u01F4\u1E20", + "H": "\u0124\u021E\u0397\u041D\u04A2\u04A4\u04C7\u04C9\u1E22\u1E24\u1E26\u1E28\u1E2A\u1FCC\uA726\u0389", + "I": "\u038A\u00CC\u00CD\u00CE\u00CF\u0128\u012A\u012C\u012E\u0130\u0196\u0208\u020A\u0399\u03AA\u0406\u0407\u04C0\u04CF\u1E2C\u1EC8\u1F38\u1F39\u1FD8\u1FD9\u1FDA\u01D0\u217C\u1E3D\u1E3B\u026B\u013E\u0140\u013C\u1E39\u038A", + "J": "\u0134\u0408\u037F", + "K": "\u0136\u0198\u01E8\u039A\u040C\u041A\u051E\u1E30\u1E32\u1E34\u20AD\u212A\u03BA", + "L": "\u0139\u013B\u013D\u013F\u0141\u053C\u1E36\u1E38\u1E3A\u1E3C\u216C", + "M": "\u039C\u041C\u04CD\u1E3E\u1E40\u1E42\u216F", + "N": "\u00D1\u0143\u0145\u0147\u01F8\u039D\u1E44\u1E46\u1E48\u1E4A\u019D", + "O": "\u03B8\u236C\u00D2\u00D3\u00D4\u00D5\u00D6\u014C\u014E\u0150\u019F\u01A0\u01D1\u020E\u022E\u0230\u0398\u039F\u041E\u04E6\u0555\u1ECC\u1ECE\u1ED4\u1FF9\u038C", + "P": "\u01A4\u03A1\u0420\u048E\u1E54\u1E56\u1FEC", + "Q": "\u051A", + "R": "\u0154\u0156\u0158\u0210\u0212\u1E58\u1E5A\u1E5C\u1E5E\u211E\u024C\u2C64", + "S": "\u015A\u015C\u015E\u0160\u0218\u0405\u054F\u1E60\u1E62\u1E68\u1E64\u1E66", + "T": "\u0162\u0164\u0166\u01AE\u021A\u03A4\u0422\u04AC\u1E6A\u1E6C\u1E6E\u1E70\u038A\u1FDB\uA68C\u0372\u0373\u03C4", + "U": "\u016A\u016C\u016E\u0170\u0172\u01AF\u01D3\u1EE8\u1EEA\u1EEC\u1EEE\u0544", + "V": "\u0474\u0476\u1E7C\u1E7E\u22C1\u2164", + "W": "\u051C\u1E80\u1E82\u1E84\u1E86\u1E88\u019C", + "X": "\u03A7\u0425\u04B2\u1E8A\u1E8C\u2169", + "Y": "\u01B3\u0232\u03A5\u03AB\u03D3\u0423\u04AE\u04B0\u1E8E\u1EF2\u1EF4\u038E", + "Z": "\u0179\u017B\u017D\u0224\u0396\u1E90\u1E92\u1E94", + "": "\u200B\u200C\u200D", +}).map(([char, alts]) => alts.split("").map(alt => [alt, char] as const)).flat(1)); +export const multiCharSubstitutions:Array<[RegExp, string]> = [ + [/\|-\|/g, "H"] +]; +//#endregion +//#region misc +/** Used for anti-impersonation. Make sure to replace numbers with letters, for example, balam314 -> balamei4. */ +export const adminNames = ["fish", "balamei4", "clashgone", "darthscion", "firefridge", "aricia", "rawsewage", "skeledragon", "edh8e", "everydayhuman8e", "benjamonsrl"]; +export const heuristics = { + /** Will trip if more than this many blocks are broken within 25 seconds of joining. */ + blocksBrokenAfterJoin: 40, +}; +export const stopAntiEvadeTime = Duration.minutes(30); +export const backendIP = '45.79.202.111:5082'; +export const Mode = { + localDebug: new Fi("config/.debug").exists(), + noBackend: new Fi("config/.debug").exists() && !backendIP.startsWith("127.0.0.1:"), + isChristmas: new Date().getMonth() == 11, + isAprilFools: new Date().getMonth() == 3 && new Date().getDate() == 1, +}; +//#endregion +//#region servers +/** Stores the repository url for the maps for each gamemode. */ +export const mapRepoURLs:Record = { + attack: "https://api.github.com/repos/Fish-Community/fish-maps/contents/attack", + survival: "https://api.github.com/repos/Fish-Community/fish-maps/contents/survival", + pvp: "https://api.github.com/repos/Fish-Community/fish-maps/contents/pvp", + hexed: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hexed", + sandbox: "https://api.github.com/repos/Fish-Community/fish-maps/contents/sandbox", + hardcore: "https://api.github.com/repos/Fish-Community/fish-maps/contents/hardcore", + testsrv: "https://api.github.com/repos/Fish-Community/fish-maps/contents/testsrv", + minigame: "https://api.github.com/repos/Fish-Community/fish-maps/contents/minigame", +}; + + +/** Stores the names and addresses of each active server. */ +export class FishServer { + constructor( + public name:string, + public ip:string, + public port:string, + public aliases:string[], + /** If set, this permission is required to switch to or get information about this server. */ + public requiredPerm?:PermType, + ){ + FishServer.all.push(this); + } + + static all: FishServer[] = []; + static attack = new FishServer( + "attack", + "162.248.100.98", "6567", + ["attac", "atack", "atak", "atck", "atk", "a"] + ); + static survival = new FishServer( + "survival", + "162.248.101.95", "6567", + ["surviv", "surv", "sur", "su", "s", "sl"] + ); + static pvp = new FishServer( + "pvp", + "162.248.102.101", "6567", + ["pv", "p", "v", "playerversusplayer"] + ); + static sandbox = new FishServer( + "sandbox", + "162.248.101.53", "6567", + ["sand", "box", "sa", "sb"] + ); + static hexed = new FishServer( + "hexed", + "162.248.100.133", "6567", + ["h", "hx", "hxd", "hpvp", "hxpvp", "hexpvp"] + ); + static minigame = new FishServer( + "minigame", + "162.248.101.116", "6567", + ["m", "mg", "mini", "minig", "mgame", "mng", "minigame", "mpvp"] + ); + static testing = new FishServer( + "testing", + "162.248.101.52", "6567", + ["test", "testsrv", "t", "testingserver", "testserver"] + ); + static byName(input:string):FishServer | null { + input = input.toLowerCase(); + return FishServer.all.find(s => s.aliases.concat(s.name).includes(input)) ?? null; + } +}; + +export type GamemodeName = keyof typeof Gamemode extends infer K extends keyof typeof Gamemode ? K extends unknown ? + (typeof Gamemode)[K] extends (() => boolean) ? K : never +: never : never; +/** Stores functions that return whether the specified gamemode is the current gamemode. */ +export const Gamemode = { + attack: () => Gamemode.name() == "attack", + survival: () => Gamemode.name() == "survival", + pvp: () => Gamemode.name() == "pvp" || Gamemode.name() == "hexed" || Gamemode.name() == "minigame", + sandbox: () => Gamemode.name() == "sandbox", + hexed: () => Gamemode.name() == "hexed", + hardcore: () => Gamemode.name() == "hardcore", + testsrv: () => Gamemode.name() == "testsrv", + minigame: () => Gamemode.name() == "minigame", + name: () => Core.settings.get("mode", Vars.state.rules.mode().name()) as "attack" | "survival" | "pvp" | "sandbox" | "hexed" | "hardcore" | "testsrv" | "minigame", +}; +export const GamemodeNames: GamemodeName[] = Object.keys(Gamemode).filter((x): x is GamemodeName => x !== "name"); +//#endregion +//#region text content + +export const prefixes = { + marked: '[yellow]\u26A0[scarlet]Marked Griefer[]\u26A0[]', + flagged: '[yellow]\u26A0[orange]Flagged[]\u26A0[]', + muted: '[white](muted)', +}; + +export const text = { + discordURL: `https://discord.gg/VpzcYSQ33Y`, + membershipURL: `https://patreon.com/FishServers`, + reportsPing: `<@&1040193678817378305>`, + welcomeMessage: () => random([ + `[gold]Welcome![]` + ]), + chatFilterReplacement: { + message: () => `I really hope everyone is having a fun time :) <3`, + messageShort: () => `I hope we're all having a fun time :) <3`, + highlight: () => `[#f456f]`, + // `[#22AA22]Merry [#EC4444]Christmas!`, + // `[gold]Happy Holidays! [white]•*•☃*•`, + // `[gold]Happy Hanukkah!`, + // `[#EC4444]May your days be merry and bright!`, + // `[gold]Merry Fishmas! >|||> [white]☃`, + // `[gold]Deck the halls with lots of fun!`, + // `[gold]>|||> Fish wishes you [#22AA22]a merry Christmas!`, + // ]), + // chatFilterReplacement: { + // message: () => random([ + // `Have a holly jolly Christmas :) <3`, + // `I really hope everyone is jolly for the season! :D`, + // `All I want for Christmaaaaaaas is everyone having a fun time! :)`, + // `Remember to be nice in chat: Santa is watching! <3`, + // `All I want for Christmas is Fish! >|||>`, + // ]), + // highlight: () => random([ + // `[#22AA22]`, `[#EC4444]`, `[#FFFFFF]` + // ]), + } satisfies { + message: () => string; + messageShort: () => string; + highlight: () => string; + }, + dataFetchFailed: "[scarlet]\u26A0 Data fetch failed!\n[white]Please disconnect and rejoin the server if you encounter further issues, such as missing rank or statistics.", +}; + + +//TODO use this +export const FColor = ( + (data:Record):Record): string; + }> => + Object.fromEntries(Object.entries(data).map(([k, c]) => + [k, (str?:string | readonly string[], ...varChunks: ReadonlyArray) => + str != null ? + `${c}${Array.isArray(str) ? String.raw({ raw: str }, ...varChunks.map(v => String(v) + c)) : (str as string)}[]` + : c + ] + )) +)({ + discord: "[#7289DA]", + /** Used for tips and welcome messages. */ + tip: "[gold]", + member: "[pink]", + achievement: "[lime]", +}); +/** Tips that are shown to players randomly. */ +export const tips = { + ads: [ + `${FColor.member`Fish Membership`} subscribers can access the ${FColor.member`/pet`} command, which spawns a merui that follows you around. Get a Fish Membership at[sky] ${text.membershipURL} []`, + `${FColor.member`Fish Membership`} subscribers can use the ${FColor.member`/highlight`} command, which turns your chat messages to a color of your choice. Get a Fish Membership at[sky] ${text.membershipURL} []`, + `${FColor.member`Fish Membership`} subscribers can use the ${FColor.member`/rainbow`} command, which makes your name flash different colors. Get a Fish Membership at[sky] ${text.membershipURL} []`, + `Want to support the server and get some perks? Get a ${FColor.member`Fish Membership`} at[sky] ${text.membershipURL} []`, + `Join our ${FColor.discord`Discord server`}[]! ${FColor.discord(text.discordURL)} or type ${FColor.discord`/discord`}`, + ], + normal: [ + //commands + `You can spawn an [scarlet]Ohno[] with the [scarlet]/ohno[] command. Ohnos are harmless creatures that were created by fusing an alpha and an atrax.`, + `Ohnos cannot be spawned near enemy buildings, because they are peaceful and do not want to be used for attacks.`, + `You can use [white]/tp[] to teleport directly to any other player! (But only when you're in a core unit)`, + `You can unload bulk conveyors ( or ) with unloaders ( or ).`, + `Hate boulders? You can remove them with [white]/clean[].`, + `You can check our rules at any time by running [white]/rules[].`, + // `You can kill your unit by running [white]/die[].`, + `We have a tilelog system to help catch griefers. Run [white]/tilelog[], then click a tile to see what's happened there.`, + `Run [white]/tilelog 1[] to check the tile history of multiple tiles.`, + `Tilelog stores when a building is placed, broken, rotated, configured, and picked up/dropped by a payload unit. Access it with [white]/tilelog[]`, + `Tilelog doesn't just log tile actions, it also logs unit deaths! Access it with [white]/tilelog[]`, + `Did someone kill a T5 with commands? Run [white]/aoelog 0 15 killed[] to check tilelogs for unit deaths in a large area.`, + `Aoelog can show the history of tiles in an area. Select the opposite corners of a rectangle to view the history of its tiles.`, + `Aoelog is the plural version of tilelog, access it via [white]/aoelog[]`, + `You can mark yourself as AFK(away from keyboard) with [white]/afk[].`, + `Run /survival, /attack, /pvp, /sandbox, /hexed or /minigame to quickly change to another server.`, + `Need to get rid of an active griefer? Use [#6FFC7C]/s[] to send a message to all staff members across all servers.`, + `Use [white]/help to get more information about a specific command.`, + `If you want to send a message to just one player, you can use the [white]/msg[] command.`, + `Use [white]/r[] to reply to a message sent by another player.`, + `[white]/trail[] can be used to give your unit a trail of particle effects.`, + `Run [white]/ranks[] to see all the ranks on our server.`, + `Is someone impersonating a staff member? Run [white]/rank[] to see their real rank.`, + `Don't like the map? Vote to change it with [white]/rtv[].`, + `If you want to end the current map, DO NOT BREAK DEFENCES! Vote to change the map with [white]/rtv[].`, + //misc + `Anyone attempting to impersonate a ranked player, or the server, will have [scarlet]SUSSY IMPOSTOR[] prepended to their name. Beware!`, + `Griefers will often be found with the text ${prefixes.marked} prepended to their name: they are harmless and cannot grief again.`, + `Don't votekick ${prefixes.marked.slice(0, -3)}[][scarlet]s [gold]if they aren't breaking the rules: they are incapable of griefing more.`, + `Players marked as ${prefixes.flagged} have been flagged as suspicious by our detection systems, but they may not be griefers.`, + `Need to appeal a moderation action? Join the discord at ${FColor.discord(text.discordURL)} or type /discord`, + `Want to send the phrase [white]"/command"[] in chat? Type [white]"./command"[] and the [white].[] will be removed.`, + `All commands with a player as an argument support using a menu to specify the player. Just run the command leaving the argument blank (using two spaces if necessary), and a menu will show up.`, + `Players with a ${Rank.trusted.prefix} in front of their name aren't staff members, but they do have extra powers.`, + `Staff members will have the following prefixes in front of their name: ${Rank.manager.prefix}, ${Rank.admin.prefix}, ${Rank.mod.prefix}`, + `Wave cooldown too long? Skip the wait with [white]/vnw[]`, + `You can tell new players not to break power voids with [white]/void[]`, + `You can add [pink]color[] to things with color tags! Try typing "[[${["pink", "green", "cyan", "acid", "royal", "coral"][Math.floor(Math.random() * 6)]}]Hello" in chat, and see what happens!` + ], + christmas: [ + `Remember to be nice in-game, Santa is watching!`, + `Santa's checking his list, so be nice!`, + `Have a merry christmas and a happy new year!`, + `Fish becomes a bit more jolly around Christmastime!`, + `Many server maps have been changed for the season.`, + ], + staff: [ + + ], +}; +export const rules = [ + `# 1: [red]No griefing. This refers to intentionally hurting your own team in any way.`, + `# 2: [orange]False votekicking isn't allowed. Avoid votekicking if there's an active staff member in the server.`, + `# 3: [yellow]Gore, pornography, suggestive content and jokes, and flashing images aren't allowed here. Being horny and a creep in chat will result in a ban.`, + `# 4: [green]Do not harass other people. We have zero tolerance for any bigotry. Please respect everyone.`, + `# 5: [#00D8D8]Spamming is prohibited. Be reasonable with messaging staff in-game. Misuse may result in a mute.`, + `# 6: [blue]Impersonating people or ranks is prohibited.`, + `# 7: [purple]Talking about controversial or sensitive topics is not allowed in-game. Hate symbols, such as swastikas, are not permitted.`, + `# 8: [pink]No uncomfortable trolling or intentionally causing chaos. This includes any actions or messages that create an unpleasant atmosphere.`, + `Failure to follow these rules will result in consequences: likely a ${prefixes.marked} tag for any game disruption, mute for broken chat rules, and bans for repeated offenses or bypasses.` +].map(r => `[white]${r}`); +//#endregion + diff --git a/src/files.ts b/src/files.ts index a74a1dc8..c0c51d77 100644 --- a/src/files.ts +++ b/src/files.ts @@ -1,116 +1,116 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the code for automated map syncing. -Original contributor: @author Jurorno9 -Maintenance: @author BalaM314 -*/ - -import { Gamemode, mapRepoURLs } from "/config"; -import { crash } from "/funcs"; -import { Promise } from "/promise"; -import { getHash } from "/utils"; - - - - -type GitHubFile = { - name: string; - path: string; - sha: string; - size: number; - url: string; - html_url: string; - git_url: string; - download_url: string | null; - type: 'file' | 'dir'; -} - -//if we switch to a self-hosted setup, just make it respond with the githubfile object for a drop-in replacement -function fetchGithubContents(){ - return new Promise((resolve, reject) => { - const url = mapRepoURLs[Gamemode.name()]; - if(!url) return reject(`No recognized gamemode detected. please enter "host " and try again`); - Http.get(url, (res) => { - try { - //Trust github to return valid JSON data - resolve(JSON.parse(res.getResultAsString())); - } catch(e){ - reject(`Failed to parse GitHub repository contents: ${String(e)}`); - } - }, () => reject(`Network error while fetching github repository contents`)); - }); -} - -function downloadFile(address:string, filename:string):Promise { - if(!/^https?:\/\//i.test(address)){ - crash(`Invalid address, please start with 'http://' or 'https://'`); - } - - return new Promise((resolve, reject) => { - let instream:InputStream | null = null; - let outstream:OutputStream | null = null; - Log.info(`Downloading ${filename}...`); - Http.get(address, (res) => { - try { - instream = res.getResultAsStream(); - outstream = new Fi(filename).write(); - instream.transferTo(outstream); - resolve(); - } finally { - instream?.close(); - outstream?.close(); - } - }, - () => { - Log.err(`Download failed.`); - reject(`Network error while downloading a map file: ${address}`); - }); - }); -} - - -function downloadMaps(githubListing:GitHubFile[]):Promise { - return Promise.all(githubListing.map(fileEntry => { - if(!(typeof fileEntry.download_url == "string")){ - Log.warn(`Map ${fileEntry.name} has no valid download link, skipped.`); - return Promise.resolve(null! as void); - } - return downloadFile(fileEntry.download_url, Vars.customMapDirectory.child(fileEntry.name).absolutePath()); - })).then(v => {}); -} - -/** - * @returns whether any maps were changed - */ -export function updateMaps():Promise { - //get github map listing - return fetchGithubContents().then((listing) => { - //filter only valid mindustry maps - const mapList = listing - .filter(entry => entry.type == 'file') - .filter(entry => entry.name.endsWith(".msav")); - - const mapFiles:Fi[] = Vars.customMapDirectory.list(); - const mapsToDelete = mapFiles.filter(localFile => - !mapList.some(remoteFile => - remoteFile.name === localFile.name() - ) - && !localFile.name().startsWith("$$") - ); - mapsToDelete.forEach((map) => map.delete()); - - const mapsToDownload = mapList - .filter(entry => { - const file = Vars.customMapDirectory.child(entry.name); - return !file.exists() || entry.sha !== getHash(file); //sha'd - }); - - if(mapsToDownload.length == 0){ - return mapsToDelete.length > 0 ? true : false; - } - return downloadMaps(mapsToDownload).then(() => { - Vars.maps.reload(); - return true; - }); - }); +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the code for automated map syncing. +Original contributor: @author Jurorno9 +Maintenance: @author BalaM314 +*/ + +import { Gamemode, mapRepoURLs } from "/config"; +import { crash } from "/funcs"; +import { Promise } from "/promise"; +import { getHash } from "/utils"; + + + + +type GitHubFile = { + name: string; + path: string; + sha: string; + size: number; + url: string; + html_url: string; + git_url: string; + download_url: string | null; + type: 'file' | 'dir'; +} + +//if we switch to a self-hosted setup, just make it respond with the githubfile object for a drop-in replacement +function fetchGithubContents(){ + return new Promise((resolve, reject) => { + const url = mapRepoURLs[Gamemode.name()]; + if(!url) return reject(`No recognized gamemode detected. please enter "host " and try again`); + Http.get(url, (res) => { + try { + //Trust github to return valid JSON data + resolve(JSON.parse(res.getResultAsString())); + } catch(e){ + reject(`Failed to parse GitHub repository contents: ${String(e)}`); + } + }, () => reject(`Network error while fetching github repository contents`)); + }); +} + +function downloadFile(address:string, filename:string):Promise { + if(!/^https?:\/\//i.test(address)){ + crash(`Invalid address, please start with 'http://' or 'https://'`); + } + + return new Promise((resolve, reject) => { + let instream:InputStream | null = null; + let outstream:OutputStream | null = null; + Log.info(`Downloading ${filename}...`); + Http.get(address, (res) => { + try { + instream = res.getResultAsStream(); + outstream = new Fi(filename).write(); + instream.transferTo(outstream); + resolve(); + } finally { + instream?.close(); + outstream?.close(); + } + }, + () => { + Log.err(`Download failed.`); + reject(`Network error while downloading a map file: ${address}`); + }); + }); +} + + +function downloadMaps(githubListing:GitHubFile[]):Promise { + return Promise.all(githubListing.map(fileEntry => { + if(!(typeof fileEntry.download_url == "string")){ + Log.warn(`Map ${fileEntry.name} has no valid download link, skipped.`); + return Promise.resolve(null! as void); + } + return downloadFile(fileEntry.download_url, Vars.customMapDirectory.child(fileEntry.name).absolutePath()); + })).then(v => {}); +} + +/** + * @returns whether any maps were changed + */ +export function updateMaps():Promise { + //get github map listing + return fetchGithubContents().then((listing) => { + //filter only valid mindustry maps + const mapList = listing + .filter(entry => entry.type == 'file') + .filter(entry => entry.name.endsWith(".msav")); + + const mapFiles:Fi[] = Vars.customMapDirectory.list(); + const mapsToDelete = mapFiles.filter(localFile => + !mapList.some(remoteFile => + remoteFile.name === localFile.name() + ) + && !localFile.name().startsWith("$$") + ); + mapsToDelete.forEach((map) => map.delete()); + + const mapsToDownload = mapList + .filter(entry => { + const file = Vars.customMapDirectory.child(entry.name); + return !file.exists() || entry.sha !== getHash(file); //sha'd + }); + + if(mapsToDownload.length == 0){ + return mapsToDelete.length > 0 ? true : false; + } + return downloadMaps(mapsToDownload).then(() => { + Vars.maps.reload(); + return true; + }); + }); } \ No newline at end of file diff --git a/src/fjsContext.ts b/src/fjsContext.ts index 645f9410..a3c21ed0 100644 --- a/src/fjsContext.ts +++ b/src/fjsContext.ts @@ -1,109 +1,109 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the context for the "fjs" command, -which executes code with access to the plugin's internals. -*/ - -import type { FishPlayer as tFishPlayer } from "/players"; -type FishPlayer = tFishPlayer; //absurd - -const achievements = require("/achievements"); -const api = require("/api"); -const commands = require("/frameworks/commands"); -const config = require("/config"); -const { commands: consoleCommands } = require("/commands/console"); -const files = require("/files"); -const funcs = require("/funcs"); -const globals = require("/globals"); -const io = require("/frameworks/io"); -const maps = require("/maps"); -const { commands: memberCommands } = require("/commands/member"); -const menus = require("/frameworks/menus"); -const packetHandlers = require("/packetHandlers"); -const { commands: playerCommands } = require("/commands/general"); -const players = require("/players"); -const ranks = require("/ranks"); -const { commands: staffCommands } = require("/commands/staff"); -const timers = require("/timers"); -const utils = require("/utils"); -const votes = require("/votes"); -const { Promise } = require("/promise"); - -const { Achievement, Achievements } = achievements; -const { Perm, allCommands } = commands; -const { bannedWords, FishServer, Mode, Gamemode, FColor, mapRepoURLs } = config; -const { FishPlayer } = players; -const { Serializer } = io; -const { FishEvents, fishPlugin, fishState, tileHistory } = globals; -const { FMap } = maps; -const { Rank, RoleFlag } = ranks; -const { Menu } = menus; - -Object.assign(this as never as typeof globalThis, utils, funcs); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever - -const Ranks = null!; - -const $ = Object.assign( - function $(input:unknown){ - if(typeof input == "string"){ - if(Pattern.matches("[a-zA-Z0-9+/]{22}==", input)){ - return FishPlayer.getById(input); - } - } - return null; - }, - { - sussy: true, - info: function(input:unknown){ - if(typeof input == "string"){ - if(Pattern.matches("[a-zA-Z0-9+/]{22}==", input)){ - return Vars.netServer.admins.getInfo(input); - } - } - return null; - }, - create: function(input:unknown){ - if(typeof input == "string"){ - if(Pattern.matches("[a-zA-Z0-9+/]{22}==", input)){ - return FishPlayer.getFromInfo(Vars.netServer.admins.getInfo(input)); - } - } - return null; - }, - me: null as FishPlayer | null, - meM: null as mindustryPlayer | null, - } -); - -/** Used to persist variables. */ -const vars = {}; - -export function runJS( - input:string, - outputFunction:(data:any) => unknown = Log.info, - errorFunction:(data:any) => unknown = Log.err, - player?:FishPlayer -){ - if(player){ - $.me = player; - $.meM = player.player; - } else if(Groups.player.size() == 1){ - $.meM = Groups.player.first(); - $.me = players.FishPlayer.get($.meM); - } - try { - const admins = Vars.netServer.admins; - const output = eval(input); - if(output instanceof Array){ - outputFunction("&cArray: [&fr" + output.join(", ") + "&c]&fr"); - } else if(output === undefined){ - outputFunction("undefined"); - } else if(output === null){ - outputFunction("null"); - } else { - outputFunction(output); - } - } catch(err){ - errorFunction(err); - } -} +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the context for the "fjs" command, +which executes code with access to the plugin's internals. +*/ + +import type { FishPlayer as tFishPlayer } from "/players"; +type FishPlayer = tFishPlayer; //absurd + +const achievements = require("/achievements"); +const api = require("/api"); +const commands = require("/frameworks/commands"); +const config = require("/config"); +const { commands: consoleCommands } = require("/commands/console"); +const files = require("/files"); +const funcs = require("/funcs"); +const globals = require("/globals"); +const io = require("/frameworks/io"); +const maps = require("/maps"); +const { commands: memberCommands } = require("/commands/member"); +const menus = require("/frameworks/menus"); +const packetHandlers = require("/packetHandlers"); +const { commands: playerCommands } = require("/commands/general"); +const players = require("/players"); +const ranks = require("/ranks"); +const { commands: staffCommands } = require("/commands/staff"); +const timers = require("/timers"); +const utils = require("/utils"); +const votes = require("/votes"); +const { Promise } = require("/promise"); + +const { Achievement, Achievements } = achievements; +const { Perm, allCommands } = commands; +const { bannedWords, FishServer, Mode, Gamemode, FColor, mapRepoURLs } = config; +const { FishPlayer } = players; +const { Serializer } = io; +const { FishEvents, fishPlugin, fishState, tileHistory } = globals; +const { FMap } = maps; +const { Rank, RoleFlag } = ranks; +const { Menu } = menus; + +Object.assign(this as never as typeof globalThis, utils, funcs); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever + +const Ranks = null!; + +const $ = Object.assign( + function $(input:unknown){ + if(typeof input == "string"){ + if(Pattern.matches("[a-zA-Z0-9+/]{22}==", input)){ + return FishPlayer.getById(input); + } + } + return null; + }, + { + sussy: true, + info: function(input:unknown){ + if(typeof input == "string"){ + if(Pattern.matches("[a-zA-Z0-9+/]{22}==", input)){ + return Vars.netServer.admins.getInfo(input); + } + } + return null; + }, + create: function(input:unknown){ + if(typeof input == "string"){ + if(Pattern.matches("[a-zA-Z0-9+/]{22}==", input)){ + return FishPlayer.getFromInfo(Vars.netServer.admins.getInfo(input)); + } + } + return null; + }, + me: null as FishPlayer | null, + meM: null as mindustryPlayer | null, + } +); + +/** Used to persist variables. */ +const vars = {}; + +export function runJS( + input:string, + outputFunction:(data:any) => unknown = Log.info, + errorFunction:(data:any) => unknown = Log.err, + player?:FishPlayer +){ + if(player){ + $.me = player; + $.meM = player.player; + } else if(Groups.player.size() == 1){ + $.meM = Groups.player.first(); + $.me = players.FishPlayer.get($.meM); + } + try { + const admins = Vars.netServer.admins; + const output = eval(input); + if(output instanceof Array){ + outputFunction("&cArray: [&fr" + output.join(", ") + "&c]&fr"); + } else if(output === undefined){ + outputFunction("undefined"); + } else if(output === null){ + outputFunction("null"); + } else { + outputFunction(output); + } + } catch(err){ + errorFunction(err); + } +} diff --git a/src/funcs.ts b/src/funcs.ts index a0650806..33ee0ff2 100644 --- a/src/funcs.ts +++ b/src/funcs.ts @@ -1,382 +1,382 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains dozens of simple functions that do not need access to any values from other files. -For functions that do need values from other files, see utils.ts. -*/ -import type { TagFunction } from "/types"; -import type { PartialFormatString } from "/frameworks/commands"; - -const storedValues: Record = {}; -/** - * Stores the output of a function and returns that value - * instead of running the function again unless any - * dependencies have changed to improve performance with - * functions that have expensive computation. - * @param callback function to run if a dependancy has changed - * @param dep dependency array of values to monitor - * @param id arbitrary unique id of the function for storage purposes. - */ -export function memoize(callback: () => T, dep: unknown[], id: number | string): T { - if (!storedValues[id]) { - storedValues[id] = { value: callback(), dep }; - } else if (dep.some((d, ind) => d !== storedValues[id].dep[ind])) { - //If the value changed - storedValues[id].value = callback(); - storedValues[id].dep = dep; - } - return storedValues[id].value as T; -}/** - * Converts a 1D array into a 2D array. - * @param width the max length of each row. - * The last row may not be full. - */ - -export function to2DArray(array: T[], width: number) { - if (array.length == 0) return []; - const output: T[][] = [[]]; - array.forEach(el => { - if (output.at(-1)!.length >= width) { - output.push([]); - } - output.at(-1)!.push(el); - }); - return output; -} -export function setToArray(set: ObjectSet | EntityGroup): T[] { - const array: T[] = []; - set.each(item => array.push(item)); - return array; -} -export class StringBuilder { - constructor(public str: string = "") { } - add(str: string) { - this.str += str; - return this; - } - chunk(str: string) { - if (Strings.stripColors(str).length > 0) { - this.str = this.str + " " + str; - } - return this; - } -} -/** - * Used for serialization to strings. - * @deprecated use Serializer instead, which serializes to byte[] - */ -export class StringIO { - offset: number = 0; - constructor(public string: string = "") { } - read(length: number = 1) { - if (this.offset + length > this.string.length) crash(`Unexpected EOF`); - return this.string.slice(this.offset, this.offset += length); - } - write(str: string) { - this.string += str; - } - readString(/** The length of the written length. */ lenlen: number = 3) { - const length = parseInt(this.read(lenlen)); - if (length == 0) return null; - return this.read(length); - } - writeString(str: string | null, lenlen: number = 3, truncate = false) { - if (str === null) { - this.string += "0".repeat(lenlen); - } else if (typeof str !== "string") { - crash(`Attempted to serialize string ${String(str)}, but it was not a string`); - } else if (str.length > (10 ** lenlen - 1)) { - if (truncate) { - Log.err(`Cannot write strings with length greater than ${(10 ** lenlen - 1)} (was ${str.length}), truncating`); - this.string += (10 ** lenlen - 1).toString().padStart(lenlen, "0"); - this.string += str.slice(0, (10 ** lenlen - 1)); - } else { - crash(`Cannot write strings with length greater than ${(10 ** lenlen - 1)} (was ${str.length})\n String was: "${str}"`); - } - } else { - this.string += str.length.toString().padStart(lenlen, "0"); - this.string += str; - } - } - readEnumString(options: T[]): T { - const length = (options.length - 1).toString().length; - const option = this.readNumber(length); - return options[option]; - } - writeEnumString(value: T, options: T[]) { - const length = (options.length - 1).toString().length; - const option = options.indexOf(value); - if (option == -1) crash(`Attempted to write invalid value "${String(value)}" for enum, valid values are (${options.join(", ")})`); - this.writeNumber(option, length); - } - readNumber(size: number = 4) { - let data = this.read(size); - if (/^0*-\d+$/.test(data)) { - //negative numbers were incorrectly stored in previous versions - data = "-" + data.split("-")[1]; - } - if (isNaN(Number(data))) crash(`Attempted to read invalid number: ${data}`); - return Number(data); - } - writeNumber(num: number, size: number = 4, clamp = false) { - if (typeof num != "number") crash(`${String(num)} was not a number!`); - if (num.toString().length > size) { - if (clamp) { - if (num > (10 ** size) - 1) this.string += (10 ** size) - 1; - else this.string += num.toString().slice(0, size); - } else crash(`Cannot write number ${num} with length ${size}: too long`); - } - this.string += num.toString().padStart(size, "0"); - } - readBool() { - return this.read(1) == "T" ? true : false; - } - writeBool(val: boolean) { - this.write(val ? "T" : "F"); - } - writeArray(array: T[], func: (item: T, str: StringIO) => unknown, lenlen?: number) { - this.writeNumber(array.length, lenlen); - array.forEach(e => func(e, this)); - } - readArray(func: (str: StringIO) => T, lenlen?: number): T[] { - const length = this.readNumber(lenlen); - const array: T[] = []; - for (let i = 0; i < length; i++) { - array[i] = func(this); - } - return array; - } - expectEOF() { - if (this.string.length > this.offset) crash(`Expected EOF, but found extra data: "${this.string.slice(this.offset)}"`); - } - static read(data: string, func: (str: StringIO) => T): T { - const str = new StringIO(data); - try { - return func(str); - } catch (err) { - Log.err(`Error while reading compressed data!`); - Log.err(data); - throw err; - } - } - static write(data: T, func: (str: StringIO, data: T) => unknown): string { - const str = new StringIO(); - func(str, data); - return str.string; - } -} - -/** Something that emits events. */ -export class EventEmitter< - /** Mapping between event name and arguments to the handler. */ - EventMapping extends Record -> { - private listeners: { - [K in keyof EventMapping]?: Array<(t: this, ...args: EventMapping[K]) => unknown>; - } = {}; - on(event: EventType, callback: (t: this, ...args: EventMapping[EventType]) => unknown): this { - (this.listeners[event] ??= []).push(callback); - return this; - } - fire(event: EventType, args: EventMapping[EventType]) { - const listeners = this.listeners[event] ?? []; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < listeners.length; i ++) { - listeners[i](this, ...args); - } - } -} - -export function crash(message: string): never { - throw new Error(message); -} - - -/** Best effort title-capitalization of a word. */ -export function capitalizeText(text: string, separator = " "): string { - return text - .split(separator) - .map((word, i, arr) => ( - ["a", "an", "the", "in", "and", "of", "it", "is"].includes(word) && - i !== 0 && i !== arr.length - 1 - ) ? word - : word[0].toUpperCase() + word.substring(1).toLowerCase() - ).join(" "); -} - -/** Best effort prepends an indefinite article (either "a" or "an") to provided text. */ -export function indefiniteArticle(text:string):string { - const cText = Strings.stripColors(text); - if(/^[aeiou]/.test(cText) || cText == "hour") return "an " + text; - else return "a " + text; -} - -const pattern = Pattern.compile(`([*\\_~\`|:])`); -export function escapeTextDiscord(text: string): string { - return pattern.matcher(text).replaceAll("\\\\$1\u200B"); -} - - -/** Prevents Mindustry from displaying color tags in a string by escaping them. Example: turns [scarlet]red to [[scarlet]red. */ -export function escapeStringColorsClient(str: string): string { - return str.replace(/\[/g, "[["); -} -// export function highlightStringColorsClient(str:string):string { -// return str.replace(/(?( - transformer: (chunk: T, index: number, allStringChunks: readonly string[], allVarChunks: readonly T[]) => string -): TagFunction { - return function (stringChunks: readonly string[], ...varChunks: readonly T[]) { - return String.raw({ raw: stringChunks }, ...varChunks.map((chunk, i) => transformer(chunk, i, stringChunks, varChunks))); - }; -} -//third order function ._. warning: causes major confusion -/** Generates a tag template partial processor from a function that processes one value at a time. */ - -export function tagProcessorPartial( - transformer: (chunk: Tin, index: number, data: Tdata, allStringChunks: readonly string[], allVarChunks: readonly Tin[]) => string -): TagFunction> { - return (stringChunks: readonly string[], ...varChunks: readonly Tin[]) => Object.assign( - (data: Tdata) => stringChunks.map((chunk, i) => { - if (stringChunks.length <= i) return chunk; - return (i - 1) in varChunks ? transformer(varChunks[i - 1], i, data, stringChunks, varChunks) + chunk : chunk; - }).join(''), - { - __partialFormatString: true as const - } - ); -} -/** Chooses a random number between 0 and max. */ - -export function random(max: number): number; -/** Chooses a random number between min and max. */ -export function random(min: number, max: number): number; -/** Selects a random element from an array. */ -export function random(list: T[]): T; - -export function random(arg0: unknown, arg1?: number): any { - if (typeof arg0 == "number") { - let max: number, min: number; - if (arg1 == undefined) { - max = arg0; - min = 0; - } else { - min = arg0; - max = arg1; - } - return Math.random() * (max - min) + min; - } else if (arg0 instanceof Array) { - return arg0[Math.floor(Math.random() * arg0.length)]; - } -} - -export function getIPAddress(fallback:string = "127.0.0.1"):string { - return Packages.java.util.Collections.list( - Packages.java.net.NetworkInterface.getNetworkInterfaces() - ) - .stream() - .filter((i:any) => i.isUp() && !i.isLoopback()) - .findFirst() - .orElse(null) - ?.getInterfaceAddresses() - .stream() - .map((s:any) => s.getAddress()) - .filter((a:any) => a instanceof Packages.java.net.Inet4Address) - .findFirst() - .orElse(null) - ?.getHostAddress() ?? fallback; -} - -export function lazy(func:() => T){ - let value: T | null = null; - return function get(){ - return value ??= func(); - }; -} - -export function invalidtoNull(input:number):number | null { - if(isNaN(input) || !isFinite(input)) return null; - return input; -} -/** Prevents improperly formed color tags from breaking when combined with other strings. */ -export function cleanColors(input:string){ - if(input.endsWith("[") && !input.endsWith("[[")) return input + "["; - else return input; -} - -export function computeStatistics(data:number[]){ - if(data.length == 0) data = [NaN]; //return NaN for all properties - const lowest = Math.min(...data); - const highest = Math.max(...data); - return { - lowest, - highest, - range: highest - lowest, - //variance? stdev? - average: (data.reduce((a, b) => a + b, 0) / data.length), - }; -} - -/** - * Uses an array of progressively less specific search functions to return none, one, or multiple matches. - */ -export function search(...filters: Array<(x:T, query:string) => boolean>): (options: T[], query:string | undefined) => T | T[] | null { - return function(options, query){ - if(!query) return options; - for(const filter of filters){ - const result = options.filter(x => filter(x, query)); - if(result.length == 1) return result[0]; - else if(result.length > 1) return result; - } - return null; - }; -} -export function searchFixed(options:T[] | (() => T[]), filters: Array<(x:T, query:string) => boolean>, recomputeOptions?: "recomputeOptions"): (query:string | undefined) => T | T[] | null { - const func = search(...filters); - let _options = options; - return query => { - if(typeof _options == "function"){ - if(recomputeOptions) return func(_options(), query); - else return func(_options = _options(), query); - } else return func(_options, query); - }; -} -export function delay(millis:number):Promise { - return new Promise(res => Timer.schedule(res, millis / 1000)); -} - -export const Duration = { - seconds: x => x * 1000, - minutes: x => x * 60_000, - hours: x => x * 3600_000, - days: x => x * 86400_000, - months: x => x * 2592000_000, -} satisfies Record number>; -export const DurationSecs = { - minutes: x => x * 60, - hours: x => x * 3600, - days: x => x * 86400, - months: x => x * 2592000, -} satisfies Record number>; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains dozens of simple functions that do not need access to any values from other files. +For functions that do need values from other files, see utils.ts. +*/ +import type { TagFunction } from "/types"; +import type { PartialFormatString } from "/frameworks/commands"; + +const storedValues: Record = {}; +/** + * Stores the output of a function and returns that value + * instead of running the function again unless any + * dependencies have changed to improve performance with + * functions that have expensive computation. + * @param callback function to run if a dependancy has changed + * @param dep dependency array of values to monitor + * @param id arbitrary unique id of the function for storage purposes. + */ +export function memoize(callback: () => T, dep: unknown[], id: number | string): T { + if (!storedValues[id]) { + storedValues[id] = { value: callback(), dep }; + } else if (dep.some((d, ind) => d !== storedValues[id].dep[ind])) { + //If the value changed + storedValues[id].value = callback(); + storedValues[id].dep = dep; + } + return storedValues[id].value as T; +}/** + * Converts a 1D array into a 2D array. + * @param width the max length of each row. + * The last row may not be full. + */ + +export function to2DArray(array: T[], width: number) { + if (array.length == 0) return []; + const output: T[][] = [[]]; + array.forEach(el => { + if (output.at(-1)!.length >= width) { + output.push([]); + } + output.at(-1)!.push(el); + }); + return output; +} +export function setToArray(set: ObjectSet | EntityGroup): T[] { + const array: T[] = []; + set.each(item => array.push(item)); + return array; +} +export class StringBuilder { + constructor(public str: string = "") { } + add(str: string) { + this.str += str; + return this; + } + chunk(str: string) { + if (Strings.stripColors(str).length > 0) { + this.str = this.str + " " + str; + } + return this; + } +} +/** + * Used for serialization to strings. + * @deprecated use Serializer instead, which serializes to byte[] + */ +export class StringIO { + offset: number = 0; + constructor(public string: string = "") { } + read(length: number = 1) { + if (this.offset + length > this.string.length) crash(`Unexpected EOF`); + return this.string.slice(this.offset, this.offset += length); + } + write(str: string) { + this.string += str; + } + readString(/** The length of the written length. */ lenlen: number = 3) { + const length = parseInt(this.read(lenlen)); + if (length == 0) return null; + return this.read(length); + } + writeString(str: string | null, lenlen: number = 3, truncate = false) { + if (str === null) { + this.string += "0".repeat(lenlen); + } else if (typeof str !== "string") { + crash(`Attempted to serialize string ${String(str)}, but it was not a string`); + } else if (str.length > (10 ** lenlen - 1)) { + if (truncate) { + Log.err(`Cannot write strings with length greater than ${(10 ** lenlen - 1)} (was ${str.length}), truncating`); + this.string += (10 ** lenlen - 1).toString().padStart(lenlen, "0"); + this.string += str.slice(0, (10 ** lenlen - 1)); + } else { + crash(`Cannot write strings with length greater than ${(10 ** lenlen - 1)} (was ${str.length})\n String was: "${str}"`); + } + } else { + this.string += str.length.toString().padStart(lenlen, "0"); + this.string += str; + } + } + readEnumString(options: T[]): T { + const length = (options.length - 1).toString().length; + const option = this.readNumber(length); + return options[option]; + } + writeEnumString(value: T, options: T[]) { + const length = (options.length - 1).toString().length; + const option = options.indexOf(value); + if (option == -1) crash(`Attempted to write invalid value "${String(value)}" for enum, valid values are (${options.join(", ")})`); + this.writeNumber(option, length); + } + readNumber(size: number = 4) { + let data = this.read(size); + if (/^0*-\d+$/.test(data)) { + //negative numbers were incorrectly stored in previous versions + data = "-" + data.split("-")[1]; + } + if (isNaN(Number(data))) crash(`Attempted to read invalid number: ${data}`); + return Number(data); + } + writeNumber(num: number, size: number = 4, clamp = false) { + if (typeof num != "number") crash(`${String(num)} was not a number!`); + if (num.toString().length > size) { + if (clamp) { + if (num > (10 ** size) - 1) this.string += (10 ** size) - 1; + else this.string += num.toString().slice(0, size); + } else crash(`Cannot write number ${num} with length ${size}: too long`); + } + this.string += num.toString().padStart(size, "0"); + } + readBool() { + return this.read(1) == "T" ? true : false; + } + writeBool(val: boolean) { + this.write(val ? "T" : "F"); + } + writeArray(array: T[], func: (item: T, str: StringIO) => unknown, lenlen?: number) { + this.writeNumber(array.length, lenlen); + array.forEach(e => func(e, this)); + } + readArray(func: (str: StringIO) => T, lenlen?: number): T[] { + const length = this.readNumber(lenlen); + const array: T[] = []; + for (let i = 0; i < length; i++) { + array[i] = func(this); + } + return array; + } + expectEOF() { + if (this.string.length > this.offset) crash(`Expected EOF, but found extra data: "${this.string.slice(this.offset)}"`); + } + static read(data: string, func: (str: StringIO) => T): T { + const str = new StringIO(data); + try { + return func(str); + } catch (err) { + Log.err(`Error while reading compressed data!`); + Log.err(data); + throw err; + } + } + static write(data: T, func: (str: StringIO, data: T) => unknown): string { + const str = new StringIO(); + func(str, data); + return str.string; + } +} + +/** Something that emits events. */ +export class EventEmitter< + /** Mapping between event name and arguments to the handler. */ + EventMapping extends Record +> { + private listeners: { + [K in keyof EventMapping]?: Array<(t: this, ...args: EventMapping[K]) => unknown>; + } = {}; + on(event: EventType, callback: (t: this, ...args: EventMapping[EventType]) => unknown): this { + (this.listeners[event] ??= []).push(callback); + return this; + } + fire(event: EventType, args: EventMapping[EventType]) { + const listeners = this.listeners[event] ?? []; + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < listeners.length; i ++) { + listeners[i](this, ...args); + } + } +} + +export function crash(message: string): never { + throw new Error(message); +} + + +/** Best effort title-capitalization of a word. */ +export function capitalizeText(text: string, separator = " "): string { + return text + .split(separator) + .map((word, i, arr) => ( + ["a", "an", "the", "in", "and", "of", "it", "is"].includes(word) && + i !== 0 && i !== arr.length - 1 + ) ? word + : word[0].toUpperCase() + word.substring(1).toLowerCase() + ).join(" "); +} + +/** Best effort prepends an indefinite article (either "a" or "an") to provided text. */ +export function indefiniteArticle(text:string):string { + const cText = Strings.stripColors(text); + if(/^[aeiou]/.test(cText) || cText == "hour") return "an " + text; + else return "a " + text; +} + +const pattern = Pattern.compile(`([*\\_~\`|:])`); +export function escapeTextDiscord(text: string): string { + return pattern.matcher(text).replaceAll("\\\\$1\u200B"); +} + + +/** Prevents Mindustry from displaying color tags in a string by escaping them. Example: turns [scarlet]red to [[scarlet]red. */ +export function escapeStringColorsClient(str: string): string { + return str.replace(/\[/g, "[["); +} +// export function highlightStringColorsClient(str:string):string { +// return str.replace(/(?( + transformer: (chunk: T, index: number, allStringChunks: readonly string[], allVarChunks: readonly T[]) => string +): TagFunction { + return function (stringChunks: readonly string[], ...varChunks: readonly T[]) { + return String.raw({ raw: stringChunks }, ...varChunks.map((chunk, i) => transformer(chunk, i, stringChunks, varChunks))); + }; +} +//third order function ._. warning: causes major confusion +/** Generates a tag template partial processor from a function that processes one value at a time. */ + +export function tagProcessorPartial( + transformer: (chunk: Tin, index: number, data: Tdata, allStringChunks: readonly string[], allVarChunks: readonly Tin[]) => string +): TagFunction> { + return (stringChunks: readonly string[], ...varChunks: readonly Tin[]) => Object.assign( + (data: Tdata) => stringChunks.map((chunk, i) => { + if (stringChunks.length <= i) return chunk; + return (i - 1) in varChunks ? transformer(varChunks[i - 1], i, data, stringChunks, varChunks) + chunk : chunk; + }).join(''), + { + __partialFormatString: true as const + } + ); +} +/** Chooses a random number between 0 and max. */ + +export function random(max: number): number; +/** Chooses a random number between min and max. */ +export function random(min: number, max: number): number; +/** Selects a random element from an array. */ +export function random(list: T[]): T; + +export function random(arg0: unknown, arg1?: number): any { + if (typeof arg0 == "number") { + let max: number, min: number; + if (arg1 == undefined) { + max = arg0; + min = 0; + } else { + min = arg0; + max = arg1; + } + return Math.random() * (max - min) + min; + } else if (arg0 instanceof Array) { + return arg0[Math.floor(Math.random() * arg0.length)]; + } +} + +export function getIPAddress(fallback:string = "127.0.0.1"):string { + return Packages.java.util.Collections.list( + Packages.java.net.NetworkInterface.getNetworkInterfaces() + ) + .stream() + .filter((i:any) => i.isUp() && !i.isLoopback()) + .findFirst() + .orElse(null) + ?.getInterfaceAddresses() + .stream() + .map((s:any) => s.getAddress()) + .filter((a:any) => a instanceof Packages.java.net.Inet4Address) + .findFirst() + .orElse(null) + ?.getHostAddress() ?? fallback; +} + +export function lazy(func:() => T){ + let value: T | null = null; + return function get(){ + return value ??= func(); + }; +} + +export function invalidtoNull(input:number):number | null { + if(isNaN(input) || !isFinite(input)) return null; + return input; +} +/** Prevents improperly formed color tags from breaking when combined with other strings. */ +export function cleanColors(input:string){ + if(input.endsWith("[") && !input.endsWith("[[")) return input + "["; + else return input; +} + +export function computeStatistics(data:number[]){ + if(data.length == 0) data = [NaN]; //return NaN for all properties + const lowest = Math.min(...data); + const highest = Math.max(...data); + return { + lowest, + highest, + range: highest - lowest, + //variance? stdev? + average: (data.reduce((a, b) => a + b, 0) / data.length), + }; +} + +/** + * Uses an array of progressively less specific search functions to return none, one, or multiple matches. + */ +export function search(...filters: Array<(x:T, query:string) => boolean>): (options: T[], query:string | undefined) => T | T[] | null { + return function(options, query){ + if(!query) return options; + for(const filter of filters){ + const result = options.filter(x => filter(x, query)); + if(result.length == 1) return result[0]; + else if(result.length > 1) return result; + } + return null; + }; +} +export function searchFixed(options:T[] | (() => T[]), filters: Array<(x:T, query:string) => boolean>, recomputeOptions?: "recomputeOptions"): (query:string | undefined) => T | T[] | null { + const func = search(...filters); + let _options = options; + return query => { + if(typeof _options == "function"){ + if(recomputeOptions) return func(_options(), query); + else return func(_options = _options(), query); + } else return func(_options, query); + }; +} +export function delay(millis:number):Promise { + return new Promise(res => Timer.schedule(res, millis / 1000)); +} + +export const Duration = { + seconds: x => x * 1000, + minutes: x => x * 60_000, + hours: x => x * 3600_000, + days: x => x * 86400_000, + months: x => x * 2592000_000, +} satisfies Record number>; +export const DurationSecs = { + minutes: x => x * 60, + hours: x => x * 3600, + days: x => x * 86400, + months: x => x * 2592000, +} satisfies Record number>; diff --git a/src/globals.ts b/src/globals.ts index 694cdcd1..c3e4760f 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -1,55 +1,55 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains mutable global variables, and global constants. -*/ - -import { EventEmitter } from "/funcs"; -import { FishPlayer } from "/players"; -import { Label } from "/types"; - -export const tileHistory:Record = {}; -export const recentWhispers:Record = {}; -export const fishState = { - restartQueued: false, - restartLoopTask: null as null | TimerTask, - corruption_t1: null as null | TimerTask, - corruption_t2: null as null | TimerTask, - lastPranked: Date.now(), - labels: [] as Label[], - peacefulMode: false, - joinBell: false, - startTime: Date.now(), -}; -export const fishPlugin = { - directory: null as null | string, - version: null as null | string, -}; -export const ipJoins = new ObjectIntMap(); - -export const uuidPattern = /^[a-zA-Z0-9+/]{22}==$/; -export const ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; -export const ipPortPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$/; -export const ipRangeCIDRPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(1[2-9]|2[0-4])$/; //Disallow anything bigger than a /12 -export const ipRangeWildcardPattern = /^(\d{1,3}\.\d{1,3})\.(?:(\d{1,3}\.\*)|\*)$/; //Disallow anything bigger than a /16 -export const maxTime = 9999999999999; -export const unitsT5 = [UnitTypes.reign, UnitTypes.toxopid, UnitTypes.corvus, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.omura, UnitTypes.navanax, UnitTypes.conquer, UnitTypes.collaris, UnitTypes.disrupt]; - -export const FishEvents = new EventEmitter<{ - /** Fired after a team change. The current team is player.team() */ - playerTeamChange: [player:FishPlayer, previous:Team]; - /** Use this event to load data from Core.settings */ - loadData: []; - /** Use this event to save data to Core.settings */ - saveData: []; - /** Use this event to mutate things after all the data is loaded */ - dataLoaded: []; - commandUnauthorized: [player: FishPlayer, name: string]; - scriptKiddie: [player: FishPlayer]; - memoryCorruption: []; - /** Called when the "say" console command is run. */ - serverSays: []; - /** Called when map data is updated */ - saveMaps: []; - /** Fired on gameover, but before player data is reset. */ - gameOver: [winningTeam: Team]; -}>(); +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains mutable global variables, and global constants. +*/ + +import { EventEmitter } from "/funcs"; +import { FishPlayer } from "/players"; +import { Label } from "/types"; + +export const tileHistory:Record = {}; +export const recentWhispers:Record = {}; +export const fishState = { + restartQueued: false, + restartLoopTask: null as null | TimerTask, + corruption_t1: null as null | TimerTask, + corruption_t2: null as null | TimerTask, + lastPranked: Date.now(), + labels: [] as Label[], + peacefulMode: false, + joinBell: false, + startTime: Date.now(), +}; +export const fishPlugin = { + directory: null as null | string, + version: null as null | string, +}; +export const ipJoins = new ObjectIntMap(); + +export const uuidPattern = /^[a-zA-Z0-9+/]{22}==$/; +export const ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; +export const ipPortPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$/; +export const ipRangeCIDRPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(1[2-9]|2[0-4])$/; //Disallow anything bigger than a /12 +export const ipRangeWildcardPattern = /^(\d{1,3}\.\d{1,3})\.(?:(\d{1,3}\.\*)|\*)$/; //Disallow anything bigger than a /16 +export const maxTime = 9999999999999; +export const unitsT5 = [UnitTypes.reign, UnitTypes.toxopid, UnitTypes.corvus, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.omura, UnitTypes.navanax, UnitTypes.conquer, UnitTypes.collaris, UnitTypes.disrupt]; + +export const FishEvents = new EventEmitter<{ + /** Fired after a team change. The current team is player.team() */ + playerTeamChange: [player:FishPlayer, previous:Team]; + /** Use this event to load data from Core.settings */ + loadData: []; + /** Use this event to save data to Core.settings */ + saveData: []; + /** Use this event to mutate things after all the data is loaded */ + dataLoaded: []; + commandUnauthorized: [player: FishPlayer, name: string]; + scriptKiddie: [player: FishPlayer]; + memoryCorruption: []; + /** Called when the "say" console command is run. */ + serverSays: []; + /** Called when map data is updated */ + saveMaps: []; + /** Fired on gameover, but before player data is reset. */ + gameOver: [winningTeam: Team]; +}>(); diff --git a/src/index.ts b/src/index.ts index 8f8fe5f1..21ee9c9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,297 +1,297 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the main code, which calls other functions and initializes the plugin. -*/ - -import * as api from "/api"; -import { registerAll } from "/commands/aggregate"; -import { text } from "/config"; -import { handleTapEvent } from "/frameworks/commands"; -import * as menus from "/frameworks/menus"; -import { Duration } from "/funcs"; -import { FishEvents, fishPlugin, fishState, ipJoins, tileHistory } from "/globals"; -import { PartialMapRun } from "/maps"; -import { loadPacketHandlers } from "/packetHandlers"; -import { FishPlayer } from "/players"; -import * as timers from "/timers"; -import { addToTileHistory, fishCommandsRootDirPath, formatTimeRelative, matchFilter, processChat, restartNow, serverRestartLoop, tilelogAndResetAfk } from "/utils"; - - -Events.on(EventType.ConnectionEvent, (e) => { - if(Vars.netServer.admins.bannedIPs.contains(e.connection.address)){ - api.getBanned({ - ip: e.connection.address, - }, (banned) => { - if(!banned){ - //If they were previously banned locally, but the API says they aren't banned, then unban them and clear the kick that the outer function already did - Vars.netServer.admins.unbanPlayerIP(e.connection.address); - Vars.netServer.admins.kickedIPs.remove(e.connection.address); - } - }); - } else if(api.isVpnCached(e.connection.address) && FishPlayer.shouldWhackFlaggedPlayers()){ - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kick("You have been DOSblacklisted. Please join our discord for help: " + text.discordURL + "\nYou won't see this message again."); - Log.info(`&yAntibot killed connection ${e.connection.address} due to flagged while under attack`); - } -}); -Events.on(EventType.PlayerConnect, (e) => { - if(FishPlayer.shouldKickNewPlayers() && e.player.info.timesJoined == 1){ - //do not use the helper function, for maximum performance - e.player.kick(Packets.KickReason.kick, 3600_000); - } - FishPlayer.onPlayerConnect(e.player); -}); -Events.on(EventType.PlayerJoin, (e) => { - FishPlayer.onPlayerJoin(e.player); -}); -Events.on(EventType.PlayerLeave, (e) => { - FishPlayer.onPlayerLeave(e.player); -}); -Events.on(EventType.ConnectPacketEvent, (e: { packet: ConnectPacket; connection: NetConnection }) => { - if(!FishPlayer.connectRate.allow(5_000, 35)){ - FishPlayer.triggerAntibot(300_000, "Rate of player connections exceeded 35 / 5s", "automatic"); - } - ipJoins.increment(e.connection.address); - const info = Vars.netServer.admins.getInfoOptional(e.packet.uuid); - const underAttack = FishPlayer.antiBotMode(); - const newPlayer = !info || info.timesJoined < 10; - const longModName = e.packet.mods.contains((str:string) => str.length > 50); - const veryLongModName = e.packet.mods.contains((str:string) => str.length > 100); - if( - (underAttack && e.packet.mods.size > 2) || - (underAttack && longModName) || - (veryLongModName && (underAttack || newPlayer)) - ){ - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - FishPlayer.triggerAntibot( - 60_000, - (veryLongModName ? "very long mod name" : longModName ? "long mod name" : "it had mods while under attack"), - "automatic" - ); - return; - } - const suspiciousModName = e.packet.mods.contains((str:string) => str.includes('\x1B')); - if(suspiciousModName || e.packet.name.includes('\x1B')){ - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - FishPlayer.triggerAntibot( - 5_000, - "illegal characters in name or mods", - "automatic" - ); - return; - } - if(ipJoins.get(e.connection.address) >= ( (underAttack || veryLongModName) ? 3 : (newPlayer || longModName) ? 7 : 15 )){ - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - FishPlayer.triggerAntibot( - 5_000, - "too many connections", - "automatic" - ); - return; - } - /*if(e.packet.name.includes("discord.gg/GnEdS9TdV6")){ - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - FishPlayer.onBotWhack(); - Log.info(`&yAntibot killed connection ${e.connection.address} due to omni discord link`); - return; - }*/ - if(e.packet.name.includes("1`1@everyone")){ - Vars.netServer.admins.blacklistDos(e.connection.address); - e.connection.kicked = true; - FishPlayer.triggerAntibot(-1, "known bad name", "automatic"); - return; - } - if(Vars.netServer.admins.isDosBlacklisted(e.connection.address)){ - //threading moment, i think - e.connection.kicked = true; - return; - } - api.getBanned({ - ip: e.connection.address, - uuid: e.packet.uuid - }, (banned) => { - if(banned){ - Log.info(`&lrSynced ban of ${e.packet.uuid}/${e.connection.address}.`); - e.connection.kick(Packets.KickReason.banned, 1); - Vars.netServer.admins.banPlayerIP(e.connection.address); - Vars.netServer.admins.banPlayerID(e.packet.uuid); - } else { - Vars.netServer.admins.unbanPlayerIP(e.connection.address); - Vars.netServer.admins.unbanPlayerID(e.packet.uuid); - } - }); - FishPlayer.onConnectPacket(e.packet); -}); -Events.on(EventType.UnitChangeEvent, (e) => { - FishPlayer.onUnitChange(e.player, e.unit); -}); -Events.on(EventType.ContentInitEvent, () => { - //Unhide latum and renale - UnitTypes.latum.hidden = false; - UnitTypes.renale.hidden = false; -}); -Events.on(EventType.PlayerChatEvent, (e) => processChat(e.player, e.message, true)); - -Events.on(EventType.ServerLoadEvent, (e) => { - Time.mark(); - const clientHandler = Vars.netServer.clientCommands; - const serverHandler = ServerControl.instance.handler; - - FishPlayer.loadAll(); - FishEvents.fire("loadData", []); - timers.initializeTimers(); - menus.registerListeners(); - - //Cap delta - Time.setDeltaProvider(() => Math.min(Core.graphics.getDeltaTime() * 60, 10)); - - // Mute muted players - Vars.netServer.admins.addChatFilter((player, message) => processChat(player, message)); - // Vars.netServer.admins.addChatFilter((p, message) => FishPlayer.get(p).hasPerm("member") ? message : foolifyChat(message)); - // Action filters - Vars.netServer.admins.addActionFilter((action:PlayerAction) => { - const player = action.player; - const fishP = FishPlayer.get(player); - - //prevent stopped players from doing anything other than deposit items. - if(!fishP.hasPerm("play")){ - action.player.sendMessage('[scarlet]\u26A0 [yellow]You are stopped, you cant perfom this action.'); - return false; - } else { - if(action.type === Administration.ActionType.pickupBlock){ - addToTileHistory({ - pos: `${action.tile!.x},${action.tile!.y}`, - uuid: action.player.uuid(), - action: "picked up", - type: action.tile!.block()?.name ?? "nothing", - }); - } else if(action.type === Administration.ActionType.control && !action.unit?.spawnedByCore && Date.now() < fishP.blockedFromPossessingUnitsUntil){ - action.player.sendMessage(`[scarlet]\u26A0 [yellow]You are blocked from controlling units for ${formatTimeRelative(fishP.blockedFromPossessingUnitsUntil, true)}`); - return false; - } else if(action.type === Administration.ActionType.commandUnits && Date.now() < fishP.blockedFromCommandingUnitsUntil){ - action.player.sendMessage(`[scarlet]\u26A0 [yellow]You are blocked from commanding units for ${formatTimeRelative(fishP.blockedFromCommandingUnitsUntil, true)}`); - return false; - } else if(action.type === Administration.ActionType.pingLocation && action.pingText && action.pingText.length < Vars.maxPingTextLength){ - const fishP = FishPlayer.get(action.player); - if(fishP.muted){ - action.player.sendMessage(`[scarlet]\u26A0 [yellow]You are muted, you cannot send text through location pings.`); - return false; - } else if(matchFilter(action.pingText, "chat", false)){ - //Allow it, but replace - player.pingX = action.pingX; - player.pingY = action.pingY; - player.pingTime = 1; - player.pingText = text.chatFilterReplacement.messageShort(); - return false; - } - } - return true; - } - }); - - registerAll(clientHandler, serverHandler); - loadPacketHandlers(); - - //Load plugin data - try { - const path = fishCommandsRootDirPath(); - fishPlugin.directory = path.toString(); - Threads.daemon(() => { - try { - fishPlugin.version = OS.exec("git", "-C", fishPlugin.directory!, "rev-parse", "HEAD"); - } catch {} - }); - } catch(err){ - Log.err("Failed to get fish plugin information."); - Log.err(err); - } - - Runtime.getRuntime().addShutdownHook(new Thread(() => { - try { - FishPlayer.uploadAll(); - } catch { Log.err("failed to upload"); } - try { - FishEvents.fire("saveData", []); - } catch { Log.err("failed to save misc data"); } - try { - FishPlayer.saveAll(false); - } catch { Log.err("failed to save player data"); } - Log.info("Saved on exit."); - })); - - Vars.netServer.assigner = (player, players) => { - if(Vars.state.rules.pvp){ - //find team with minimum amount of players and auto-assign player to that. - const fishP = FishPlayer.get(player); - let preferredTeam: Team | null = null; - if(fishP.restoreTeam && (Date.now() - fishP.restoreTeam[1] < Duration.minutes(5)) && fishP.restoreTeam[2] == PartialMapRun.current?.startTime) - preferredTeam = fishP.restoreTeam[0]; - const re = Vars.state.teams.getActive().select(data => !( - (Vars.state.rules.waveTeam == data.team && Vars.state.rules.waves) || - !data.hasCore() || - data.team == Team.derelict || - !data.team.rules().protectCores - )).min(floatf(data => { - //Only if the team is valid - if(data.team == preferredTeam) return -1; - let count = 0; - players.forEach(other => { - if(other.team() == data.team && other != player){ - count ++; - } - }); - return count + Mathf.random(-0.1, 0.1); - })); - return re == null ? Vars.state.rules.defaultTeam : re.team; - } else { - return Vars.state.rules.defaultTeam; - } - }; - - Log.info("fish-commands: initialized in @ms (incl previous)", Time.elapsed()); -}); - -// Keeps track of any action performed on a tile for use in tilelog. - -Events.on(EventType.BlockBuildBeginEvent, tilelogAndResetAfk) -Events.on(EventType.BuildRotateEvent, tilelogAndResetAfk); -Events.on(EventType.ConfigEvent, tilelogAndResetAfk); -Events.on(EventType.PickupEvent, tilelogAndResetAfk); -Events.on(EventType.PayloadDropEvent, tilelogAndResetAfk); -Events.on(EventType.UnitDestroyEvent, addToTileHistory); -Events.on(EventType.BlockDestroyEvent, addToTileHistory); -Events.on(EventType.UnitControlEvent, tilelogAndResetAfk); - - -Events.on(EventType.TapEvent, handleTapEvent); - -Events.on(EventType.GameOverEvent, (e) => { - for(const key of Object.keys(tileHistory)){ - //clear tilelog - tileHistory[key] = null!; - delete tileHistory[key]; - } - if(fishState.restartQueued){ - //restart - Call.sendMessage(`[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 15 seconds.[]\n[accent]---[[[coral]+++[]]---`); - serverRestartLoop(12, true); - Events.on(EventType.WorldLoadBeginEvent, () => { - //Remove save - restartNow(true); - }); - } - FishPlayer.onGameOver(e.winner as Team); -}); -Events.on(EventType.WorldLoadEvent, () => FishPlayer.onGameBegin()); -Events.on(EventType.PlayerChatEvent, e => { - FishPlayer.onPlayerChat(e.player, e.message); -}); -Events.on(EventType.PlayEvent, () => { - fishState.startTime = Date.now(); -}); - -Log.info("fish-commands: parsing done in @ms", Date.now() - (this as any)._startTime); +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the main code, which calls other functions and initializes the plugin. +*/ + +import * as api from "/api"; +import { registerAll } from "/commands/aggregate"; +import { text } from "/config"; +import { handleTapEvent } from "/frameworks/commands"; +import * as menus from "/frameworks/menus"; +import { Duration } from "/funcs"; +import { FishEvents, fishPlugin, fishState, ipJoins, tileHistory } from "/globals"; +import { PartialMapRun } from "/maps"; +import { loadPacketHandlers } from "/packetHandlers"; +import { FishPlayer } from "/players"; +import * as timers from "/timers"; +import { addToTileHistory, fishCommandsRootDirPath, formatTimeRelative, matchFilter, processChat, restartNow, serverRestartLoop, tilelogAndResetAfk } from "/utils"; + + +Events.on(EventType.ConnectionEvent, (e) => { + if(Vars.netServer.admins.bannedIPs.contains(e.connection.address)){ + api.getBanned({ + ip: e.connection.address, + }, (banned) => { + if(!banned){ + //If they were previously banned locally, but the API says they aren't banned, then unban them and clear the kick that the outer function already did + Vars.netServer.admins.unbanPlayerIP(e.connection.address); + Vars.netServer.admins.kickedIPs.remove(e.connection.address); + } + }); + } else if(api.isVpnCached(e.connection.address) && FishPlayer.shouldWhackFlaggedPlayers()){ + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kick("You have been DOSblacklisted. Please join our discord for help: " + text.discordURL + "\nYou won't see this message again."); + Log.info(`&yAntibot killed connection ${e.connection.address} due to flagged while under attack`); + } +}); +Events.on(EventType.PlayerConnect, (e) => { + if(FishPlayer.shouldKickNewPlayers() && e.player.info.timesJoined == 1){ + //do not use the helper function, for maximum performance + e.player.kick(Packets.KickReason.kick, 3600_000); + } + FishPlayer.onPlayerConnect(e.player); +}); +Events.on(EventType.PlayerJoin, (e) => { + FishPlayer.onPlayerJoin(e.player); +}); +Events.on(EventType.PlayerLeave, (e) => { + FishPlayer.onPlayerLeave(e.player); +}); +Events.on(EventType.ConnectPacketEvent, (e: { packet: ConnectPacket; connection: NetConnection }) => { + if(!FishPlayer.connectRate.allow(5_000, 35)){ + FishPlayer.triggerAntibot(300_000, "Rate of player connections exceeded 35 / 5s", "automatic"); + } + ipJoins.increment(e.connection.address); + const info = Vars.netServer.admins.getInfoOptional(e.packet.uuid); + const underAttack = FishPlayer.antiBotMode(); + const newPlayer = !info || info.timesJoined < 10; + const longModName = e.packet.mods.contains((str:string) => str.length > 50); + const veryLongModName = e.packet.mods.contains((str:string) => str.length > 100); + if( + (underAttack && e.packet.mods.size > 2) || + (underAttack && longModName) || + (veryLongModName && (underAttack || newPlayer)) + ){ + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + FishPlayer.triggerAntibot( + 60_000, + (veryLongModName ? "very long mod name" : longModName ? "long mod name" : "it had mods while under attack"), + "automatic" + ); + return; + } + const suspiciousModName = e.packet.mods.contains((str:string) => str.includes('\x1B')); + if(suspiciousModName || e.packet.name.includes('\x1B')){ + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + FishPlayer.triggerAntibot( + 5_000, + "illegal characters in name or mods", + "automatic" + ); + return; + } + if(ipJoins.get(e.connection.address) >= ( (underAttack || veryLongModName) ? 3 : (newPlayer || longModName) ? 7 : 15 )){ + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + FishPlayer.triggerAntibot( + 5_000, + "too many connections", + "automatic" + ); + return; + } + /*if(e.packet.name.includes("discord.gg/GnEdS9TdV6")){ + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + FishPlayer.onBotWhack(); + Log.info(`&yAntibot killed connection ${e.connection.address} due to omni discord link`); + return; + }*/ + if(e.packet.name.includes("1`1@everyone")){ + Vars.netServer.admins.blacklistDos(e.connection.address); + e.connection.kicked = true; + FishPlayer.triggerAntibot(-1, "known bad name", "automatic"); + return; + } + if(Vars.netServer.admins.isDosBlacklisted(e.connection.address)){ + //threading moment, i think + e.connection.kicked = true; + return; + } + api.getBanned({ + ip: e.connection.address, + uuid: e.packet.uuid + }, (banned) => { + if(banned){ + Log.info(`&lrSynced ban of ${e.packet.uuid}/${e.connection.address}.`); + e.connection.kick(Packets.KickReason.banned, 1); + Vars.netServer.admins.banPlayerIP(e.connection.address); + Vars.netServer.admins.banPlayerID(e.packet.uuid); + } else { + Vars.netServer.admins.unbanPlayerIP(e.connection.address); + Vars.netServer.admins.unbanPlayerID(e.packet.uuid); + } + }); + FishPlayer.onConnectPacket(e.packet); +}); +Events.on(EventType.UnitChangeEvent, (e) => { + FishPlayer.onUnitChange(e.player, e.unit); +}); +Events.on(EventType.ContentInitEvent, () => { + //Unhide latum and renale + UnitTypes.latum.hidden = false; + UnitTypes.renale.hidden = false; +}); +Events.on(EventType.PlayerChatEvent, (e) => processChat(e.player, e.message, true)); + +Events.on(EventType.ServerLoadEvent, (e) => { + Time.mark(); + const clientHandler = Vars.netServer.clientCommands; + const serverHandler = ServerControl.instance.handler; + + FishPlayer.loadAll(); + FishEvents.fire("loadData", []); + timers.initializeTimers(); + menus.registerListeners(); + + //Cap delta + Time.setDeltaProvider(() => Math.min(Core.graphics.getDeltaTime() * 60, 10)); + + // Mute muted players + Vars.netServer.admins.addChatFilter((player, message) => processChat(player, message)); + // Vars.netServer.admins.addChatFilter((p, message) => FishPlayer.get(p).hasPerm("member") ? message : foolifyChat(message)); + // Action filters + Vars.netServer.admins.addActionFilter((action:PlayerAction) => { + const player = action.player; + const fishP = FishPlayer.get(player); + + //prevent stopped players from doing anything other than deposit items. + if(!fishP.hasPerm("play")){ + action.player.sendMessage('[scarlet]\u26A0 [yellow]You are stopped, you cant perfom this action.'); + return false; + } else { + if(action.type === Administration.ActionType.pickupBlock){ + addToTileHistory({ + pos: `${action.tile!.x},${action.tile!.y}`, + uuid: action.player.uuid(), + action: "picked up", + type: action.tile!.block()?.name ?? "nothing", + }); + } else if(action.type === Administration.ActionType.control && !action.unit?.spawnedByCore && Date.now() < fishP.blockedFromPossessingUnitsUntil){ + action.player.sendMessage(`[scarlet]\u26A0 [yellow]You are blocked from controlling units for ${formatTimeRelative(fishP.blockedFromPossessingUnitsUntil, true)}`); + return false; + } else if(action.type === Administration.ActionType.commandUnits && Date.now() < fishP.blockedFromCommandingUnitsUntil){ + action.player.sendMessage(`[scarlet]\u26A0 [yellow]You are blocked from commanding units for ${formatTimeRelative(fishP.blockedFromCommandingUnitsUntil, true)}`); + return false; + } else if(action.type === Administration.ActionType.pingLocation && action.pingText && action.pingText.length < Vars.maxPingTextLength){ + const fishP = FishPlayer.get(action.player); + if(fishP.muted){ + action.player.sendMessage(`[scarlet]\u26A0 [yellow]You are muted, you cannot send text through location pings.`); + return false; + } else if(matchFilter(action.pingText, "chat", false)){ + //Allow it, but replace + player.pingX = action.pingX; + player.pingY = action.pingY; + player.pingTime = 1; + player.pingText = text.chatFilterReplacement.messageShort(); + return false; + } + } + return true; + } + }); + + registerAll(clientHandler, serverHandler); + loadPacketHandlers(); + + //Load plugin data + try { + const path = fishCommandsRootDirPath(); + fishPlugin.directory = path.toString(); + Threads.daemon(() => { + try { + fishPlugin.version = OS.exec("git", "-C", fishPlugin.directory!, "rev-parse", "HEAD"); + } catch {} + }); + } catch(err){ + Log.err("Failed to get fish plugin information."); + Log.err(err); + } + + Runtime.getRuntime().addShutdownHook(new Thread(() => { + try { + FishPlayer.uploadAll(); + } catch { Log.err("failed to upload"); } + try { + FishEvents.fire("saveData", []); + } catch { Log.err("failed to save misc data"); } + try { + FishPlayer.saveAll(false); + } catch { Log.err("failed to save player data"); } + Log.info("Saved on exit."); + })); + + Vars.netServer.assigner = (player, players) => { + if(Vars.state.rules.pvp){ + //find team with minimum amount of players and auto-assign player to that. + const fishP = FishPlayer.get(player); + let preferredTeam: Team | null = null; + if(fishP.restoreTeam && (Date.now() - fishP.restoreTeam[1] < Duration.minutes(5)) && fishP.restoreTeam[2] == PartialMapRun.current?.startTime) + preferredTeam = fishP.restoreTeam[0]; + const re = Vars.state.teams.getActive().select(data => !( + (Vars.state.rules.waveTeam == data.team && Vars.state.rules.waves) || + !data.hasCore() || + data.team == Team.derelict || + !data.team.rules().protectCores + )).min(floatf(data => { + //Only if the team is valid + if(data.team == preferredTeam) return -1; + let count = 0; + players.forEach(other => { + if(other.team() == data.team && other != player){ + count ++; + } + }); + return count + Mathf.random(-0.1, 0.1); + })); + return re == null ? Vars.state.rules.defaultTeam : re.team; + } else { + return Vars.state.rules.defaultTeam; + } + }; + + Log.info("fish-commands: initialized in @ms (incl previous)", Time.elapsed()); +}); + +// Keeps track of any action performed on a tile for use in tilelog. + +Events.on(EventType.BlockBuildBeginEvent, tilelogAndResetAfk) +Events.on(EventType.BuildRotateEvent, tilelogAndResetAfk); +Events.on(EventType.ConfigEvent, tilelogAndResetAfk); +Events.on(EventType.PickupEvent, tilelogAndResetAfk); +Events.on(EventType.PayloadDropEvent, tilelogAndResetAfk); +Events.on(EventType.UnitDestroyEvent, addToTileHistory); +Events.on(EventType.BlockDestroyEvent, addToTileHistory); +Events.on(EventType.UnitControlEvent, tilelogAndResetAfk); + + +Events.on(EventType.TapEvent, handleTapEvent); + +Events.on(EventType.GameOverEvent, (e) => { + for(const key of Object.keys(tileHistory)){ + //clear tilelog + tileHistory[key] = null!; + delete tileHistory[key]; + } + if(fishState.restartQueued){ + //restart + Call.sendMessage(`[accent]---[[[coral]+++[]]---\n[accent]Server restart imminent. [green]We'll be back after 15 seconds.[]\n[accent]---[[[coral]+++[]]---`); + serverRestartLoop(12, true); + Events.on(EventType.WorldLoadBeginEvent, () => { + //Remove save + restartNow(true); + }); + } + FishPlayer.onGameOver(e.winner as Team); +}); +Events.on(EventType.WorldLoadEvent, () => FishPlayer.onGameBegin()); +Events.on(EventType.PlayerChatEvent, e => { + FishPlayer.onPlayerChat(e.player, e.message); +}); +Events.on(EventType.PlayEvent, () => { + fishState.startTime = Date.now(); +}); + +Log.info("fish-commands: parsing done in @ms", Date.now() - (this as any)._startTime); diff --git a/src/main.js b/src/main.js index aca832dc..e9422c7e 100644 --- a/src/main.js +++ b/src/main.js @@ -1,76 +1,76 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This is a special file which is automatically loaded by the game server. -It only contains polyfills, and requires index.js. -*/ -//WARNING: changes to this file must be manually copied to /build/scripts/main.js - -this._startTime = Date.now(); -Log.info("fish-commands: loading"); - -importPackage(Packages.arc); -importClass(Packages.arc.util.CommandHandler); -importPackage(Packages.mindustry.type); -importClass(Packages.mindustry.server.ServerControl); -importClass(Packages.java.lang.Thread); -importClass(Packages.java.lang.Runtime); -importClass(Packages.java.lang.ProcessBuilder); -importClass(Packages.java.nio.file.Paths); -importClass(Packages.java.io.ByteArrayOutputStream); -importClass(Packages.java.io.DataOutputStream); -importClass(Packages.java.io.ByteArrayInputStream); -importClass(Packages.java.io.DataInputStream); -importPackage(Packages.java.util.concurrent.atomic); -importPackage(Packages.java.util.regex); - -//Polyfills -Object.entries = o => Object.keys(o).map(k => [k, o[k]]); -Object.values = o => Object.keys(o).map(k => o[k]); -Object.fromEntries = a => a.reduce((o, [k, v]) => { o[k] = v; return o; }, {}); -//Arrow functions do not bind to "this" -Array.prototype.at = function(i){ - return this[i < 0 ? this.length + i : i]; -}; -String.prototype.at = function(i){ - return this[i < 0 ? this.length + i : i]; -}; -Array.prototype.flat = function(depth){ - depth = (depth == undefined) ? 1 : depth; - return depth > 0 ? this.reduce((acc, item) => - acc.concat(Array.isArray(item) ? item.flat(depth - 1) : item) - , []) : this; -}; -String.raw = function(callSite){ - const substitutions = Array.prototype.slice.call(arguments, 1); - return Array.from(callSite.raw).map((chunk, i) => { - if (callSite.raw.length <= i) { - return chunk; - } - return substitutions[i - 1] ? substitutions[i - 1] + chunk : chunk; - }).join(''); -}; -const Arrayfrom = Array.from; -Array.from = function(iterable, mapfn){ - if(mapfn) throw new Error(`Array.from does not work with mapfn due to incorrectly generating sparse arrays. Please use Array(length).fill().map() instead.`); - return Arrayfrom(iterable); -}; -//Fix rhino regex -if(/ae?a/.test("aeea")){ - RegExp.prototype.test = function(input){ - //overwrite with java regex - return java.util.regex.Pattern.compile(this.source).matcher(input).find(); - }; -} -//Fix rhino Number.prototype.toFixed -if(12.34.toFixed(1) !== '12.3'){ - const toFixed = Number.prototype.toFixed; - Number.prototype.toFixed = function(fractionDigits){ - const floorLog = Math.floor(Math.log10(this)); - const output = toFixed.call(this, Math.max(floorLog, -1) + 1 + fractionDigits); - return output.toString().slice(0, Math.max(floorLog, 0) + 2 + fractionDigits); - }; -} - -this.ArcReflect = Reflect; -this.Promise = require('/promise').Promise; -require("index"); +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This is a special file which is automatically loaded by the game server. +It only contains polyfills, and requires index.js. +*/ +//WARNING: changes to this file must be manually copied to /build/scripts/main.js + +this._startTime = Date.now(); +Log.info("fish-commands: loading"); + +importPackage(Packages.arc); +importClass(Packages.arc.util.CommandHandler); +importPackage(Packages.mindustry.type); +importClass(Packages.mindustry.server.ServerControl); +importClass(Packages.java.lang.Thread); +importClass(Packages.java.lang.Runtime); +importClass(Packages.java.lang.ProcessBuilder); +importClass(Packages.java.nio.file.Paths); +importClass(Packages.java.io.ByteArrayOutputStream); +importClass(Packages.java.io.DataOutputStream); +importClass(Packages.java.io.ByteArrayInputStream); +importClass(Packages.java.io.DataInputStream); +importPackage(Packages.java.util.concurrent.atomic); +importPackage(Packages.java.util.regex); + +//Polyfills +Object.entries = o => Object.keys(o).map(k => [k, o[k]]); +Object.values = o => Object.keys(o).map(k => o[k]); +Object.fromEntries = a => a.reduce((o, [k, v]) => { o[k] = v; return o; }, {}); +//Arrow functions do not bind to "this" +Array.prototype.at = function(i){ + return this[i < 0 ? this.length + i : i]; +}; +String.prototype.at = function(i){ + return this[i < 0 ? this.length + i : i]; +}; +Array.prototype.flat = function(depth){ + depth = (depth == undefined) ? 1 : depth; + return depth > 0 ? this.reduce((acc, item) => + acc.concat(Array.isArray(item) ? item.flat(depth - 1) : item) + , []) : this; +}; +String.raw = function(callSite){ + const substitutions = Array.prototype.slice.call(arguments, 1); + return Array.from(callSite.raw).map((chunk, i) => { + if (callSite.raw.length <= i) { + return chunk; + } + return substitutions[i - 1] ? substitutions[i - 1] + chunk : chunk; + }).join(''); +}; +const Arrayfrom = Array.from; +Array.from = function(iterable, mapfn){ + if(mapfn) throw new Error(`Array.from does not work with mapfn due to incorrectly generating sparse arrays. Please use Array(length).fill().map() instead.`); + return Arrayfrom(iterable); +}; +//Fix rhino regex +if(/ae?a/.test("aeea")){ + RegExp.prototype.test = function(input){ + //overwrite with java regex + return java.util.regex.Pattern.compile(this.source).matcher(input).find(); + }; +} +//Fix rhino Number.prototype.toFixed +if(12.34.toFixed(1) !== '12.3'){ + const toFixed = Number.prototype.toFixed; + Number.prototype.toFixed = function(fractionDigits){ + const floorLog = Math.floor(Math.log10(this)); + const output = toFixed.call(this, Math.max(floorLog, -1) + 1 + fractionDigits); + return output.toString().slice(0, Math.max(floorLog, 0) + 2 + fractionDigits); + }; +} + +this.ArcReflect = Reflect; +this.Promise = require('/promise').Promise; +require("index"); diff --git a/src/maps.ts b/src/maps.ts index 6d35fb67..9d7a5301 100644 --- a/src/maps.ts +++ b/src/maps.ts @@ -1,302 +1,302 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the map run tracker and statistics computation. -*/ - -import { Gamemode } from "/config"; -import { FFunction } from "/frameworks/commands"; -import { dataClass, serialize } from "/frameworks/io"; -import { computeStatistics, Duration } from "/funcs"; -import { FishEvents } from "/globals"; -import { formatTime, formatTimeShort, formatTimestamp, match } from "/utils"; - -type FinishedMapRunData = { - winTeam:Team; - success:boolean; //winTeam == Vars.state.rules.defaultTeam - startTime:number; - endTime:number; - maxPlayerCount:number; - wave:number; -} -export class FinishedMapRun extends dataClass() { - //this constructor is useless, but rhino crashes with a bizarre error when trying to run the emitted code - //do not remove this useless constructor - constructor(data:FinishedMapRunData){ - super(data); - } - duration(){ - return this.endTime - this.startTime; - } - outcome(){ - if(Gamemode.pvp()){ - if(this.winTeam === Team.derelict) { - if(this.duration() > Duration.minutes(20)) return ["rtv", "late rtv"] as const; - else return ["rtv", "early rtv"] as const; - } else return ["win", "win"] as const; - } else { - if(this.success) return ["win", "win"] as const; - else if(this.winTeam === Team.derelict){ - if(this.duration() > Duration.minutes(3)) return ["loss", "late rtv"] as const; - else return ["rtv", "early rtv"] as const; - } else return ["loss", "loss"] as const; - } - } -} - -export class PartialMapRun { - static readonly key = "fish-partial-map-run"; - static current: PartialMapRun | null = null; - static { - FishEvents.on("saveData", () => { - if(this.current) Core.settings.put(this.key, this.current.write()); - }); - FishEvents.on("loadData", () => { - const data = Core.settings.getString(this.key); - if(data){ - this.current = this.read(data); - } else { - //loading a map, but there is no run information, create one - this.current = new this(); - } - }); - Events.on(EventType.SaveLoadEvent, e => { - //create a new run, if there isn't one already - //loadData will have run first if it is a server restart - this.current ??= new this(); - }); - Timer.schedule(() => { - this.current?.update(); - }, 0, 5); - Events.on(EventType.GameOverEvent, e => { - if(this.current){ - const finishedRun = this.current.finish({ winTeam: e.winner ?? Team.derelict }); - const fmap = FMap.getCreate(Vars.state.map); - if(!fmap) return; - - //Highscore message - if(Gamemode.attack() && finishedRun.success){ - const bestPreviousTime = fmap.stats().shortestWinTime; - const duration = finishedRun.duration(); - Call.sendMessage( -`[orange]-------- -${finishedRun.success && duration < bestPreviousTime ? - `[green]New highscore! Map completed in [accent]${formatTimeShort(duration)}[]` -: `[orange]Map completed in [accent]${formatTimeShort(duration)}[]. Current highscore: [green]${formatTimeShort(bestPreviousTime)}[]`} -[orange]--------` - ); - } else if(Gamemode.survival()){ - const bestPreviousWave = fmap.stats().highestWave; - const wave = finishedRun.wave; - Call.sendMessage( -`[orange]-------- -${finishedRun.success && wave < bestPreviousWave ? - `[green]New highscore! Reached wave [accent]${wave}[].` -: `[orange]Reached wave [accent]${wave}[]. Current highscore: [green]${bestPreviousWave}[]`} -[orange]--------` - ); - } - fmap.runs.push(finishedRun); - FishEvents.fire("saveMaps", []); - } - Core.settings.remove(this.key); - this.current = null; - }); - } - - startTime:number = Date.now(); - maxPlayerCount:number = 0; - /** In milliseconds */ - duration(){ - return Date.now() - this.startTime; - } - update(){ - this.maxPlayerCount = Math.max(this.maxPlayerCount, Groups.player.size()); - } - finish({winTeam}:{ - winTeam: Team; - }):FinishedMapRun { - return new FinishedMapRun({ - winTeam, - success: Gamemode.pvp() ? true : winTeam == Vars.state.rules.defaultTeam, - startTime: this.startTime, - endTime: Date.now(), - maxPlayerCount: this.maxPlayerCount, - wave: Vars.state.wave, - }); - } - //Used for continuing through a restart - write():string { - return `${Date.now() - this.startTime}/${this.maxPlayerCount}`; - } - static read(data:string):PartialMapRun { - const [duration, maxPlayerCount] = data.split("/").map(Number); - if(isNaN(duration) || isNaN(maxPlayerCount)){ - Log.err(`_FINDTAG_ failed to load map run stats data: ${data}`); - } - const out = new PartialMapRun(); - out.startTime = Date.now() - duration; //move start time forward by time when the server was off - out.maxPlayerCount = maxPlayerCount; - return out; - } -} - - -type FMapData = { - runs: FinishedMapRun[]; - mapFileName: string; -}; -export class FMap extends dataClass() { - constructor( - data:FMapData, - //O(n^2)... should be fine? - public map:MMap | null = Vars.maps.customMaps().find(m => m.file.name() === data.mapFileName) - ){ super(data); } - - @serialize("fish-map-data", () => ["version", 1, ["array", "u16", ["class", FMap, [ - ["runs", ["array", "u32", ["class", FinishedMapRun, [ - ["startTime", ["number", "i64"]], - ["endTime", ["number", "i64"]], - ["maxPlayerCount", ["number", "u8"]], - ["success", ["boolean"]], - ["winTeam", ["team"]], - ["wave", ["number", "u16"]] - ]]]], - ["mapFileName", ["string"]], - ]]]], () => ["array", "u16", ["class", FMap, [ - ["runs", ["array", "u32", ["class", FinishedMapRun, [ - ["startTime", ["number", "i64"]], - ["endTime", ["number", "i64"]], - ["maxPlayerCount", ["number", "u8"]], - ["success", ["boolean"]], - ["winTeam", ["team"]], - ]]]], - ["mapFileName", ["string"]], - ]]], undefined, "saveMaps") - static allMaps:FMap[] | null = null; - private static maps:Record = {}; - static { - FishEvents.on("dataLoaded", () => { - //This event listener runs after the data has been loaded into allMaps - (FMap.allMaps ??= []).forEach(map => { - FMap.maps[map.mapFileName] = map; - map.runs.forEach(run => { - //this should not even happen, I think GameOverEvent is sending winTeam as null sometimes?? - run.winTeam ??= Team.derelict; - }); - }); - //create all the data - Vars.maps.customMaps().each(m => void FMap.getCreate(m)); - }); - } - - static getCreate(map:MMap){ - if(this.allMaps == null) return null; - const mapFileName = map.file.name(); - if(Object.prototype.hasOwnProperty.call(this.maps, mapFileName)) - return this.maps[mapFileName]; - const fmap = new this({ - runs: [], - mapFileName - }, map); - this.maps[mapFileName] = fmap; - this.allMaps.push(fmap); - return fmap; - } - - rules():Rules | undefined { - return this.map?.rules(); - } - - stats(){ - const runs = this.runs.filter(r => r.maxPlayerCount > 0); //Remove all runs with no players on - const allRunCount = runs.length; - const victories = runs.filter(r => r.outcome()[1] === "win"); - const losses = runs.filter(r => r.outcome()[0] === "loss").length; - const earlyRTVs = runs.filter(r => r.outcome()[1] === "early rtv").length; - const lateRTVs = runs.filter(r => r.outcome()[1] === "late rtv").length; - const significantRunCount = allRunCount - earlyRTVs; - const totalLosses = losses + lateRTVs; - const durations = runs.filter(r => r.outcome()[0] !== "rtv").map(r => r.duration()); - const durationStats = computeStatistics(durations); - const winDurationStats = computeStatistics(runs.filter(r => r.outcome()[0] === "win").map(r => r.duration())); - const teamWins = runs.filter(r => r.outcome()[1] !== "early rtv").reduce((acc, item) => { - acc[item.winTeam.name] = (acc[item.winTeam.name] ?? 0) + 1; - return acc; - }, {} as Record); - const teamWinRate = Object.fromEntries(Object.entries(teamWins).map(([team, wins]) => [team, wins / significantRunCount])); - //Remove runs that were on wave 0, due to a silly bug we have thousands of runs with a max wave of 0 - const waveStats = computeStatistics(runs.filter(r => r.outcome()[0] !== "rtv" && r.wave !== 0).map(r => r.wave)); - return { - allRunCount, - significantRunCount, - victories: victories.length, - losses, - totalLosses, - earlyRTVs, - lateRTVs, - earlyRTVRate: earlyRTVs / allRunCount, - winRate: victories.length / significantRunCount, - lossRate: losses / significantRunCount, - averagePlaytime: durationStats.average, - shortestWinTime: winDurationStats.lowest, - longestTime: durationStats.highest, - shortestTime: durationStats.lowest, - averageHighestPlayerCount: computeStatistics(runs.map(r => r.maxPlayerCount)).average, - teamWins, - teamWinRate, - highestWave: waveStats.highest, - averageWave: waveStats.average, - mostRecentWin: victories.at(-1)?.startTime - }; - } - displayStats(f:FFunction):string | null { - const map = this.map; if(!map) return null; - const stats = this.stats(); - const rules = this.rules()!; - - const modeSpecificStats = match(Gamemode.name(), { - attack: `\ -[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.victories} wins, ${stats.totalLosses} losses, ${stats.earlyRTVs} RTVs) -[#CCFFCC]Outcomes: ${f.percent(stats.winRate, 1)} wins, ${f.percent(stats.lossRate, 1)} losses, ${f.percent(stats.earlyRTVRate, 1)} RTVs -[#CCFFCC]Average playtime: ${formatTime(stats.averagePlaytime)} -[#CCFFCC]Shortest win time: ${formatTime(stats.shortestWinTime)} -[#CCFFCC]Most recent win: ${stats.mostRecentWin ? formatTimestamp(stats.mostRecentWin) : "[red]none[]"}`, - survival: `\ -[#CCFFCC]Highest wave reached: ${stats.highestWave} -[#CCFFCC]Average wave reached: ${stats.averageWave} -[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.earlyRTVs} RTVs) -[#CCFFCC]RTV rate: ${f.percent(stats.earlyRTVRate, 1)} -[#CCFFCC]Average duration: ${formatTime(stats.averagePlaytime)} -[#CCFFCC]Longest duration: ${formatTime(stats.longestTime)}`, - pvp: `\ -[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.earlyRTVs} RTVs) -[#CCFFCC]Team win rates: ${Object.entries(stats.teamWinRate).map(([team, rate]) => `${team} ${f.percent(rate, 1)}`).join(", ")} -[#CCFFCC]RTV rate: ${f.percent(stats.earlyRTVRate, 1)} -[#CCFFCC]Average match duration: ${formatTime(stats.averagePlaytime)} -[#CCFFCC]Shortest match duration: ${formatTime(stats.shortestWinTime)}`, - hexed: `\ -[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.earlyRTVs} RTVs) -[#CCFFCC]RTV rate: ${f.percent(stats.earlyRTVRate, 1)} -[#CCFFCC]Average match duration: ${formatTime(stats.averagePlaytime)} -[#CCFFCC]Shortest match duration: ${formatTime(stats.shortestWinTime)}`, - sandbox: `\ -[#CCFFCC]Total plays: ${stats.allRunCount} -[#CCFFCC]Average play time: ${formatTime(stats.averagePlaytime)} -[#CCFFCC]Shortest play time: ${formatTime(stats.shortestTime)}`, - }, ""); - return (`\ -[coral]${map.name()} -[gray](${map.file.name()}) - -[accent]Map by: [white]${map.author()} -[accent]Description: [white]${map.description()} -[accent]Size: [white]${map.width}x${map.height} -[accent]Last updated: [white]${new Date(map.file.lastModified()).toLocaleDateString()} -[accent]BvB allowed: ${f.boolGood(rules.placeRangeCheck)}, unit item transfer allowed: ${f.boolGood(rules.onlyDepositCore)} - -${modeSpecificStats} -[#CCFFCC]Longest play time: ${formatTime(stats.longestTime)} -[#CCFFCC]Average player count: ${f.number(stats.averageHighestPlayerCount, 1)}` - ); - } -} +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the map run tracker and statistics computation. +*/ + +import { Gamemode } from "/config"; +import { FFunction } from "/frameworks/commands"; +import { dataClass, serialize } from "/frameworks/io"; +import { computeStatistics, Duration } from "/funcs"; +import { FishEvents } from "/globals"; +import { formatTime, formatTimeShort, formatTimestamp, match } from "/utils"; + +type FinishedMapRunData = { + winTeam:Team; + success:boolean; //winTeam == Vars.state.rules.defaultTeam + startTime:number; + endTime:number; + maxPlayerCount:number; + wave:number; +} +export class FinishedMapRun extends dataClass() { + //this constructor is useless, but rhino crashes with a bizarre error when trying to run the emitted code + //do not remove this useless constructor + constructor(data:FinishedMapRunData){ + super(data); + } + duration(){ + return this.endTime - this.startTime; + } + outcome(){ + if(Gamemode.pvp()){ + if(this.winTeam === Team.derelict) { + if(this.duration() > Duration.minutes(20)) return ["rtv", "late rtv"] as const; + else return ["rtv", "early rtv"] as const; + } else return ["win", "win"] as const; + } else { + if(this.success) return ["win", "win"] as const; + else if(this.winTeam === Team.derelict){ + if(this.duration() > Duration.minutes(3)) return ["loss", "late rtv"] as const; + else return ["rtv", "early rtv"] as const; + } else return ["loss", "loss"] as const; + } + } +} + +export class PartialMapRun { + static readonly key = "fish-partial-map-run"; + static current: PartialMapRun | null = null; + static { + FishEvents.on("saveData", () => { + if(this.current) Core.settings.put(this.key, this.current.write()); + }); + FishEvents.on("loadData", () => { + const data = Core.settings.getString(this.key); + if(data){ + this.current = this.read(data); + } else { + //loading a map, but there is no run information, create one + this.current = new this(); + } + }); + Events.on(EventType.SaveLoadEvent, e => { + //create a new run, if there isn't one already + //loadData will have run first if it is a server restart + this.current ??= new this(); + }); + Timer.schedule(() => { + this.current?.update(); + }, 0, 5); + Events.on(EventType.GameOverEvent, e => { + if(this.current){ + const finishedRun = this.current.finish({ winTeam: e.winner ?? Team.derelict }); + const fmap = FMap.getCreate(Vars.state.map); + if(!fmap) return; + + //Highscore message + if(Gamemode.attack() && finishedRun.success){ + const bestPreviousTime = fmap.stats().shortestWinTime; + const duration = finishedRun.duration(); + Call.sendMessage( +`[orange]-------- +${finishedRun.success && duration < bestPreviousTime ? + `[green]New highscore! Map completed in [accent]${formatTimeShort(duration)}[]` +: `[orange]Map completed in [accent]${formatTimeShort(duration)}[]. Current highscore: [green]${formatTimeShort(bestPreviousTime)}[]`} +[orange]--------` + ); + } else if(Gamemode.survival()){ + const bestPreviousWave = fmap.stats().highestWave; + const wave = finishedRun.wave; + Call.sendMessage( +`[orange]-------- +${finishedRun.success && wave < bestPreviousWave ? + `[green]New highscore! Reached wave [accent]${wave}[].` +: `[orange]Reached wave [accent]${wave}[]. Current highscore: [green]${bestPreviousWave}[]`} +[orange]--------` + ); + } + fmap.runs.push(finishedRun); + FishEvents.fire("saveMaps", []); + } + Core.settings.remove(this.key); + this.current = null; + }); + } + + startTime:number = Date.now(); + maxPlayerCount:number = 0; + /** In milliseconds */ + duration(){ + return Date.now() - this.startTime; + } + update(){ + this.maxPlayerCount = Math.max(this.maxPlayerCount, Groups.player.size()); + } + finish({winTeam}:{ + winTeam: Team; + }):FinishedMapRun { + return new FinishedMapRun({ + winTeam, + success: Gamemode.pvp() ? true : winTeam == Vars.state.rules.defaultTeam, + startTime: this.startTime, + endTime: Date.now(), + maxPlayerCount: this.maxPlayerCount, + wave: Vars.state.wave, + }); + } + //Used for continuing through a restart + write():string { + return `${Date.now() - this.startTime}/${this.maxPlayerCount}`; + } + static read(data:string):PartialMapRun { + const [duration, maxPlayerCount] = data.split("/").map(Number); + if(isNaN(duration) || isNaN(maxPlayerCount)){ + Log.err(`_FINDTAG_ failed to load map run stats data: ${data}`); + } + const out = new PartialMapRun(); + out.startTime = Date.now() - duration; //move start time forward by time when the server was off + out.maxPlayerCount = maxPlayerCount; + return out; + } +} + + +type FMapData = { + runs: FinishedMapRun[]; + mapFileName: string; +}; +export class FMap extends dataClass() { + constructor( + data:FMapData, + //O(n^2)... should be fine? + public map:MMap | null = Vars.maps.customMaps().find(m => m.file.name() === data.mapFileName) + ){ super(data); } + + @serialize("fish-map-data", () => ["version", 1, ["array", "u16", ["class", FMap, [ + ["runs", ["array", "u32", ["class", FinishedMapRun, [ + ["startTime", ["number", "i64"]], + ["endTime", ["number", "i64"]], + ["maxPlayerCount", ["number", "u8"]], + ["success", ["boolean"]], + ["winTeam", ["team"]], + ["wave", ["number", "u16"]] + ]]]], + ["mapFileName", ["string"]], + ]]]], () => ["array", "u16", ["class", FMap, [ + ["runs", ["array", "u32", ["class", FinishedMapRun, [ + ["startTime", ["number", "i64"]], + ["endTime", ["number", "i64"]], + ["maxPlayerCount", ["number", "u8"]], + ["success", ["boolean"]], + ["winTeam", ["team"]], + ]]]], + ["mapFileName", ["string"]], + ]]], undefined, "saveMaps") + static allMaps:FMap[] | null = null; + private static maps:Record = {}; + static { + FishEvents.on("dataLoaded", () => { + //This event listener runs after the data has been loaded into allMaps + (FMap.allMaps ??= []).forEach(map => { + FMap.maps[map.mapFileName] = map; + map.runs.forEach(run => { + //this should not even happen, I think GameOverEvent is sending winTeam as null sometimes?? + run.winTeam ??= Team.derelict; + }); + }); + //create all the data + Vars.maps.customMaps().each(m => void FMap.getCreate(m)); + }); + } + + static getCreate(map:MMap){ + if(this.allMaps == null) return null; + const mapFileName = map.file.name(); + if(Object.prototype.hasOwnProperty.call(this.maps, mapFileName)) + return this.maps[mapFileName]; + const fmap = new this({ + runs: [], + mapFileName + }, map); + this.maps[mapFileName] = fmap; + this.allMaps.push(fmap); + return fmap; + } + + rules():Rules | undefined { + return this.map?.rules(); + } + + stats(){ + const runs = this.runs.filter(r => r.maxPlayerCount > 0); //Remove all runs with no players on + const allRunCount = runs.length; + const victories = runs.filter(r => r.outcome()[1] === "win"); + const losses = runs.filter(r => r.outcome()[0] === "loss").length; + const earlyRTVs = runs.filter(r => r.outcome()[1] === "early rtv").length; + const lateRTVs = runs.filter(r => r.outcome()[1] === "late rtv").length; + const significantRunCount = allRunCount - earlyRTVs; + const totalLosses = losses + lateRTVs; + const durations = runs.filter(r => r.outcome()[0] !== "rtv").map(r => r.duration()); + const durationStats = computeStatistics(durations); + const winDurationStats = computeStatistics(runs.filter(r => r.outcome()[0] === "win").map(r => r.duration())); + const teamWins = runs.filter(r => r.outcome()[1] !== "early rtv").reduce((acc, item) => { + acc[item.winTeam.name] = (acc[item.winTeam.name] ?? 0) + 1; + return acc; + }, {} as Record); + const teamWinRate = Object.fromEntries(Object.entries(teamWins).map(([team, wins]) => [team, wins / significantRunCount])); + //Remove runs that were on wave 0, due to a silly bug we have thousands of runs with a max wave of 0 + const waveStats = computeStatistics(runs.filter(r => r.outcome()[0] !== "rtv" && r.wave !== 0).map(r => r.wave)); + return { + allRunCount, + significantRunCount, + victories: victories.length, + losses, + totalLosses, + earlyRTVs, + lateRTVs, + earlyRTVRate: earlyRTVs / allRunCount, + winRate: victories.length / significantRunCount, + lossRate: losses / significantRunCount, + averagePlaytime: durationStats.average, + shortestWinTime: winDurationStats.lowest, + longestTime: durationStats.highest, + shortestTime: durationStats.lowest, + averageHighestPlayerCount: computeStatistics(runs.map(r => r.maxPlayerCount)).average, + teamWins, + teamWinRate, + highestWave: waveStats.highest, + averageWave: waveStats.average, + mostRecentWin: victories.at(-1)?.startTime + }; + } + displayStats(f:FFunction):string | null { + const map = this.map; if(!map) return null; + const stats = this.stats(); + const rules = this.rules()!; + + const modeSpecificStats = match(Gamemode.name(), { + attack: `\ +[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.victories} wins, ${stats.totalLosses} losses, ${stats.earlyRTVs} RTVs) +[#CCFFCC]Outcomes: ${f.percent(stats.winRate, 1)} wins, ${f.percent(stats.lossRate, 1)} losses, ${f.percent(stats.earlyRTVRate, 1)} RTVs +[#CCFFCC]Average playtime: ${formatTime(stats.averagePlaytime)} +[#CCFFCC]Shortest win time: ${formatTime(stats.shortestWinTime)} +[#CCFFCC]Most recent win: ${stats.mostRecentWin ? formatTimestamp(stats.mostRecentWin) : "[red]none[]"}`, + survival: `\ +[#CCFFCC]Highest wave reached: ${stats.highestWave} +[#CCFFCC]Average wave reached: ${stats.averageWave} +[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.earlyRTVs} RTVs) +[#CCFFCC]RTV rate: ${f.percent(stats.earlyRTVRate, 1)} +[#CCFFCC]Average duration: ${formatTime(stats.averagePlaytime)} +[#CCFFCC]Longest duration: ${formatTime(stats.longestTime)}`, + pvp: `\ +[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.earlyRTVs} RTVs) +[#CCFFCC]Team win rates: ${Object.entries(stats.teamWinRate).map(([team, rate]) => `${team} ${f.percent(rate, 1)}`).join(", ")} +[#CCFFCC]RTV rate: ${f.percent(stats.earlyRTVRate, 1)} +[#CCFFCC]Average match duration: ${formatTime(stats.averagePlaytime)} +[#CCFFCC]Shortest match duration: ${formatTime(stats.shortestWinTime)}`, + hexed: `\ +[#CCFFCC]Total runs: ${stats.allRunCount} (${stats.earlyRTVs} RTVs) +[#CCFFCC]RTV rate: ${f.percent(stats.earlyRTVRate, 1)} +[#CCFFCC]Average match duration: ${formatTime(stats.averagePlaytime)} +[#CCFFCC]Shortest match duration: ${formatTime(stats.shortestWinTime)}`, + sandbox: `\ +[#CCFFCC]Total plays: ${stats.allRunCount} +[#CCFFCC]Average play time: ${formatTime(stats.averagePlaytime)} +[#CCFFCC]Shortest play time: ${formatTime(stats.shortestTime)}`, + }, ""); + return (`\ +[coral]${map.name()} +[gray](${map.file.name()}) + +[accent]Map by: [white]${map.author()} +[accent]Description: [white]${map.description()} +[accent]Size: [white]${map.width}x${map.height} +[accent]Last updated: [white]${new Date(map.file.lastModified()).toLocaleDateString()} +[accent]BvB allowed: ${f.boolGood(rules.placeRangeCheck)}, unit item transfer allowed: ${f.boolGood(rules.onlyDepositCore)} + +${modeSpecificStats} +[#CCFFCC]Longest play time: ${formatTime(stats.longestTime)} +[#CCFFCC]Average player count: ${f.number(stats.averageHighestPlayerCount, 1)}` + ); + } +} diff --git a/src/metrics.ts b/src/metrics.ts index 32a322ac..76935143 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,67 +1,67 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the player count tracking. -*/ - - -type MetricsWeek = number[] & { - length: 2520; /* 15 * 24 * 7 */ -}; -export class Metrics { - /** 4 May 2025 */ - static readonly startDate = new Date(2025, 4, 4).getTime(); - static readonly millisPerWeek = 604800_000; - static readonly millisBetweenReadings = 240_000; - static readonly noData = -1; - /** - * Weeks are numbered starting at the week of 4 May 2025. - * A value is taken every 4 minutes, for a total of 15 readings per hour. - */ - // @serialize("player-count-data", () => ["version", 0, - // ["array", "u16", ["array", 2520, ["number", "i8"]]] - // ], undefined, weeks => { - // for(let i = 0; i <= Metrics.weekNumber(); i ++){ - // weeks[i] ??= Metrics.newWeek(); - // } - // return weeks; - // }) - static weeks: MetricsWeek[] = Array(this.weekNumber() + 1).fill(0).map(() => this.newWeek()); - - static { - Timer.schedule(() => Metrics.update(), 15, 60); - } - - static weekNumber(date = Date.now()){ - return Math.floor((date - this.startDate) / this.millisPerWeek); - } - static readingNumber(date = Date.now()){ - return Math.floor(((date - this.startDate) % this.millisPerWeek) / this.millisBetweenReadings); - } - static newWeek() { - return Array(2520 satisfies MetricsWeek["length"]).fill(this.noData) as MetricsWeek; - } - static currentWeek(){ - return this.weeks[this.weekNumber()] ??= this.newWeek(); - } - static update(){ - Time.mark(); - const playerCount = Groups.player.size(); - this.currentWeek()[this.readingNumber()] = - Math.max(playerCount, this.currentWeek()[this.readingNumber()]); - Log.debug("metrics update @", Time.elapsed()); - } - - static exportRange(startDate = this.startDate, endDate = Date.now()){ - if(typeof startDate !== "number") throw new Error('startDate should be a number'); - const startWeek = this.weekNumber(startDate); - const endWeek = this.weekNumber(endDate); - return this.weeks.slice(startWeek, endWeek + 1).map((week, weekNumber) => - week.filter(v => v >= 0).map((v, i) => [ - v, - this.startDate + - weekNumber * this.millisPerWeek + - i * this.millisBetweenReadings - ] as [value:number, timestamp:number]) - ).flat(); - } -} +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the player count tracking. +*/ + + +type MetricsWeek = number[] & { + length: 2520; /* 15 * 24 * 7 */ +}; +export class Metrics { + /** 4 May 2025 */ + static readonly startDate = new Date(2025, 4, 4).getTime(); + static readonly millisPerWeek = 604800_000; + static readonly millisBetweenReadings = 240_000; + static readonly noData = -1; + /** + * Weeks are numbered starting at the week of 4 May 2025. + * A value is taken every 4 minutes, for a total of 15 readings per hour. + */ + // @serialize("player-count-data", () => ["version", 0, + // ["array", "u16", ["array", 2520, ["number", "i8"]]] + // ], undefined, weeks => { + // for(let i = 0; i <= Metrics.weekNumber(); i ++){ + // weeks[i] ??= Metrics.newWeek(); + // } + // return weeks; + // }) + static weeks: MetricsWeek[] = Array(this.weekNumber() + 1).fill(0).map(() => this.newWeek()); + + static { + Timer.schedule(() => Metrics.update(), 15, 60); + } + + static weekNumber(date = Date.now()){ + return Math.floor((date - this.startDate) / this.millisPerWeek); + } + static readingNumber(date = Date.now()){ + return Math.floor(((date - this.startDate) % this.millisPerWeek) / this.millisBetweenReadings); + } + static newWeek() { + return Array(2520 satisfies MetricsWeek["length"]).fill(this.noData) as MetricsWeek; + } + static currentWeek(){ + return this.weeks[this.weekNumber()] ??= this.newWeek(); + } + static update(){ + Time.mark(); + const playerCount = Groups.player.size(); + this.currentWeek()[this.readingNumber()] = + Math.max(playerCount, this.currentWeek()[this.readingNumber()]); + Log.debug("metrics update @", Time.elapsed()); + } + + static exportRange(startDate = this.startDate, endDate = Date.now()){ + if(typeof startDate !== "number") throw new Error('startDate should be a number'); + const startWeek = this.weekNumber(startDate); + const endWeek = this.weekNumber(endDate); + return this.weeks.slice(startWeek, endWeek + 1).map((week, weekNumber) => + week.filter(v => v >= 0).map((v, i) => [ + v, + this.startDate + + weekNumber * this.millisPerWeek + + i * this.millisBetweenReadings + ] as [value:number, timestamp:number]) + ).flat(); + } +} diff --git a/src/mindustryTypes.ts b/src/mindustryTypes.ts index 87f46643..770cb4e4 100644 --- a/src/mindustryTypes.ts +++ b/src/mindustryTypes.ts @@ -1,971 +1,971 @@ - -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains TypeScript type definitions for Mindustry's code. -Mindustry is written in Java, which has strong types. -Mindustry supports loading Javascript, which does not have types. -Javascript will have access to Mindustry's functions, which have types. -We are writing Typescript, which does have types. We are able to call Mindustry's functions, but because those are written in Java we cannot directly use those types. -This file contains some of those type definitions, ported over from the Java definitions. -*/ -//this is fine - -declare global { - -/** Helper function to produce an arc.func.Floatf from a rhino function. */ -function floatf(func:(input:T) => number):Floatf; - -type Floatf = ((input:T) => number) & {__brand: "floatf"}; - -const Call: any; -const Log: { - debug(this:void, message:string, ...extra:unknown[]):void; - info(this:void, message:string, ...extra:unknown[]):void; - warn(this:void, message:string, ...extra:unknown[]):void; - err(this:void, message:string, ...extra:unknown[]):void; - err(this:void, error:unknown):void; - level: LogLevel; - LogLevel: { - debug: LogLevel; - info: LogLevel; - warn: LogLevel; - err: LogLevel; - none: LogLevel; - }; -}; -type LogLevel = { readonly _brand: unique symbol }; -type LogLevelName = Exclude; -const Strings: { - stripColors(string:string):string; - sanitizeFilename(name:string):string; -}; -const NetServer: { - kickDuration: number; -}; -class Rules { - constructor(); - mode(): Gamemode; - defaultTeam: Team; - waveTeam: Team; - waves: boolean; - winWave: number; - waitEnemies: boolean; - env: number; - fog: boolean; - pvpAutoPause: boolean; - placeRangeCheck: boolean; - onlyDepositCore: boolean; - infiniteResources: boolean; - getClass(): typeof Rules; - attackMode: boolean; - pvp: boolean; - editor: boolean; -} - -const Vars: { - logic: { - skipWave():void; - } - netServer: { - admins: Administration; - clientCommands: CommandHandler; - kickAll(kickReason:any):void; - addPacketHandler(name:string, handler:(player:mindustryPlayer, content:string) => unknown):void; - currentlyKicking: VoteSession | null; - votesRequired():number; - assigner: (player:Player, players:MIterable) => Team; - } - net: { - send(object:any, reliable:boolean):void; - closeServer():void; - } - mods: { - getScripts(): Scripts; - } - maps: Maps; - state: { - rules: Rules; - planet: Planet | null; - set(state:State):void; - gameOver:boolean; - wave:number; - map: MMap; - isMenu():boolean; - wavetime:number; - enemies:number; - /** Time in ticks, 60/s */ - tick:number; - teams: Teams; - }; - indexer: BlockIndexer; - saveExtension: string; - saveDirectory: Fi; - modDirectory: Fi; - customMapDirectory: Fi; - content: Content; - tilesize: 8; - world: World; - maxPingTextLength: number; -}; -class Teams { - active: Seq; - getActive(): Seq; -} -class BlockIndexer { - getFlagged(team: Team, flag: BlockFlag): Seq; -} -class BlockFlag { - static storage: BlockFlag; -} -type State = {__state: ""}; -class Planet { - name: string; -} -type Scripts = any; -type CommandHandler = any; -const CommandHandler: CommandHandler; -type Content = { - items(): Seq; - units(): Seq; - blocks(): Seq; -}; -class World { - build(x:number, y:number):Building | null; - tile(x:number, y:number):Tile | null; - width(): number; - height(): number; - tiles: { - eachTile(func:(tile:Tile) => unknown):void; - }; -} -class Gamemode { - static survival:Gamemode; - static attack:Gamemode; - static pvp:Gamemode; - static sandbox:Gamemode; - static editor:Gamemode; - name():string; - valid(map:MMap):boolean; -} -type Throwable = any; -class Administration { - dosBlacklist: ObjectSet; - kickedIPs: ObjectMap; - bannedIPs: Seq; - subnetBans: Seq; - findByName(info:string):ObjectSet; - searchNames(name:string):ObjectSet; - getInfo(uuid:string):PlayerInfo; - getInfoOptional(uuid:string):PlayerInfo | null; - findByIP(ip:string):PlayerInfo | null; - findByIPs(ip:string):Seq; - isIPBanned(ip:string):boolean; - isIDBanned(uuid:string):boolean; - banPlayerIP(ip:string):boolean; - banPlayerID(uuid:string):boolean; - banPlayer(uuid:string):boolean; - unbanPlayerIP(ip:string):boolean; - unbanPlayerID(uuid:string):boolean; - adminPlayer(uuid:string, usid:string):boolean; - unAdminPlayer(uuid:string):boolean; - blacklistDos(ip:string):void; - isDosBlacklisted(ip:string):boolean; - save():void; - addChatFilter(filter:(player:mindustryPlayer, message:string) => string | null):void; - addActionFilter(filter:(action:PlayerAction) => boolean):void; - static ActionType: ActionType; - static PlayerInfo: typeof PlayerInfo; -} -const Events: { - on(event:EventType, handler:(e:any) => void):void; - fire(event:MEvent):void; -}; -type MEvent = any; -class Tile { - x:number; y:number; - build: Building | null; - breakable():boolean; - block():Block; - floor():Block; - remove():void; - removeNet():void; - setNet(block:Block, team:Team, rotation:number):void; - getLinkedTiles(callback:(t:Tile) => void):void; -} -const Menus: { - registerMenu(listener:BuiltinMenuListener):number; -}; -type BuiltinMenuListener = (player:mindustryPlayer, option:number) => unknown; -const UnitTypes: Record; -const Sounds: Record; -type Sound = any; -const Blocks: Record; -class Block { - name: string; - buildType: Building; - id: number; - localizedName: string; - emoji(): string; -} -class Building { - block: Block; - tile: Tile; - items: ItemModule; - power: PowerModule; - liquids: LiquidModule; - team: Team; - changeTeam: Team; - enabled: boolean; - health: number; - ammo?: Seq<{item: Item}>; - range?: () => number; - warmup?: number; - storageCapacity?: number; - dead: boolean; - linkedCore: Building | null; - timeScale(): number; - kill():void; - tileX():number; - tileY():number; -} -const Items: Record<"scrap" | "copper" | "lead" | "graphite" | "coal" | "titanium" | "thorium" | "silicon" | "plastanium" | "phaseFabric" | "surgeAlloy" | "sporePod" | "sand" | "blastCompound" | "pyratite" | "metaglass" | "beryllium" | "tungsten" | "oxide" | "carbide" | "fissileMatter" | "dormantCyst", Item> & { - serpuloItems: Seq; - erekirItems: Seq; -}; -class Item { - name: string; - localizedName: string; - hidden: boolean; - emoji(): string; -} -const Liquids: Record; -class Liquid { - name: string; - gas: boolean; -} -class LiquidModule { - current(): Liquid; -} -class ItemModule { - get(item: Item):number; - set(item: Item, value: number):void; - add(item: Item, value: number):void; - has(item: Item, amount: number): boolean; - has(stacks: ItemStack[]): boolean; -} -class PowerModule { - graph: PowerGraph; -} -class PowerGraph { - lastPowerProduced: number; - lastPowerNeeded: number; - producers: Seq; - consumers: Seq; -} -class ItemStack { - constructor(item: Item, amount: number); -} -class Team { - static derelict:Team; - static sharded:Team; - static crux:Team; - static malis:Team; - static green:Team; - static blue:Team; - static all:Team[]; - static baseTeams:Team[]; - name:string; - active():boolean; - isAlive():boolean; - data():TeamData; - core():Building | null; - items():ItemModule; - coloredName():string; - id:number; - static get(index:number):Team; - cores(): Seq; - rules(): TeamRules; -} -type TeamData = { - team: Team; - units: Seq; - buildings: Seq; - cores: Seq; - countType(type:UnitType):number; - hasCore(): boolean; -}; -type TeamRules = { - protectCores: boolean; -} -const Units: { - getCap(team:Team):number; -}; -const StatusEffects: Record; -type StatusEffect = any; -const Fx: Record; -type Effect = any; -const Align: Record; -const Groups: { - player: EntityGroup; - unit: EntityGroup; - fire: EntityGroup; - build: EntityGroup; - powerGraph: EntityGroup<{graph: PowerGraph}>; -}; -type Fire = any; -class Vec2 { - constructor(x:number, y:number); - set(v:Vec2):Vec2; - set(x:number, y:number):Vec2; - len():number; -} -/* mindustry.gen.Player */ -class Player { - id:number; - name:string; - admin:boolean; - x:number; y:number; - con:NetConnection; - mouseX:number; mouseY:number; - shooting:boolean; - pingX: number; - pingY: number; - pingTime: number; - pingText: string | null; - ip():string; - kick(kickReason?:KickReason | string, duration?:number):void; - uuid():string; - usid():string; - sendMessage(message:string):void; - unit():Unit | null; - unit(unit:Unit):void; - team():Team; - team(team:Team):void; - dead():boolean; - clearUnit():void; - checkSpawn():void; - getInfo():PlayerInfo; -} -type mindustryPlayer = Player; -class Color { - constructor(); - constructor(rgba8888:number); - constructor(r:number, g:number, b:number); - constructor(r:number, g:number, b:number, a:number); - constructor(color:Color); - static white: Color; static lightGray: Color; static gray: Color; static darkGray: Color; static black: Color; static clear: Color; static blue: Color; static navy: Color; static royal: Color; static slate: Color; static sky: Color; static cyan: Color; static teal: Color; static green: Color; static acid: Color; static lime: Color; static forest: Color; static olive: Color; static yellow: Color; static gold: Color; static goldenrod: Color; static orange: Color; static brown: Color; static tan: Color; static brick: Color; static red: Color; static scarlet: Color; static crimson: Color; static coral: Color; static salmon: Color; static pink: Color; static magenta: Color; static purple: Color; static violet: Color; static maroon: Color; - static valueOf(string:string):Color; - static valueOf(color:Color, hex:string):Color; - static HSVtoRGB(hue:number, saturation:number, value:number):Color; - r: number; - g: number; - b: number; - a: number; - cpy():Color; - rand():Color; -} -const Version: { - type: string; - modifier: string; - number: number; - build: number; - revision: number; -}; -const Pal: Record<"orangeSpark" | "adminChat" | "logicBlocks" | "vent" | "lightishGray" | "darkishGray" | "thoriumPink" | "shadow" | "boostFrom" | "sapBullet" | "darkestGray" | "lightishOrange" | "placing" | "unitBack" | "lightFlame" | "bar" | "freeze" | "plastanium" | "plastaniumFront" | "breakInvalid" | "boostTo" | "logicControl" | "surge" | "redLight" | "darkMetal" | "powerLight" | "meltdownHit" | "reactorPurple" | "darkerMetal" | "logicUnits" | "plastaniumBack" | "vent2" | "techBlue" | "darkPyraFlame" | "turretHeat" | "logicOperations" | "bulletYellow" | "negativeStat" | "accentBack" | "items" | "plasticBurn" | "shield" | "missileYellowBack" | "logicIo" | "darkerGray" | "lightPyraFlame" | "regen" | "range" | "redSpark" | "logicWorld" | "lighterOrange" | "remove" | "noplace" | "gray" | "engine" | "lightOrange" | "heal" | "freezeBack" | "rubble" | "place" | "power" | "coalBlack" | "missileYellow" | "metalGrayDark" | "neoplasmOutline" | "slagOrange" | "plasticSmoke" | "berylShot" | "sapBulletBack" | "stat" | "powerBar" | "redDust" | "sap" | "ammo" | "placeRotate" | "darkOutline" | "lightTrail" | "muddy" | "stoneGray" | "health" | "darkestMetal" | "darkFlame" | "suppress" | "redderDust" | "spore" | "accent" | "command" | "reactorPurple2" | "lancerLaser" | "bulletYellowBack" | "removeBack" | "neoplasm1" | "tungstenShot" | "neoplasm2" | "unitFront" | "neoplasmMid", Color>; -type ApplicationListener = Partial<{ - init(): void; - update(): void; - pause(): void; - resume(): void; - dispose(): void; - exit(): void; -}>; - -const Core: { - settings: { - get(key:string, defaultValue?:T):T; - getBytes(key:string):number[]; - getString(key:string):string | null; - getDataDirectory():Fi; - getInt(key:string, defaultValue?:number):number; - put(key:string, value:any):void; - has(key:string):boolean; - remove(key:string):void; - manualSave():void; - } - app: { - post(func:() => unknown):void; - exit():void; - getJavaHeap():number; - listeners: any[]; - addListener(listener:ApplicationListener):void; - } - graphics: { - getFramesPerSecond():number; - getDeltaTime():number; - } -}; -const Damage: { - damage(team:Team, x:number, y:number, radius:number, damage:number, pierce:boolean, air:boolean, ground:boolean):void; -}; -const Mathf: { - halfPi: number; - PI2: number; - - ceil(val:number):number; - round(val:number, step?:number):number; - random(min:number, max:number):number; - len(x:number, y:number):number; - atan2(x:number, y:number):number; - dst(x1:number, y1:number, x2:number, y2:number):number; -}; -const SaveIO: { - save(file:Fi):void; -}; -const Timer: { - schedule(func:() => unknown, delaySeconds:number, intervalSeconds?:number, repeatCount?:number):TimerTask; -}; -class TimerTask { - cancel():void; -} -const Time: { - millis(): number; - nanos(): number; - elapsed(): number; - mark(): void; - delta: number; - timeSinceMillis(millis: number): number; - setDeltaProvider(provider: () => number):void; -}; -const GameState: { - State: Record<"playing" | "paused" | "menu", any>; -}; -class HttpRequest { - submit(func:(response:HttpResponse) => void):void; - error(func:(exception:any) => void):void; - header(name:string, value:string):HttpRequest; - timeout: number; -} -class HttpResponse { - getResultAsString():string; - getResultAsStream():InputStream - getResult():number[]; -} -class InputStream { - close():void; - transferTo(outputsteam:OutputStream):number; - mark(readlimit:number):void; - reset():void; -} -class OutputStream { - close():void; -} -class DataOutputStream extends OutputStream { - constructor(stream:OutputStream); - write(b:number[]):void; - write(b:number[], offset:number, length:number):void; - write(b:number):void; - writeBoolean(v:boolean):void; - writeByte(v:number):void; - writeBytes(s:string):void; - writeChar(v:number):void; - writeChars(s:string):void; - writeDouble(v:number):void; - writeFloat(v:number):void; - writeInt(v:number):void; - writeLong(v:number):void; - writeShort(v:number):void; - writeUTF(s:string):void; -} -class DataInputStream extends InputStream { - constructor(stream:InputStream); - read(b:number[]):number; - read(b:number[], off:number, len:number):number; - readBoolean():boolean; - readByte():number; - readChar():number; - readDouble():number; - readFloat():number; - readFully(b:number[]):void; - readFully(b:number[], off:number, len:number):void; - readInt():number; - readLine():string; - readLong():number; - readShort():number; - readUnsignedByte():number; - readUnsignedShort():number; - readUTF():string; - skipBytes(n:number):number; -} -class ByteArrayOutputStream extends OutputStream { - constructor(); - toByteArray():number[]; -} -class ByteArrayInputStream extends InputStream { - constructor(bytes:number[]); -} -const Http: { - post(url:string, content:string):HttpRequest; - get(url:string):HttpRequest; - get(url:string, callback:(res:HttpResponse) => unknown, error:(err:any) => unknown):void; -}; -class Seq { - items: Array; - size: number; - constructor(); - constructor(capacity:number); - static with(...items:T[]):Seq; - static with(items:MIterable):Seq; - add(item:T):this; - addUnique(item:T):this; - contains(item:T):boolean; - contains(pred:Boolf):boolean; - count(pred:(item:T) => boolean):number; - allMatch(pred:(item:T) => boolean):boolean; - /** @deprecated Use select() or retainAll() */ - filter(pred:(item:T) => boolean):Seq; - retainAll(pred:(item:T) => boolean):Seq; - /** @returns whether an item was removed */ - remove(pred:(item:T) => boolean):boolean; - removeAll(pred:(item:T) => boolean):Seq; - select(pred:(item:T) => boolean):Seq; - find(pred:(item:T) => boolean):T | null; - each(func:(item:T) => unknown):void; - each(pred:(item:T) => boolean, func:(item:T) => unknown):void; - isEmpty():boolean; - map(mapFunc:(item:T) => R):Seq; - flatMap(mapFunc:(item:T) => Seq):Seq; - toString(separator?:string, stringifier?:(item:T) => string):string; - toArray():T[]; - copy():Seq; - sort(comparator?:(item:T) => number):Seq; - min(comparator?:Floatf):T; - max(comparator?:Floatf):T; - random():T | null; - get(index:number):T; - first():T; - peek():T; - firstOpt():T | null; - clear():void; -} - -class ObjectSet { - size:number; - select(predicate:(item:T) => boolean):ObjectSet; - each(func:(item:T) => unknown):void; - add(item:T):boolean; - remove(item:T):boolean; - isEmpty():boolean; - contains(item:T):boolean; - toSeq():Seq; - get(key:T):T; - first():T; - clear():void; - toString():string; -} -class ObjectMap { - put(key:K, value:V):void; - get(key:K):V; - containsKey(key:K):boolean; - remove(key:K):V | null; - clear():void; - size:number; - entries(): unknown; -} -class ObjectIntMap { - put(key:K, value:number):void; - get(key:K):number; - increment(key:K):void; - clear():void; - remove(key:K):number | null; - size:number; - forEach(func:(_:{key:K, value:number}) => void):void; - entries(): { - toArray():Seq>; - }; -} -class ObjectIntMapEntry { - key:K; - value:number; -} -class EntityGroup { - add(type:T):void - copy():Seq; - copy(seq:Seq):Seq; - each(func:(item:T) => unknown):void; - each(predicate:(item:T) => boolean, func:(item:T) => unknown):void; - getByID(id:number):T; - isEmpty():boolean; - size():number; - contains(pred:(item:T) => boolean):boolean; - find(pred:(item:T) => boolean):T; - first():T; - index(index:number):T; - clear():void; - //iterator():Iterator -} - -function importPackage(package:any):void; -const Packages: Record; -const EventType: Record; -type EventType = any; -type PlayerAction = { - player:mindustryPlayer; - pingText:string | null; - pingX:number; - pingY:number; - type:ActionType; - tile:Tile | null; - unit:Unit | null; -} -type ActionType = any; -const ActionType:Record; -type Unit = { - health: number; - shield: number; - maxHealth: number; - type: UnitType; - x: number; - y: number; - team: Team; - dead: boolean; - spawnedByCore: boolean; - added: boolean; - id: number; - hitSize: number; - tileOn():Tile | null; - tile?: () => Building; - kill():void; - add():void; - isAdded():boolean; - set(x: number, y:number):void; - approach(vec: Vec2):void; - hasPayload: undefined | (() => boolean); - getPlayer():Player | null; - resetController():void; - apply(effect:StatusEffect, ticks:number):void; - clearStatuses():void; - within(pos: Building | Unit, distance: number):boolean; -}; -type NetConnection = any; -class Command { - text:string; - paramText:string; - description:string; - params:any[]; -} - -/** java.io.File */ -class JavaFile { - path: string; -} -class Fi { - constructor(path:string); - file(): JavaFile; - child(path:string): Fi; - exists(): boolean; - absolutePath():string; - writeBytes(bytes:number[], append?:boolean):void; - static tempFile(prefix:string):Fi; - delete():boolean; - length():number; - lastModified():number; - write():OutputStream; - list():Fi[]; - name():string; - readBytes():number[]; -} -class Bullet { - owner: Unit | Building | null; -} -class Pattern { - static matches(regex:string, target:string):boolean; - static compile(regex:string):Pattern; - matcher(input:string):Matcher; -} -class Matcher { - replaceAll(replacement:string):string; - matches():boolean; - group(index:number):string; -} -class Runtime { - static getRuntime():Runtime; - exec(command:string, envp:string[] | null, dir:JavaFile):Process; - addShutdownHook(callback: Thread):void; -} -class Thread { - constructor(runnable: () => void); - run(): void; -} -class ProcessBuilder { - constructor(...args:string[]); - directory(file?:JavaFile):ProcessBuilder; - redirectErrorStream(value:boolean):ProcessBuilder; - redirectOutput(value:any):ProcessBuilder; - start():Process; - - static Redirect: { - PIPE: any; - INHERIT: any; - }; -} -class Process { - waitFor():void; - exitValue():number; -} - -const Packets: { - KickReason: Record<"kick" | "clientOutdated" | "serverOutdated" | "banned" | "gameover" | "recentKick" | "nameInUse" | "idInUse" | "nameEmpty" | "customClient" | "serverClose" | "vote" | "typeMismatch" | "whitelist" | "playerLimit" | "serverRestarting", KickReason>; -}; -type KickReason = { quiet: boolean }; - -class ConstructBlock { - static ConstructBuild: any; -} -class CoreBlock { - -} -const Prop: any; - -function print(message:string):void; - -class PlayerInfo { - /** uuid */ - id: string; - lastName: string; - lastIP: string; - ips: Seq; - names: Seq; - adminUsid: string | null; - timesKicked: number; - timesJoined: number; - admin: boolean; - banned: boolean; - lastKicked: number; - plainLastName(): string; -} - -class UnitType { - spawn(team:Team, x:number, y:number):Unit; - create(team:Team):Unit; - supportsEnv(env:number):boolean; - emoji():string; - health: number; - hidden: boolean; - internal: boolean; - name: string; - localizedName: string; -} -class MissileUnitType extends UnitType {} -class LogicAI { - controller: Building | null; -} -type MapTags = { - name:string; - description?:string; - author?:string; - steamid?:string; - /** JSON rules */ - rules?:string; - build?:number; - genfilters?:string; -} -class Maps { - setNextMapOverride(map:MMap | null):void; - all():Seq; - customMaps():Seq; - byName(name:string):MMap | null; - reload():void; - saveMap(baseTags:MapTags):MMap; -} -class MMap { - readonly custom:boolean; - readonly file:Fi; - width:number; - height:number; - build:number; - name():string; - author():string; - description():string; - plainName():string; - plainAuthor():string; - plainDescription():string; - rules():Rules; -} - -class Sort { - static instance():Sort; - sort(input:Seq | unknown[]):void; - sort(input:Seq | unknown[], fromIndex:number, toIndex:number):void; -} -class ServerControl { - static instance: ServerControl; - handler: CommandHandler; -} - -class VoteSession { - private target: mindustryPlayer; - private task: TimerTask; - private voted: ObjectIntMap; - private votes: number; -} - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -interface Array { - filter(predicate: BooleanConstructor, thisArg?: any): Array; -} -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -interface ReadonlyArray { - map, U>(this:TThis, fn:(v:T, i:number, a:TThis) => U): number extends TThis["length"] ? U[] : { [K in keyof TThis]: U }; -} -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -interface ObjectConstructor { - entries(input:Record):Array<[K, V]>; - fromEntries(input:Array<[K, V]>):Record; -} -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -interface SymbolConstructor { - readonly metadata: unique symbol; -} - -const Threads: { - daemon(callback:() => unknown):void; - thread(callback:() => unknown):void; -}; -const Tmp: { - //not full - v1:Vec2; - v2:Vec2; - v3:Vec2; - v4:Vec2; - v5:Vec2; - v6:Vec2; - - v31:Vec2; - v32:Vec2; - v33:Vec2; - v34:Vec2; - - c1:Color; - c2:Color; - c3:Color; - c4:Color; -}; -class EffectCallPacket2 { - effect:Effect; - x:number; - y:number; - rotation:number; - color:Color; - data:any; -} -class LabelReliableCallPacket { - message:string; - duration:number; - worldx:number; - worldy:number; -} -class ConnectPacket { - version: number; - versionType: string; - mods: Seq; - name: string; - locale: string; - uuid: string; - usid: string; - mobile: boolean; - color: number; -} - -type ByteBuffer = { - put(bytes:number[]):void; - flip():void; -}; -type MessageDigest = { - update(buffer:ByteBuffer):void; - digest():number[]; -}; - -/** java.nio.file.Paths */ -const Paths: { - get(path:string):Path; -}; -/** java.nio.file.Path */ -type Path = { - toRealPath():Path; - toString():string; - getParent():Path; -} - -/** arc.util.OS */ -const OS: { - /** - * Blocking, please run this in a thread - * @throws RuntimeException - */ - exec(...command:string[]):string; -}; -const Trigger: Record<'shock'|'cannotUpgrade'|'openConsole'|'blastFreeze'|'impactPower'|'blastGenerator'|'shockwaveTowerUse'|'forceProjectorBreak'|'thoriumReactorOverheat'|'neoplasmReact'|'fireExtinguish'|'acceleratorUse'|'newGame'|'tutorialComplete'|'flameAmmo'|'resupplyTurret'|'turretCool'|'enablePixelation'|'exclusionDeath'|'suicideBomb'|'openWiki'|'teamCoreDamage'|'socketConfigChanged'|'update'|'beforeGameUpdate'|'afterGameUpdate'|'unitCommandChange'|'unitCommandPosition'|'unitCommandAttack'|'importMod'|'draw'|'drawOver'|'preDraw'|'postDraw'|'uiDrawBegin'|'uiDrawEnd'|'universeDrawBegin'|'universeDraw'|'universeDrawEnd', Trigger>; -type Trigger = { - __brand: 'trigger'; -}; -class CommandRunner { - accept: (args:string[], parameter: T) => void; - constructor(_: {accept: (args:string[], parameter: T) => void}); -} - -class WorldReloader { - begin():void; - end():void; -} - -class Bits { - constructor(capacity?: number); - get(index:number):boolean; - /** - * @param value Default true - */ - set(index:number, value?:boolean):void; - set(index:number, value:number):void; -} - -type JavaClass = any; - -const JsonIO: { - write(object:{}): string; - read(clazz: JavaClass, data: string): T; -}; - -class Boolf { - constructor(_: {get: (value: T) => boolean}); -} -function boolf(func: (value: T) => boolean): Boolf; - -const Iconc: Record<"rotate" | "modeSurvival" | "power" | "left" | "redditAlien" | "edit" | "downOpen" | "pencil" | "file" | "lockOpen" | "right" | "infoCircle" | "pick" | "settings" | "spray1" | "terrain" | "exit" | "wrench" | "lock" | "discord" | "eye" | "none" | "play" | "diagonal" | "eraser" | "trash" | "liquid" | "fileImage" | "defense" | "layers" | "grid" | "admin" | "steam" | "star" | "chartBar" | "chat" | "android" | "image" | "map" | "logic" | "menu" | "commandRally" | "editor" | "folder" | "units" | "commandAttack" | "copy" | "filter" | "cancel" | "terminal" | "upload" | "eyeOff" | "save" | "planeOutline" | "fill" | "distribution" | "upOpen" | "rightOpen" | "modePvp" | "download" | "list" | "flipX" | "flipY" | "effect" | "paste" | "planet" | "waves" | "up" | "warning" | "tree" | "add" | "down" | "host" | "spray" | "info" | "players" | "resize" | "refresh1" | "production" | "crafting" | "pause" | "googleplay" | "hammer" | "fileText" | "modeAttack" | "move" | "zoom" | "bookOpen" | "refresh" | "ok" | "home" | "githubSquare" | "powerOld" | "github" | "undo" | "box" | "trello" | "book" | "export" | "fileTextFill" | "rightOpenOut" | "turret" | "leftOpen" | "line" | "itchio" | "link" | "filters" | "redo", number>; - -const ArcReflect: { - get(thing:any, key:string):any; - get(clazz:any, thing:any, key:string):any; - set(thing:any, key:string, value:any):void; -}; -class Ratekeeper { - occurences:number; - lastTime:number; - allow(spacingMS:number, cap:number):boolean; -} - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -interface MIterable { - iterator(): Iterator; - forEach(_:(item:T) => void):void; -} - -class AtomicInteger { - constructor(value?:number); - decrementAndGet():number; - getAndIncrement():number; - get():number; - set(int:number):void; -} - + +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains TypeScript type definitions for Mindustry's code. +Mindustry is written in Java, which has strong types. +Mindustry supports loading Javascript, which does not have types. +Javascript will have access to Mindustry's functions, which have types. +We are writing Typescript, which does have types. We are able to call Mindustry's functions, but because those are written in Java we cannot directly use those types. +This file contains some of those type definitions, ported over from the Java definitions. +*/ +//this is fine + +declare global { + +/** Helper function to produce an arc.func.Floatf from a rhino function. */ +function floatf(func:(input:T) => number):Floatf; + +type Floatf = ((input:T) => number) & {__brand: "floatf"}; + +const Call: any; +const Log: { + debug(this:void, message:string, ...extra:unknown[]):void; + info(this:void, message:string, ...extra:unknown[]):void; + warn(this:void, message:string, ...extra:unknown[]):void; + err(this:void, message:string, ...extra:unknown[]):void; + err(this:void, error:unknown):void; + level: LogLevel; + LogLevel: { + debug: LogLevel; + info: LogLevel; + warn: LogLevel; + err: LogLevel; + none: LogLevel; + }; +}; +type LogLevel = { readonly _brand: unique symbol }; +type LogLevelName = Exclude; +const Strings: { + stripColors(string:string):string; + sanitizeFilename(name:string):string; +}; +const NetServer: { + kickDuration: number; +}; +class Rules { + constructor(); + mode(): Gamemode; + defaultTeam: Team; + waveTeam: Team; + waves: boolean; + winWave: number; + waitEnemies: boolean; + env: number; + fog: boolean; + pvpAutoPause: boolean; + placeRangeCheck: boolean; + onlyDepositCore: boolean; + infiniteResources: boolean; + getClass(): typeof Rules; + attackMode: boolean; + pvp: boolean; + editor: boolean; +} + +const Vars: { + logic: { + skipWave():void; + } + netServer: { + admins: Administration; + clientCommands: CommandHandler; + kickAll(kickReason:any):void; + addPacketHandler(name:string, handler:(player:mindustryPlayer, content:string) => unknown):void; + currentlyKicking: VoteSession | null; + votesRequired():number; + assigner: (player:Player, players:MIterable) => Team; + } + net: { + send(object:any, reliable:boolean):void; + closeServer():void; + } + mods: { + getScripts(): Scripts; + } + maps: Maps; + state: { + rules: Rules; + planet: Planet | null; + set(state:State):void; + gameOver:boolean; + wave:number; + map: MMap; + isMenu():boolean; + wavetime:number; + enemies:number; + /** Time in ticks, 60/s */ + tick:number; + teams: Teams; + }; + indexer: BlockIndexer; + saveExtension: string; + saveDirectory: Fi; + modDirectory: Fi; + customMapDirectory: Fi; + content: Content; + tilesize: 8; + world: World; + maxPingTextLength: number; +}; +class Teams { + active: Seq; + getActive(): Seq; +} +class BlockIndexer { + getFlagged(team: Team, flag: BlockFlag): Seq; +} +class BlockFlag { + static storage: BlockFlag; +} +type State = {__state: ""}; +class Planet { + name: string; +} +type Scripts = any; +type CommandHandler = any; +const CommandHandler: CommandHandler; +type Content = { + items(): Seq; + units(): Seq; + blocks(): Seq; +}; +class World { + build(x:number, y:number):Building | null; + tile(x:number, y:number):Tile | null; + width(): number; + height(): number; + tiles: { + eachTile(func:(tile:Tile) => unknown):void; + }; +} +class Gamemode { + static survival:Gamemode; + static attack:Gamemode; + static pvp:Gamemode; + static sandbox:Gamemode; + static editor:Gamemode; + name():string; + valid(map:MMap):boolean; +} +type Throwable = any; +class Administration { + dosBlacklist: ObjectSet; + kickedIPs: ObjectMap; + bannedIPs: Seq; + subnetBans: Seq; + findByName(info:string):ObjectSet; + searchNames(name:string):ObjectSet; + getInfo(uuid:string):PlayerInfo; + getInfoOptional(uuid:string):PlayerInfo | null; + findByIP(ip:string):PlayerInfo | null; + findByIPs(ip:string):Seq; + isIPBanned(ip:string):boolean; + isIDBanned(uuid:string):boolean; + banPlayerIP(ip:string):boolean; + banPlayerID(uuid:string):boolean; + banPlayer(uuid:string):boolean; + unbanPlayerIP(ip:string):boolean; + unbanPlayerID(uuid:string):boolean; + adminPlayer(uuid:string, usid:string):boolean; + unAdminPlayer(uuid:string):boolean; + blacklistDos(ip:string):void; + isDosBlacklisted(ip:string):boolean; + save():void; + addChatFilter(filter:(player:mindustryPlayer, message:string) => string | null):void; + addActionFilter(filter:(action:PlayerAction) => boolean):void; + static ActionType: ActionType; + static PlayerInfo: typeof PlayerInfo; +} +const Events: { + on(event:EventType, handler:(e:any) => void):void; + fire(event:MEvent):void; +}; +type MEvent = any; +class Tile { + x:number; y:number; + build: Building | null; + breakable():boolean; + block():Block; + floor():Block; + remove():void; + removeNet():void; + setNet(block:Block, team:Team, rotation:number):void; + getLinkedTiles(callback:(t:Tile) => void):void; +} +const Menus: { + registerMenu(listener:BuiltinMenuListener):number; +}; +type BuiltinMenuListener = (player:mindustryPlayer, option:number) => unknown; +const UnitTypes: Record; +const Sounds: Record; +type Sound = any; +const Blocks: Record; +class Block { + name: string; + buildType: Building; + id: number; + localizedName: string; + emoji(): string; +} +class Building { + block: Block; + tile: Tile; + items: ItemModule; + power: PowerModule; + liquids: LiquidModule; + team: Team; + changeTeam: Team; + enabled: boolean; + health: number; + ammo?: Seq<{item: Item}>; + range?: () => number; + warmup?: number; + storageCapacity?: number; + dead: boolean; + linkedCore: Building | null; + timeScale(): number; + kill():void; + tileX():number; + tileY():number; +} +const Items: Record<"scrap" | "copper" | "lead" | "graphite" | "coal" | "titanium" | "thorium" | "silicon" | "plastanium" | "phaseFabric" | "surgeAlloy" | "sporePod" | "sand" | "blastCompound" | "pyratite" | "metaglass" | "beryllium" | "tungsten" | "oxide" | "carbide" | "fissileMatter" | "dormantCyst", Item> & { + serpuloItems: Seq; + erekirItems: Seq; +}; +class Item { + name: string; + localizedName: string; + hidden: boolean; + emoji(): string; +} +const Liquids: Record; +class Liquid { + name: string; + gas: boolean; +} +class LiquidModule { + current(): Liquid; +} +class ItemModule { + get(item: Item):number; + set(item: Item, value: number):void; + add(item: Item, value: number):void; + has(item: Item, amount: number): boolean; + has(stacks: ItemStack[]): boolean; +} +class PowerModule { + graph: PowerGraph; +} +class PowerGraph { + lastPowerProduced: number; + lastPowerNeeded: number; + producers: Seq; + consumers: Seq; +} +class ItemStack { + constructor(item: Item, amount: number); +} +class Team { + static derelict:Team; + static sharded:Team; + static crux:Team; + static malis:Team; + static green:Team; + static blue:Team; + static all:Team[]; + static baseTeams:Team[]; + name:string; + active():boolean; + isAlive():boolean; + data():TeamData; + core():Building | null; + items():ItemModule; + coloredName():string; + id:number; + static get(index:number):Team; + cores(): Seq; + rules(): TeamRules; +} +type TeamData = { + team: Team; + units: Seq; + buildings: Seq; + cores: Seq; + countType(type:UnitType):number; + hasCore(): boolean; +}; +type TeamRules = { + protectCores: boolean; +} +const Units: { + getCap(team:Team):number; +}; +const StatusEffects: Record; +type StatusEffect = any; +const Fx: Record; +type Effect = any; +const Align: Record; +const Groups: { + player: EntityGroup; + unit: EntityGroup; + fire: EntityGroup; + build: EntityGroup; + powerGraph: EntityGroup<{graph: PowerGraph}>; +}; +type Fire = any; +class Vec2 { + constructor(x:number, y:number); + set(v:Vec2):Vec2; + set(x:number, y:number):Vec2; + len():number; +} +/* mindustry.gen.Player */ +class Player { + id:number; + name:string; + admin:boolean; + x:number; y:number; + con:NetConnection; + mouseX:number; mouseY:number; + shooting:boolean; + pingX: number; + pingY: number; + pingTime: number; + pingText: string | null; + ip():string; + kick(kickReason?:KickReason | string, duration?:number):void; + uuid():string; + usid():string; + sendMessage(message:string):void; + unit():Unit | null; + unit(unit:Unit):void; + team():Team; + team(team:Team):void; + dead():boolean; + clearUnit():void; + checkSpawn():void; + getInfo():PlayerInfo; +} +type mindustryPlayer = Player; +class Color { + constructor(); + constructor(rgba8888:number); + constructor(r:number, g:number, b:number); + constructor(r:number, g:number, b:number, a:number); + constructor(color:Color); + static white: Color; static lightGray: Color; static gray: Color; static darkGray: Color; static black: Color; static clear: Color; static blue: Color; static navy: Color; static royal: Color; static slate: Color; static sky: Color; static cyan: Color; static teal: Color; static green: Color; static acid: Color; static lime: Color; static forest: Color; static olive: Color; static yellow: Color; static gold: Color; static goldenrod: Color; static orange: Color; static brown: Color; static tan: Color; static brick: Color; static red: Color; static scarlet: Color; static crimson: Color; static coral: Color; static salmon: Color; static pink: Color; static magenta: Color; static purple: Color; static violet: Color; static maroon: Color; + static valueOf(string:string):Color; + static valueOf(color:Color, hex:string):Color; + static HSVtoRGB(hue:number, saturation:number, value:number):Color; + r: number; + g: number; + b: number; + a: number; + cpy():Color; + rand():Color; +} +const Version: { + type: string; + modifier: string; + number: number; + build: number; + revision: number; +}; +const Pal: Record<"orangeSpark" | "adminChat" | "logicBlocks" | "vent" | "lightishGray" | "darkishGray" | "thoriumPink" | "shadow" | "boostFrom" | "sapBullet" | "darkestGray" | "lightishOrange" | "placing" | "unitBack" | "lightFlame" | "bar" | "freeze" | "plastanium" | "plastaniumFront" | "breakInvalid" | "boostTo" | "logicControl" | "surge" | "redLight" | "darkMetal" | "powerLight" | "meltdownHit" | "reactorPurple" | "darkerMetal" | "logicUnits" | "plastaniumBack" | "vent2" | "techBlue" | "darkPyraFlame" | "turretHeat" | "logicOperations" | "bulletYellow" | "negativeStat" | "accentBack" | "items" | "plasticBurn" | "shield" | "missileYellowBack" | "logicIo" | "darkerGray" | "lightPyraFlame" | "regen" | "range" | "redSpark" | "logicWorld" | "lighterOrange" | "remove" | "noplace" | "gray" | "engine" | "lightOrange" | "heal" | "freezeBack" | "rubble" | "place" | "power" | "coalBlack" | "missileYellow" | "metalGrayDark" | "neoplasmOutline" | "slagOrange" | "plasticSmoke" | "berylShot" | "sapBulletBack" | "stat" | "powerBar" | "redDust" | "sap" | "ammo" | "placeRotate" | "darkOutline" | "lightTrail" | "muddy" | "stoneGray" | "health" | "darkestMetal" | "darkFlame" | "suppress" | "redderDust" | "spore" | "accent" | "command" | "reactorPurple2" | "lancerLaser" | "bulletYellowBack" | "removeBack" | "neoplasm1" | "tungstenShot" | "neoplasm2" | "unitFront" | "neoplasmMid", Color>; +type ApplicationListener = Partial<{ + init(): void; + update(): void; + pause(): void; + resume(): void; + dispose(): void; + exit(): void; +}>; + +const Core: { + settings: { + get(key:string, defaultValue?:T):T; + getBytes(key:string):number[]; + getString(key:string):string | null; + getDataDirectory():Fi; + getInt(key:string, defaultValue?:number):number; + put(key:string, value:any):void; + has(key:string):boolean; + remove(key:string):void; + manualSave():void; + } + app: { + post(func:() => unknown):void; + exit():void; + getJavaHeap():number; + listeners: any[]; + addListener(listener:ApplicationListener):void; + } + graphics: { + getFramesPerSecond():number; + getDeltaTime():number; + } +}; +const Damage: { + damage(team:Team, x:number, y:number, radius:number, damage:number, pierce:boolean, air:boolean, ground:boolean):void; +}; +const Mathf: { + halfPi: number; + PI2: number; + + ceil(val:number):number; + round(val:number, step?:number):number; + random(min:number, max:number):number; + len(x:number, y:number):number; + atan2(x:number, y:number):number; + dst(x1:number, y1:number, x2:number, y2:number):number; +}; +const SaveIO: { + save(file:Fi):void; +}; +const Timer: { + schedule(func:() => unknown, delaySeconds:number, intervalSeconds?:number, repeatCount?:number):TimerTask; +}; +class TimerTask { + cancel():void; +} +const Time: { + millis(): number; + nanos(): number; + elapsed(): number; + mark(): void; + delta: number; + timeSinceMillis(millis: number): number; + setDeltaProvider(provider: () => number):void; +}; +const GameState: { + State: Record<"playing" | "paused" | "menu", any>; +}; +class HttpRequest { + submit(func:(response:HttpResponse) => void):void; + error(func:(exception:any) => void):void; + header(name:string, value:string):HttpRequest; + timeout: number; +} +class HttpResponse { + getResultAsString():string; + getResultAsStream():InputStream + getResult():number[]; +} +class InputStream { + close():void; + transferTo(outputsteam:OutputStream):number; + mark(readlimit:number):void; + reset():void; +} +class OutputStream { + close():void; +} +class DataOutputStream extends OutputStream { + constructor(stream:OutputStream); + write(b:number[]):void; + write(b:number[], offset:number, length:number):void; + write(b:number):void; + writeBoolean(v:boolean):void; + writeByte(v:number):void; + writeBytes(s:string):void; + writeChar(v:number):void; + writeChars(s:string):void; + writeDouble(v:number):void; + writeFloat(v:number):void; + writeInt(v:number):void; + writeLong(v:number):void; + writeShort(v:number):void; + writeUTF(s:string):void; +} +class DataInputStream extends InputStream { + constructor(stream:InputStream); + read(b:number[]):number; + read(b:number[], off:number, len:number):number; + readBoolean():boolean; + readByte():number; + readChar():number; + readDouble():number; + readFloat():number; + readFully(b:number[]):void; + readFully(b:number[], off:number, len:number):void; + readInt():number; + readLine():string; + readLong():number; + readShort():number; + readUnsignedByte():number; + readUnsignedShort():number; + readUTF():string; + skipBytes(n:number):number; +} +class ByteArrayOutputStream extends OutputStream { + constructor(); + toByteArray():number[]; +} +class ByteArrayInputStream extends InputStream { + constructor(bytes:number[]); +} +const Http: { + post(url:string, content:string):HttpRequest; + get(url:string):HttpRequest; + get(url:string, callback:(res:HttpResponse) => unknown, error:(err:any) => unknown):void; +}; +class Seq { + items: Array; + size: number; + constructor(); + constructor(capacity:number); + static with(...items:T[]):Seq; + static with(items:MIterable):Seq; + add(item:T):this; + addUnique(item:T):this; + contains(item:T):boolean; + contains(pred:Boolf):boolean; + count(pred:(item:T) => boolean):number; + allMatch(pred:(item:T) => boolean):boolean; + /** @deprecated Use select() or retainAll() */ + filter(pred:(item:T) => boolean):Seq; + retainAll(pred:(item:T) => boolean):Seq; + /** @returns whether an item was removed */ + remove(pred:(item:T) => boolean):boolean; + removeAll(pred:(item:T) => boolean):Seq; + select(pred:(item:T) => boolean):Seq; + find(pred:(item:T) => boolean):T | null; + each(func:(item:T) => unknown):void; + each(pred:(item:T) => boolean, func:(item:T) => unknown):void; + isEmpty():boolean; + map(mapFunc:(item:T) => R):Seq; + flatMap(mapFunc:(item:T) => Seq):Seq; + toString(separator?:string, stringifier?:(item:T) => string):string; + toArray():T[]; + copy():Seq; + sort(comparator?:(item:T) => number):Seq; + min(comparator?:Floatf):T; + max(comparator?:Floatf):T; + random():T | null; + get(index:number):T; + first():T; + peek():T; + firstOpt():T | null; + clear():void; +} + +class ObjectSet { + size:number; + select(predicate:(item:T) => boolean):ObjectSet; + each(func:(item:T) => unknown):void; + add(item:T):boolean; + remove(item:T):boolean; + isEmpty():boolean; + contains(item:T):boolean; + toSeq():Seq; + get(key:T):T; + first():T; + clear():void; + toString():string; +} +class ObjectMap { + put(key:K, value:V):void; + get(key:K):V; + containsKey(key:K):boolean; + remove(key:K):V | null; + clear():void; + size:number; + entries(): unknown; +} +class ObjectIntMap { + put(key:K, value:number):void; + get(key:K):number; + increment(key:K):void; + clear():void; + remove(key:K):number | null; + size:number; + forEach(func:(_:{key:K, value:number}) => void):void; + entries(): { + toArray():Seq>; + }; +} +class ObjectIntMapEntry { + key:K; + value:number; +} +class EntityGroup { + add(type:T):void + copy():Seq; + copy(seq:Seq):Seq; + each(func:(item:T) => unknown):void; + each(predicate:(item:T) => boolean, func:(item:T) => unknown):void; + getByID(id:number):T; + isEmpty():boolean; + size():number; + contains(pred:(item:T) => boolean):boolean; + find(pred:(item:T) => boolean):T; + first():T; + index(index:number):T; + clear():void; + //iterator():Iterator +} + +function importPackage(package:any):void; +const Packages: Record; +const EventType: Record; +type EventType = any; +type PlayerAction = { + player:mindustryPlayer; + pingText:string | null; + pingX:number; + pingY:number; + type:ActionType; + tile:Tile | null; + unit:Unit | null; +} +type ActionType = any; +const ActionType:Record; +type Unit = { + health: number; + shield: number; + maxHealth: number; + type: UnitType; + x: number; + y: number; + team: Team; + dead: boolean; + spawnedByCore: boolean; + added: boolean; + id: number; + hitSize: number; + tileOn():Tile | null; + tile?: () => Building; + kill():void; + add():void; + isAdded():boolean; + set(x: number, y:number):void; + approach(vec: Vec2):void; + hasPayload: undefined | (() => boolean); + getPlayer():Player | null; + resetController():void; + apply(effect:StatusEffect, ticks:number):void; + clearStatuses():void; + within(pos: Building | Unit, distance: number):boolean; +}; +type NetConnection = any; +class Command { + text:string; + paramText:string; + description:string; + params:any[]; +} + +/** java.io.File */ +class JavaFile { + path: string; +} +class Fi { + constructor(path:string); + file(): JavaFile; + child(path:string): Fi; + exists(): boolean; + absolutePath():string; + writeBytes(bytes:number[], append?:boolean):void; + static tempFile(prefix:string):Fi; + delete():boolean; + length():number; + lastModified():number; + write():OutputStream; + list():Fi[]; + name():string; + readBytes():number[]; +} +class Bullet { + owner: Unit | Building | null; +} +class Pattern { + static matches(regex:string, target:string):boolean; + static compile(regex:string):Pattern; + matcher(input:string):Matcher; +} +class Matcher { + replaceAll(replacement:string):string; + matches():boolean; + group(index:number):string; +} +class Runtime { + static getRuntime():Runtime; + exec(command:string, envp:string[] | null, dir:JavaFile):Process; + addShutdownHook(callback: Thread):void; +} +class Thread { + constructor(runnable: () => void); + run(): void; +} +class ProcessBuilder { + constructor(...args:string[]); + directory(file?:JavaFile):ProcessBuilder; + redirectErrorStream(value:boolean):ProcessBuilder; + redirectOutput(value:any):ProcessBuilder; + start():Process; + + static Redirect: { + PIPE: any; + INHERIT: any; + }; +} +class Process { + waitFor():void; + exitValue():number; +} + +const Packets: { + KickReason: Record<"kick" | "clientOutdated" | "serverOutdated" | "banned" | "gameover" | "recentKick" | "nameInUse" | "idInUse" | "nameEmpty" | "customClient" | "serverClose" | "vote" | "typeMismatch" | "whitelist" | "playerLimit" | "serverRestarting", KickReason>; +}; +type KickReason = { quiet: boolean }; + +class ConstructBlock { + static ConstructBuild: any; +} +class CoreBlock { + +} +const Prop: any; + +function print(message:string):void; + +class PlayerInfo { + /** uuid */ + id: string; + lastName: string; + lastIP: string; + ips: Seq; + names: Seq; + adminUsid: string | null; + timesKicked: number; + timesJoined: number; + admin: boolean; + banned: boolean; + lastKicked: number; + plainLastName(): string; +} + +class UnitType { + spawn(team:Team, x:number, y:number):Unit; + create(team:Team):Unit; + supportsEnv(env:number):boolean; + emoji():string; + health: number; + hidden: boolean; + internal: boolean; + name: string; + localizedName: string; +} +class MissileUnitType extends UnitType {} +class LogicAI { + controller: Building | null; +} +type MapTags = { + name:string; + description?:string; + author?:string; + steamid?:string; + /** JSON rules */ + rules?:string; + build?:number; + genfilters?:string; +} +class Maps { + setNextMapOverride(map:MMap | null):void; + all():Seq; + customMaps():Seq; + byName(name:string):MMap | null; + reload():void; + saveMap(baseTags:MapTags):MMap; +} +class MMap { + readonly custom:boolean; + readonly file:Fi; + width:number; + height:number; + build:number; + name():string; + author():string; + description():string; + plainName():string; + plainAuthor():string; + plainDescription():string; + rules():Rules; +} + +class Sort { + static instance():Sort; + sort(input:Seq | unknown[]):void; + sort(input:Seq | unknown[], fromIndex:number, toIndex:number):void; +} +class ServerControl { + static instance: ServerControl; + handler: CommandHandler; +} + +class VoteSession { + private target: mindustryPlayer; + private task: TimerTask; + private voted: ObjectIntMap; + private votes: number; +} + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +interface Array { + filter(predicate: BooleanConstructor, thisArg?: any): Array; +} +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +interface ReadonlyArray { + map, U>(this:TThis, fn:(v:T, i:number, a:TThis) => U): number extends TThis["length"] ? U[] : { [K in keyof TThis]: U }; +} +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +interface ObjectConstructor { + entries(input:Record):Array<[K, V]>; + fromEntries(input:Array<[K, V]>):Record; +} +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +interface SymbolConstructor { + readonly metadata: unique symbol; +} + +const Threads: { + daemon(callback:() => unknown):void; + thread(callback:() => unknown):void; +}; +const Tmp: { + //not full + v1:Vec2; + v2:Vec2; + v3:Vec2; + v4:Vec2; + v5:Vec2; + v6:Vec2; + + v31:Vec2; + v32:Vec2; + v33:Vec2; + v34:Vec2; + + c1:Color; + c2:Color; + c3:Color; + c4:Color; +}; +class EffectCallPacket2 { + effect:Effect; + x:number; + y:number; + rotation:number; + color:Color; + data:any; +} +class LabelReliableCallPacket { + message:string; + duration:number; + worldx:number; + worldy:number; +} +class ConnectPacket { + version: number; + versionType: string; + mods: Seq; + name: string; + locale: string; + uuid: string; + usid: string; + mobile: boolean; + color: number; +} + +type ByteBuffer = { + put(bytes:number[]):void; + flip():void; +}; +type MessageDigest = { + update(buffer:ByteBuffer):void; + digest():number[]; +}; + +/** java.nio.file.Paths */ +const Paths: { + get(path:string):Path; +}; +/** java.nio.file.Path */ +type Path = { + toRealPath():Path; + toString():string; + getParent():Path; +} + +/** arc.util.OS */ +const OS: { + /** + * Blocking, please run this in a thread + * @throws RuntimeException + */ + exec(...command:string[]):string; +}; +const Trigger: Record<'shock'|'cannotUpgrade'|'openConsole'|'blastFreeze'|'impactPower'|'blastGenerator'|'shockwaveTowerUse'|'forceProjectorBreak'|'thoriumReactorOverheat'|'neoplasmReact'|'fireExtinguish'|'acceleratorUse'|'newGame'|'tutorialComplete'|'flameAmmo'|'resupplyTurret'|'turretCool'|'enablePixelation'|'exclusionDeath'|'suicideBomb'|'openWiki'|'teamCoreDamage'|'socketConfigChanged'|'update'|'beforeGameUpdate'|'afterGameUpdate'|'unitCommandChange'|'unitCommandPosition'|'unitCommandAttack'|'importMod'|'draw'|'drawOver'|'preDraw'|'postDraw'|'uiDrawBegin'|'uiDrawEnd'|'universeDrawBegin'|'universeDraw'|'universeDrawEnd', Trigger>; +type Trigger = { + __brand: 'trigger'; +}; +class CommandRunner { + accept: (args:string[], parameter: T) => void; + constructor(_: {accept: (args:string[], parameter: T) => void}); +} + +class WorldReloader { + begin():void; + end():void; +} + +class Bits { + constructor(capacity?: number); + get(index:number):boolean; + /** + * @param value Default true + */ + set(index:number, value?:boolean):void; + set(index:number, value:number):void; +} + +type JavaClass = any; + +const JsonIO: { + write(object:{}): string; + read(clazz: JavaClass, data: string): T; +}; + +class Boolf { + constructor(_: {get: (value: T) => boolean}); +} +function boolf(func: (value: T) => boolean): Boolf; + +const Iconc: Record<"rotate" | "modeSurvival" | "power" | "left" | "redditAlien" | "edit" | "downOpen" | "pencil" | "file" | "lockOpen" | "right" | "infoCircle" | "pick" | "settings" | "spray1" | "terrain" | "exit" | "wrench" | "lock" | "discord" | "eye" | "none" | "play" | "diagonal" | "eraser" | "trash" | "liquid" | "fileImage" | "defense" | "layers" | "grid" | "admin" | "steam" | "star" | "chartBar" | "chat" | "android" | "image" | "map" | "logic" | "menu" | "commandRally" | "editor" | "folder" | "units" | "commandAttack" | "copy" | "filter" | "cancel" | "terminal" | "upload" | "eyeOff" | "save" | "planeOutline" | "fill" | "distribution" | "upOpen" | "rightOpen" | "modePvp" | "download" | "list" | "flipX" | "flipY" | "effect" | "paste" | "planet" | "waves" | "up" | "warning" | "tree" | "add" | "down" | "host" | "spray" | "info" | "players" | "resize" | "refresh1" | "production" | "crafting" | "pause" | "googleplay" | "hammer" | "fileText" | "modeAttack" | "move" | "zoom" | "bookOpen" | "refresh" | "ok" | "home" | "githubSquare" | "powerOld" | "github" | "undo" | "box" | "trello" | "book" | "export" | "fileTextFill" | "rightOpenOut" | "turret" | "leftOpen" | "line" | "itchio" | "link" | "filters" | "redo", number>; + +const ArcReflect: { + get(thing:any, key:string):any; + get(clazz:any, thing:any, key:string):any; + set(thing:any, key:string, value:any):void; +}; +class Ratekeeper { + occurences:number; + lastTime:number; + allow(spacingMS:number, cap:number):boolean; +} + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +interface MIterable { + iterator(): Iterator; + forEach(_:(item:T) => void):void; +} + +class AtomicInteger { + constructor(value?:number); + decrementAndGet():number; + getAndIncrement():number; + get():number; + set(int:number):void; +} + } \ No newline at end of file diff --git a/src/packetHandlers.ts b/src/packetHandlers.ts index 0ad1a939..335ff05f 100644 --- a/src/packetHandlers.ts +++ b/src/packetHandlers.ts @@ -1,340 +1,340 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the visual effects system. -Original contributor: @author TheEt1234 -Fixes: @author BalaM314 -Fixes: @author Dart25 -Fixes: @author Jurorno9 -*/ - -import { Perm, commandList } from "/frameworks/commands"; -import { FishPlayer } from "/players"; - -//some much needed restrictions -/** point in which effects will refuse to render */ -const MIN_EFFECT_TPS = 20; -/** maximum duration for user-created labels (seconds) */ -const MAX_LABEL_TIME = 20; - -//info tracker -let lastLabel = ''; -let lastAccessedBulkLabel:FishPlayer | null = null; -let lastAccessedLabel:FishPlayer | null = null; -let lastAccessedBulkLine:FishPlayer | null = null; -let lastAccessedLine:FishPlayer | null = null; - -const bulkLimit = 1000; - -const noPermissionText = "[red]You don't have permission to use this packet."; -const invalidContentText = '[red]Invalid label content.'; -const tooLongText = '[red]Bulk content length exceeded, please use fewer effects.'; -const bulkSeparator = '|'; -const procError = '[red]An error occured while processing your request.'; -const invalidReq = '[red]Invalid request. Please consult the documentation.'; -const lowTPSError = '[red]Low server TPS, skipping request.'; - -const tmpLinePacket = new EffectCallPacket2(); -const tmpLabelPacket = new LabelReliableCallPacket(); - -export function loadPacketHandlers() { - //initialize line packet - tmpLinePacket.effect = Fx.pointBeam; - tmpLinePacket.rotation = 0.0; - tmpLinePacket.color = Tmp.c1; - tmpLinePacket.data = Tmp.v1; - - //labels - - //fmt: "content,duration,x,y" - Vars.netServer.addPacketHandler('label', (player:mindustryPlayer, content:string) => { - const p = FishPlayer.get(player); - try { - if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm("visualEffects")) { - p.sendMessage(noPermissionText, 1000); - return; - } - - lastAccessedLabel = p; - - handleLabel(player, content, true); - } catch { - p.sendMessage(procError, 1000); - } - }); - - Vars.netServer.addPacketHandler('bulkLabel', (player:mindustryPlayer, content:string) => { - const p = FishPlayer.get(player); - try { - if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm('bulkVisualEffects')) { - p.sendMessage(noPermissionText, 1000); - return; - } - - lastAccessedBulkLabel = p; - - //get individual labels - const labels:string[] = []; - let inQuotes = false; - let startIdx = 0; - - for (let i = 0; i < content.length; i++) { - switch (content[i]) { - case '"': - if (i > 0 && content[i-1] == '\\') break; - inQuotes = !inQuotes; - break; - //separate - case bulkSeparator: - if (inQuotes) break; - - labels.push(content.substring(startIdx, i)); - startIdx = i + 1; - break; - default: - break; - } - } - - //last label - if (startIdx < content.length) { - labels.push(content.substring(startIdx, content.length - 1)); - } - - if(labels.length > bulkLimit){ - p.sendMessage(tooLongText, 1000); - return; - } - - //display labels - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < labels.length; i++) { - const label = labels[i]; - if (label.trim().length <= 0) continue; - if (!handleLabel(player, label, false)) return; - } - } catch { - p.sendMessage(procError, 1000); - } - }); - - //lines - Vars.netServer.addPacketHandler('lineEffect', (player:mindustryPlayer, content:string) => { - const p = FishPlayer.get(player); - try { - if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm("visualEffects")) { - p.sendMessage(noPermissionText, 1000); - return; - } - - if (!handleLine(content, player)) return; - lastAccessedLine = p; - } catch { - p.sendMessage(procError, 1000); - } - }); - - //this is the silas effect but it's way too real - Vars.netServer.addPacketHandler('bulkLineEffect', (player:mindustryPlayer, content:string) => { - const p = FishPlayer.get(player); - if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ - p.sendMessage(lowTPSError, 1000); - return; - } - if (!p.hasPerm('bulkVisualEffects')) { - p.sendMessage(noPermissionText, 1000); - return; - } - try { - - const lines = content.split(bulkSeparator); - - if(lines.length > bulkLimit){ - p.sendMessage(tooLongText, 1000); - return; - } - - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.trim().length <= 0) continue; - if (!handleLine(line, player)) return; - } - - lastAccessedBulkLine = p; - } catch { - p.sendMessage(procError, 1000); - } - }); -} - -//commands -export const commands = commandList({ - pklast: { - args: [], - description: 'Tells you who last accessed the packet handlers.', - perm: Perm.none, - handler({output}) { - const outputLines:string[] = []; - - if (lastAccessedLabel && lastLabel) { - outputLines.push(`${lastAccessedLabel.name}[white] created label "${lastLabel}".`); - } - if (lastAccessedBulkLabel) { - outputLines.push(`${lastAccessedBulkLabel.name}[white] last used the bulk label effect.`); - } - if (lastAccessedLine) { - outputLines.push(`${lastAccessedLine.name}[white] last used the line effect.`); - } - if (lastAccessedBulkLine) { - outputLines.push(`${lastAccessedBulkLine.name}[white] last used the bulk line effect.`); - } - - output(outputLines.length > 0 ? outputLines.join('\n') : 'No packet handlers have been accessed yet.'); - } - }, - pkdocs: { - description: 'Packet handler documentation.', - args: [], - perm: Perm.none, - handler({sender, output}){ - output( -` [blue]FISH[white] Packet Handler Docs -[white]Usage:[accent] - - Run the javascript function "Call.serverPacketReliable()" to send these. (!js in foos) - - You need to multiply world coordinates by Vars.tilesize (8) for things to work properly. This is a relic from the v3 days where every tile was 8 pixels. - -[white]Packet types[accent]: - - Line effect: "lineEffect", "x0,y0,x1,y1,hexColor" (for example "20.7,19.3,50.4,28.9,#FF0000") - - Bulk line effect: "bulkLineEffect", equivalent to multiple lineEffect packets, with every line separated by a '|' symbol. - - Label effect: "label", "content,duration,x,y" (for example ""Hi!",10,20,28") - - Bulk label effect: "bulkLabel", equivalent to multiple label packets, with every label separated by a '|' symbol. - -[white]Limitations[accent]: - - You ${(sender.hasPerm('bulkVisualEffects')?(`[green]have been granted[accent]`):(`[red]do not have[accent]`))} access to bulk effects. - - Effects will no longer be drawn at ${MIN_EFFECT_TPS} for server preformance. - - Labels cannot last longer than ${MAX_LABEL_TIME} seconds. - - There is a set ratelimit for sending packets, be careful ... - -[white]Starter Example[accent]: - - To place a label saying "hello" at (0,0); - Foos users: [lightgray]!js Call.serverPacketReliable("label", ["\\"hello\\"", 10, 0, 0].join(","))[accent] - newConsole users: [lightgrey]Call.serverPacketReliable("label", ["hello", 10, 0, 10].join(","))[accent] - -[white]Comments and Credits[accent]: - - 'These packet handlers and everything related to them were made by [green]frog[accent]. - - 'The code style when submitted was beyond drunk... but it worked... barely' -BalaM314 - - "worst error handling i have ever seen, why kick the player???" -ASimpleBeginner' - - Most of the code was rewritten in 2024 by [#6e00fb]D[#9e15de]a[#cd29c2]r[#fd3ea5]t[accent].' - - Small tweaks by [#00cf]s[#00bf]w[#009f]a[#007f]m[#005f]p[accent]`); - } - } -}); - -//#region utils - -function findEndQuote(content:string, startPos:number) { - if (content[startPos] != '"') { - //not a start quote?? - return -1; - } - - for (let i = startPos + 1; i < content.length; i++) { - if (content[i] == '"' && (i < 1 || content[i-1] != '\\')) { - return i; - } - } - - return -1; -} - -function handleLabel(player:mindustryPlayer, content:string, isSingle:boolean):boolean { - const endPos = findEndQuote(content, 0); - if (endPos == -1) { - //invalid content - player.sendMessage(invalidContentText); - return false; - } - - //label, clean up \"s - const message = content.substring(1, endPos).replace('\\"', '"'); - const parts = content.substring(endPos + 2).split(','); - - if (parts.length != 3) { //dur,x,y - player.sendMessage(invalidReq); - return false; - } - - if(isSingle && Strings.stripColors(message).length > 150){ - player.sendMessage('Label too large. Maximum is 150 characters, not including color tags.'); - } - - if (isSingle) { - lastLabel = message; - } - - const duration = Number(parts[0]); - const x = Number(parts[1]), y = Number(parts[2]); - if(Number.isNaN(duration) || duration > MAX_LABEL_TIME || Number.isNaN(x) || Number.isNaN(y)){ - player.sendMessage(invalidReq); - return false; - } - - /*Call.labelReliable( - message, //message - Number(parts[0]), //duration - Number(parts[1]), //x - Number(parts[2]) //y - );*/ - tmpLabelPacket.message = message; - tmpLabelPacket.duration = duration; - tmpLabelPacket.worldx = x; - tmpLabelPacket.worldy = y; - Vars.net.send(tmpLabelPacket, false); - return true; -} - -function handleLine(content:string, player:mindustryPlayer):boolean { - const parts = content.split(','); - - if (parts.length != 5) { //x0,y0,x1,y1,color - player.sendMessage(invalidReq); - return false; - } - - Tmp.v1.set(Number(parts[2]), Number(parts[3])); //x1,y1 - Color.valueOf(Tmp.c1, parts[4]); //color - - /*Call.effect( - Fx.pointBeam, - Number(parts[0]), Number(parts[1]), //x,y - 0, Tmp.c1, //color - Tmp.v1 //x1,y1 - );*/ - tmpLinePacket.x = Number(parts[0]); - tmpLinePacket.y = Number(parts[1]); - Vars.net.send(tmpLinePacket, false); - - return true; -} - -export function bulkInfoMsg(messages:string[], conn:NetConnection) { - for (let i = messages.length - 1; i >= 0; i--) { - Call.infoMessage(conn, messages[i]); - } -} - - +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the visual effects system. +Original contributor: @author TheEt1234 +Fixes: @author BalaM314 +Fixes: @author Dart25 +Fixes: @author Jurorno9 +*/ + +import { Perm, commandList } from "/frameworks/commands"; +import { FishPlayer } from "/players"; + +//some much needed restrictions +/** point in which effects will refuse to render */ +const MIN_EFFECT_TPS = 20; +/** maximum duration for user-created labels (seconds) */ +const MAX_LABEL_TIME = 20; + +//info tracker +let lastLabel = ''; +let lastAccessedBulkLabel:FishPlayer | null = null; +let lastAccessedLabel:FishPlayer | null = null; +let lastAccessedBulkLine:FishPlayer | null = null; +let lastAccessedLine:FishPlayer | null = null; + +const bulkLimit = 1000; + +const noPermissionText = "[red]You don't have permission to use this packet."; +const invalidContentText = '[red]Invalid label content.'; +const tooLongText = '[red]Bulk content length exceeded, please use fewer effects.'; +const bulkSeparator = '|'; +const procError = '[red]An error occured while processing your request.'; +const invalidReq = '[red]Invalid request. Please consult the documentation.'; +const lowTPSError = '[red]Low server TPS, skipping request.'; + +const tmpLinePacket = new EffectCallPacket2(); +const tmpLabelPacket = new LabelReliableCallPacket(); + +export function loadPacketHandlers() { + //initialize line packet + tmpLinePacket.effect = Fx.pointBeam; + tmpLinePacket.rotation = 0.0; + tmpLinePacket.color = Tmp.c1; + tmpLinePacket.data = Tmp.v1; + + //labels + + //fmt: "content,duration,x,y" + Vars.netServer.addPacketHandler('label', (player:mindustryPlayer, content:string) => { + const p = FishPlayer.get(player); + try { + if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm("visualEffects")) { + p.sendMessage(noPermissionText, 1000); + return; + } + + lastAccessedLabel = p; + + handleLabel(player, content, true); + } catch { + p.sendMessage(procError, 1000); + } + }); + + Vars.netServer.addPacketHandler('bulkLabel', (player:mindustryPlayer, content:string) => { + const p = FishPlayer.get(player); + try { + if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm('bulkVisualEffects')) { + p.sendMessage(noPermissionText, 1000); + return; + } + + lastAccessedBulkLabel = p; + + //get individual labels + const labels:string[] = []; + let inQuotes = false; + let startIdx = 0; + + for (let i = 0; i < content.length; i++) { + switch (content[i]) { + case '"': + if (i > 0 && content[i-1] == '\\') break; + inQuotes = !inQuotes; + break; + //separate + case bulkSeparator: + if (inQuotes) break; + + labels.push(content.substring(startIdx, i)); + startIdx = i + 1; + break; + default: + break; + } + } + + //last label + if (startIdx < content.length) { + labels.push(content.substring(startIdx, content.length - 1)); + } + + if(labels.length > bulkLimit){ + p.sendMessage(tooLongText, 1000); + return; + } + + //display labels + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < labels.length; i++) { + const label = labels[i]; + if (label.trim().length <= 0) continue; + if (!handleLabel(player, label, false)) return; + } + } catch { + p.sendMessage(procError, 1000); + } + }); + + //lines + Vars.netServer.addPacketHandler('lineEffect', (player:mindustryPlayer, content:string) => { + const p = FishPlayer.get(player); + try { + if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm("visualEffects")) { + p.sendMessage(noPermissionText, 1000); + return; + } + + if (!handleLine(content, player)) return; + lastAccessedLine = p; + } catch { + p.sendMessage(procError, 1000); + } + }); + + //this is the silas effect but it's way too real + Vars.netServer.addPacketHandler('bulkLineEffect', (player:mindustryPlayer, content:string) => { + const p = FishPlayer.get(player); + if(Core.graphics.getFramesPerSecond() < MIN_EFFECT_TPS){ + p.sendMessage(lowTPSError, 1000); + return; + } + if (!p.hasPerm('bulkVisualEffects')) { + p.sendMessage(noPermissionText, 1000); + return; + } + try { + + const lines = content.split(bulkSeparator); + + if(lines.length > bulkLimit){ + p.sendMessage(tooLongText, 1000); + return; + } + + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.trim().length <= 0) continue; + if (!handleLine(line, player)) return; + } + + lastAccessedBulkLine = p; + } catch { + p.sendMessage(procError, 1000); + } + }); +} + +//commands +export const commands = commandList({ + pklast: { + args: [], + description: 'Tells you who last accessed the packet handlers.', + perm: Perm.none, + handler({output}) { + const outputLines:string[] = []; + + if (lastAccessedLabel && lastLabel) { + outputLines.push(`${lastAccessedLabel.name}[white] created label "${lastLabel}".`); + } + if (lastAccessedBulkLabel) { + outputLines.push(`${lastAccessedBulkLabel.name}[white] last used the bulk label effect.`); + } + if (lastAccessedLine) { + outputLines.push(`${lastAccessedLine.name}[white] last used the line effect.`); + } + if (lastAccessedBulkLine) { + outputLines.push(`${lastAccessedBulkLine.name}[white] last used the bulk line effect.`); + } + + output(outputLines.length > 0 ? outputLines.join('\n') : 'No packet handlers have been accessed yet.'); + } + }, + pkdocs: { + description: 'Packet handler documentation.', + args: [], + perm: Perm.none, + handler({sender, output}){ + output( +` [blue]FISH[white] Packet Handler Docs +[white]Usage:[accent] + - Run the javascript function "Call.serverPacketReliable()" to send these. (!js in foos) + - You need to multiply world coordinates by Vars.tilesize (8) for things to work properly. This is a relic from the v3 days where every tile was 8 pixels. + +[white]Packet types[accent]: + - Line effect: "lineEffect", "x0,y0,x1,y1,hexColor" (for example "20.7,19.3,50.4,28.9,#FF0000") + - Bulk line effect: "bulkLineEffect", equivalent to multiple lineEffect packets, with every line separated by a '|' symbol. + - Label effect: "label", "content,duration,x,y" (for example ""Hi!",10,20,28") + - Bulk label effect: "bulkLabel", equivalent to multiple label packets, with every label separated by a '|' symbol. + +[white]Limitations[accent]: + - You ${(sender.hasPerm('bulkVisualEffects')?(`[green]have been granted[accent]`):(`[red]do not have[accent]`))} access to bulk effects. + - Effects will no longer be drawn at ${MIN_EFFECT_TPS} for server preformance. + - Labels cannot last longer than ${MAX_LABEL_TIME} seconds. + - There is a set ratelimit for sending packets, be careful ... + +[white]Starter Example[accent]: + + To place a label saying "hello" at (0,0); + Foos users: [lightgray]!js Call.serverPacketReliable("label", ["\\"hello\\"", 10, 0, 0].join(","))[accent] + newConsole users: [lightgrey]Call.serverPacketReliable("label", ["hello", 10, 0, 10].join(","))[accent] + +[white]Comments and Credits[accent]: + - 'These packet handlers and everything related to them were made by [green]frog[accent]. + - 'The code style when submitted was beyond drunk... but it worked... barely' -BalaM314 + - "worst error handling i have ever seen, why kick the player???" -ASimpleBeginner' + - Most of the code was rewritten in 2024 by [#6e00fb]D[#9e15de]a[#cd29c2]r[#fd3ea5]t[accent].' + - Small tweaks by [#00cf]s[#00bf]w[#009f]a[#007f]m[#005f]p[accent]`); + } + } +}); + +//#region utils + +function findEndQuote(content:string, startPos:number) { + if (content[startPos] != '"') { + //not a start quote?? + return -1; + } + + for (let i = startPos + 1; i < content.length; i++) { + if (content[i] == '"' && (i < 1 || content[i-1] != '\\')) { + return i; + } + } + + return -1; +} + +function handleLabel(player:mindustryPlayer, content:string, isSingle:boolean):boolean { + const endPos = findEndQuote(content, 0); + if (endPos == -1) { + //invalid content + player.sendMessage(invalidContentText); + return false; + } + + //label, clean up \"s + const message = content.substring(1, endPos).replace('\\"', '"'); + const parts = content.substring(endPos + 2).split(','); + + if (parts.length != 3) { //dur,x,y + player.sendMessage(invalidReq); + return false; + } + + if(isSingle && Strings.stripColors(message).length > 150){ + player.sendMessage('Label too large. Maximum is 150 characters, not including color tags.'); + } + + if (isSingle) { + lastLabel = message; + } + + const duration = Number(parts[0]); + const x = Number(parts[1]), y = Number(parts[2]); + if(Number.isNaN(duration) || duration > MAX_LABEL_TIME || Number.isNaN(x) || Number.isNaN(y)){ + player.sendMessage(invalidReq); + return false; + } + + /*Call.labelReliable( + message, //message + Number(parts[0]), //duration + Number(parts[1]), //x + Number(parts[2]) //y + );*/ + tmpLabelPacket.message = message; + tmpLabelPacket.duration = duration; + tmpLabelPacket.worldx = x; + tmpLabelPacket.worldy = y; + Vars.net.send(tmpLabelPacket, false); + return true; +} + +function handleLine(content:string, player:mindustryPlayer):boolean { + const parts = content.split(','); + + if (parts.length != 5) { //x0,y0,x1,y1,color + player.sendMessage(invalidReq); + return false; + } + + Tmp.v1.set(Number(parts[2]), Number(parts[3])); //x1,y1 + Color.valueOf(Tmp.c1, parts[4]); //color + + /*Call.effect( + Fx.pointBeam, + Number(parts[0]), Number(parts[1]), //x,y + 0, Tmp.c1, //color + Tmp.v1 //x1,y1 + );*/ + tmpLinePacket.x = Number(parts[0]); + tmpLinePacket.y = Number(parts[1]); + Vars.net.send(tmpLinePacket, false); + + return true; +} + +export function bulkInfoMsg(messages:string[], conn:NetConnection) { + for (let i = messages.length - 1; i >= 0; i--) { + Call.infoMessage(conn, messages[i]); + } +} + + //#endregion \ No newline at end of file diff --git a/src/players.ts b/src/players.ts index c05118bb..9a426fdd 100644 --- a/src/players.ts +++ b/src/players.ts @@ -1,1688 +1,1688 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the FishPlayer class, and many player-related functions. -*/ - -import * as api from "/api"; -import { FColor, Gamemode, heuristics, Mode, prefixes, rules, stopAntiEvadeTime, text, tips } from "/config"; -import { FishCommandArgType, Perm, PermType } from "/frameworks/commands"; -import { Menu } from "/frameworks/menus"; -import { crash, Duration, escapeStringColorsClient, escapeStringColorsServer, escapeTextDiscord, parseError, search, setToArray, StringIO } from "/funcs"; -import * as globals from "/globals"; -import { FishEvents, uuidPattern } from "/globals"; -import { PartialMapRun } from "/maps"; -import { Rank, RankName, RoleFlag, RoleFlagName } from "/ranks"; -import type { FishPlayerData, PlayerHistoryEntry, Stats, UploadedFishPlayerData } from "/types"; -import { cleanText, formatTime, formatTimeRelative, isImpersonator, logAction, logHTrip, matchFilter, updateBans } from "/utils"; - - -export class FishPlayer { - //#region Static constants - /** Save version used for serialized FishPlayers. */ - static readonly saveVersion = 12; - /** Maximum chunk size used when writing FishPlayer data to Core.settings. */ - static readonly chunkSize = 50000; - //#endregion - - //#region Static transients - /** Stores all currently loaded FishPlayer objects. */ - static cachedPlayers:Record = {}; - static stats = { - numIpsChecked: 0, - numIpsFlagged: 0, - numIpsErrored: 0, - }; - /** The last player that was kicked due to a USID mismatch. */ - static lastAuthKicked:FishPlayer | null = null; - /** - * List of IPs that were recently punished. - * If a new account joins from one of these IPs, - * we assume they are trying to evade the punishment - * and the IP gets banned. - */ - static punishedIPs = [] as Array<[ip:string, uuid:string, expiryTime:number]>; - static lastMapStartTime = 0; - /** Stores the 10 most recent players that left. */ - static recentLeaves:FishPlayer[] = []; - //Used for the antibot. Some of these values are reset by timers. - static antibotExpires = -1; - static lastAntibotReason = ""; - static autoflagRate = new Ratekeeper(); - static connectRate = new Ratekeeper(); - static votekickActionRate = new Ratekeeper(); - static lastVKActions = [] as Array<{ - type: "vote y" | "start"; - player: FishPlayer; - playerSusLevel: 0 | 1 | 2 | 3; - time: number; - target: mindustryPlayer; - targetSusLevel: 0 | 1 | 2 | 3; - reason?: string; - }>; - //#endregion - - //#region Transient properties - //Commands framework - /** Front-to-back queue of menus to show. */ - activeMenus: Array<{ - callback: (option:number) => void; - }> = []; - /** Mapping from command to usage data. */ - usageData: Record = {}; - tapInfo = { - commandName: null as string | null, - lastArgs: {} as Record, - mode: "once" as "once" | "on", - }; - //Misc - player:mindustryPlayer | null = null; - /** Used for the /trail command. */ - trail: { - type: string; - color: Color; - } | null = null; - cleanedName:string = "Unnamed player [ERROR}"; - prefixedName:string = "Unnamed player [ERROR}"; - /** Used to freeze players when votekicking. */ - frozen:boolean = false; - /** Used to avoid spamming players with ads by the tip message system */ - lastShownAd:number = globals.maxTime; - /** Used to avoid spamming players with ads by the tip message system */ - showAdNext:boolean = false; - /** Transient statistics, used by the automatic griefer detection. */ - tstats = { - //remember to clear this in updateSavedInfoFromPlayer! - blocksBroken: 0, - blockInteractionsThisMap: 0, - lastMapStartTime: 0, - lastMapPlayedTime: 0, - wavesSurvived: 0, - }; - /** Whether the player has manually marked themselves as AFK. */ - manualAfk = false; - //Used for AFK detection. - lastMousePosition = [0, 0] as [x:number, y:number]; - lastUnitPosition = [0, 0] as [x:number, y:number]; - lastActive:number = Date.now(); - /** Set this to false to disable automatic name updates. Used for the rename console command. */ - shouldUpdateName = true; - /** Used by the sendMessage() ratelimit system. */ - lastRatelimitedMessage = -1; - /** Keeps track of whether a player has changed team this match, for win rate calculation. */ - changedTeam = false; - /** Whether the player's IP was detected as a VPN. */ - ipDetectedVpn = false; - /** - * If a player's IP is detected as a VPN on their first join, - * they are autoflagged and cannot build or talk in chat. - */ - autoflagged = false; - /** Timestamp until which this player will not be allowed to control units. */ - blockedFromPossessingUnitsUntil = 0; - /** Timestamp until which this player will not be allowed to control units. */ - blockedFromCommandingUnitsUntil = 0; - /** The original name that this player used to join the server. */ - originalName?: string; - // Used by the data syncing framework. - infoUpdated = false; - dataSynced = false; - restoreTeam = null as null | [team:Team, timestamp:number, runStartTime:number]; - //#endregion - - //#region Stored data - uuid: string; - name: string = "Unnamed player [ERROR}"; - muted: boolean = false; - unmarkTime: number = -1; - rank: Rank = Rank.player; - flags = new Set(); - /** Used to color chat messages for the member command */ - highlight: string | null = null; - /** Used to color the player's name for the member command */ - rainbow: { - speed: number; - } | null = null; - /** List of all moderation actions that have been performed on this player. */ - history: PlayerHistoryEntry[] = []; - /** - * The USID for this player. - * USID stands for Unique Server IDentifier. It is like a UUID, but unique to each server (by IP and port). - * It cannot be viewed by admins and it cannot be obtained by other servers. - */ - usid: string | null = null; - /** If chat strictness is set to "strict", the player will not be allowed to swear. */ - chatStrictness: "chat" | "strict" = "chat"; - /** -1 represents unknown */ - lastJoined:number = -1; - /** -1 represents unknown */ - firstJoined:number = -1; - /** -1 represents unknown */ - globalLastJoined:number = -1; - /** -1 represents unknown */ - globalFirstJoined:number = -1; - stats: Stats = { - blocksBroken: 0, - blocksPlaced: 0, - timeInGame: 0, - chatMessagesSent: 0, - gamesFinished: 0, - gamesWon: 0, - }; - globalStats: Stats = this.stats; - /** Used for the /vanish command. */ - showRankPrefix:boolean = true; - achievements: Bits = new Bits(); - //#endregion - - constructor(uuid:string, data:Partial, player:mindustryPlayer | null){ - this.uuid = uuid; - this.player = player; - this.updateData(data); - } - - //#region getplayer - //Contains methods used to get FishPlayer instances. - static createFromPlayer(player:mindustryPlayer){ - return new this(player.uuid(), {}, player); - } - static createFromInfo(playerInfo:PlayerInfo){ - return new this(playerInfo.id, { - uuid: playerInfo.id, - name: playerInfo.lastName, - usid: playerInfo.adminUsid ?? null - }, null); - } - static getFromInfo(this:void, playerInfo:PlayerInfo){ - return FishPlayer.cachedPlayers[playerInfo.id] ??= FishPlayer.createFromInfo(playerInfo); - } - static get(this:void, player:mindustryPlayer):FishPlayer { - return FishPlayer.cachedPlayers[player.uuid()] ??= FishPlayer.createFromPlayer(player); - } - static resolve(this:void, player:mindustryPlayer | FishPlayer):FishPlayer { - if(player instanceof FishPlayer) return player; - else return FishPlayer.cachedPlayers[player.uuid()] ??= FishPlayer.createFromPlayer(player); - } - static getById(id:string):FishPlayer | null { - return this.cachedPlayers[id] ?? null; - } - /** Returns the FishPlayer representing the first online player matching a given name. */ - static getByName(name:string):FishPlayer | null { - if(name == "") return null; - const realPlayer = Groups.player.find(p => { - return p.name === name || - p.name.includes(name) || - p.name.toLowerCase().includes(name.toLowerCase()) || - Strings.stripColors(p.name).toLowerCase() === name.toLowerCase() || - Strings.stripColors(p.name).toLowerCase().includes(name.toLowerCase()) || - false; - }); - return realPlayer ? this.get(realPlayer) : null; - }; - - /** Returns the FishPlayers representing all online players matching a given name. */ - static getAllByName(name:string, strict = true):FishPlayer[] { - if(name == "") return []; - const output:FishPlayer[] = []; - Groups.player.each(p => { - const fishP = FishPlayer.get(p); - if(fishP.connected() && fishP.cleanedName.includes(name) || (!strict && fishP.cleanedName.toLowerCase().includes(name))) - output.push(fishP); - }); - return output; - } - static search = search( - (p, str) => p.uuid === str, - (p, str) => p.player!.id.toString() === str, - (p, str) => p.name.toLowerCase() === str.toLowerCase(), - // (p, str) => p.cleanedName === str, - (p, str) => p.cleanedName.toLowerCase() === str.toLowerCase(), - (p, str) => p.name.toLowerCase().includes(str.toLowerCase()), - // (p, str) => p.cleanedName.includes(str), - (p, str) => p.cleanedName.toLowerCase().includes(str.toLowerCase()), - ); - static getOneMindustryPlayerByName(str:string):mindustryPlayer | "none" | "multiple" { - if(str == "") return "none"; - const players = setToArray(Groups.player); - let matchingPlayers:mindustryPlayer[]; - - const filters:Array<(p:mindustryPlayer) => boolean> = [ - p => p.name === str, - // p => Strings.stripColors(p.name) === str, - p => Strings.stripColors(p.name).toLowerCase() === str.toLowerCase(), - // p => p.name.includes(str), - p => p.name.toLowerCase().includes(str.toLowerCase()), - p => Strings.stripColors(p.name).includes(str), - p => Strings.stripColors(p.name).toLowerCase().includes(str.toLowerCase()), - ]; - - for(const filter of filters){ - matchingPlayers = players.filter(filter); - if(matchingPlayers.length == 1) return matchingPlayers[0]; - else if(matchingPlayers.length > 1) return "multiple"; - } - return "none"; - } - //This method exists only because there is no easy way to turn an entitygroup into an array - static getAllOnline(){ - const players:FishPlayer[] = []; - Groups.player.each((p:mindustryPlayer) => { - const fishP = FishPlayer.get(p); - if(fishP.connected()) players.push(fishP); - }); - return players; - } - /** Returns all cached FishPlayers with names matching the search string. */ - static getAllOfflineByName(name:string){ - const matching:FishPlayer[] = []; - for(const [uuid, player] of Object.entries(this.cachedPlayers)){ - if(player.cleanedName.toLowerCase().includes(name)) matching.push(player); - } - return matching; - } - //#endregion - - //#region datasync - //Please see docs/data-management.md for a description of the update syncing algorithm. - static dataFetchFailedUuids = new Set(); - static onConnectPacket({uuid, name}:ConnectPacket){ - const entry = this.cachedPlayers[uuid]; - if(entry){ - entry.infoUpdated = false; - entry.dataSynced = false; - entry.name = name; - } - api.getFishPlayerData(uuid).then(data => { - if(!data) return; //nothing to sync - let fishP; - if(!(uuid in this.cachedPlayers)){ - fishP = new FishPlayer(uuid, data, null); - fishP.originalName = name; - fishP.dataSynced = true; - this.cachedPlayers[uuid] = fishP; - } else { - fishP = this.cachedPlayers[uuid]; - fishP.dataSynced = true; - fishP.updateData(data); - if(fishP.infoUpdated){ - //Player has already connected - //Run it again - if(fishP.player) fishP.updateSavedInfoFromPlayer(fishP.player, true); - } else { - //Player has not connected yet, nothing further needed - } - } - if(fishP.connected()){ - fishP.checkUsid(); - fishP.updateMemberExclusiveState(); - fishP.updateName(); - fishP.updateAdminStatus(); - fishP.updateAutoflaggedStatus(); - fishP.checkAutoRanks(); - fishP.sendWelcomeMessage(); - } - }, () => { - const fishP = this.cachedPlayers[uuid]; - fishP.updateAdminStatus(); - fishP.updateAutoflaggedStatus(); - fishP.sendWelcomeMessage(); - if(fishP?.player) fishP.player.sendMessage(text.dataFetchFailed); - else this.dataFetchFailedUuids.add(uuid); - }); - } - /** Must be called at player join, before updateName(). */ - updateSavedInfoFromPlayer(player:mindustryPlayer, repeated = false){ - this.player = player; - if(repeated){ - this.name = this.originalName!; - } else { - this.originalName = this.name = player.name; - } - if(this.firstJoined < 1) this.firstJoined = Date.now(); - - //Do not update USID here - this.manualAfk = false; - this.cleanedName = Strings.stripColors(player.name); - this.lastJoined = Date.now(); - this.lastMousePosition = [0, 0]; - this.lastActive = Date.now(); - if(this.highlight === "[white]") this.highlight = null; - this.shouldUpdateName = true; - this.changedTeam = false; - this.ipDetectedVpn = false; - this.tstats.blocksBroken = 0; - if(this.tstats.lastMapPlayedTime != FishPlayer.lastMapStartTime){ - this.tstats.blockInteractionsThisMap = 0; - this.tstats.lastMapPlayedTime = FishPlayer.lastMapStartTime; - } - this.infoUpdated = true; - } - updateData(data: Partial){ - if(data.name != undefined) this.name = data.name; - if(data.muted != undefined) this.muted = data.muted; - if(data.unmarkTime != undefined) this.unmarkTime = data.unmarkTime; - if(data.lastJoined != undefined) this.lastJoined = data.lastJoined; - if(data.firstJoined != undefined) this.firstJoined = data.firstJoined; - if(data.globalLastJoined != undefined) this.globalLastJoined = data.globalLastJoined; - if(data.globalFirstJoined != undefined) this.globalFirstJoined = data.globalFirstJoined; - if(data.highlight != undefined) this.highlight = data.highlight; - if(data.history != undefined) this.history = data.history; - if(data.rainbow != undefined) this.rainbow = data.rainbow; - if(data.usid != undefined) this.usid = data.usid; - if(data.chatStrictness != undefined) this.chatStrictness = data.chatStrictness; - if(data.stats != undefined) this.stats = data.stats; - if(data.globalStats != undefined) this.globalStats = data.globalStats; - if(data.showRankPrefix != undefined) this.showRankPrefix = data.showRankPrefix; - if(data.rank != undefined) this.rank = Rank.getByName(data.rank) ?? Rank.player; - if(data.flags != undefined) this.flags = new Set(data.flags.map(RoleFlag.getByName).filter(Boolean)); - if(data.achievements != undefined) this.achievements = JsonIO.read(Bits, `{bits:${data.achievements}}`); - } - getData():UploadedFishPlayerData { - const { uuid, name, muted, unmarkTime, rank, flags, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix } = this; - return { - uuid, name, muted, unmarkTime, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix, - rank: rank.name, - flags: [...flags.values()].map(f => f.name), - achievements: JsonIO.write(Reflect.get(this.achievements, "bits")) - }; - } - /** Warning: the "update" callback is run twice. */ - async updateSynced( - update: (fishP:FishPlayer) => void, - beforeFetch?: (fishP:FishPlayer) => void, - afterFetch?: (fishP:FishPlayer) => void, - ){ - update(this); - beforeFetch?.(this); - const data = await api.getFishPlayerData(this.uuid); - if(data) this.updateData(data); - update(this); - //of course, this is a race condition - //but it's unlikely to happen - //could be fixed by transmitting the update operation to the server as a mongo update command - afterFetch?.(this); - await api.setFishPlayerData(this.getData(), 1, false); - } - //#endregion - - //#region actively synced data updates - stop(by:FishPlayer | string, duration:number, message?:string, notify = true){ - if(duration > 60_000) this.setPunishedIP(stopAntiEvadeTime); - this.showRankPrefix = true; - return this.updateSynced(() => { - this.unmarkTime = Date.now() + duration; - if(this.unmarkTime > globals.maxTime) this.unmarkTime = globals.maxTime; - this.updateName(); - }, () => { - this.setUnmarkTimer(duration); - if(this.connected() && notify){ - this.stopUnit(); - this.sendMessage( - message - ? `[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer for reason: [white]${message}[]` - : `[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer.`); - if(duration < Duration.hours(1)){ - //less than one hour - this.sendMessage(`[yellow]Your mark will expire in ${formatTime(duration)}.`); - } - } - }, () => this.addHistoryEntry({ - action: 'stopped', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - })); - } - free(by:FishPlayer | string){ - by ??= "console"; - - this.autoflagged = false; //Might as well set autoflagged to false - FishPlayer.removePunishedIP(this.ip()); - FishPlayer.removePunishedUUID(this.uuid); - return this.updateSynced(() => { - this.unmarkTime = -1; - }, () => { - if(this.connected()){ - this.sendMessage('[yellow]Looks like someone had mercy on you.'); - this.updateName(); - this.forceRespawn(); - } - }, () => this.addHistoryEntry({ - action: 'freed', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - })); - } - async setRank(rank:Rank){ - if(typeof rank === "string" || !rank){ - rank satisfies never; - crash(`Type error in FishPlayer.setFlag(): rank is invalid`); - } - if(rank == Rank.pi && !Mode.localDebug) throw new TypeError(`Cannot find function setRank in object [object Object].`); - await this.updateSynced(() => { - this.rank = rank; - this.updateName(); - this.updateAdminStatus(); - }, () => FishPlayer.saveAll()); - } - async setFlag(flag_:RoleFlag | RoleFlagName, value:boolean){ - const flag = typeof flag_ == "string" ? - (RoleFlag.getByName(flag_)) - : flag_; - - // eslint-disable-next-line @typescript-eslint/no-base-to-string - if(!flag) crash(`Type error in FishPlayer.setFlag(): flag ${String(flag_)} is invalid`); - - await this.updateSynced(() => { - if(value){ - this.flags.add(flag); - } else { - this.flags.delete(flag); - } - this.updateMemberExclusiveState(); - this.updateName(); - }); - } - mute(by:FishPlayer | string){ - if(this.muted) return; - this.showRankPrefix = true; - return this.updateSynced(() => { - this.muted = true; - this.updateName(); - }, () => { - this.sendMessage(`[yellow]Hey! You have been muted. You cannot send messages to other players. You can still send messages to staff members.`); - this.setPunishedIP(stopAntiEvadeTime); - }, () => this.addHistoryEntry({ - action: 'muted', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - })); - } - unmute(by:FishPlayer | string){ - if(!this.muted) return; - FishPlayer.removePunishedIP(this.ip()); - FishPlayer.removePunishedUUID(this.uuid); - return this.updateSynced(() => { - this.muted = false; - this.updateName(); - }, () => { - this.sendMessage(`[green]You have been unmuted.`); - }, () => this.addHistoryEntry({ - action: 'muted', - by: by instanceof FishPlayer ? by.name : by, - time: Date.now(), - })); - } - //#endregion - - //#region eventhandling - //Contains methods that handle an event and must be called by other code (usually through Events.on). - /** Must be run on PlayerConnectEvent. */ - static onPlayerConnect(player:mindustryPlayer){ - const fishPlayer = this.cachedPlayers[player.uuid()] ??= this.createFromPlayer(player); - const previousJoin = fishPlayer.lastJoined; - fishPlayer.updateSavedInfoFromPlayer(player); - if(fishPlayer.validate()){ - if(!fishPlayer.hasPerm("bypassNameCheck")){ - const message = isImpersonator(fishPlayer.name, fishPlayer.ranksAtLeast("admin")); - if(message !== false){ - fishPlayer.sendMessage(`[scarlet]\u26A0[] [gold]Oh no! Our systems think you are a [scarlet]SUSSY IMPERSONATOR[]!\n[gold]Reason: ${message}\n[gold]Change your name to remove the tag.`); - } else if(cleanText(player.name, true).includes("hacker")){ - fishPlayer.sendMessage("[scarlet]\u26A0 Don't be a script kiddie!"); - FishEvents.fire("scriptKiddie", [fishPlayer]); - } - } - fishPlayer.updateAdminStatus(); - fishPlayer.checkVPNAndJoins(); - fishPlayer.updateName(); - //I think this is a better spot for this - if(fishPlayer.firstJoin()) void Menu.menu( - "Rules for [#0000ff] >|||> FISH [white] servers [white]", - rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].", - ["[green]I understand and agree to these terms"], - fishPlayer - ); - - } - } - /** Must be run on PlayerJoinEvent. */ - static onPlayerJoin(player:mindustryPlayer){ - const fishPlayer = this.cachedPlayers[player.uuid()] ??= (() => { - Log.err(`onPlayerJoin: no fish player was created? ${player.uuid()}`); - return this.createFromPlayer(player); - })(); - //Don't activate heuristics until they've joined - //a lot of time can pass between connect and join - //also the player might connect but fail to join for a lot of reasons, - //or connect, fail to join, then connect again and join successfully - //which would cause heuristics to activate twice - fishPlayer.activateHeuristics(); - } - static updateAFKCheck(){ - //TODO better AFK check - this.forEachPlayer((fishP, mp) => { - fishP.lastMousePosition = [mp.mouseX, mp.mouseY]; - fishP.lastUnitPosition = [mp.x, mp.y]; - fishP.updateName(); - }); - } - /** Must be run on PlayerLeaveEvent. */ - static onPlayerLeave(player:mindustryPlayer){ - const fishP = this.cachedPlayers[player.uuid()]; - if(!fishP) return; - - if( - Vars.netServer.currentlyKicking && - Reflect.get(Vars.netServer.currentlyKicking, "target") == player - ){ - //Anti votekick evasion - const votes = Reflect.get(Vars.netServer.currentlyKicking, "votes") as number; - if((() => { - if(fishP.hasPerm("bypassVotekick")) return false; - if(fishP.hasPerm("bypassVoteFreeze")) return votes >= Vars.netServer.votesRequired(); - if(fishP.info().timesJoined > 50) return votes >= 2; - return votes >= 1; - })()){ - const kickDuration = NetServer.kickDuration; - //Pass the votekick - Call.sendMessage(`[orange]Vote passed.[scarlet] ${player.name}[orange] will be banned from the server for ${kickDuration / 60} minutes.`); - player.kick(Packets.KickReason.vote, kickDuration * 1000); //it is stored in seconds but needs to be converted to millis - (Reflect.get(Vars.netServer.currentlyKicking, "task") as TimerTask).cancel(); - Vars.netServer.currentlyKicking = null; - } - } - - //Clear temporary states such as menu and taphandler - fishP.activeMenus = []; - fishP.tapInfo.commandName = null; - fishP.updateStats(stats => stats.timeInGame += (Date.now() - fishP.lastJoined)); //Time between joining and leaving - fishP.lastJoined = Date.now(); - this.recentLeaves.unshift(fishP); - if(this.recentLeaves.length > 10) this.recentLeaves.pop(); - void api.setFishPlayerData(fishP.getData(), 1, true); - - const currentRun = PartialMapRun.current?.startTime; - if(currentRun) Core.app.post(() => { - //Wait for the /spectate command's handler to fix their team before saving it - fishP.restoreTeam = [fishP.player!.team(), Date.now(), currentRun]; - }); - } - static easterEggVotekickTarget: FishPlayer | null = null; - static validateVotekickSession(){ - if(!Vars.netServer.currentlyKicking) return; - const target = this.get(Reflect.get(Vars.netServer.currentlyKicking, "target")); - const voted = Reflect.get(Vars.netServer.currentlyKicking, "voted") as ObjectIntMap; - if(voted.size == 2){ - //Try to find the UUID of the initiator - let uuid:string | null = null; - voted.entries().toArray().each(e => { - if(uuidPattern.test(e.key)) uuid = e.key; - }); - if(uuid){ - const initiator = this.getById(uuid); - if(initiator?.stelled()){ - if(initiator.hasPerm("bypassVotekick")){ - if(target !== this.easterEggVotekickTarget){ - this.easterEggVotekickTarget = target; - const msg = (new Error()).stack?.split("\n").slice(0, 4).join("\n"); - Call.sendMessage( - `[scarlet]Server[lightgray] has voted on kicking[orange] ${initiator.prefixedName}[lightgray].[accent] (\u221E/${Vars.netServer.votesRequired()}) - [scarlet]Error: failed to kick player ${initiator.name} - ${msg} - [scarlet]Error: failed to cancel votekick - ${msg}` - ); - } - return; - } - Call.sendMessage( -`[scarlet]Server[lightgray] has voted on kicking[orange] ${initiator.prefixedName}[lightgray].[accent] (\u221E/${Vars.netServer.votesRequired()}) -[scarlet]Vote passed.` - ); - initiator.kick("You are not allowed to votekick other players while marked.", 2); - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - return; - } else if(initiator?.hasPerm("immediatelyVotekickNewPlayers") && target.isSuspicious("high") && !target.hasPerm("bypassVotekick")){ - Call.sendMessage( -`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (${Vars.netServer.votesRequired()}/${Vars.netServer.votesRequired()}) -[scarlet]Vote passed.` - ); - target.kick(Packets.KickReason.vote, Duration.minutes(30)); - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - return; - } else if(target.isSuspicious("high") && !target.hasPerm("bypassVotekick") && !target.ranksAtLeast("trusted")){ - //Increase votes by 1, from 1 to 2 - Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(2)); - voted.put("__server__", 1); - Call.sendMessage( -`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (2/${Vars.netServer.votesRequired()}) -[lightgray]Type[orange] /vote [] to agree.` - ); - return; - } - } - } - if(target.hasPerm("bypassVotekick")){ - Call.sendMessage( -`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (-\u221E/${Vars.netServer.votesRequired()}) -[scarlet]Vote cancelled.` - ); - Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - } else if(target.ranksAtLeast("trusted") && Groups.player.size() > 4 && voted.get("__server__") == 0){ - //decrease votes by two, goes from 1 to negative 1 - Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(-1)); - voted.put("__server__", -2); - Call.sendMessage( -`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (-1/${Vars.netServer.votesRequired()}) -[lightgray]Type[orange] /vote [] to agree.` - ); - } - } - static onPlayerChat(player:mindustryPlayer, message:string){ - const fishP = this.get(player); - if(message.trim().toLowerCase().startsWith("/vote y") || message.startsWith("/votekick ")){ - this.checkVotekickAction(fishP, message); - } - fishP.lastActive = Date.now(); - fishP.updateStats(stats => stats.chatMessagesSent ++); - } - static checkVotekickAction(fishP:FishPlayer, message:string){ - const sus = fishP.suspicionLevel(); - const timeSinceJoin = Date.now() - fishP.lastJoined; - let target: mindustryPlayer; - if(message.startsWith("/votekick")){ - const id = message.split(" ")[1]?.split("#")[1]; - target = Groups.player.getByID(Number(id)); - if(!target) return; //invalid votekick command, harmless - } else { //TODO these "harmless" actions could be indications of a malfunctioning vkbot and should be logged if they repeat a lot (eg more than 5 times per minute) - if(!Vars.netServer.currentlyKicking) return; //nobody to votekick, harmless - target = Reflect.get(Vars.netServer.currentlyKicking, "target"); - } - const targetSusLevel = FishPlayer.get(target).suspicionLevel(); - - //Evaluate if this action should be blocked - if(sus <= 1) return; - let reason: string | undefined = undefined; - if(!this.votekickActionRate.allow(108_000, 8)) - reason = "Exceeded 8 votekick actions in the last 2 minutes"; - else if(sus == 3 && this.lastVKActions.find(a => Date.now() - a.time < 10_000 && a.playerSusLevel == 3) && timeSinceJoin < 6_000) - reason = "Performed votekick within 6 seconds of joining and there was a recent suspicious vote"; - else if(sus == 3 && timeSinceJoin < 80000 && this.lastVKActions.find(a => a.player == fishP) && targetSusLevel <= 1) - reason = "Two votekick actions within 80 seconds of joining and the target is not suspicious"; - else if(sus >= 2 && this.lastVKActions.filter(a => a.playerSusLevel == 3 && Date.now() - a.time < 33_000).length >= 3) - reason = "More than 3 recent votekick actions by suspicious players"; - else if(sus >= 2 && this.lastVKActions.filter(a => a.playerSusLevel >= 2).length >= 6 && this.lastVKActions.filter(a => a.player == fishP).length >= 3) - reason = "More than 6 slightly suspicious votekick actions within the past 20 minutes and this player has already performed 3 of them"; - if(reason != undefined){ - //Should we ban everyone? - const suspiciousActions = this.lastVKActions.filter(action => - (action.playerSusLevel == 3 || (action.targetSusLevel <= 2 && action.playerSusLevel >= 2) || action.player == fishP) && Date.now() - action.time < 78_000 - ); - if(suspiciousActions.length >= 3){ - //Ban everyone - const playersToBan = suspiciousActions.map(a => a.player).reduce((map, p) => { - map.set(p, (map.get(p) ?? 0) + 1); - return map; - }, new Map()); - //Only ban players that appeared in the list twice or are high suslevel - const { admins } = Vars.netServer; - for(const [p, times] of playersToBan){ - if(p.suspicionLevel() == 3 || p.suspicionLevel() == 2 && times > 1){ - admins.banPlayerID(p.uuid); - admins.banPlayerIP(p.ip()); - api.ban({ ip: p.ip(), uuid: p.uuid }); - logHTrip(p, "votekick abuse", - (p == fishP ? `Player banned automatically` : `Player banned automatically based on previous activity`) + - `. Trigger reason: ${reason}` - ); - } - } - updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked automatically for suspected votekick abuse.`); - //Pardon most of the votekick targets (the ones that weren't voted on by a non-sus player) - const candidatePardons = new Set(FishPlayer.lastVKActions.map(a => a.target)); - for(const action of FishPlayer.lastVKActions){ - if(action.playerSusLevel <= 1) candidatePardons.delete(action.target); - } - const playersToPardon = [...candidatePardons].map(FishPlayer.get); - //Don't pardon players with suslevel 3 - for(const p of playersToPardon){ - if(!p.isSuspicious("high")){ - p.info().lastKicked = 0; - admins.kickedIPs.remove(p.ip()); - Log.info("Pardoned player @ (@/@)", p.name, p.uuid, p.ip()); - logAction("pardoned", "automod", p, "kicked by suspected votekick bot"); - } - } - } else { - //Just kick the player - logHTrip(fishP, "votekick abuse", `sus=${sus}`); - fishP.kick(`You have been kicked [accent]automatically[] due to suspicious behavior. Please wait [accent]35[] seconds before rejoining.`, 30_000); - Call.sendMessage(`[scarlet]Player [yellow]${fishP.prefixedName}[scarlet] was kicked due to suspected votekick abuse.`); - //If this message is going to start a votekick, cancel it - if(message.startsWith("/votekick") && Vars.netServer.currentlyKicking == null) Core.app.post(() => { - Call.sendMessage( - `[scarlet]Server[lightgray] has voted on kicking[orange] ${target.name}[lightgray].[accent] (-\u221E/${Vars.netServer.votesRequired()}) - [scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.` - ); - if(Vars.netServer.currentlyKicking) Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - }); - //If there is an ongoing votekick and the initiator is suspicious, cancel that - else if(FishPlayer.lastVKActions.slice().reverse().find(a => a.type == "start")?.playerSusLevel == 3){ - Call.sendMessage( - `[scarlet]Server[lightgray] has voted on kicking[orange] ${target.name}[lightgray].[accent] (-\u221E/${Vars.netServer.votesRequired()}) - [scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.` - ); - if(Vars.netServer.currentlyKicking) Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); - Vars.netServer.currentlyKicking = null; - } - //Otherwise, revoke the vote - else Core.app.post(() => { - if(Vars.netServer.currentlyKicking){ - const votes = Reflect.get(Vars.netServer.currentlyKicking, "votes") - 1; - Reflect.set(Vars.netServer.currentlyKicking, "votes", votes); - const voted = Reflect.get(Vars.netServer.currentlyKicking, "voted"); - voted.put(fishP.uuid, 0); - voted.put(fishP.ip(), 0); - Call.sendMessage(`[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.`); - } - }); - } - } - - //Update state to catch future actions - this.lastVKActions.push({ - player: fishP, - playerSusLevel: sus, - target, - targetSusLevel, - time: Date.now(), - type: message.startsWith("/votekick") ? "start" : "vote y", - reason: message.startsWith("/votekick") ? message.split(" ").slice(2).join(" ") : undefined - }); - - this.lastVKActions = this.lastVKActions.filter(a => Date.now() - a.time < Duration.minutes(10)); - } - static onPlayerCommand(player:FishPlayer, command:string, unjoinedRawArgs:string[]){ - if(command == "msg" && unjoinedRawArgs[1] == "Please do not use that logic, as it is attem83 logic and is bad to use. For more information please read www.mindustry.dev/attem") - return; //Attemwarfare message, not sent by the player - player.lastActive = Date.now(); - } - private static ignoreGameOver = false; - static onGameOver(winningTeam:Team){ - FishEvents.fire("gameOver", [winningTeam]); - this.forEachPlayer((fishPlayer) => { - //Clear temporary states such as menu and taphandler - fishPlayer.activeMenus = []; - fishPlayer.tapInfo.commandName = null; - //Update stats - if(!this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict){ - fishPlayer.updateStats(stats => stats.gamesFinished ++); - if(fishPlayer.changedTeam){ - fishPlayer.sendMessage(`Refusing to update stats due to a team change.`); - } else { - if(fishPlayer.team() == winningTeam) fishPlayer.updateStats(stats => stats.gamesWon ++); - } - } - fishPlayer.changedTeam = false; - fishPlayer.tstats.wavesSurvived = 0; - fishPlayer.tstats.blockInteractionsThisMap = 0; - }); - } - static ignoreGameover(callback:() => unknown){ - this.ignoreGameOver = true; - callback(); - this.ignoreGameOver = false; - } - static onGameBegin(){ - const startTime = Date.now(); - FishPlayer.lastMapStartTime = startTime; - //wait 7 seconds for players to join - Timer.schedule(() => FishPlayer.forEachPlayer(p => p.tstats.lastMapStartTime = startTime), 7); - } - /** Must be run on UnitChangeEvent. */ - static onUnitChange(player:mindustryPlayer, unit:Unit | null){ - if(unit?.spawnedByCore) - this.onRespawn(player); - } - private static onRespawn(player:mindustryPlayer){ - const fishP = this.get(player); - if(fishP.stelled()) fishP.stopUnit(); - } - static forEachPlayer(func:(fishPlayer:FishPlayer, mindustryPlayer:mindustryPlayer) => unknown){ - Groups.player.each(player => { - if(player == null){ - Log.err(".FINDTAG. Groups.player.each() returned a null player???"); - return; - } - const fishP = this.get(player); - func(fishP, player); - }); - } - static mapPlayers(func:(player:FishPlayer) => T):T[]{ - const out:T[] = []; - Groups.player.each(player => { - if(player == null){ - Log.err(".FINDTAG. Groups.player.each() returned a null player???"); - return; - } - out.push(func(this.get(player))); - }); - return out; - } - updateMemberExclusiveState(){ - if(!this.hasPerm("member")){ - this.highlight = null; - this.rainbow = null; - } - } - /** Updates the mindustry player's name, using the prefixes of the current rank and role flags. */ - updateName(){ - if(!this.connected() || !this.shouldUpdateName) return;//No player, no need to update - const name = this.originalName ?? this.name; - if(this.marked()) this.showRankPrefix = true; - let prefix = ''; - if(!this.hasPerm("bypassNameCheck") && isImpersonator(name, this.ranksAtLeast("admin"))) - prefix += "[scarlet]SUSSY IMPOSTOR[]"; - if(this.marked()) prefix += prefixes.marked; - else if(this.autoflagged) prefix += prefixes.flagged; - if(this.muted) prefix += prefixes.muted; - if(this.afk()) prefix += "[orange]\uE876 AFK \uE876 | [white]"; - if(this.showRankPrefix){ - for(const flag of this.flags){ - prefix += flag.prefix; - } - prefix += this.rank.prefix; - } - if(prefix.length > 0 && !prefix.endsWith(" ")) prefix += " "; - let replacedName; - if(cleanText(name, true).includes("hacker")){ - //"Don't be a script kiddie" - //-LiveOverflow, 2015 - if(/h.*a.*c.*k.*[3e].*r/i.test(name)){ //try to only replace the part that contains "hacker" if it can be found with a simple regex - replacedName = name.replace(/h.*a.*c.*k.*[3e].*r/gi, "[brown]script kiddie[]"); - } else { - replacedName = "[brown]script kiddie"; - } - } else if(this.name.endsWith("[") && !this.name.endsWith("[[")){ - replacedName = name + "["; - } else replacedName = name; - this.player!.name = this.prefixedName = prefix + replacedName; - } - updateAdminStatus(){ - if(!this.connected()) return; - if(this.hasPerm("admin")){ - Vars.netServer.admins.adminPlayer(this.uuid, this.player!.usid()); - this.player!.admin = true; - } else { - Vars.netServer.admins.unAdminPlayer(this.uuid); - this.player!.admin = false; - } - } - updateAutoflaggedStatus(){ - if(this.ranksAtLeast("active")){ - this.autoflagged = false; - } - } - checkAntiEvasion(){ - FishPlayer.updatePunishedIPs(); - for(const [ip, uuid] of FishPlayer.punishedIPs){ - if(ip == this.ip() && uuid != this.uuid && !this.ranksAtLeast("mod")){ - api.sendModerationMessage( -`Automatically banned player \`${this.cleanedName}\` (\`${this.uuid}\`/\`${this.ip()}\`) for suspected punishment evasion. -Previously used UUID \`${uuid}\`(${Vars.netServer.admins.getInfoOptional(uuid)?.plainLastName()}), currently using UUID \`${this.uuid}\` from the same IP address.` - ); - Log.warn( -`&yAutomatically banned player &b${this.cleanedName}&y (&b${this.uuid}&y/&b${this.ip()}&y) for suspected punishment evasion. -&yPreviously used UUID &b${uuid}&y(&b${Vars.netServer.admins.getInfoOptional(uuid)?.plainLastName()}&y), currently using UUID &b${this.uuid}&y from the same IP address.` - ); - FishPlayer.messageStaff(`[yellow]Automatically banned player [cyan]${this.cleanedName}[] for suspected punishment evasion.`); - Vars.netServer.admins.banPlayerIP(ip); - api.ban({ip, uuid}); - this.kick(Packets.KickReason.banned); - return false; - } - } - return true; - } - static updatePunishedIPs(){ - for(let i = 0; i < this.punishedIPs.length; i ++){ - if(this.punishedIPs[i][2] < Date.now()){ - this.punishedIPs.splice(i, 1); - } - } - } - checkVPNAndJoins(){ - const ip = this.ip(); - const info:PlayerInfo = this.info(); - api.isVpn(ip, isVpn => { - if(isVpn){ - Log.warn(`IP ${ip} was flagged as VPN. Flag rate: ${FishPlayer.stats.numIpsFlagged}/${FishPlayer.stats.numIpsChecked} (${100 * FishPlayer.stats.numIpsFlagged / FishPlayer.stats.numIpsChecked}%)`); - this.ipDetectedVpn = true; - if(!FishPlayer.autoflagRate.allow(30_000, 5)){ - FishPlayer.triggerAntibot(Duration.minutes(3), "rate of flagged IPs exceeded 5 / 30s", "automatic"); - return; - } - if( - (info.timesJoined <= 1 || (FishPlayer.autoflagRate.occurences > 3 && info.timesJoined <= 10)) //is this smart? - && !this.ranksAtLeast("active") - && FishPlayer.punishedIPs.length > 0 - ){ - this.autoflagged = true; - this.stopUnit(); - this.updateName(); - if(FishPlayer.shouldWhackFlaggedPlayers()){ - FishPlayer.whackFlaggedPlayers(); //calls whack all flagged players - } else { - logAction("autoflagged", "AntiVPN", this); - api.sendStaffMessage(`Autoflagged player ${this.name}[cyan] for suspected vpn!`, "AntiVPN", true); - FishPlayer.messageStaff(`[yellow]WARNING:[scarlet] player [cyan]"${this.name}[cyan]"[yellow] is new (${info.timesJoined - 1} joins) and using a vpn. They have been automatically stopped and muted. Unless there is an ongoing griefer raid, they are most likely innocent. Free them with /free.`); - Log.warn(`Player ${this.name} (${this.uuid}) was autoflagged.`); - void Menu.buttons( - this, - "[gold]Welcome to Fish Community!", - `[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ${FColor.discord`Join our Discord`} to request a staff member come online if none are on.`, - [[ - { data: "Close", text: "Close" }, - { data: "Discord", text: FColor.discord("Discord") }, - ]] - ).then((option) => { - if(option == "Discord"){ - Call.openURI(this.con, text.discordURL); - } - }); - this.sendMessage(`[gold]Welcome to Fish Community!\n[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ${FColor.discord`Join our Discord`} to request a staff member come online if none are on.`); - } - } else if(info.timesJoined < 5){ - FishPlayer.messageStaff(`[yellow]WARNING:[scarlet] player [cyan]"${this.name}[cyan]"[yellow] is new (${info.timesJoined - 1} joins) and using a vpn.`); - } - } else { - if(info.timesJoined == 1){ - FishPlayer.messageTrusted(`[yellow]Player "${this.cleanedName}" is on first join.`); - } - } - if(info.timesJoined == 1){ - let message = `&lrNew player joined: &c${this.cleanedName}&lr (&c${this.uuid}&lr/&c${ip}&lr)`; - //Add BEL, this causes an audible noise - if(globals.fishState.joinBell) message += '\x07'; - Log.info(message); - } - }, err => { - Log.err(`Error while checking for VPN status of ip ${ip}!`); - Log.err(err); - }); - } - validate(){ - return this.checkName() && this.checkUsid() && this.checkAntiEvasion(); - } - /** Checks if this player's name is allowed. */ - checkName(){ - if(matchFilter(this.name, "name")){ - this.kick( -`[scarlet]"${this.name}[scarlet]" is not an allowed name because it contains a banned word. - -If you are unable to change it, please download Mindustry from Steam or itch.io.`, - 1); - } else if(Strings.stripColors(this.name.replace(/[\u3164]/g, "")).trim().length == 0){ - this.kick( -`[scarlet]"${escapeStringColorsClient(this.name)}[scarlet]" is not an allowed name because it is empty. Please change it.`, - 1); - } else { - return true; - } - return false; - } - /** Checks if this player's USID is correct. */ - checkUsid(){ - const storedUSID = this.usid; - const usidMissing = storedUSID == null || !storedUSID; - const receivedUSID = this.player!.usid(); - if(this.hasPerm("usidCheck")){ - if(usidMissing){ - if(this.hasPerm("mod")){ - //Staff missing USID, don't let them in - Log.err(`&rUSID missing for privileged player &c"${this.cleanedName}"&r: no stored usid, cannot authenticate.\nRun &lgsetusid ${this.uuid} ${receivedUSID}&fr if you have verified this connection attempt.`); - this.kick(`Authorization failure! Please ask a staff member with Console Access to approve this connection.`, 1); - FishPlayer.lastAuthKicked = this; - return false; - } else { - Log.info(`Acquired USID for player &c"${this.cleanedName}"&fr: &c"${receivedUSID}"&fr`); - } - } else { - if(receivedUSID != storedUSID){ - Log.err(`&rUSID mismatch for player &c"${this.cleanedName}"&r: stored usid is &c${storedUSID}&r, but they tried to connect with usid &c${receivedUSID}&r\nRun &lgsetusid ${this.uuid} ${receivedUSID}&fr if you have verified this connection attempt.`); - this.kick(`Authorization failure!`, 1); - FishPlayer.lastAuthKicked = this; - return false; - } - } - } else { - if(!usidMissing && receivedUSID != storedUSID){ - Log.err(`&rUSID mismatch for player &c"${this.cleanedName}"&r: stored usid is &c${storedUSID}&r, but they tried to connect with usid &c${receivedUSID}&r`); - } - } - this.usid = receivedUSID; - return true; - } - displayTrail(){ - if(this.trail) Call.effect(Fx[this.trail.type], this.player!.x, this.player!.y, 0, this.trail.color); - } - sendWelcomeMessage(){ - const appealLine = `To appeal, ${FColor.discord`join our discord`} with ${FColor.discord`/discord`}, or ask a ${Rank.mod.color}staff member[] in-game.`; - if(FishPlayer.dataFetchFailedUuids.has(this.uuid)){ - this.sendMessage(text.dataFetchFailed); - FishPlayer.dataFetchFailedUuids.delete(this.uuid); - } - if(this.marked()) this.sendMessage( -`[gold]Hello there! You are currently [scarlet]marked as a griefer[]. You cannot do anything in-game while marked. -${appealLine} -Your mark will expire automatically ${this.unmarkTime == globals.maxTime ? "in [red]never[]" : `[green]${formatTimeRelative(this.unmarkTime)}[]`}. -We apologize for the inconvenience.` - ); else if(this.muted) this.sendMessage( -`[gold]Hello there! You are currently [red]muted[]. You can still play normally, but cannot send chat messages to other non-staff players while muted. -${appealLine} -We apologize for the inconvenience.` - ); else if(this.autoflagged) this.sendMessage( -`[gold]Hello there! You are currently [red]flagged as suspicious[]. You cannot do anything in-game. -${appealLine} -We apologize for the inconvenience.` - ); else if(!this.showRankPrefix) this.sendMessage( -`[gold]Hello there! Your rank prefix is currently hidden. You can show it again by running [white]/vanish[].` - ); else { - this.sendMessage(text.welcomeMessage()); - - //show tips - let showAd = false; - if(Date.now() - this.lastShownAd > Duration.days(1)){ - this.lastShownAd = Date.now(); - this.showAdNext = true; - } else if(this.lastShownAd == globals.maxTime){ - //this is the first time they joined, show ad the next time they join - this.showAdNext = true; - this.lastShownAd = Date.now(); - } else if(this.showAdNext){ - this.showAdNext = false; - showAd = true; - } - const messagePool = showAd ? tips.ads : (Mode.isChristmas && Math.random() > 0.6) ? tips.christmas : tips.normal; - const messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; - const message = showAd ? `[gold]${messageText}[]` : `[gold]Tip: ${messageText}[]`; - - //Delay sending the message so it doesn't get lost in the spam of messages that usually occurs when you join - Timer.schedule(() => this.sendMessage(message), 3); - } - } - checkAutoRanks(){ - if(this.stelled()) return; - for(const rankToAssign of Rank.autoRanks){ - if(!this.ranksAtLeast(rankToAssign) && rankToAssign.autoRankData){ - if( - this.joinsAtLeast(rankToAssign.autoRankData.joins) && - this.globalStats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && - this.globalStats.timeInGame >= rankToAssign.autoRankData.playtime && - this.globalStats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && - (Date.now() - this.globalFirstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin - ){ - void this.setRank(rankToAssign).then(() => - this.sendMessage(`You have been automatically promoted to rank ${rankToAssign.coloredName()}!`) - ); - } - } - } - - } - //#endregion - - //#region I/O - static read(version:number, fishPlayerData:StringIO, player:mindustryPlayer | null):FishPlayer { - switch(version){ - case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: - crash(`Version ${version} is not longer supported, this should not be possible`); - break; - case 10: { - const uuid = fishPlayerData.readString(2) ?? crash("Failed to deserialize FishPlayer: UUID was null."); - const fishP = new this(uuid, { - name: fishPlayerData.readString(2) ?? "Unnamed player [ERROR]", - muted: (() => { - const muted = fishPlayerData.readBool(); - void fishPlayerData.readBool(); //discard the stored data for autoflagged - return muted; - })(), - unmarkTime: fishPlayerData.readNumber(13), - highlight: fishPlayerData.readString(2), - history: fishPlayerData.readArray(str => ({ - action: str.readString(2) ?? "null", - by: str.readString(2) ?? "null", - time: str.readNumber(15) - })), - rainbow: (n => n == 0 ? null : {speed: n})(fishPlayerData.readNumber(2)), - rank: fishPlayerData.readString(2) ?? "", - flags: fishPlayerData.readArray(str => str.readString(2), 2).filter((s):s is string => s != null), - usid: fishPlayerData.readString(2), - chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), - lastJoined: fishPlayerData.readNumber(15), - firstJoined: fishPlayerData.readNumber(15), - stats: { - blocksBroken: fishPlayerData.readNumber(10), - blocksPlaced: fishPlayerData.readNumber(10), - timeInGame: fishPlayerData.readNumber(15), - chatMessagesSent: fishPlayerData.readNumber(7), - gamesFinished: fishPlayerData.readNumber(5), - gamesWon: fishPlayerData.readNumber(5), - }, - showRankPrefix: fishPlayerData.readBool(), - }, player); - fishPlayerData.readNumber(1); //discard pollResponse - return fishP; - } - case 11: { - const uuid = fishPlayerData.readString(2) ?? crash("Failed to deserialize FishPlayer: UUID was null."); - return new this(uuid, { - name: fishPlayerData.readString(2) ?? "Unnamed player [ERROR]", - muted: (() => { - const muted = fishPlayerData.readBool(); - void fishPlayerData.readBool(); //discard the stored data for autoflagged - return muted; - })(), - unmarkTime: fishPlayerData.readNumber(13), - highlight: fishPlayerData.readString(2), - history: fishPlayerData.readArray(str => ({ - action: str.readString(2) ?? "null", - by: str.readString(2) ?? "null", - time: str.readNumber(15) - })), - rainbow: (n => n == 0 ? null : {speed: n})(fishPlayerData.readNumber(2)), - rank: fishPlayerData.readString(2) ?? "", - flags: fishPlayerData.readArray(str => str.readString(2), 2).filter((s):s is string => s != null), - usid: fishPlayerData.readString(2), - chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), - lastJoined: fishPlayerData.readNumber(15), - firstJoined: fishPlayerData.readNumber(15), - stats: { - blocksBroken: fishPlayerData.readNumber(10), - blocksPlaced: fishPlayerData.readNumber(10), - timeInGame: fishPlayerData.readNumber(15), - chatMessagesSent: fishPlayerData.readNumber(7), - gamesFinished: fishPlayerData.readNumber(5), - gamesWon: fishPlayerData.readNumber(5), - }, - showRankPrefix: fishPlayerData.readBool(), - }, player); - } - case 12: { - const uuid = fishPlayerData.readString(2) ?? crash("Failed to deserialize FishPlayer: UUID was null."); - return new this(uuid, { - name: fishPlayerData.readString(2) ?? "Unnamed player [ERROR]", - muted: fishPlayerData.readBool(), - unmarkTime: fishPlayerData.readNumber(13), - highlight: fishPlayerData.readString(2), - history: fishPlayerData.readArray(str => ({ - action: str.readString(2) ?? "null", - by: str.readString(2) ?? "null", - time: str.readNumber(15) - })), - rainbow: (n => n == 0 ? null : {speed: n})(fishPlayerData.readNumber(2)), - rank: fishPlayerData.readString(2) ?? "", - flags: fishPlayerData.readArray(str => str.readString(2), 2).filter((s):s is string => s != null), - usid: fishPlayerData.readString(2), - chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), - lastJoined: fishPlayerData.readNumber(15), - firstJoined: fishPlayerData.readNumber(15), - stats: { - blocksBroken: fishPlayerData.readNumber(10), - blocksPlaced: fishPlayerData.readNumber(10), - timeInGame: fishPlayerData.readNumber(15), - chatMessagesSent: fishPlayerData.readNumber(7), - gamesFinished: fishPlayerData.readNumber(5), - gamesWon: fishPlayerData.readNumber(5), - }, - showRankPrefix: fishPlayerData.readBool(), - }, player); - } - default: crash(`Unknown save version ${version}`); - } - } - write(out:StringIO){ - if(typeof this.unmarkTime === "string") this.unmarkTime = 0; - out.writeString(this.uuid, 2); - out.writeString(this.name, 2, true); - out.writeBool(this.muted); - out.writeNumber(this.unmarkTime, 13);// this will stop working in 2286! https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs#Year_2286 - out.writeString(this.highlight, 2, true); - out.writeArray(this.history.slice(-5), (i, str) => { - str.writeString(i.action, 2); - str.writeString(i.by.slice(0, 98), 2, true); - str.writeNumber(i.time, 15); - }); - out.writeNumber(this.rainbow?.speed ?? 0, 2); - out.writeString(this.rank.name, 2); - out.writeArray(Array.from(this.flags), (f, str) => str.writeString(f.name, 2), 2); - out.writeString(this.usid, 2); - out.writeEnumString(this.chatStrictness, ["chat", "strict"]); - out.writeNumber(this.lastJoined, 15); - out.writeNumber(this.firstJoined, 15); - out.writeNumber(this.stats.blocksBroken, 10, true); - out.writeNumber(this.stats.blocksPlaced, 10, true); - out.writeNumber(this.stats.timeInGame, 15, true); - out.writeNumber(this.stats.chatMessagesSent, 7, true); - out.writeNumber(this.stats.gamesFinished, 5, true); - out.writeNumber(this.stats.gamesWon, 5, true); - out.writeBool(this.showRankPrefix); - } - /** Saves cached FishPlayers to JSON in Core.settings. */ - static saveAll(forceSaveSettings = true){ - const out = new StringIO(); - out.writeNumber(this.saveVersion, 2); - out.writeArray( - Object.entries(this.cachedPlayers).filter(([uuid, fishP]) => fishP.shouldCache()), - ([uuid, player]) => player.write(out), - 6 - ); - let string = out.string; - const numKeys = Math.ceil(string.length / this.chunkSize); - Core.settings.put('fish-subkeys', Packages.java.lang.Integer(numKeys)); - for(let i = 1; i <= numKeys; i ++){ - Core.settings.put(`fish-playerdata-part-${i}`, string.slice(0, this.chunkSize)); - string = string.slice(this.chunkSize); - } - if(forceSaveSettings) Core.settings.manualSave(); - } - shouldCache(){ - return this.ranksAtLeast("mod"); - } - static uploadAll(){ - FishPlayer.forEachPlayer(fishP => - void api.setFishPlayerData(fishP.getData(), 1, true) - ); - } - /** Does not include stats */ - hasData(){ - return (this.rank != Rank.player) || this.muted || (this.flags.size > 0) || this.chatStrictness != "chat"; - } - static getFishPlayersString(){ - if(Core.settings.has("fish-subkeys")){ - const subkeys:number = Core.settings.get("fish-subkeys", 1); - let string = ""; - for(let i = 1; i <= subkeys; i ++){ - string += Core.settings.get(`fish-playerdata-part-${i}`, ""); - } - return string; - } else { - return Core.settings.get("fish", ""); - } - } - /** Loads cached FishPlayers from JSON in Core.settings. */ - static loadAll(string = this.getFishPlayersString()){ - try { - if(string == "") return; //If it's empty, don't try to load anything - const out = new StringIO(string); - const version = out.readNumber(2); - const players = out.readArray(str => FishPlayer.read(version, str, null), 6); - out.expectEOF(); - players.forEach(p => this.cachedPlayers[p.uuid] = p); - } catch(err){ - Log.err(`[CRITICAL] FAILED TO LOAD CACHED FISH PLAYER DATA`); - Log.err(parseError(err)); - Log.err("============================="); - Log.err(string); - Log.err("============================="); - } - } - //#endregion - - //#region antibot - static antiBotMode(){ - return Date.now() < this.antibotExpires; - } - static shouldKickNewPlayers(){ - return false; - } - static shouldWhackFlaggedPlayers(){ - return Date.now() < this.antibotExpires; - } - static whackFlaggedPlayers(){ - this.forEachPlayer(p => { - if(p.autoflagged){ - Vars.netServer.admins.blacklistDos(p.ip()); - Log.info(`&yAntibot killed connection ${p.ip()} due to flagged while under attack`); - p.player!.kick(Packets.KickReason.banned, 10000000); - } - }); - } - static triggerAntibot(duration:number, reason:string, category:"manual" | "automatic"){ - if(category == "automatic"){ - //Ping reports based on - if(Date.now() - this.antibotExpires > Duration.hours(1)) - api.sendModerationMessage(`!!! ${text.reportsPing} Possible ongoing bot attack in **${Gamemode.name()}** Reason: ${escapeTextDiscord(reason)}`); - else if(Date.now() - this.antibotExpires > Duration.minutes(10)) - api.sendModerationMessage(`!!! Possible ongoing bot attack in **${Gamemode.name()}** Reason: ${escapeTextDiscord(reason)}`); - } - if(Date.now() > this.antibotExpires || reason != this.lastAntibotReason) - Log.info(`&yAntibot triggered: ${escapeStringColorsServer(reason)}`); - this.antibotExpires = Math.max(this.antibotExpires, Date.now() + duration); - this.lastAntibotReason = reason; - if(this.shouldWhackFlaggedPlayers()) this.whackFlaggedPlayers(); - } - //#endregion - - //#region util - /** - * Sends a message to staff only. - * @returns if the message was received by anyone. - */ - static messageStaff(senderName:string, message:string):boolean; - static messageStaff(message:string):boolean; - static messageStaff(arg1:string, arg2?:string):boolean { - const message = arg2 ? `[gray]<[cyan]staff[gray]>[white]${arg1}[green]: [cyan]${arg2}` : arg1; - let messageReceived = false; - Groups.player.each(pl => { - const fishP = FishPlayer.get(pl); - if(fishP.hasPerm("mod")){ - pl.sendMessage(message); - messageReceived = true; - } - }); - return messageReceived; - } - /** - * Sends a message to trusted players only. - */ - static messageTrusted(senderName:string, message:string):void; - static messageTrusted(message:string):void; - static messageTrusted(arg1:string, arg2?:string){ - const message = arg2 ? `[gray]<[${Rank.trusted.color}]trusted[gray]>[white]${arg1}[green]: [cyan]${arg2}` : arg1; - FishPlayer.forEachPlayer(fishP => { - if(fishP.ranksAtLeast("trusted")) fishP.sendMessage(message); - }); - } - /** - * Sends a message to muted players only. - * @returns if the message was received by anyone. - */ - static messageMuted(senderName:string, message:string):boolean; - static messageMuted(senderName:string):boolean; - static messageMuted(arg1:string, arg2?:string):boolean { - const message = arg2 ? `[gray]<[red]muted[gray]>[white]${arg1}[coral]: [lightgray]${arg2}` : arg1; - let messageReceived = false; - Groups.player.each(pl => { - const fishP = FishPlayer.get(pl); - if(fishP.hasPerm("seeMutedMessages")){ - pl.sendMessage(message); - messageReceived = true; - } - }); - return messageReceived; - } - static messageAllExcept(exclude:FishPlayer, message:string){ - FishPlayer.forEachPlayer(fishP => { - if(fishP !== exclude) fishP.sendMessage(message); - }); - } - static messageAllWithPerm(perm:PermType | undefined, message:string){ - if(perm){ - FishPlayer.forEachPlayer(fishP => { - if(fishP.hasPerm(perm)) fishP.sendMessage(message); - }); - } else { - Call.sendMessage(message); - } - } - position():string { - return `(${Math.floor(this.player!.x / 8)}, ${Math.floor(this.player!.y / 8)})`; - } - connected():boolean { - return this.player != null && !this.con.hasDisconnected; - } - voteWeight():number { - //TODO vote weighting based on rank and joins - return 1; - } - /** - * @returns whether a player can perform a moderation action on another player. - * @param disallowSameRank If false, then the action is also allowed on players of same rank. - * @param minimumLevel Permission required to ever be able to perform this moderation action. Default: mod. - */ - canModerate(player:FishPlayer, disallowSameRank:boolean = true, minimumLevel:PermType = "mod", allowSelfIfUnauthorized = false){ - if(player == this && allowSelfIfUnauthorized) return true; - if(!this.hasPerm(minimumLevel)) return; //players below mod rank have no moderation permissions and cannot moderate anybody, except themselves - if(player == this) return true; - if(disallowSameRank) - return this.rank.level > player.rank.level; - else - return this.rank.level >= player.rank.level; - } - ranksAtLeast(rank:Rank | RankName){ - if(typeof rank == "string") rank = Rank.getByName(rank)!; - return this.rank.level >= rank.level; - } - hasPerm(perm:PermType){ - return Perm[perm].check(this); - } - unit():Unit | null; - unit(unit:Unit):void; - unit(unit?:Unit):Unit | null | void { - if(unit) return this.player!.unit(unit); - else return this.player!.unit(); - } - team():Team { - return this.player!.team(); - } - setTeam(team:Team):void { - const oldTeam = this.player!.team(); - this.player!.team(team); - globals.FishEvents.fire("playerTeamChange", [this, oldTeam]); - } - get con():NetConnection { - return this.player?.con; - } - ip():string { - if(this.connected()) return this.player!.con.address; - else return this.info().lastIP; - } - info():PlayerInfo { - return Vars.netServer.admins.getInfo(this.uuid); - } - /** - * Sends this player a chat message. - * @param ratelimit Time in milliseconds before sending another ratelimited message. - */ - sendMessage(message:string, ratelimit:number = 0){ - if(Date.now() - this.lastRatelimitedMessage >= ratelimit){ - this.player?.sendMessage(message); - this.lastRatelimitedMessage = Date.now(); - } - } - hasFlag(flagName:RoleFlagName){ - const flag = RoleFlag.getByName(flagName); - if(flag) return this.flags.has(flag); - else return false; - } - forceRespawn(){ - this.player!.clearUnit(); - this.player!.checkSpawn(); - } - getUsageData(command:string){ - return this.usageData[command] ??= { - lastUsed: -1, - lastUsedSuccessfully: -1, - tapLastUsed: -1, - tapLastUsedSuccessfully: -1, - }; - } - immutable(){ - return this.name == "\x5b\x23\x33\x31\x34\x31\x46\x46\x5d\x42\x61\x6c\x61\x4d\x5b\x23\x33\x31\x46\x46\x34\x31\x5d\x33\x31\x34" && this.rank == Rank.pi; - } - firstJoin(){ - return this.info().timesJoined == 1; - } - joinsAtLeast(amount:number){ - return this.info().timesJoined >= amount; - } - joinsLessThan(amount:number){ - return this.info().timesJoined < amount; - } - /** - * 3 for first join or less than 2 minutes in game - * 2 for relatively new players - * 1 for players who we're fairly certain are not griefers (10 joins, 150 chat messages, 2 hours ingame) - * 0 for active ranked players - */ - suspicionLevel(): 3 | 2 | 1 | 0 { - if(this.ranksAtLeast("active") || this.stats.chatMessagesSent > 2000) return 0; - if( - this.info().timesJoined == 1 && this.stats.timeInGame <= Duration.hours(1) || - this.info().timesJoined == 2 && this.stats.timeInGame < Duration.minutes(8) || - this.stats.timeInGame < 120_000 - ) return 3; - if(( - + (this.info().timesJoined > 40) + - + (this.info().timesJoined > 10) + - + (this.stats.blocksBroken > 1000 && this.stats.blocksPlaced > 2000) + - + (this.stats.chatMessagesSent > 150) + - + (this.stats.timeInGame > Duration.hours(2)) + - + (this.stats.timeInGame > Duration.hours(5)) - ) < 3) return 2; - return 1; - } - isSuspicious(level: "high" | "medium" | "low"):boolean { - const num = this.suspicionLevel(); - switch(level){ - case "high": return num >= 3; - case "medium": return num >= 2; - case "low": return num >= 1; - } - } - - updateStats(func:(stats:Stats) => void):void { - func(this.stats); - func(this.globalStats); - } - - /** - * Returns a score between 0 and 1, as an estimate of the player's skill level. - * Defaults to 0.2 (guessing that the best trusted players can beat 5 noobs) - */ - teamBalanceScore(){ - /** A number between 0 and 0.7 */ - const score = (() => { - if(this.stats.gamesFinished < 10) return 0.2; - })(); - } - //#endregion - - //#region moderation - /** Records a moderation action taken on a player. */ - addHistoryEntry(entry:PlayerHistoryEntry){ - this.history.push(entry); - } - static addPlayerHistory(id:string, entry:PlayerHistoryEntry){ - this.getById(id)?.addHistoryEntry(entry); - } - - marked():boolean { - return this.unmarkTime > Date.now(); - } - afk():boolean { - return Date.now() - this.lastActive > 60_000 || this.manualAfk; - } - stelled():boolean { - return this.marked() || this.autoflagged; - } - setUnmarkTimer(duration:number){ - const oldUnmarkTime = this.unmarkTime; - Timer.schedule(() => { - if(this.unmarkTime === oldUnmarkTime && this.connected()){ - //Only run the code if the unmark time hasn't changed - this.forceRespawn(); - this.updateName(); - this.sendMessage("[yellow]Your mark has automatically expired."); - } - }, duration / 1000); - } - kick(reason:string | KickReason = Packets.KickReason.kick, duration:number = 30_000){ - this.player?.kick(reason, duration); - } - setPunishedIP(duration:number){ - FishPlayer.punishedIPs.push([this.ip(), this.uuid, Date.now() + duration]); - } - static removePunishedIP(target:string){ - let ipIndex:number; - if((ipIndex = FishPlayer.punishedIPs.findIndex(([ip]) => ip == target)) != -1){ - FishPlayer.punishedIPs.splice(ipIndex, 1); - return true; - } else return false; - } - static removePunishedUUID(target:string){ - let uuidIndex:number; - if((uuidIndex = FishPlayer.punishedIPs.findIndex(([, uuid]) => uuid == target)) != -1){ - FishPlayer.punishedIPs.splice(uuidIndex, 1); - return true; - } else return false; - } - trollName(name:string){ - this.shouldUpdateName = false; - this.player!.name = name; - } - freeze(){ - this.frozen = true; - this.sendMessage("You have been temporarily frozen."); - } - unfreeze(){ - this.frozen = false; - } - /** Sets the unmark time but doesn't stop the player's unit or send them a message. */ - updateStopTime(duration:number):Promise { - return this.updateSynced(() => { - const time = Math.min(Date.now() + duration, globals.maxTime); - this.unmarkTime = time; - this.updateName(); - }, () => this.setUnmarkTimer(duration)); - } - - stopUnit(){ - const unit = this.unit(); - if(this.connected() && unit){ - if(unit.spawnedByCore){ - unit.type = UnitTypes.stell; - unit.health = UnitTypes.stell.health; - unit.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); - } else { - this.forceRespawn(); - //This will cause FishPlayer.onRespawn to run, calling this function again, but then the player will be in a core unit, which can be safely stell'd - } - } - } - //#endregion - - //#region heuristics - activateHeuristics(){ - if(Gamemode.hexed() || Gamemode.sandbox()) return; - //Blocks broken check - if(this.joinsLessThan(5)){ - let tripped = false; - Timer.schedule(() => { - if(this.connected() && !tripped){ - if(this.tstats.blocksBroken > heuristics.blocksBrokenAfterJoin){ - tripped = true; - logHTrip(this, "blocks broken after join", `${this.tstats.blocksBroken}/${heuristics.blocksBrokenAfterJoin}`); - void this.stop("automod", globals.maxTime, `Automatic stop due to suspicious activity`); - FishPlayer.messageAllExcept(this, -`[yellow]Player ${this.cleanedName} has been stopped automatically due to suspected griefing. -Please look at ${this.position()} and see if they were actually griefing. If they were not, please inform a staff member.`); - } - } - }, 0, 1, this.firstJoin() ? 30 : this.joinsLessThan(3) ? 25 : 15); - } - } - //#endregion - -} - -//TODO convert all the unnecessary event handlers to simple calls to Events.on -Events.on(EventType.WaveEvent, () => FishPlayer.forEachPlayer(p => p.tstats.wavesSurvived ++)); +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the FishPlayer class, and many player-related functions. +*/ + +import * as api from "/api"; +import { FColor, Gamemode, heuristics, Mode, prefixes, rules, stopAntiEvadeTime, text, tips } from "/config"; +import { FishCommandArgType, Perm, PermType } from "/frameworks/commands"; +import { Menu } from "/frameworks/menus"; +import { crash, Duration, escapeStringColorsClient, escapeStringColorsServer, escapeTextDiscord, parseError, search, setToArray, StringIO } from "/funcs"; +import * as globals from "/globals"; +import { FishEvents, uuidPattern } from "/globals"; +import { PartialMapRun } from "/maps"; +import { Rank, RankName, RoleFlag, RoleFlagName } from "/ranks"; +import type { FishPlayerData, PlayerHistoryEntry, Stats, UploadedFishPlayerData } from "/types"; +import { cleanText, formatTime, formatTimeRelative, isImpersonator, logAction, logHTrip, matchFilter, updateBans } from "/utils"; + + +export class FishPlayer { + //#region Static constants + /** Save version used for serialized FishPlayers. */ + static readonly saveVersion = 12; + /** Maximum chunk size used when writing FishPlayer data to Core.settings. */ + static readonly chunkSize = 50000; + //#endregion + + //#region Static transients + /** Stores all currently loaded FishPlayer objects. */ + static cachedPlayers:Record = {}; + static stats = { + numIpsChecked: 0, + numIpsFlagged: 0, + numIpsErrored: 0, + }; + /** The last player that was kicked due to a USID mismatch. */ + static lastAuthKicked:FishPlayer | null = null; + /** + * List of IPs that were recently punished. + * If a new account joins from one of these IPs, + * we assume they are trying to evade the punishment + * and the IP gets banned. + */ + static punishedIPs = [] as Array<[ip:string, uuid:string, expiryTime:number]>; + static lastMapStartTime = 0; + /** Stores the 10 most recent players that left. */ + static recentLeaves:FishPlayer[] = []; + //Used for the antibot. Some of these values are reset by timers. + static antibotExpires = -1; + static lastAntibotReason = ""; + static autoflagRate = new Ratekeeper(); + static connectRate = new Ratekeeper(); + static votekickActionRate = new Ratekeeper(); + static lastVKActions = [] as Array<{ + type: "vote y" | "start"; + player: FishPlayer; + playerSusLevel: 0 | 1 | 2 | 3; + time: number; + target: mindustryPlayer; + targetSusLevel: 0 | 1 | 2 | 3; + reason?: string; + }>; + //#endregion + + //#region Transient properties + //Commands framework + /** Front-to-back queue of menus to show. */ + activeMenus: Array<{ + callback: (option:number) => void; + }> = []; + /** Mapping from command to usage data. */ + usageData: Record = {}; + tapInfo = { + commandName: null as string | null, + lastArgs: {} as Record, + mode: "once" as "once" | "on", + }; + //Misc + player:mindustryPlayer | null = null; + /** Used for the /trail command. */ + trail: { + type: string; + color: Color; + } | null = null; + cleanedName:string = "Unnamed player [ERROR}"; + prefixedName:string = "Unnamed player [ERROR}"; + /** Used to freeze players when votekicking. */ + frozen:boolean = false; + /** Used to avoid spamming players with ads by the tip message system */ + lastShownAd:number = globals.maxTime; + /** Used to avoid spamming players with ads by the tip message system */ + showAdNext:boolean = false; + /** Transient statistics, used by the automatic griefer detection. */ + tstats = { + //remember to clear this in updateSavedInfoFromPlayer! + blocksBroken: 0, + blockInteractionsThisMap: 0, + lastMapStartTime: 0, + lastMapPlayedTime: 0, + wavesSurvived: 0, + }; + /** Whether the player has manually marked themselves as AFK. */ + manualAfk = false; + //Used for AFK detection. + lastMousePosition = [0, 0] as [x:number, y:number]; + lastUnitPosition = [0, 0] as [x:number, y:number]; + lastActive:number = Date.now(); + /** Set this to false to disable automatic name updates. Used for the rename console command. */ + shouldUpdateName = true; + /** Used by the sendMessage() ratelimit system. */ + lastRatelimitedMessage = -1; + /** Keeps track of whether a player has changed team this match, for win rate calculation. */ + changedTeam = false; + /** Whether the player's IP was detected as a VPN. */ + ipDetectedVpn = false; + /** + * If a player's IP is detected as a VPN on their first join, + * they are autoflagged and cannot build or talk in chat. + */ + autoflagged = false; + /** Timestamp until which this player will not be allowed to control units. */ + blockedFromPossessingUnitsUntil = 0; + /** Timestamp until which this player will not be allowed to control units. */ + blockedFromCommandingUnitsUntil = 0; + /** The original name that this player used to join the server. */ + originalName?: string; + // Used by the data syncing framework. + infoUpdated = false; + dataSynced = false; + restoreTeam = null as null | [team:Team, timestamp:number, runStartTime:number]; + //#endregion + + //#region Stored data + uuid: string; + name: string = "Unnamed player [ERROR}"; + muted: boolean = false; + unmarkTime: number = -1; + rank: Rank = Rank.player; + flags = new Set(); + /** Used to color chat messages for the member command */ + highlight: string | null = null; + /** Used to color the player's name for the member command */ + rainbow: { + speed: number; + } | null = null; + /** List of all moderation actions that have been performed on this player. */ + history: PlayerHistoryEntry[] = []; + /** + * The USID for this player. + * USID stands for Unique Server IDentifier. It is like a UUID, but unique to each server (by IP and port). + * It cannot be viewed by admins and it cannot be obtained by other servers. + */ + usid: string | null = null; + /** If chat strictness is set to "strict", the player will not be allowed to swear. */ + chatStrictness: "chat" | "strict" = "chat"; + /** -1 represents unknown */ + lastJoined:number = -1; + /** -1 represents unknown */ + firstJoined:number = -1; + /** -1 represents unknown */ + globalLastJoined:number = -1; + /** -1 represents unknown */ + globalFirstJoined:number = -1; + stats: Stats = { + blocksBroken: 0, + blocksPlaced: 0, + timeInGame: 0, + chatMessagesSent: 0, + gamesFinished: 0, + gamesWon: 0, + }; + globalStats: Stats = this.stats; + /** Used for the /vanish command. */ + showRankPrefix:boolean = true; + achievements: Bits = new Bits(); + //#endregion + + constructor(uuid:string, data:Partial, player:mindustryPlayer | null){ + this.uuid = uuid; + this.player = player; + this.updateData(data); + } + + //#region getplayer + //Contains methods used to get FishPlayer instances. + static createFromPlayer(player:mindustryPlayer){ + return new this(player.uuid(), {}, player); + } + static createFromInfo(playerInfo:PlayerInfo){ + return new this(playerInfo.id, { + uuid: playerInfo.id, + name: playerInfo.lastName, + usid: playerInfo.adminUsid ?? null + }, null); + } + static getFromInfo(this:void, playerInfo:PlayerInfo){ + return FishPlayer.cachedPlayers[playerInfo.id] ??= FishPlayer.createFromInfo(playerInfo); + } + static get(this:void, player:mindustryPlayer):FishPlayer { + return FishPlayer.cachedPlayers[player.uuid()] ??= FishPlayer.createFromPlayer(player); + } + static resolve(this:void, player:mindustryPlayer | FishPlayer):FishPlayer { + if(player instanceof FishPlayer) return player; + else return FishPlayer.cachedPlayers[player.uuid()] ??= FishPlayer.createFromPlayer(player); + } + static getById(id:string):FishPlayer | null { + return this.cachedPlayers[id] ?? null; + } + /** Returns the FishPlayer representing the first online player matching a given name. */ + static getByName(name:string):FishPlayer | null { + if(name == "") return null; + const realPlayer = Groups.player.find(p => { + return p.name === name || + p.name.includes(name) || + p.name.toLowerCase().includes(name.toLowerCase()) || + Strings.stripColors(p.name).toLowerCase() === name.toLowerCase() || + Strings.stripColors(p.name).toLowerCase().includes(name.toLowerCase()) || + false; + }); + return realPlayer ? this.get(realPlayer) : null; + }; + + /** Returns the FishPlayers representing all online players matching a given name. */ + static getAllByName(name:string, strict = true):FishPlayer[] { + if(name == "") return []; + const output:FishPlayer[] = []; + Groups.player.each(p => { + const fishP = FishPlayer.get(p); + if(fishP.connected() && fishP.cleanedName.includes(name) || (!strict && fishP.cleanedName.toLowerCase().includes(name))) + output.push(fishP); + }); + return output; + } + static search = search( + (p, str) => p.uuid === str, + (p, str) => p.player!.id.toString() === str, + (p, str) => p.name.toLowerCase() === str.toLowerCase(), + // (p, str) => p.cleanedName === str, + (p, str) => p.cleanedName.toLowerCase() === str.toLowerCase(), + (p, str) => p.name.toLowerCase().includes(str.toLowerCase()), + // (p, str) => p.cleanedName.includes(str), + (p, str) => p.cleanedName.toLowerCase().includes(str.toLowerCase()), + ); + static getOneMindustryPlayerByName(str:string):mindustryPlayer | "none" | "multiple" { + if(str == "") return "none"; + const players = setToArray(Groups.player); + let matchingPlayers:mindustryPlayer[]; + + const filters:Array<(p:mindustryPlayer) => boolean> = [ + p => p.name === str, + // p => Strings.stripColors(p.name) === str, + p => Strings.stripColors(p.name).toLowerCase() === str.toLowerCase(), + // p => p.name.includes(str), + p => p.name.toLowerCase().includes(str.toLowerCase()), + p => Strings.stripColors(p.name).includes(str), + p => Strings.stripColors(p.name).toLowerCase().includes(str.toLowerCase()), + ]; + + for(const filter of filters){ + matchingPlayers = players.filter(filter); + if(matchingPlayers.length == 1) return matchingPlayers[0]; + else if(matchingPlayers.length > 1) return "multiple"; + } + return "none"; + } + //This method exists only because there is no easy way to turn an entitygroup into an array + static getAllOnline(){ + const players:FishPlayer[] = []; + Groups.player.each((p:mindustryPlayer) => { + const fishP = FishPlayer.get(p); + if(fishP.connected()) players.push(fishP); + }); + return players; + } + /** Returns all cached FishPlayers with names matching the search string. */ + static getAllOfflineByName(name:string){ + const matching:FishPlayer[] = []; + for(const [uuid, player] of Object.entries(this.cachedPlayers)){ + if(player.cleanedName.toLowerCase().includes(name)) matching.push(player); + } + return matching; + } + //#endregion + + //#region datasync + //Please see docs/data-management.md for a description of the update syncing algorithm. + static dataFetchFailedUuids = new Set(); + static onConnectPacket({uuid, name}:ConnectPacket){ + const entry = this.cachedPlayers[uuid]; + if(entry){ + entry.infoUpdated = false; + entry.dataSynced = false; + entry.name = name; + } + api.getFishPlayerData(uuid).then(data => { + if(!data) return; //nothing to sync + let fishP; + if(!(uuid in this.cachedPlayers)){ + fishP = new FishPlayer(uuid, data, null); + fishP.originalName = name; + fishP.dataSynced = true; + this.cachedPlayers[uuid] = fishP; + } else { + fishP = this.cachedPlayers[uuid]; + fishP.dataSynced = true; + fishP.updateData(data); + if(fishP.infoUpdated){ + //Player has already connected + //Run it again + if(fishP.player) fishP.updateSavedInfoFromPlayer(fishP.player, true); + } else { + //Player has not connected yet, nothing further needed + } + } + if(fishP.connected()){ + fishP.checkUsid(); + fishP.updateMemberExclusiveState(); + fishP.updateName(); + fishP.updateAdminStatus(); + fishP.updateAutoflaggedStatus(); + fishP.checkAutoRanks(); + fishP.sendWelcomeMessage(); + } + }, () => { + const fishP = this.cachedPlayers[uuid]; + fishP.updateAdminStatus(); + fishP.updateAutoflaggedStatus(); + fishP.sendWelcomeMessage(); + if(fishP?.player) fishP.player.sendMessage(text.dataFetchFailed); + else this.dataFetchFailedUuids.add(uuid); + }); + } + /** Must be called at player join, before updateName(). */ + updateSavedInfoFromPlayer(player:mindustryPlayer, repeated = false){ + this.player = player; + if(repeated){ + this.name = this.originalName!; + } else { + this.originalName = this.name = player.name; + } + if(this.firstJoined < 1) this.firstJoined = Date.now(); + + //Do not update USID here + this.manualAfk = false; + this.cleanedName = Strings.stripColors(player.name); + this.lastJoined = Date.now(); + this.lastMousePosition = [0, 0]; + this.lastActive = Date.now(); + if(this.highlight === "[white]") this.highlight = null; + this.shouldUpdateName = true; + this.changedTeam = false; + this.ipDetectedVpn = false; + this.tstats.blocksBroken = 0; + if(this.tstats.lastMapPlayedTime != FishPlayer.lastMapStartTime){ + this.tstats.blockInteractionsThisMap = 0; + this.tstats.lastMapPlayedTime = FishPlayer.lastMapStartTime; + } + this.infoUpdated = true; + } + updateData(data: Partial){ + if(data.name != undefined) this.name = data.name; + if(data.muted != undefined) this.muted = data.muted; + if(data.unmarkTime != undefined) this.unmarkTime = data.unmarkTime; + if(data.lastJoined != undefined) this.lastJoined = data.lastJoined; + if(data.firstJoined != undefined) this.firstJoined = data.firstJoined; + if(data.globalLastJoined != undefined) this.globalLastJoined = data.globalLastJoined; + if(data.globalFirstJoined != undefined) this.globalFirstJoined = data.globalFirstJoined; + if(data.highlight != undefined) this.highlight = data.highlight; + if(data.history != undefined) this.history = data.history; + if(data.rainbow != undefined) this.rainbow = data.rainbow; + if(data.usid != undefined) this.usid = data.usid; + if(data.chatStrictness != undefined) this.chatStrictness = data.chatStrictness; + if(data.stats != undefined) this.stats = data.stats; + if(data.globalStats != undefined) this.globalStats = data.globalStats; + if(data.showRankPrefix != undefined) this.showRankPrefix = data.showRankPrefix; + if(data.rank != undefined) this.rank = Rank.getByName(data.rank) ?? Rank.player; + if(data.flags != undefined) this.flags = new Set(data.flags.map(RoleFlag.getByName).filter(Boolean)); + if(data.achievements != undefined) this.achievements = JsonIO.read(Bits, `{bits:${data.achievements}}`); + } + getData():UploadedFishPlayerData { + const { uuid, name, muted, unmarkTime, rank, flags, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix } = this; + return { + uuid, name, muted, unmarkTime, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix, + rank: rank.name, + flags: [...flags.values()].map(f => f.name), + achievements: JsonIO.write(Reflect.get(this.achievements, "bits")) + }; + } + /** Warning: the "update" callback is run twice. */ + async updateSynced( + update: (fishP:FishPlayer) => void, + beforeFetch?: (fishP:FishPlayer) => void, + afterFetch?: (fishP:FishPlayer) => void, + ){ + update(this); + beforeFetch?.(this); + const data = await api.getFishPlayerData(this.uuid); + if(data) this.updateData(data); + update(this); + //of course, this is a race condition + //but it's unlikely to happen + //could be fixed by transmitting the update operation to the server as a mongo update command + afterFetch?.(this); + await api.setFishPlayerData(this.getData(), 1, false); + } + //#endregion + + //#region actively synced data updates + stop(by:FishPlayer | string, duration:number, message?:string, notify = true){ + if(duration > 60_000) this.setPunishedIP(stopAntiEvadeTime); + this.showRankPrefix = true; + return this.updateSynced(() => { + this.unmarkTime = Date.now() + duration; + if(this.unmarkTime > globals.maxTime) this.unmarkTime = globals.maxTime; + this.updateName(); + }, () => { + this.setUnmarkTimer(duration); + if(this.connected() && notify){ + this.stopUnit(); + this.sendMessage( + message + ? `[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer for reason: [white]${message}[]` + : `[scarlet]Oopsy Whoopsie! You've been stopped, and marked as a griefer.`); + if(duration < Duration.hours(1)){ + //less than one hour + this.sendMessage(`[yellow]Your mark will expire in ${formatTime(duration)}.`); + } + } + }, () => this.addHistoryEntry({ + action: 'stopped', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + })); + } + free(by:FishPlayer | string){ + by ??= "console"; + + this.autoflagged = false; //Might as well set autoflagged to false + FishPlayer.removePunishedIP(this.ip()); + FishPlayer.removePunishedUUID(this.uuid); + return this.updateSynced(() => { + this.unmarkTime = -1; + }, () => { + if(this.connected()){ + this.sendMessage('[yellow]Looks like someone had mercy on you.'); + this.updateName(); + this.forceRespawn(); + } + }, () => this.addHistoryEntry({ + action: 'freed', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + })); + } + async setRank(rank:Rank){ + if(typeof rank === "string" || !rank){ + rank satisfies never; + crash(`Type error in FishPlayer.setFlag(): rank is invalid`); + } + if(rank == Rank.pi && !Mode.localDebug) throw new TypeError(`Cannot find function setRank in object [object Object].`); + await this.updateSynced(() => { + this.rank = rank; + this.updateName(); + this.updateAdminStatus(); + }, () => FishPlayer.saveAll()); + } + async setFlag(flag_:RoleFlag | RoleFlagName, value:boolean){ + const flag = typeof flag_ == "string" ? + (RoleFlag.getByName(flag_)) + : flag_; + + // eslint-disable-next-line @typescript-eslint/no-base-to-string + if(!flag) crash(`Type error in FishPlayer.setFlag(): flag ${String(flag_)} is invalid`); + + await this.updateSynced(() => { + if(value){ + this.flags.add(flag); + } else { + this.flags.delete(flag); + } + this.updateMemberExclusiveState(); + this.updateName(); + }); + } + mute(by:FishPlayer | string){ + if(this.muted) return; + this.showRankPrefix = true; + return this.updateSynced(() => { + this.muted = true; + this.updateName(); + }, () => { + this.sendMessage(`[yellow]Hey! You have been muted. You cannot send messages to other players. You can still send messages to staff members.`); + this.setPunishedIP(stopAntiEvadeTime); + }, () => this.addHistoryEntry({ + action: 'muted', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + })); + } + unmute(by:FishPlayer | string){ + if(!this.muted) return; + FishPlayer.removePunishedIP(this.ip()); + FishPlayer.removePunishedUUID(this.uuid); + return this.updateSynced(() => { + this.muted = false; + this.updateName(); + }, () => { + this.sendMessage(`[green]You have been unmuted.`); + }, () => this.addHistoryEntry({ + action: 'muted', + by: by instanceof FishPlayer ? by.name : by, + time: Date.now(), + })); + } + //#endregion + + //#region eventhandling + //Contains methods that handle an event and must be called by other code (usually through Events.on). + /** Must be run on PlayerConnectEvent. */ + static onPlayerConnect(player:mindustryPlayer){ + const fishPlayer = this.cachedPlayers[player.uuid()] ??= this.createFromPlayer(player); + const previousJoin = fishPlayer.lastJoined; + fishPlayer.updateSavedInfoFromPlayer(player); + if(fishPlayer.validate()){ + if(!fishPlayer.hasPerm("bypassNameCheck")){ + const message = isImpersonator(fishPlayer.name, fishPlayer.ranksAtLeast("admin")); + if(message !== false){ + fishPlayer.sendMessage(`[scarlet]\u26A0[] [gold]Oh no! Our systems think you are a [scarlet]SUSSY IMPERSONATOR[]!\n[gold]Reason: ${message}\n[gold]Change your name to remove the tag.`); + } else if(cleanText(player.name, true).includes("hacker")){ + fishPlayer.sendMessage("[scarlet]\u26A0 Don't be a script kiddie!"); + FishEvents.fire("scriptKiddie", [fishPlayer]); + } + } + fishPlayer.updateAdminStatus(); + fishPlayer.checkVPNAndJoins(); + fishPlayer.updateName(); + //I think this is a better spot for this + if(fishPlayer.firstJoin()) void Menu.menu( + "Rules for [#0000ff] >|||> FISH [white] servers [white]", + rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].", + ["[green]I understand and agree to these terms"], + fishPlayer + ); + + } + } + /** Must be run on PlayerJoinEvent. */ + static onPlayerJoin(player:mindustryPlayer){ + const fishPlayer = this.cachedPlayers[player.uuid()] ??= (() => { + Log.err(`onPlayerJoin: no fish player was created? ${player.uuid()}`); + return this.createFromPlayer(player); + })(); + //Don't activate heuristics until they've joined + //a lot of time can pass between connect and join + //also the player might connect but fail to join for a lot of reasons, + //or connect, fail to join, then connect again and join successfully + //which would cause heuristics to activate twice + fishPlayer.activateHeuristics(); + } + static updateAFKCheck(){ + //TODO better AFK check + this.forEachPlayer((fishP, mp) => { + fishP.lastMousePosition = [mp.mouseX, mp.mouseY]; + fishP.lastUnitPosition = [mp.x, mp.y]; + fishP.updateName(); + }); + } + /** Must be run on PlayerLeaveEvent. */ + static onPlayerLeave(player:mindustryPlayer){ + const fishP = this.cachedPlayers[player.uuid()]; + if(!fishP) return; + + if( + Vars.netServer.currentlyKicking && + Reflect.get(Vars.netServer.currentlyKicking, "target") == player + ){ + //Anti votekick evasion + const votes = Reflect.get(Vars.netServer.currentlyKicking, "votes") as number; + if((() => { + if(fishP.hasPerm("bypassVotekick")) return false; + if(fishP.hasPerm("bypassVoteFreeze")) return votes >= Vars.netServer.votesRequired(); + if(fishP.info().timesJoined > 50) return votes >= 2; + return votes >= 1; + })()){ + const kickDuration = NetServer.kickDuration; + //Pass the votekick + Call.sendMessage(`[orange]Vote passed.[scarlet] ${player.name}[orange] will be banned from the server for ${kickDuration / 60} minutes.`); + player.kick(Packets.KickReason.vote, kickDuration * 1000); //it is stored in seconds but needs to be converted to millis + (Reflect.get(Vars.netServer.currentlyKicking, "task") as TimerTask).cancel(); + Vars.netServer.currentlyKicking = null; + } + } + + //Clear temporary states such as menu and taphandler + fishP.activeMenus = []; + fishP.tapInfo.commandName = null; + fishP.updateStats(stats => stats.timeInGame += (Date.now() - fishP.lastJoined)); //Time between joining and leaving + fishP.lastJoined = Date.now(); + this.recentLeaves.unshift(fishP); + if(this.recentLeaves.length > 10) this.recentLeaves.pop(); + void api.setFishPlayerData(fishP.getData(), 1, true); + + const currentRun = PartialMapRun.current?.startTime; + if(currentRun) Core.app.post(() => { + //Wait for the /spectate command's handler to fix their team before saving it + fishP.restoreTeam = [fishP.player!.team(), Date.now(), currentRun]; + }); + } + static easterEggVotekickTarget: FishPlayer | null = null; + static validateVotekickSession(){ + if(!Vars.netServer.currentlyKicking) return; + const target = this.get(Reflect.get(Vars.netServer.currentlyKicking, "target")); + const voted = Reflect.get(Vars.netServer.currentlyKicking, "voted") as ObjectIntMap; + if(voted.size == 2){ + //Try to find the UUID of the initiator + let uuid:string | null = null; + voted.entries().toArray().each(e => { + if(uuidPattern.test(e.key)) uuid = e.key; + }); + if(uuid){ + const initiator = this.getById(uuid); + if(initiator?.stelled()){ + if(initiator.hasPerm("bypassVotekick")){ + if(target !== this.easterEggVotekickTarget){ + this.easterEggVotekickTarget = target; + const msg = (new Error()).stack?.split("\n").slice(0, 4).join("\n"); + Call.sendMessage( + `[scarlet]Server[lightgray] has voted on kicking[orange] ${initiator.prefixedName}[lightgray].[accent] (\u221E/${Vars.netServer.votesRequired()}) + [scarlet]Error: failed to kick player ${initiator.name} + ${msg} + [scarlet]Error: failed to cancel votekick + ${msg}` + ); + } + return; + } + Call.sendMessage( +`[scarlet]Server[lightgray] has voted on kicking[orange] ${initiator.prefixedName}[lightgray].[accent] (\u221E/${Vars.netServer.votesRequired()}) +[scarlet]Vote passed.` + ); + initiator.kick("You are not allowed to votekick other players while marked.", 2); + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + return; + } else if(initiator?.hasPerm("immediatelyVotekickNewPlayers") && target.isSuspicious("high") && !target.hasPerm("bypassVotekick")){ + Call.sendMessage( +`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (${Vars.netServer.votesRequired()}/${Vars.netServer.votesRequired()}) +[scarlet]Vote passed.` + ); + target.kick(Packets.KickReason.vote, Duration.minutes(30)); + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + return; + } else if(target.isSuspicious("high") && !target.hasPerm("bypassVotekick") && !target.ranksAtLeast("trusted")){ + //Increase votes by 1, from 1 to 2 + Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(2)); + voted.put("__server__", 1); + Call.sendMessage( +`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (2/${Vars.netServer.votesRequired()}) +[lightgray]Type[orange] /vote [] to agree.` + ); + return; + } + } + } + if(target.hasPerm("bypassVotekick")){ + Call.sendMessage( +`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (-\u221E/${Vars.netServer.votesRequired()}) +[scarlet]Vote cancelled.` + ); + Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + } else if(target.ranksAtLeast("trusted") && Groups.player.size() > 4 && voted.get("__server__") == 0){ + //decrease votes by two, goes from 1 to negative 1 + Reflect.set(Vars.netServer.currentlyKicking, "votes", Packages.java.lang.Integer(-1)); + voted.put("__server__", -2); + Call.sendMessage( +`[scarlet]Server[lightgray] has voted on kicking[orange] ${target.prefixedName}[lightgray].[accent] (-1/${Vars.netServer.votesRequired()}) +[lightgray]Type[orange] /vote [] to agree.` + ); + } + } + static onPlayerChat(player:mindustryPlayer, message:string){ + const fishP = this.get(player); + if(message.trim().toLowerCase().startsWith("/vote y") || message.startsWith("/votekick ")){ + this.checkVotekickAction(fishP, message); + } + fishP.lastActive = Date.now(); + fishP.updateStats(stats => stats.chatMessagesSent ++); + } + static checkVotekickAction(fishP:FishPlayer, message:string){ + const sus = fishP.suspicionLevel(); + const timeSinceJoin = Date.now() - fishP.lastJoined; + let target: mindustryPlayer; + if(message.startsWith("/votekick")){ + const id = message.split(" ")[1]?.split("#")[1]; + target = Groups.player.getByID(Number(id)); + if(!target) return; //invalid votekick command, harmless + } else { //TODO these "harmless" actions could be indications of a malfunctioning vkbot and should be logged if they repeat a lot (eg more than 5 times per minute) + if(!Vars.netServer.currentlyKicking) return; //nobody to votekick, harmless + target = Reflect.get(Vars.netServer.currentlyKicking, "target"); + } + const targetSusLevel = FishPlayer.get(target).suspicionLevel(); + + //Evaluate if this action should be blocked + if(sus <= 1) return; + let reason: string | undefined = undefined; + if(!this.votekickActionRate.allow(108_000, 8)) + reason = "Exceeded 8 votekick actions in the last 2 minutes"; + else if(sus == 3 && this.lastVKActions.find(a => Date.now() - a.time < 10_000 && a.playerSusLevel == 3) && timeSinceJoin < 6_000) + reason = "Performed votekick within 6 seconds of joining and there was a recent suspicious vote"; + else if(sus == 3 && timeSinceJoin < 80000 && this.lastVKActions.find(a => a.player == fishP) && targetSusLevel <= 1) + reason = "Two votekick actions within 80 seconds of joining and the target is not suspicious"; + else if(sus >= 2 && this.lastVKActions.filter(a => a.playerSusLevel == 3 && Date.now() - a.time < 33_000).length >= 3) + reason = "More than 3 recent votekick actions by suspicious players"; + else if(sus >= 2 && this.lastVKActions.filter(a => a.playerSusLevel >= 2).length >= 6 && this.lastVKActions.filter(a => a.player == fishP).length >= 3) + reason = "More than 6 slightly suspicious votekick actions within the past 20 minutes and this player has already performed 3 of them"; + if(reason != undefined){ + //Should we ban everyone? + const suspiciousActions = this.lastVKActions.filter(action => + (action.playerSusLevel == 3 || (action.targetSusLevel <= 2 && action.playerSusLevel >= 2) || action.player == fishP) && Date.now() - action.time < 78_000 + ); + if(suspiciousActions.length >= 3){ + //Ban everyone + const playersToBan = suspiciousActions.map(a => a.player).reduce((map, p) => { + map.set(p, (map.get(p) ?? 0) + 1); + return map; + }, new Map()); + //Only ban players that appeared in the list twice or are high suslevel + const { admins } = Vars.netServer; + for(const [p, times] of playersToBan){ + if(p.suspicionLevel() == 3 || p.suspicionLevel() == 2 && times > 1){ + admins.banPlayerID(p.uuid); + admins.banPlayerIP(p.ip()); + api.ban({ ip: p.ip(), uuid: p.uuid }); + logHTrip(p, "votekick abuse", + (p == fishP ? `Player banned automatically` : `Player banned automatically based on previous activity`) + + `. Trigger reason: ${reason}` + ); + } + } + updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked automatically for suspected votekick abuse.`); + //Pardon most of the votekick targets (the ones that weren't voted on by a non-sus player) + const candidatePardons = new Set(FishPlayer.lastVKActions.map(a => a.target)); + for(const action of FishPlayer.lastVKActions){ + if(action.playerSusLevel <= 1) candidatePardons.delete(action.target); + } + const playersToPardon = [...candidatePardons].map(FishPlayer.get); + //Don't pardon players with suslevel 3 + for(const p of playersToPardon){ + if(!p.isSuspicious("high")){ + p.info().lastKicked = 0; + admins.kickedIPs.remove(p.ip()); + Log.info("Pardoned player @ (@/@)", p.name, p.uuid, p.ip()); + logAction("pardoned", "automod", p, "kicked by suspected votekick bot"); + } + } + } else { + //Just kick the player + logHTrip(fishP, "votekick abuse", `sus=${sus}`); + fishP.kick(`You have been kicked [accent]automatically[] due to suspicious behavior. Please wait [accent]35[] seconds before rejoining.`, 30_000); + Call.sendMessage(`[scarlet]Player [yellow]${fishP.prefixedName}[scarlet] was kicked due to suspected votekick abuse.`); + //If this message is going to start a votekick, cancel it + if(message.startsWith("/votekick") && Vars.netServer.currentlyKicking == null) Core.app.post(() => { + Call.sendMessage( + `[scarlet]Server[lightgray] has voted on kicking[orange] ${target.name}[lightgray].[accent] (-\u221E/${Vars.netServer.votesRequired()}) + [scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.` + ); + if(Vars.netServer.currentlyKicking) Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + }); + //If there is an ongoing votekick and the initiator is suspicious, cancel that + else if(FishPlayer.lastVKActions.slice().reverse().find(a => a.type == "start")?.playerSusLevel == 3){ + Call.sendMessage( + `[scarlet]Server[lightgray] has voted on kicking[orange] ${target.name}[lightgray].[accent] (-\u221E/${Vars.netServer.votesRequired()}) + [scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.` + ); + if(Vars.netServer.currentlyKicking) Reflect.get(Vars.netServer.currentlyKicking, "task").cancel(); + Vars.netServer.currentlyKicking = null; + } + //Otherwise, revoke the vote + else Core.app.post(() => { + if(Vars.netServer.currentlyKicking){ + const votes = Reflect.get(Vars.netServer.currentlyKicking, "votes") - 1; + Reflect.set(Vars.netServer.currentlyKicking, "votes", votes); + const voted = Reflect.get(Vars.netServer.currentlyKicking, "voted"); + voted.put(fishP.uuid, 0); + voted.put(fishP.ip(), 0); + Call.sendMessage(`[scarlet]Vote cancelled due to suspected abuse. [accent]If this is in error, please report it to staff.`); + } + }); + } + } + + //Update state to catch future actions + this.lastVKActions.push({ + player: fishP, + playerSusLevel: sus, + target, + targetSusLevel, + time: Date.now(), + type: message.startsWith("/votekick") ? "start" : "vote y", + reason: message.startsWith("/votekick") ? message.split(" ").slice(2).join(" ") : undefined + }); + + this.lastVKActions = this.lastVKActions.filter(a => Date.now() - a.time < Duration.minutes(10)); + } + static onPlayerCommand(player:FishPlayer, command:string, unjoinedRawArgs:string[]){ + if(command == "msg" && unjoinedRawArgs[1] == "Please do not use that logic, as it is attem83 logic and is bad to use. For more information please read www.mindustry.dev/attem") + return; //Attemwarfare message, not sent by the player + player.lastActive = Date.now(); + } + private static ignoreGameOver = false; + static onGameOver(winningTeam:Team){ + FishEvents.fire("gameOver", [winningTeam]); + this.forEachPlayer((fishPlayer) => { + //Clear temporary states such as menu and taphandler + fishPlayer.activeMenus = []; + fishPlayer.tapInfo.commandName = null; + //Update stats + if(!this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict){ + fishPlayer.updateStats(stats => stats.gamesFinished ++); + if(fishPlayer.changedTeam){ + fishPlayer.sendMessage(`Refusing to update stats due to a team change.`); + } else { + if(fishPlayer.team() == winningTeam) fishPlayer.updateStats(stats => stats.gamesWon ++); + } + } + fishPlayer.changedTeam = false; + fishPlayer.tstats.wavesSurvived = 0; + fishPlayer.tstats.blockInteractionsThisMap = 0; + }); + } + static ignoreGameover(callback:() => unknown){ + this.ignoreGameOver = true; + callback(); + this.ignoreGameOver = false; + } + static onGameBegin(){ + const startTime = Date.now(); + FishPlayer.lastMapStartTime = startTime; + //wait 7 seconds for players to join + Timer.schedule(() => FishPlayer.forEachPlayer(p => p.tstats.lastMapStartTime = startTime), 7); + } + /** Must be run on UnitChangeEvent. */ + static onUnitChange(player:mindustryPlayer, unit:Unit | null){ + if(unit?.spawnedByCore) + this.onRespawn(player); + } + private static onRespawn(player:mindustryPlayer){ + const fishP = this.get(player); + if(fishP.stelled()) fishP.stopUnit(); + } + static forEachPlayer(func:(fishPlayer:FishPlayer, mindustryPlayer:mindustryPlayer) => unknown){ + Groups.player.each(player => { + if(player == null){ + Log.err(".FINDTAG. Groups.player.each() returned a null player???"); + return; + } + const fishP = this.get(player); + func(fishP, player); + }); + } + static mapPlayers(func:(player:FishPlayer) => T):T[]{ + const out:T[] = []; + Groups.player.each(player => { + if(player == null){ + Log.err(".FINDTAG. Groups.player.each() returned a null player???"); + return; + } + out.push(func(this.get(player))); + }); + return out; + } + updateMemberExclusiveState(){ + if(!this.hasPerm("member")){ + this.highlight = null; + this.rainbow = null; + } + } + /** Updates the mindustry player's name, using the prefixes of the current rank and role flags. */ + updateName(){ + if(!this.connected() || !this.shouldUpdateName) return;//No player, no need to update + const name = this.originalName ?? this.name; + if(this.marked()) this.showRankPrefix = true; + let prefix = ''; + if(!this.hasPerm("bypassNameCheck") && isImpersonator(name, this.ranksAtLeast("admin"))) + prefix += "[scarlet]SUSSY IMPOSTOR[]"; + if(this.marked()) prefix += prefixes.marked; + else if(this.autoflagged) prefix += prefixes.flagged; + if(this.muted) prefix += prefixes.muted; + if(this.afk()) prefix += "[orange]\uE876 AFK \uE876 | [white]"; + if(this.showRankPrefix){ + for(const flag of this.flags){ + prefix += flag.prefix; + } + prefix += this.rank.prefix; + } + if(prefix.length > 0 && !prefix.endsWith(" ")) prefix += " "; + let replacedName; + if(cleanText(name, true).includes("hacker")){ + //"Don't be a script kiddie" + //-LiveOverflow, 2015 + if(/h.*a.*c.*k.*[3e].*r/i.test(name)){ //try to only replace the part that contains "hacker" if it can be found with a simple regex + replacedName = name.replace(/h.*a.*c.*k.*[3e].*r/gi, "[brown]script kiddie[]"); + } else { + replacedName = "[brown]script kiddie"; + } + } else if(this.name.endsWith("[") && !this.name.endsWith("[[")){ + replacedName = name + "["; + } else replacedName = name; + this.player!.name = this.prefixedName = prefix + replacedName; + } + updateAdminStatus(){ + if(!this.connected()) return; + if(this.hasPerm("admin")){ + Vars.netServer.admins.adminPlayer(this.uuid, this.player!.usid()); + this.player!.admin = true; + } else { + Vars.netServer.admins.unAdminPlayer(this.uuid); + this.player!.admin = false; + } + } + updateAutoflaggedStatus(){ + if(this.ranksAtLeast("active")){ + this.autoflagged = false; + } + } + checkAntiEvasion(){ + FishPlayer.updatePunishedIPs(); + for(const [ip, uuid] of FishPlayer.punishedIPs){ + if(ip == this.ip() && uuid != this.uuid && !this.ranksAtLeast("mod")){ + api.sendModerationMessage( +`Automatically banned player \`${this.cleanedName}\` (\`${this.uuid}\`/\`${this.ip()}\`) for suspected punishment evasion. +Previously used UUID \`${uuid}\`(${Vars.netServer.admins.getInfoOptional(uuid)?.plainLastName()}), currently using UUID \`${this.uuid}\` from the same IP address.` + ); + Log.warn( +`&yAutomatically banned player &b${this.cleanedName}&y (&b${this.uuid}&y/&b${this.ip()}&y) for suspected punishment evasion. +&yPreviously used UUID &b${uuid}&y(&b${Vars.netServer.admins.getInfoOptional(uuid)?.plainLastName()}&y), currently using UUID &b${this.uuid}&y from the same IP address.` + ); + FishPlayer.messageStaff(`[yellow]Automatically banned player [cyan]${this.cleanedName}[] for suspected punishment evasion.`); + Vars.netServer.admins.banPlayerIP(ip); + api.ban({ip, uuid}); + this.kick(Packets.KickReason.banned); + return false; + } + } + return true; + } + static updatePunishedIPs(){ + for(let i = 0; i < this.punishedIPs.length; i ++){ + if(this.punishedIPs[i][2] < Date.now()){ + this.punishedIPs.splice(i, 1); + } + } + } + checkVPNAndJoins(){ + const ip = this.ip(); + const info:PlayerInfo = this.info(); + api.isVpn(ip, isVpn => { + if(isVpn){ + Log.warn(`IP ${ip} was flagged as VPN. Flag rate: ${FishPlayer.stats.numIpsFlagged}/${FishPlayer.stats.numIpsChecked} (${100 * FishPlayer.stats.numIpsFlagged / FishPlayer.stats.numIpsChecked}%)`); + this.ipDetectedVpn = true; + if(!FishPlayer.autoflagRate.allow(30_000, 5)){ + FishPlayer.triggerAntibot(Duration.minutes(3), "rate of flagged IPs exceeded 5 / 30s", "automatic"); + return; + } + if( + (info.timesJoined <= 1 || (FishPlayer.autoflagRate.occurences > 3 && info.timesJoined <= 10)) //is this smart? + && !this.ranksAtLeast("active") + && FishPlayer.punishedIPs.length > 0 + ){ + this.autoflagged = true; + this.stopUnit(); + this.updateName(); + if(FishPlayer.shouldWhackFlaggedPlayers()){ + FishPlayer.whackFlaggedPlayers(); //calls whack all flagged players + } else { + logAction("autoflagged", "AntiVPN", this); + api.sendStaffMessage(`Autoflagged player ${this.name}[cyan] for suspected vpn!`, "AntiVPN", true); + FishPlayer.messageStaff(`[yellow]WARNING:[scarlet] player [cyan]"${this.name}[cyan]"[yellow] is new (${info.timesJoined - 1} joins) and using a vpn. They have been automatically stopped and muted. Unless there is an ongoing griefer raid, they are most likely innocent. Free them with /free.`); + Log.warn(`Player ${this.name} (${this.uuid}) was autoflagged.`); + void Menu.buttons( + this, + "[gold]Welcome to Fish Community!", + `[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ${FColor.discord`Join our Discord`} to request a staff member come online if none are on.`, + [[ + { data: "Close", text: "Close" }, + { data: "Discord", text: FColor.discord("Discord") }, + ]] + ).then((option) => { + if(option == "Discord"){ + Call.openURI(this.con, text.discordURL); + } + }); + this.sendMessage(`[gold]Welcome to Fish Community!\n[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ${FColor.discord`Join our Discord`} to request a staff member come online if none are on.`); + } + } else if(info.timesJoined < 5){ + FishPlayer.messageStaff(`[yellow]WARNING:[scarlet] player [cyan]"${this.name}[cyan]"[yellow] is new (${info.timesJoined - 1} joins) and using a vpn.`); + } + } else { + if(info.timesJoined == 1){ + FishPlayer.messageTrusted(`[yellow]Player "${this.cleanedName}" is on first join.`); + } + } + if(info.timesJoined == 1){ + let message = `&lrNew player joined: &c${this.cleanedName}&lr (&c${this.uuid}&lr/&c${ip}&lr)`; + //Add BEL, this causes an audible noise + if(globals.fishState.joinBell) message += '\x07'; + Log.info(message); + } + }, err => { + Log.err(`Error while checking for VPN status of ip ${ip}!`); + Log.err(err); + }); + } + validate(){ + return this.checkName() && this.checkUsid() && this.checkAntiEvasion(); + } + /** Checks if this player's name is allowed. */ + checkName(){ + if(matchFilter(this.name, "name")){ + this.kick( +`[scarlet]"${this.name}[scarlet]" is not an allowed name because it contains a banned word. + +If you are unable to change it, please download Mindustry from Steam or itch.io.`, + 1); + } else if(Strings.stripColors(this.name.replace(/[\u3164]/g, "")).trim().length == 0){ + this.kick( +`[scarlet]"${escapeStringColorsClient(this.name)}[scarlet]" is not an allowed name because it is empty. Please change it.`, + 1); + } else { + return true; + } + return false; + } + /** Checks if this player's USID is correct. */ + checkUsid(){ + const storedUSID = this.usid; + const usidMissing = storedUSID == null || !storedUSID; + const receivedUSID = this.player!.usid(); + if(this.hasPerm("usidCheck")){ + if(usidMissing){ + if(this.hasPerm("mod")){ + //Staff missing USID, don't let them in + Log.err(`&rUSID missing for privileged player &c"${this.cleanedName}"&r: no stored usid, cannot authenticate.\nRun &lgsetusid ${this.uuid} ${receivedUSID}&fr if you have verified this connection attempt.`); + this.kick(`Authorization failure! Please ask a staff member with Console Access to approve this connection.`, 1); + FishPlayer.lastAuthKicked = this; + return false; + } else { + Log.info(`Acquired USID for player &c"${this.cleanedName}"&fr: &c"${receivedUSID}"&fr`); + } + } else { + if(receivedUSID != storedUSID){ + Log.err(`&rUSID mismatch for player &c"${this.cleanedName}"&r: stored usid is &c${storedUSID}&r, but they tried to connect with usid &c${receivedUSID}&r\nRun &lgsetusid ${this.uuid} ${receivedUSID}&fr if you have verified this connection attempt.`); + this.kick(`Authorization failure!`, 1); + FishPlayer.lastAuthKicked = this; + return false; + } + } + } else { + if(!usidMissing && receivedUSID != storedUSID){ + Log.err(`&rUSID mismatch for player &c"${this.cleanedName}"&r: stored usid is &c${storedUSID}&r, but they tried to connect with usid &c${receivedUSID}&r`); + } + } + this.usid = receivedUSID; + return true; + } + displayTrail(){ + if(this.trail) Call.effect(Fx[this.trail.type], this.player!.x, this.player!.y, 0, this.trail.color); + } + sendWelcomeMessage(){ + const appealLine = `To appeal, ${FColor.discord`join our discord`} with ${FColor.discord`/discord`}, or ask a ${Rank.mod.color}staff member[] in-game.`; + if(FishPlayer.dataFetchFailedUuids.has(this.uuid)){ + this.sendMessage(text.dataFetchFailed); + FishPlayer.dataFetchFailedUuids.delete(this.uuid); + } + if(this.marked()) this.sendMessage( +`[gold]Hello there! You are currently [scarlet]marked as a griefer[]. You cannot do anything in-game while marked. +${appealLine} +Your mark will expire automatically ${this.unmarkTime == globals.maxTime ? "in [red]never[]" : `[green]${formatTimeRelative(this.unmarkTime)}[]`}. +We apologize for the inconvenience.` + ); else if(this.muted) this.sendMessage( +`[gold]Hello there! You are currently [red]muted[]. You can still play normally, but cannot send chat messages to other non-staff players while muted. +${appealLine} +We apologize for the inconvenience.` + ); else if(this.autoflagged) this.sendMessage( +`[gold]Hello there! You are currently [red]flagged as suspicious[]. You cannot do anything in-game. +${appealLine} +We apologize for the inconvenience.` + ); else if(!this.showRankPrefix) this.sendMessage( +`[gold]Hello there! Your rank prefix is currently hidden. You can show it again by running [white]/vanish[].` + ); else { + this.sendMessage(text.welcomeMessage()); + + //show tips + let showAd = false; + if(Date.now() - this.lastShownAd > Duration.days(1)){ + this.lastShownAd = Date.now(); + this.showAdNext = true; + } else if(this.lastShownAd == globals.maxTime){ + //this is the first time they joined, show ad the next time they join + this.showAdNext = true; + this.lastShownAd = Date.now(); + } else if(this.showAdNext){ + this.showAdNext = false; + showAd = true; + } + const messagePool = showAd ? tips.ads : (Mode.isChristmas && Math.random() > 0.6) ? tips.christmas : tips.normal; + const messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; + const message = showAd ? `[gold]${messageText}[]` : `[gold]Tip: ${messageText}[]`; + + //Delay sending the message so it doesn't get lost in the spam of messages that usually occurs when you join + Timer.schedule(() => this.sendMessage(message), 3); + } + } + checkAutoRanks(){ + if(this.stelled()) return; + for(const rankToAssign of Rank.autoRanks){ + if(!this.ranksAtLeast(rankToAssign) && rankToAssign.autoRankData){ + if( + this.joinsAtLeast(rankToAssign.autoRankData.joins) && + this.globalStats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && + this.globalStats.timeInGame >= rankToAssign.autoRankData.playtime && + this.globalStats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && + (Date.now() - this.globalFirstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin + ){ + void this.setRank(rankToAssign).then(() => + this.sendMessage(`You have been automatically promoted to rank ${rankToAssign.coloredName()}!`) + ); + } + } + } + + } + //#endregion + + //#region I/O + static read(version:number, fishPlayerData:StringIO, player:mindustryPlayer | null):FishPlayer { + switch(version){ + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: + crash(`Version ${version} is not longer supported, this should not be possible`); + break; + case 10: { + const uuid = fishPlayerData.readString(2) ?? crash("Failed to deserialize FishPlayer: UUID was null."); + const fishP = new this(uuid, { + name: fishPlayerData.readString(2) ?? "Unnamed player [ERROR]", + muted: (() => { + const muted = fishPlayerData.readBool(); + void fishPlayerData.readBool(); //discard the stored data for autoflagged + return muted; + })(), + unmarkTime: fishPlayerData.readNumber(13), + highlight: fishPlayerData.readString(2), + history: fishPlayerData.readArray(str => ({ + action: str.readString(2) ?? "null", + by: str.readString(2) ?? "null", + time: str.readNumber(15) + })), + rainbow: (n => n == 0 ? null : {speed: n})(fishPlayerData.readNumber(2)), + rank: fishPlayerData.readString(2) ?? "", + flags: fishPlayerData.readArray(str => str.readString(2), 2).filter((s):s is string => s != null), + usid: fishPlayerData.readString(2), + chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), + lastJoined: fishPlayerData.readNumber(15), + firstJoined: fishPlayerData.readNumber(15), + stats: { + blocksBroken: fishPlayerData.readNumber(10), + blocksPlaced: fishPlayerData.readNumber(10), + timeInGame: fishPlayerData.readNumber(15), + chatMessagesSent: fishPlayerData.readNumber(7), + gamesFinished: fishPlayerData.readNumber(5), + gamesWon: fishPlayerData.readNumber(5), + }, + showRankPrefix: fishPlayerData.readBool(), + }, player); + fishPlayerData.readNumber(1); //discard pollResponse + return fishP; + } + case 11: { + const uuid = fishPlayerData.readString(2) ?? crash("Failed to deserialize FishPlayer: UUID was null."); + return new this(uuid, { + name: fishPlayerData.readString(2) ?? "Unnamed player [ERROR]", + muted: (() => { + const muted = fishPlayerData.readBool(); + void fishPlayerData.readBool(); //discard the stored data for autoflagged + return muted; + })(), + unmarkTime: fishPlayerData.readNumber(13), + highlight: fishPlayerData.readString(2), + history: fishPlayerData.readArray(str => ({ + action: str.readString(2) ?? "null", + by: str.readString(2) ?? "null", + time: str.readNumber(15) + })), + rainbow: (n => n == 0 ? null : {speed: n})(fishPlayerData.readNumber(2)), + rank: fishPlayerData.readString(2) ?? "", + flags: fishPlayerData.readArray(str => str.readString(2), 2).filter((s):s is string => s != null), + usid: fishPlayerData.readString(2), + chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), + lastJoined: fishPlayerData.readNumber(15), + firstJoined: fishPlayerData.readNumber(15), + stats: { + blocksBroken: fishPlayerData.readNumber(10), + blocksPlaced: fishPlayerData.readNumber(10), + timeInGame: fishPlayerData.readNumber(15), + chatMessagesSent: fishPlayerData.readNumber(7), + gamesFinished: fishPlayerData.readNumber(5), + gamesWon: fishPlayerData.readNumber(5), + }, + showRankPrefix: fishPlayerData.readBool(), + }, player); + } + case 12: { + const uuid = fishPlayerData.readString(2) ?? crash("Failed to deserialize FishPlayer: UUID was null."); + return new this(uuid, { + name: fishPlayerData.readString(2) ?? "Unnamed player [ERROR]", + muted: fishPlayerData.readBool(), + unmarkTime: fishPlayerData.readNumber(13), + highlight: fishPlayerData.readString(2), + history: fishPlayerData.readArray(str => ({ + action: str.readString(2) ?? "null", + by: str.readString(2) ?? "null", + time: str.readNumber(15) + })), + rainbow: (n => n == 0 ? null : {speed: n})(fishPlayerData.readNumber(2)), + rank: fishPlayerData.readString(2) ?? "", + flags: fishPlayerData.readArray(str => str.readString(2), 2).filter((s):s is string => s != null), + usid: fishPlayerData.readString(2), + chatStrictness: fishPlayerData.readEnumString(["chat", "strict"]), + lastJoined: fishPlayerData.readNumber(15), + firstJoined: fishPlayerData.readNumber(15), + stats: { + blocksBroken: fishPlayerData.readNumber(10), + blocksPlaced: fishPlayerData.readNumber(10), + timeInGame: fishPlayerData.readNumber(15), + chatMessagesSent: fishPlayerData.readNumber(7), + gamesFinished: fishPlayerData.readNumber(5), + gamesWon: fishPlayerData.readNumber(5), + }, + showRankPrefix: fishPlayerData.readBool(), + }, player); + } + default: crash(`Unknown save version ${version}`); + } + } + write(out:StringIO){ + if(typeof this.unmarkTime === "string") this.unmarkTime = 0; + out.writeString(this.uuid, 2); + out.writeString(this.name, 2, true); + out.writeBool(this.muted); + out.writeNumber(this.unmarkTime, 13);// this will stop working in 2286! https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs#Year_2286 + out.writeString(this.highlight, 2, true); + out.writeArray(this.history.slice(-5), (i, str) => { + str.writeString(i.action, 2); + str.writeString(i.by.slice(0, 98), 2, true); + str.writeNumber(i.time, 15); + }); + out.writeNumber(this.rainbow?.speed ?? 0, 2); + out.writeString(this.rank.name, 2); + out.writeArray(Array.from(this.flags), (f, str) => str.writeString(f.name, 2), 2); + out.writeString(this.usid, 2); + out.writeEnumString(this.chatStrictness, ["chat", "strict"]); + out.writeNumber(this.lastJoined, 15); + out.writeNumber(this.firstJoined, 15); + out.writeNumber(this.stats.blocksBroken, 10, true); + out.writeNumber(this.stats.blocksPlaced, 10, true); + out.writeNumber(this.stats.timeInGame, 15, true); + out.writeNumber(this.stats.chatMessagesSent, 7, true); + out.writeNumber(this.stats.gamesFinished, 5, true); + out.writeNumber(this.stats.gamesWon, 5, true); + out.writeBool(this.showRankPrefix); + } + /** Saves cached FishPlayers to JSON in Core.settings. */ + static saveAll(forceSaveSettings = true){ + const out = new StringIO(); + out.writeNumber(this.saveVersion, 2); + out.writeArray( + Object.entries(this.cachedPlayers).filter(([uuid, fishP]) => fishP.shouldCache()), + ([uuid, player]) => player.write(out), + 6 + ); + let string = out.string; + const numKeys = Math.ceil(string.length / this.chunkSize); + Core.settings.put('fish-subkeys', Packages.java.lang.Integer(numKeys)); + for(let i = 1; i <= numKeys; i ++){ + Core.settings.put(`fish-playerdata-part-${i}`, string.slice(0, this.chunkSize)); + string = string.slice(this.chunkSize); + } + if(forceSaveSettings) Core.settings.manualSave(); + } + shouldCache(){ + return this.ranksAtLeast("mod"); + } + static uploadAll(){ + FishPlayer.forEachPlayer(fishP => + void api.setFishPlayerData(fishP.getData(), 1, true) + ); + } + /** Does not include stats */ + hasData(){ + return (this.rank != Rank.player) || this.muted || (this.flags.size > 0) || this.chatStrictness != "chat"; + } + static getFishPlayersString(){ + if(Core.settings.has("fish-subkeys")){ + const subkeys:number = Core.settings.get("fish-subkeys", 1); + let string = ""; + for(let i = 1; i <= subkeys; i ++){ + string += Core.settings.get(`fish-playerdata-part-${i}`, ""); + } + return string; + } else { + return Core.settings.get("fish", ""); + } + } + /** Loads cached FishPlayers from JSON in Core.settings. */ + static loadAll(string = this.getFishPlayersString()){ + try { + if(string == "") return; //If it's empty, don't try to load anything + const out = new StringIO(string); + const version = out.readNumber(2); + const players = out.readArray(str => FishPlayer.read(version, str, null), 6); + out.expectEOF(); + players.forEach(p => this.cachedPlayers[p.uuid] = p); + } catch(err){ + Log.err(`[CRITICAL] FAILED TO LOAD CACHED FISH PLAYER DATA`); + Log.err(parseError(err)); + Log.err("============================="); + Log.err(string); + Log.err("============================="); + } + } + //#endregion + + //#region antibot + static antiBotMode(){ + return Date.now() < this.antibotExpires; + } + static shouldKickNewPlayers(){ + return false; + } + static shouldWhackFlaggedPlayers(){ + return Date.now() < this.antibotExpires; + } + static whackFlaggedPlayers(){ + this.forEachPlayer(p => { + if(p.autoflagged){ + Vars.netServer.admins.blacklistDos(p.ip()); + Log.info(`&yAntibot killed connection ${p.ip()} due to flagged while under attack`); + p.player!.kick(Packets.KickReason.banned, 10000000); + } + }); + } + static triggerAntibot(duration:number, reason:string, category:"manual" | "automatic"){ + if(category == "automatic"){ + //Ping reports based on + if(Date.now() - this.antibotExpires > Duration.hours(1)) + api.sendModerationMessage(`!!! ${text.reportsPing} Possible ongoing bot attack in **${Gamemode.name()}** Reason: ${escapeTextDiscord(reason)}`); + else if(Date.now() - this.antibotExpires > Duration.minutes(10)) + api.sendModerationMessage(`!!! Possible ongoing bot attack in **${Gamemode.name()}** Reason: ${escapeTextDiscord(reason)}`); + } + if(Date.now() > this.antibotExpires || reason != this.lastAntibotReason) + Log.info(`&yAntibot triggered: ${escapeStringColorsServer(reason)}`); + this.antibotExpires = Math.max(this.antibotExpires, Date.now() + duration); + this.lastAntibotReason = reason; + if(this.shouldWhackFlaggedPlayers()) this.whackFlaggedPlayers(); + } + //#endregion + + //#region util + /** + * Sends a message to staff only. + * @returns if the message was received by anyone. + */ + static messageStaff(senderName:string, message:string):boolean; + static messageStaff(message:string):boolean; + static messageStaff(arg1:string, arg2?:string):boolean { + const message = arg2 ? `[gray]<[cyan]staff[gray]>[white]${arg1}[green]: [cyan]${arg2}` : arg1; + let messageReceived = false; + Groups.player.each(pl => { + const fishP = FishPlayer.get(pl); + if(fishP.hasPerm("mod")){ + pl.sendMessage(message); + messageReceived = true; + } + }); + return messageReceived; + } + /** + * Sends a message to trusted players only. + */ + static messageTrusted(senderName:string, message:string):void; + static messageTrusted(message:string):void; + static messageTrusted(arg1:string, arg2?:string){ + const message = arg2 ? `[gray]<[${Rank.trusted.color}]trusted[gray]>[white]${arg1}[green]: [cyan]${arg2}` : arg1; + FishPlayer.forEachPlayer(fishP => { + if(fishP.ranksAtLeast("trusted")) fishP.sendMessage(message); + }); + } + /** + * Sends a message to muted players only. + * @returns if the message was received by anyone. + */ + static messageMuted(senderName:string, message:string):boolean; + static messageMuted(senderName:string):boolean; + static messageMuted(arg1:string, arg2?:string):boolean { + const message = arg2 ? `[gray]<[red]muted[gray]>[white]${arg1}[coral]: [lightgray]${arg2}` : arg1; + let messageReceived = false; + Groups.player.each(pl => { + const fishP = FishPlayer.get(pl); + if(fishP.hasPerm("seeMutedMessages")){ + pl.sendMessage(message); + messageReceived = true; + } + }); + return messageReceived; + } + static messageAllExcept(exclude:FishPlayer, message:string){ + FishPlayer.forEachPlayer(fishP => { + if(fishP !== exclude) fishP.sendMessage(message); + }); + } + static messageAllWithPerm(perm:PermType | undefined, message:string){ + if(perm){ + FishPlayer.forEachPlayer(fishP => { + if(fishP.hasPerm(perm)) fishP.sendMessage(message); + }); + } else { + Call.sendMessage(message); + } + } + position():string { + return `(${Math.floor(this.player!.x / 8)}, ${Math.floor(this.player!.y / 8)})`; + } + connected():boolean { + return this.player != null && !this.con.hasDisconnected; + } + voteWeight():number { + //TODO vote weighting based on rank and joins + return 1; + } + /** + * @returns whether a player can perform a moderation action on another player. + * @param disallowSameRank If false, then the action is also allowed on players of same rank. + * @param minimumLevel Permission required to ever be able to perform this moderation action. Default: mod. + */ + canModerate(player:FishPlayer, disallowSameRank:boolean = true, minimumLevel:PermType = "mod", allowSelfIfUnauthorized = false){ + if(player == this && allowSelfIfUnauthorized) return true; + if(!this.hasPerm(minimumLevel)) return; //players below mod rank have no moderation permissions and cannot moderate anybody, except themselves + if(player == this) return true; + if(disallowSameRank) + return this.rank.level > player.rank.level; + else + return this.rank.level >= player.rank.level; + } + ranksAtLeast(rank:Rank | RankName){ + if(typeof rank == "string") rank = Rank.getByName(rank)!; + return this.rank.level >= rank.level; + } + hasPerm(perm:PermType){ + return Perm[perm].check(this); + } + unit():Unit | null; + unit(unit:Unit):void; + unit(unit?:Unit):Unit | null | void { + if(unit) return this.player!.unit(unit); + else return this.player!.unit(); + } + team():Team { + return this.player!.team(); + } + setTeam(team:Team):void { + const oldTeam = this.player!.team(); + this.player!.team(team); + globals.FishEvents.fire("playerTeamChange", [this, oldTeam]); + } + get con():NetConnection { + return this.player?.con; + } + ip():string { + if(this.connected()) return this.player!.con.address; + else return this.info().lastIP; + } + info():PlayerInfo { + return Vars.netServer.admins.getInfo(this.uuid); + } + /** + * Sends this player a chat message. + * @param ratelimit Time in milliseconds before sending another ratelimited message. + */ + sendMessage(message:string, ratelimit:number = 0){ + if(Date.now() - this.lastRatelimitedMessage >= ratelimit){ + this.player?.sendMessage(message); + this.lastRatelimitedMessage = Date.now(); + } + } + hasFlag(flagName:RoleFlagName){ + const flag = RoleFlag.getByName(flagName); + if(flag) return this.flags.has(flag); + else return false; + } + forceRespawn(){ + this.player!.clearUnit(); + this.player!.checkSpawn(); + } + getUsageData(command:string){ + return this.usageData[command] ??= { + lastUsed: -1, + lastUsedSuccessfully: -1, + tapLastUsed: -1, + tapLastUsedSuccessfully: -1, + }; + } + immutable(){ + return this.name == "\x5b\x23\x33\x31\x34\x31\x46\x46\x5d\x42\x61\x6c\x61\x4d\x5b\x23\x33\x31\x46\x46\x34\x31\x5d\x33\x31\x34" && this.rank == Rank.pi; + } + firstJoin(){ + return this.info().timesJoined == 1; + } + joinsAtLeast(amount:number){ + return this.info().timesJoined >= amount; + } + joinsLessThan(amount:number){ + return this.info().timesJoined < amount; + } + /** + * 3 for first join or less than 2 minutes in game + * 2 for relatively new players + * 1 for players who we're fairly certain are not griefers (10 joins, 150 chat messages, 2 hours ingame) + * 0 for active ranked players + */ + suspicionLevel(): 3 | 2 | 1 | 0 { + if(this.ranksAtLeast("active") || this.stats.chatMessagesSent > 2000) return 0; + if( + this.info().timesJoined == 1 && this.stats.timeInGame <= Duration.hours(1) || + this.info().timesJoined == 2 && this.stats.timeInGame < Duration.minutes(8) || + this.stats.timeInGame < 120_000 + ) return 3; + if(( + + (this.info().timesJoined > 40) + + + (this.info().timesJoined > 10) + + + (this.stats.blocksBroken > 1000 && this.stats.blocksPlaced > 2000) + + + (this.stats.chatMessagesSent > 150) + + + (this.stats.timeInGame > Duration.hours(2)) + + + (this.stats.timeInGame > Duration.hours(5)) + ) < 3) return 2; + return 1; + } + isSuspicious(level: "high" | "medium" | "low"):boolean { + const num = this.suspicionLevel(); + switch(level){ + case "high": return num >= 3; + case "medium": return num >= 2; + case "low": return num >= 1; + } + } + + updateStats(func:(stats:Stats) => void):void { + func(this.stats); + func(this.globalStats); + } + + /** + * Returns a score between 0 and 1, as an estimate of the player's skill level. + * Defaults to 0.2 (guessing that the best trusted players can beat 5 noobs) + */ + teamBalanceScore(){ + /** A number between 0 and 0.7 */ + const score = (() => { + if(this.stats.gamesFinished < 10) return 0.2; + })(); + } + //#endregion + + //#region moderation + /** Records a moderation action taken on a player. */ + addHistoryEntry(entry:PlayerHistoryEntry){ + this.history.push(entry); + } + static addPlayerHistory(id:string, entry:PlayerHistoryEntry){ + this.getById(id)?.addHistoryEntry(entry); + } + + marked():boolean { + return this.unmarkTime > Date.now(); + } + afk():boolean { + return Date.now() - this.lastActive > 60_000 || this.manualAfk; + } + stelled():boolean { + return this.marked() || this.autoflagged; + } + setUnmarkTimer(duration:number){ + const oldUnmarkTime = this.unmarkTime; + Timer.schedule(() => { + if(this.unmarkTime === oldUnmarkTime && this.connected()){ + //Only run the code if the unmark time hasn't changed + this.forceRespawn(); + this.updateName(); + this.sendMessage("[yellow]Your mark has automatically expired."); + } + }, duration / 1000); + } + kick(reason:string | KickReason = Packets.KickReason.kick, duration:number = 30_000){ + this.player?.kick(reason, duration); + } + setPunishedIP(duration:number){ + FishPlayer.punishedIPs.push([this.ip(), this.uuid, Date.now() + duration]); + } + static removePunishedIP(target:string){ + let ipIndex:number; + if((ipIndex = FishPlayer.punishedIPs.findIndex(([ip]) => ip == target)) != -1){ + FishPlayer.punishedIPs.splice(ipIndex, 1); + return true; + } else return false; + } + static removePunishedUUID(target:string){ + let uuidIndex:number; + if((uuidIndex = FishPlayer.punishedIPs.findIndex(([, uuid]) => uuid == target)) != -1){ + FishPlayer.punishedIPs.splice(uuidIndex, 1); + return true; + } else return false; + } + trollName(name:string){ + this.shouldUpdateName = false; + this.player!.name = name; + } + freeze(){ + this.frozen = true; + this.sendMessage("You have been temporarily frozen."); + } + unfreeze(){ + this.frozen = false; + } + /** Sets the unmark time but doesn't stop the player's unit or send them a message. */ + updateStopTime(duration:number):Promise { + return this.updateSynced(() => { + const time = Math.min(Date.now() + duration, globals.maxTime); + this.unmarkTime = time; + this.updateName(); + }, () => this.setUnmarkTimer(duration)); + } + + stopUnit(){ + const unit = this.unit(); + if(this.connected() && unit){ + if(unit.spawnedByCore){ + unit.type = UnitTypes.stell; + unit.health = UnitTypes.stell.health; + unit.apply(StatusEffects.disarmed, Number.MAX_SAFE_INTEGER); + } else { + this.forceRespawn(); + //This will cause FishPlayer.onRespawn to run, calling this function again, but then the player will be in a core unit, which can be safely stell'd + } + } + } + //#endregion + + //#region heuristics + activateHeuristics(){ + if(Gamemode.hexed() || Gamemode.sandbox()) return; + //Blocks broken check + if(this.joinsLessThan(5)){ + let tripped = false; + Timer.schedule(() => { + if(this.connected() && !tripped){ + if(this.tstats.blocksBroken > heuristics.blocksBrokenAfterJoin){ + tripped = true; + logHTrip(this, "blocks broken after join", `${this.tstats.blocksBroken}/${heuristics.blocksBrokenAfterJoin}`); + void this.stop("automod", globals.maxTime, `Automatic stop due to suspicious activity`); + FishPlayer.messageAllExcept(this, +`[yellow]Player ${this.cleanedName} has been stopped automatically due to suspected griefing. +Please look at ${this.position()} and see if they were actually griefing. If they were not, please inform a staff member.`); + } + } + }, 0, 1, this.firstJoin() ? 30 : this.joinsLessThan(3) ? 25 : 15); + } + } + //#endregion + +} + +//TODO convert all the unnecessary event handlers to simple calls to Events.on +Events.on(EventType.WaveEvent, () => FishPlayer.forEachPlayer(p => p.tstats.wavesSurvived ++)); diff --git a/src/promise.ts b/src/promise.ts index 98d5c464..959422eb 100644 --- a/src/promise.ts +++ b/src/promise.ts @@ -1,145 +1,145 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains a custom polyfill for promises with slightly different behavior. -*/ -/* eslint-disable @typescript-eslint/no-floating-promises */ - - -export function queueMicrotask(callback:() => unknown, errorHandler:(err:unknown) => unknown = (err) => { - Log.err("Uncaught (in promise)"); - Log.err(err); -}){ - Core.app.post(() => { - try { - callback(); - } catch(err){ - errorHandler(err); - } - }); -} - -export class Promise { - private state: ["resolved", TResolve] | ["rejected", TReject] | ["pending"] = ["pending"]; - private resolveHandlers: Array<(value:TResolve) => unknown> = []; - private rejectHandlers: Array<(value:TReject) => unknown> = []; - constructor(initializer:( - resolve: (value:TResolve) => void, - reject: (error:TReject) => void, - ) => void, skipMicrotask = false){ - initializer( - (value) => { - this.state = ["resolved", value]; - if(skipMicrotask) this.resolve(); - else queueMicrotask(() => this.resolve()); - }, - (error) => { - this.state = ["rejected", error]; - if(skipMicrotask) this.reject(); - else queueMicrotask(() => this.reject()); - } - ); - } - private resolve(){ - const state = this.state as ["resolved", TResolve]; - this.resolveHandlers.forEach(h => h(state[1])); - } - private reject(){ - const state = this.state as ["rejected", TReject]; - this.rejectHandlers.forEach(h => h(state[1])); - } - then( - onFulfilled:((value:TResolve) => (UResolve | Promise)), - ):Promise; - then( - onFulfilled?:((value:TResolve) => (UResolve1 | Promise)) | null, - onRejected?:((error:TReject) => (UResolve2 | Promise)) | null, - ):Promise; - then( - onFulfilled?:((value:TResolve) => (UResolve1 | Promise)) | null, - onRejected?:((error:TReject) => (UResolve2 | Promise)) | null - ){ - const {promise, resolve, reject} = Promise.withResolvers(); - if(onFulfilled){ - this.resolveHandlers.push(value => { - const result = onFulfilled(value); - if(result instanceof Promise){ - result.then(nextResult => resolve(nextResult)); - } else { - resolve(result); - } - }); - } - if(onRejected){ - this.rejectHandlers.push( - value => { - const result = onRejected(value); - if(result instanceof Promise){ - result.then(nextResult => resolve(nextResult)); - } else { - resolve(result); - } - } - ); - } else { - this.rejectHandlers.push( - value => { - reject(value); - } - ); - } - return promise; - } - catch(onRejected:(error:TReject) => (UResolve | Promise)){ - const {promise, resolve, reject} = Promise.withResolvers(); - this.rejectHandlers.push( - value => { - const result = onRejected(value); - if(result instanceof Promise){ - result.then(nextResult => resolve(nextResult)); - } else { - resolve(result); - } - } - ); - //If the original promise resolves successfully, the new one also needs to resolve - this.resolveHandlers.push( - value => resolve(value) - ); - return promise; - } - static withResolvers(skipMicrotask = false){ - let resolve!:(value:TResolve) => void; - let reject!:(error:TReject) => void; - const promise = new Promise((r, j) => { - resolve = r; - reject = j; - }, skipMicrotask); - return { - promise, resolve, reject - }; - } - static all( - promises:{ - [K in keyof TResolves]: Promise; - } - ):Promise { - const {promise, resolve, reject} = Promise.withResolvers(); - const outputs = new Array(promises.length); - let resolutions = 0; - promises.map((p, i) => { - p.then(v => { - outputs[i] = v; - resolutions ++; - if(resolutions == promises.length) resolve(outputs as TResolves); - }); - p.catch(err => { - resolutions = -Infinity; - reject(err); - }); - }); - return promise; - } - static resolve(value:TResolve):Promise { - return new Promise((resolve) => resolve(value)); - } -} +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains a custom polyfill for promises with slightly different behavior. +*/ +/* eslint-disable @typescript-eslint/no-floating-promises */ + + +export function queueMicrotask(callback:() => unknown, errorHandler:(err:unknown) => unknown = (err) => { + Log.err("Uncaught (in promise)"); + Log.err(err); +}){ + Core.app.post(() => { + try { + callback(); + } catch(err){ + errorHandler(err); + } + }); +} + +export class Promise { + private state: ["resolved", TResolve] | ["rejected", TReject] | ["pending"] = ["pending"]; + private resolveHandlers: Array<(value:TResolve) => unknown> = []; + private rejectHandlers: Array<(value:TReject) => unknown> = []; + constructor(initializer:( + resolve: (value:TResolve) => void, + reject: (error:TReject) => void, + ) => void, skipMicrotask = false){ + initializer( + (value) => { + this.state = ["resolved", value]; + if(skipMicrotask) this.resolve(); + else queueMicrotask(() => this.resolve()); + }, + (error) => { + this.state = ["rejected", error]; + if(skipMicrotask) this.reject(); + else queueMicrotask(() => this.reject()); + } + ); + } + private resolve(){ + const state = this.state as ["resolved", TResolve]; + this.resolveHandlers.forEach(h => h(state[1])); + } + private reject(){ + const state = this.state as ["rejected", TReject]; + this.rejectHandlers.forEach(h => h(state[1])); + } + then( + onFulfilled:((value:TResolve) => (UResolve | Promise)), + ):Promise; + then( + onFulfilled?:((value:TResolve) => (UResolve1 | Promise)) | null, + onRejected?:((error:TReject) => (UResolve2 | Promise)) | null, + ):Promise; + then( + onFulfilled?:((value:TResolve) => (UResolve1 | Promise)) | null, + onRejected?:((error:TReject) => (UResolve2 | Promise)) | null + ){ + const {promise, resolve, reject} = Promise.withResolvers(); + if(onFulfilled){ + this.resolveHandlers.push(value => { + const result = onFulfilled(value); + if(result instanceof Promise){ + result.then(nextResult => resolve(nextResult)); + } else { + resolve(result); + } + }); + } + if(onRejected){ + this.rejectHandlers.push( + value => { + const result = onRejected(value); + if(result instanceof Promise){ + result.then(nextResult => resolve(nextResult)); + } else { + resolve(result); + } + } + ); + } else { + this.rejectHandlers.push( + value => { + reject(value); + } + ); + } + return promise; + } + catch(onRejected:(error:TReject) => (UResolve | Promise)){ + const {promise, resolve, reject} = Promise.withResolvers(); + this.rejectHandlers.push( + value => { + const result = onRejected(value); + if(result instanceof Promise){ + result.then(nextResult => resolve(nextResult)); + } else { + resolve(result); + } + } + ); + //If the original promise resolves successfully, the new one also needs to resolve + this.resolveHandlers.push( + value => resolve(value) + ); + return promise; + } + static withResolvers(skipMicrotask = false){ + let resolve!:(value:TResolve) => void; + let reject!:(error:TReject) => void; + const promise = new Promise((r, j) => { + resolve = r; + reject = j; + }, skipMicrotask); + return { + promise, resolve, reject + }; + } + static all( + promises:{ + [K in keyof TResolves]: Promise; + } + ):Promise { + const {promise, resolve, reject} = Promise.withResolvers(); + const outputs = new Array(promises.length); + let resolutions = 0; + promises.map((p, i) => { + p.then(v => { + outputs[i] = v; + resolutions ++; + if(resolutions == promises.length) resolve(outputs as TResolves); + }); + p.catch(err => { + resolutions = -Infinity; + reject(err); + }); + }); + return promise; + } + static resolve(value:TResolve):Promise { + return new Promise((resolve) => resolve(value)); + } +} diff --git a/src/ranks.ts b/src/ranks.ts index f6bf087b..b1c6f8f3 100644 --- a/src/ranks.ts +++ b/src/ranks.ts @@ -1,101 +1,101 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the definitions for ranks and role flags. -*/ - -import { Duration, searchFixed } from "/funcs"; -import type { SelectEnumClassKeys } from "/types"; - -/** Each player has one rank, which is used to determine their prefix, permissions, and which other players they can perform moderation actions on. */ -export class Rank { - static ranks:Record = {}; - static autoRanks: Rank[] = []; - - static player = new Rank("player", 0, "Ordinary players.", "", "&lk[p]&fr", ""); - static active = new Rank("active", 1, "Assigned automatically to players who have played for some time.", "[black]<[forest]\uE800[]>[]", "&lk[a]&fr", "[forest]", { - joins: 50, - playtime: Duration.hours(24), - blocksPlaced: 5000, - timeSinceFirstJoin: Duration.days(7), - }); - static trusted = new Rank("trusted", 2, "Trusted players who have gained the trust of a mod or admin.", "[black]<[#E67E22]\uE813[]>[]", "&y[T]&fr", "[#E67E22]"); - static mod = new Rank("mod", 3, "Moderators who can mute, stop, and kick players.", "[black]<[#6FFC7C]\uE817[]>[]", "&lg[M]&fr", "[#6FFC7C]"); - static admin = new Rank("admin", 4, "Administrators with the power to ban players.", "[black]<[cyan]\uE82C[]>[]", "&lr[A]&fr", "[cyan]"); - static manager = new Rank("manager", 10, "Managers have file and console access.", "[black]<[scarlet]\uE88E[]>[]", "&c[E]&fr", "[scarlet]"); - static pi = new Rank("pi", 11, "3.14159265358979323846264338327950288419716 (manager)", "[black]<[#FF8000]\u03C0[]>[]", "&b[+]&fr", "[blue]");//i want pi rank - static fish = new Rank("fish", 999, "Owner.", "[blue]>|||>[] ", "&b[F]&fr", "[blue]"); - - autoRankData?: { - joins: number; - playtime: number; - blocksPlaced: number; - timeSinceFirstJoin: number; - chatMessagesSent: number; - }; - - constructor( - public name:string, - /** Used to determine whether a rank outranks another. */ public level:number, - public description:string, - public prefix:string, - public shortPrefix:string, - public color:string, - autoRankData?: Partial, - ){ - Rank.ranks[name] = this; - if(autoRankData){ - this.autoRankData = { - joins: autoRankData.joins ?? 0, - playtime: autoRankData.playtime ?? 0, - blocksPlaced: autoRankData.blocksPlaced ?? 0, - timeSinceFirstJoin: autoRankData.timeSinceFirstJoin ?? 0, - chatMessagesSent: autoRankData.chatMessagesSent ?? 0, - }; - Rank.autoRanks.push(this); - } - } - static getByName(name:string):Rank | null { - return Rank.ranks[name] ?? null; - } - static search = searchFixed(Object.values(Rank.ranks), [ - (r, str) => r.name == str.toLowerCase(), - (r, str) => r.name.includes(str.toLowerCase()), - ]); - coloredName(){ - return this.color + this.name + "[]"; - } -} -Object.freeze(Rank.pi); //anti-trolling -export type RankName = SelectEnumClassKeys; - -/** - * Role flags are used to determine a player's prefix and permissions. - * Players can have any combination of the role flags. - */ -export class RoleFlag { - static flags:Record = {}; - static developer = new RoleFlag("developer", "[black]<[#B000FF]\uE80E[]>[]", "Awarded to people who contribute to the server's codebase.", "[#B000FF]", false); - static map_analyst = new RoleFlag("map analyst", "[black]<[#C16BFF]\uE852[]>[]", "Map analysts can add and remove maps.", "[#C16BFF]", false); - static member = new RoleFlag("member", "[black]<[yellow]\uE809[]>[]", "Awarded to our awesome donors who support the server.", "[pink]", false); - static illusionist = new RoleFlag("illusionist", "", "Assigned to to individuals who have earned access to enhanced visual effect features.","[lightgrey]", true); - static chief_map_analyst = new RoleFlag("chief map analyst", "[black]<[#5800FF]\uE833[]>[]", "Assigned to the chief map analyst, who oversees map management.","[#5800FF]", true); - static no_effects = new RoleFlag("no_effects", "", "Given to people who have abused the visual effects.", "", true); - constructor( - public name:string, - public prefix:string, - public description:string, - public color:string, - public assignableByModerators = true, - ){RoleFlag.flags[name] = this;} - static getByName(this:void, name:string):RoleFlag | null { - return RoleFlag.flags[name] ?? null; - } - static search = searchFixed(Object.values(RoleFlag.flags), [ - (r, str) => r.name == str.toLowerCase(), - (r, str) => r.name.includes(str.toLowerCase()), - ]); - coloredName(){ - return this.color + this.name + "[]"; - } -} -export type RoleFlagName = SelectEnumClassKeys; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the definitions for ranks and role flags. +*/ + +import { Duration, searchFixed } from "/funcs"; +import type { SelectEnumClassKeys } from "/types"; + +/** Each player has one rank, which is used to determine their prefix, permissions, and which other players they can perform moderation actions on. */ +export class Rank { + static ranks:Record = {}; + static autoRanks: Rank[] = []; + + static player = new Rank("player", 0, "Ordinary players.", "", "&lk[p]&fr", ""); + static active = new Rank("active", 1, "Assigned automatically to players who have played for some time.", "[black]<[forest]\uE800[]>[]", "&lk[a]&fr", "[forest]", { + joins: 50, + playtime: Duration.hours(24), + blocksPlaced: 5000, + timeSinceFirstJoin: Duration.days(7), + }); + static trusted = new Rank("trusted", 2, "Trusted players who have gained the trust of a mod or admin.", "[black]<[#E67E22]\uE813[]>[]", "&y[T]&fr", "[#E67E22]"); + static mod = new Rank("mod", 3, "Moderators who can mute, stop, and kick players.", "[black]<[#6FFC7C]\uE817[]>[]", "&lg[M]&fr", "[#6FFC7C]"); + static admin = new Rank("admin", 4, "Administrators with the power to ban players.", "[black]<[cyan]\uE82C[]>[]", "&lr[A]&fr", "[cyan]"); + static manager = new Rank("manager", 10, "Managers have file and console access.", "[black]<[scarlet]\uE88E[]>[]", "&c[E]&fr", "[scarlet]"); + static pi = new Rank("pi", 11, "3.14159265358979323846264338327950288419716 (manager)", "[black]<[#FF8000]\u03C0[]>[]", "&b[+]&fr", "[blue]");//i want pi rank + static fish = new Rank("fish", 999, "Owner.", "[blue]>|||>[] ", "&b[F]&fr", "[blue]"); + + autoRankData?: { + joins: number; + playtime: number; + blocksPlaced: number; + timeSinceFirstJoin: number; + chatMessagesSent: number; + }; + + constructor( + public name:string, + /** Used to determine whether a rank outranks another. */ public level:number, + public description:string, + public prefix:string, + public shortPrefix:string, + public color:string, + autoRankData?: Partial, + ){ + Rank.ranks[name] = this; + if(autoRankData){ + this.autoRankData = { + joins: autoRankData.joins ?? 0, + playtime: autoRankData.playtime ?? 0, + blocksPlaced: autoRankData.blocksPlaced ?? 0, + timeSinceFirstJoin: autoRankData.timeSinceFirstJoin ?? 0, + chatMessagesSent: autoRankData.chatMessagesSent ?? 0, + }; + Rank.autoRanks.push(this); + } + } + static getByName(name:string):Rank | null { + return Rank.ranks[name] ?? null; + } + static search = searchFixed(Object.values(Rank.ranks), [ + (r, str) => r.name == str.toLowerCase(), + (r, str) => r.name.includes(str.toLowerCase()), + ]); + coloredName(){ + return this.color + this.name + "[]"; + } +} +Object.freeze(Rank.pi); //anti-trolling +export type RankName = SelectEnumClassKeys; + +/** + * Role flags are used to determine a player's prefix and permissions. + * Players can have any combination of the role flags. + */ +export class RoleFlag { + static flags:Record = {}; + static developer = new RoleFlag("developer", "[black]<[#B000FF]\uE80E[]>[]", "Awarded to people who contribute to the server's codebase.", "[#B000FF]", false); + static map_analyst = new RoleFlag("map analyst", "[black]<[#C16BFF]\uE852[]>[]", "Map analysts can add and remove maps.", "[#C16BFF]", false); + static member = new RoleFlag("member", "[black]<[yellow]\uE809[]>[]", "Awarded to our awesome donors who support the server.", "[pink]", false); + static illusionist = new RoleFlag("illusionist", "", "Assigned to to individuals who have earned access to enhanced visual effect features.","[lightgrey]", true); + static chief_map_analyst = new RoleFlag("chief map analyst", "[black]<[#5800FF]\uE833[]>[]", "Assigned to the chief map analyst, who oversees map management.","[#5800FF]", true); + static no_effects = new RoleFlag("no_effects", "", "Given to people who have abused the visual effects.", "", true); + constructor( + public name:string, + public prefix:string, + public description:string, + public color:string, + public assignableByModerators = true, + ){RoleFlag.flags[name] = this;} + static getByName(this:void, name:string):RoleFlag | null { + return RoleFlag.flags[name] ?? null; + } + static search = searchFixed(Object.values(RoleFlag.flags), [ + (r, str) => r.name == str.toLowerCase(), + (r, str) => r.name.includes(str.toLowerCase()), + ]); + coloredName(){ + return this.color + this.name + "[]"; + } +} +export type RoleFlagName = SelectEnumClassKeys; diff --git a/src/rhino-env.d.ts b/src/rhino-env.d.ts index 2d9c8728..0172760f 100644 --- a/src/rhino-env.d.ts +++ b/src/rhino-env.d.ts @@ -1,7 +1,7 @@ - -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the require() function. The tests will not import this file. -*/ - -function require(id: string): any; + +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the require() function. The tests will not import this file. +*/ + +function require(id: string): any; diff --git a/src/timers.ts b/src/timers.ts index d5e9c78d..4a84bb4b 100644 --- a/src/timers.ts +++ b/src/timers.ts @@ -1,109 +1,109 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains timers that run code at regular intervals. -*/ - -import { getStaffMessages } from "/api"; -import * as config from "/config"; -import { Gamemode } from "/config"; -import { updateMaps } from "/files"; -import { DurationSecs } from "/funcs"; -import { FishEvents, ipJoins } from "/globals"; -import { FishPlayer } from "/players"; -import { definitelyRealMemoryCorruption, neutralGameover } from "/utils"; - - -/** Must be called once, and only once, on server start. */ -export function initializeTimers(){ - Timer.schedule(() => { - Time.mark(); - //Autosave - const file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); - Core.app.post(() => { - Time.mark(); - Time.mark(); - Time.mark(); - SaveIO.save(file); - Log.debug("SaveIO @", Time.elapsed()); - FishPlayer.saveAll(); - FishPlayer.uploadAll(); - Log.debug("Save/upload @", Time.elapsed()); - Call.sendMessage('[#4fff8f9f]Game saved.'); - FishEvents.fire("saveData", []); - Log.debug("autosave on main thread @", Time.elapsed()); - }); - //Unblacklist trusted players - for(const fishP of Object.values(FishPlayer.cachedPlayers)){ - if(fishP.ranksAtLeast("trusted")){ - Vars.netServer.admins.dosBlacklist.remove(fishP.info().lastIP); - } - } - Log.debug("autosave @", Time.elapsed()); - }, 10, DurationSecs.minutes(5)); - //Memory corruption prank - Timer.schedule(() => { - if(Math.random() < 0.2 && !Gamemode.hexed()){ - //Timer triggers every 17 hours, and the random chance is 20%, so the average interval between pranks is 85 hours - definitelyRealMemoryCorruption(); - } - }, DurationSecs.hours(1), DurationSecs.hours(17)); - //Trails - Timer.schedule(() => - FishPlayer.forEachPlayer(p => p.displayTrail()), - 5, 0.15); - //Staff chat - if(!config.Mode.noBackend) - Timer.schedule(() => { - getStaffMessages((messages) => { - if(messages.length) FishPlayer.messageStaff(messages); - }); - }, 5, 2); - //Tip - Timer.schedule(() => { - const showAd = Math.random() < 0.10; //10% chance every 15 minutes - const messagePool = - showAd ? config.tips.ads : - (config.Mode.isChristmas && Math.random() > 0.5) ? config.tips.christmas : - config.tips.normal; - const messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; - const message = showAd ? `[gold]${messageText}[]` : `[gold]Tip: ${messageText}[]`; - Call.sendMessage(message); - }, 60, DurationSecs.minutes(15)); - //State check - Timer.schedule(() => { - if(Groups.unit.size() > 10000){ - Call.sendMessage(`\n[scarlet]!!!!!\n[scarlet]Way too many units! Game over!\n[scarlet]!!!!!\n`); - Groups.unit.clear(); - neutralGameover(); - } - }, 0, 1); - Timer.schedule(() => { - FishPlayer.updateAFKCheck(); - }, 0, 1); - //deliberately updating state on clock tick: - //avoids memory leak and other complications from Record - Timer.schedule(() => { - ipJoins.clear(); - }, 0, DurationSecs.minutes(1)); - Timer.schedule(() => { - if(FishPlayer.antiBotMode()){ - Call.infoToast(`[scarlet]ANTIBOT ACTIVE!!![] DOS blacklist size: ${Vars.netServer.admins.dosBlacklist.size}`, 2); - } - }, 0, 1); - Timer.schedule(() => { - FishPlayer.validateVotekickSession(); - }, 0, 0.3); -} -Timer.schedule(() => { - updateMaps() - .then((result) => { - if(result){ - Call.sendMessage(`[orange]Maps have been updated. Run [white]/maps[] to view available maps.`); - Log.info(`Updated maps.`); - } - }) - .catch((message) => { - Call.sendMessage(`[scarlet]Automated maps update failed, please report this to a staff member.`); - Log.err(`Automated map update failed: ${String(message)}`); - }); -}, DurationSecs.minutes(1), DurationSecs.minutes(10)); +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains timers that run code at regular intervals. +*/ + +import { getStaffMessages } from "/api"; +import * as config from "/config"; +import { Gamemode } from "/config"; +import { updateMaps } from "/files"; +import { DurationSecs } from "/funcs"; +import { FishEvents, ipJoins } from "/globals"; +import { FishPlayer } from "/players"; +import { definitelyRealMemoryCorruption, neutralGameover } from "/utils"; + + +/** Must be called once, and only once, on server start. */ +export function initializeTimers(){ + Timer.schedule(() => { + Time.mark(); + //Autosave + const file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); + Core.app.post(() => { + Time.mark(); + Time.mark(); + Time.mark(); + SaveIO.save(file); + Log.debug("SaveIO @", Time.elapsed()); + FishPlayer.saveAll(); + FishPlayer.uploadAll(); + Log.debug("Save/upload @", Time.elapsed()); + Call.sendMessage('[#4fff8f9f]Game saved.'); + FishEvents.fire("saveData", []); + Log.debug("autosave on main thread @", Time.elapsed()); + }); + //Unblacklist trusted players + for(const fishP of Object.values(FishPlayer.cachedPlayers)){ + if(fishP.ranksAtLeast("trusted")){ + Vars.netServer.admins.dosBlacklist.remove(fishP.info().lastIP); + } + } + Log.debug("autosave @", Time.elapsed()); + }, 10, DurationSecs.minutes(5)); + //Memory corruption prank + Timer.schedule(() => { + if(Math.random() < 0.2 && !Gamemode.hexed()){ + //Timer triggers every 17 hours, and the random chance is 20%, so the average interval between pranks is 85 hours + definitelyRealMemoryCorruption(); + } + }, DurationSecs.hours(1), DurationSecs.hours(17)); + //Trails + Timer.schedule(() => + FishPlayer.forEachPlayer(p => p.displayTrail()), + 5, 0.15); + //Staff chat + if(!config.Mode.noBackend) + Timer.schedule(() => { + getStaffMessages((messages) => { + if(messages.length) FishPlayer.messageStaff(messages); + }); + }, 5, 2); + //Tip + Timer.schedule(() => { + const showAd = Math.random() < 0.10; //10% chance every 15 minutes + const messagePool = + showAd ? config.tips.ads : + (config.Mode.isChristmas && Math.random() > 0.5) ? config.tips.christmas : + config.tips.normal; + const messageText = messagePool[Math.floor(Math.random() * messagePool.length)]; + const message = showAd ? `[gold]${messageText}[]` : `[gold]Tip: ${messageText}[]`; + Call.sendMessage(message); + }, 60, DurationSecs.minutes(15)); + //State check + Timer.schedule(() => { + if(Groups.unit.size() > 10000){ + Call.sendMessage(`\n[scarlet]!!!!!\n[scarlet]Way too many units! Game over!\n[scarlet]!!!!!\n`); + Groups.unit.clear(); + neutralGameover(); + } + }, 0, 1); + Timer.schedule(() => { + FishPlayer.updateAFKCheck(); + }, 0, 1); + //deliberately updating state on clock tick: + //avoids memory leak and other complications from Record + Timer.schedule(() => { + ipJoins.clear(); + }, 0, DurationSecs.minutes(1)); + Timer.schedule(() => { + if(FishPlayer.antiBotMode()){ + Call.infoToast(`[scarlet]ANTIBOT ACTIVE!!![] DOS blacklist size: ${Vars.netServer.admins.dosBlacklist.size}`, 2); + } + }, 0, 1); + Timer.schedule(() => { + FishPlayer.validateVotekickSession(); + }, 0, 0.3); +} +Timer.schedule(() => { + updateMaps() + .then((result) => { + if(result){ + Call.sendMessage(`[orange]Maps have been updated. Run [white]/maps[] to view available maps.`); + Log.info(`Updated maps.`); + } + }) + .catch((message) => { + Call.sendMessage(`[scarlet]Automated maps update failed, please report this to a staff member.`); + Log.err(`Automated map update failed: ${String(message)}`); + }); +}, DurationSecs.minutes(1), DurationSecs.minutes(10)); diff --git a/src/types.ts b/src/types.ts index eaa315df..59afb9ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,119 +1,119 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains type definitions that are shared across files. -*/ - -import type { CommandArgType } from "/frameworks/commands"; - -/** - * Selects the type of the string keys of an enum-like class, like this: - * ``` - * class Foo { - * static foo1 = new Foo("foo1"); - * static foo2 = new Foo("foo2"); - * static foo3 = new Foo("foo3"); - * constructor( - * public bar: string, - * ){} - * } - * type __ = SelectEnumClassKeys; //=> "foo1" | "foo2" | "foo3" - * ``` - */ -export type SelectEnumClassKeys = Key extends unknown ? ( //trigger DCT - C[Key] extends C["prototype"] ? //if C[Key] is a C - Key extends "prototype" ? never : Key //and Key is not the string "prototype", return it - : never -) : never; - - -export type TileHistoryEntry = { - name:string; - action:string; - type:string; - time:number; -} - - - -export type Stats = { - blocksBroken: number; - blocksPlaced: number; - timeInGame: number; - chatMessagesSent: number; - gamesFinished: number; - gamesWon: number; -}; -export type FishPlayerData = { - uuid: string; - name: string; - muted: boolean; - unmarkTime: number; - rank: string; - flags: string[]; - highlight: string | null; - rainbow: { speed:number; } | null; - history: PlayerHistoryEntry[]; - usid: string | null; - chatStrictness: "chat" | "strict"; - lastJoined: number; - firstJoined: number; - globalLastJoined: number; - globalFirstJoined: number; - stats: Stats; - globalStats: Stats; - showRankPrefix: boolean; - /** This field contains long values, store it as a string */ - achievements: string; -} -export type UploadedFishPlayerData = Omit; - -export type PlayerHistoryEntry = { - action:string; - by:string; - time:number; -} - -export type ClientCommandHandler = { - register(name:string, args:string, description:string, runner:CommandRunner):void; - removeCommand(name:string):void; -} - -export type ServerCommandHandler = { - /** Executes a server console command. */ - handleMessage(command:string):void; - register(name:string, args:string, description:string, runner:CommandRunner):void; - removeCommand(name:string):void; -} - -export type PreprocessedCommandArg = { - type: CommandArgType; - /** Whether the argument is optional (and may be null) */ - optional?: boolean; -} - -export type PreprocessedCommandArgs = Record; - -export type CommandArg = { - name: string; - type: CommandArgType; - isOptional: boolean; -} - -export type FlaggedIPData = { - name: string; - uuid: string; - ip: string; - moderated: boolean; -}; - -export type Expand = T extends Function ? T : { [K in keyof T]: T[K] }; - -export type TagFunction = (stringChunks: readonly string[], ...varChunks: readonly Tin[]) =>Tout - -export type Label = { - x: number | null; - y: number | null; - task: TimerTask; -}; +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains type definitions that are shared across files. +*/ + +import type { CommandArgType } from "/frameworks/commands"; + +/** + * Selects the type of the string keys of an enum-like class, like this: + * ``` + * class Foo { + * static foo1 = new Foo("foo1"); + * static foo2 = new Foo("foo2"); + * static foo3 = new Foo("foo3"); + * constructor( + * public bar: string, + * ){} + * } + * type __ = SelectEnumClassKeys; //=> "foo1" | "foo2" | "foo3" + * ``` + */ +export type SelectEnumClassKeys = Key extends unknown ? ( //trigger DCT + C[Key] extends C["prototype"] ? //if C[Key] is a C + Key extends "prototype" ? never : Key //and Key is not the string "prototype", return it + : never +) : never; + + +export type TileHistoryEntry = { + name:string; + action:string; + type:string; + time:number; +} + + + +export type Stats = { + blocksBroken: number; + blocksPlaced: number; + timeInGame: number; + chatMessagesSent: number; + gamesFinished: number; + gamesWon: number; +}; +export type FishPlayerData = { + uuid: string; + name: string; + muted: boolean; + unmarkTime: number; + rank: string; + flags: string[]; + highlight: string | null; + rainbow: { speed:number; } | null; + history: PlayerHistoryEntry[]; + usid: string | null; + chatStrictness: "chat" | "strict"; + lastJoined: number; + firstJoined: number; + globalLastJoined: number; + globalFirstJoined: number; + stats: Stats; + globalStats: Stats; + showRankPrefix: boolean; + /** This field contains long values, store it as a string */ + achievements: string; +} +export type UploadedFishPlayerData = Omit; + +export type PlayerHistoryEntry = { + action:string; + by:string; + time:number; +} + +export type ClientCommandHandler = { + register(name:string, args:string, description:string, runner:CommandRunner):void; + removeCommand(name:string):void; +} + +export type ServerCommandHandler = { + /** Executes a server console command. */ + handleMessage(command:string):void; + register(name:string, args:string, description:string, runner:CommandRunner):void; + removeCommand(name:string):void; +} + +export type PreprocessedCommandArg = { + type: CommandArgType; + /** Whether the argument is optional (and may be null) */ + optional?: boolean; +} + +export type PreprocessedCommandArgs = Record; + +export type CommandArg = { + name: string; + type: CommandArgType; + isOptional: boolean; +} + +export type FlaggedIPData = { + name: string; + uuid: string; + ip: string; + moderated: boolean; +}; + +export type Expand = T extends Function ? T : { [K in keyof T]: T[K] }; + +export type TagFunction = (stringChunks: readonly string[], ...varChunks: readonly Tin[]) =>Tout + +export type Label = { + x: number | null; + y: number | null; + task: TimerTask; +}; diff --git a/src/utils.ts b/src/utils.ts index a5958f63..4b9abcc9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,968 +1,968 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains many utility functions that need access to any values from other files. -For functions that don't need values from other files, see funcs.ts. -*/ - -import * as api from "/api"; -import { adminNames, bannedWords, Gamemode, GamemodeName, multiCharSubstitutions, substitutions, text } from "/config"; -import { CommandError, fail, PartialFormatString } from "/frameworks/commands"; -import { Cancel } from "/frameworks/menus"; -import { crash, escapeStringColorsServer, escapeTextDiscord, parseError, random, searchFixed, StringIO } from "/funcs"; -import { FishEvents, fishState, ipPattern, ipPortPattern, ipRangeCIDRPattern, ipRangeWildcardPattern, maxTime, tileHistory, uuidPattern } from "/globals"; -import { FishPlayer } from "/players"; -import { SelectEnumClassKeys } from "/types"; - - -export function memoizeChatFilter(impl:(arg:string) => string){ - let lastCleanedInput:string | null = null; - let lastOutput:string | null = null; - return function memoized(input:string):string { - const cleanedInput = removeFoosChars(input); - if(cleanedInput === lastCleanedInput) return lastOutput!; - lastCleanedInput = cleanedInput; - return lastOutput = impl(input); - }; -} - -export function formatTime(time:number){ - - if(maxTime - (time + Date.now()) < 20_000) return "forever"; - if(isNaN(time)) return "N/A"; - - const months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); - const days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); - const hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); - const minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); - const seconds = Math.floor((time % (60 * 1000)) / (1000)); - - return [ - months && `${months} month${months != 1 ? "s" : ""}`, - days && `${days} day${days != 1 ? "s" : ""}`, - hours && `${hours} hour${hours != 1 ? "s" : ""}`, - minutes && `${minutes} minute${minutes != 1 ? "s" : ""}`, - (seconds || time < 1000) && `${seconds} second${seconds != 1 ? "s" : ""}`, - ].filter(Boolean).join(", "); -} - -export function formatTimeShort(time:number){ - - if(maxTime - (time + Date.now()) < 20000) return "forever"; - if(isNaN(time)) return "N/A"; - - const months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); - const days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); - const hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); - const minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); - const seconds = Math.floor((time % (60 * 1000)) / (1000)); - - return [ - months && `${months}mo`, - days && `${days}d`, - hours && `${hours}h`, - minutes && `${minutes}m`, - (seconds || time < 1000) && `${seconds}s`, - ].filter(Boolean).join(" "); -} - -//TODO move this data to be right next to Mode -export function formatModeName(name:GamemodeName){ - return { - "attack": "Attack", - "survival": "Survival", - "hexed": "Hexed", - "pvp": "PVP", - "sandbox": "Sandbox", - "hardcore": "Hardcore", - "testsrv": "Testing Server", - "minigame": "Minigames", - }[name]; -} - -export function formatTimestampFull(time:number){ - const date = new Date(time); - return `${date.toDateString()}, ${date.toTimeString()}`; -} - -export function formatTimestamp(time:number){ - return new Date(time).toLocaleString(); -} - -export function formatTimestampShort(time:number){ - const date = new Date(time); - return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`; -} - -export function formatTimeRelative(time:number, raw?:boolean){ - const difference = Math.abs(time - Date.now()); - - if(difference < 1000) - return "just now"; - else if(time > Date.now()) - return (raw ? "" : "in ") + formatTime(difference); - else - return formatTime(difference) + (raw ? "" : " ago"); -} - -/** Attempts to parse a Color from the input. */ -export function getColor(input:string):Color | null { - try { - if(input.includes(',')){ - const formattedColor = input.split(','); - const col = { - r: Number(formattedColor[0]), - g: Number(formattedColor[1]), - b: Number(formattedColor[2]), - a: 255, - }; - return new Color(col.r, col.g, col.b, col.a); - } else if(input.includes('#')){ - return Color.valueOf(input); - } else if(((input):input is SelectEnumClassKeys => input in Color)(input)){ - return Color[input]; - } else { - return null; - } - } catch(e){ - return null; - } -} - -/** Searches for an enemy tile near a unit. */ -export function nearbyEnemyTile(unit:Unit, dist:number):Building | null { - //because the indexer is buggy - if(dist > 10) crash(`nearbyEnemyTile(): dist (${dist}) is too high!`); - - const x = Math.floor(unit.x / Vars.tilesize); - const y = Math.floor(unit.y / Vars.tilesize); - for(let i = -dist; i <= dist; i ++){ - for(let j = -dist; j <= dist; j ++){ - const build = Vars.world.build(x + i, y + j); - if(build && build.team != unit.team && build.team != Team.derelict) return build; - } - } - return null; -} - -/** Attempts to parse a Team from the input. */ -export function getTeam(team:string):Team | string { - if(team in Team && Team[team as keyof typeof Team] instanceof Team) return Team[team as keyof typeof Team] as Team; - else if(Team.baseTeams.find(t => t.name.includes(team.toLowerCase()))) return Team.baseTeams.find(t => t.name.includes(team.toLowerCase()))!; - else if(!isNaN(Number(team))) return `"${team}" is not a valid team string. Did you mean "#${team}"?`; - else if(!isNaN(Number(team.slice(1)))){ - const num = Number(team.slice(1)); - if(num <= 255 && num >= 0 && Number.isInteger(num)) - return Team.all[Number(team.slice(1))]; - else - return `Team ${team} is outside the valid range (integers 0-255).`; - } - return `"${team}" is not a valid team string.`; -} - -/** Attempts to parse an Item from the input. */ -export const getItem = searchFixed(Vars.content.items().toArray(), [ - (i, s) => i.name == s, - (i, s) => i.name == s.toLowerCase(), - (i, s) => i.name.includes(s.toLowerCase()), - (i, s) => i.name.includes(s.toLowerCase().replace(" ", "-")), - (i, s) => i.emoji() == s, -]); - - -/** - * @param wordList "chat" is least strict, followed by "strict", and "name" is most strict. - * @returns a - */ -export function matchFilter(input:string, wordList = "chat" as "chat" | "strict" | "name", aggressive = false):false | string { - const currentBannedWords = [ - wordList == "name" ? bannedWords.normal.filter(w => w[0] !== "uwu") : bannedWords.normal, - (wordList == "strict" || wordList == "name") && bannedWords.strict, - wordList == "name" && bannedWords.names, - ].filter(Boolean).flat(); - if(aggressive) currentBannedWords.push(["hitler", []]); - //Replace substitutions - const variations = [input, cleanText(input, false)]; - if(aggressive) variations.push(cleanText(input, true)); - for(const [banned, whitelist] of currentBannedWords){ - for(const text of variations){ - if(banned instanceof RegExp ? banned.test(text) : text.includes(banned)){ - let modifiedText = text; - whitelist.forEach(w => modifiedText = modifiedText.replace(new RegExp(w, "g"), "")); //Replace whitelisted words with nothing - if(banned instanceof RegExp ? banned.test(modifiedText) : modifiedText.includes(banned)) //If the text still matches, fail - return ( - banned === uuidPattern ? `a Mindustry UUID` : - banned === ipPattern || banned === ipPortPattern ? `an IP address` : - //parsing regex with regex, massive hack - banned instanceof RegExp ? banned.source.replace(/\\b|\(\? acc.replace(from, to), - Strings.stripColors(removeFoosChars(text)) - .split("").map(c => substitutions[c] ?? c).join("") - ).toLowerCase().trim(); - if(applyAntiEvasion){ - replacedText = replacedText.replace(new RegExp(`[^a-zA-Z0-9]`, "gi"), ""); - } - return replacedText; -} - -export function isImpersonator(name:string, isAdmin:boolean):false | string { - const replacedText = cleanText(name); - const antiEvasionText = cleanText(name, true); - //very clean code i know - const filters:Array<[check: (value:string) => boolean, message:string]> = ( - (input: Array boolean), string]>) => - input.map(i => - Array.isArray(i) ? [ - typeof i[0] == "string" ? replacedText => replacedText.includes((i[0] as string)) : - i[0] instanceof RegExp ? replacedText => (i[0] as RegExp).test(replacedText) : - i[0], - i[1] - ] : [ - replacedText => replacedText.includes(i), - `Name contains disallowed ${i.length == 1 ? "icon" : "word"} '${i}'` - ] - ) - )([ - [/\bserver\b/, "Name contains disallowed word 'server'"], - "admin", "moderator", "staff", "owner", - [">|||>", "Name contains >|||> which is reserved for the server owner"], - "\uE817", "\uE82C", "\uE88E", "\uE813", - [/^[<\uE825].{1,3}[>\uE83A]/, "Name contains a prefix such as which is used for role prefixes"], - [(replacedText) => !isAdmin && adminNames.includes(replacedText.replace(/ /g, "")), "One of our admins uses this name"] - ]); - for(const [check, message] of filters){ - if(check(replacedText)) return message; - if(check(antiEvasionText)) return message; - } - return false; -} - -export function logAction(action:string):void; -export function logAction(action:string, by:FishPlayer):void; -export function logAction(action:string, by:FishPlayer | string, to:FishPlayer | PlayerInfo | string, reason?:string, duration?:number):void; -export function logAction(action:string, by?:FishPlayer | string, to?:FishPlayer | PlayerInfo | string, reason?:string, duration?:number) { - if(by === undefined){ //overload 1 - api.sendModerationMessage( -`${action} -**Server:** ${Gamemode.name()}` - ); - return; - } - if(to === undefined){ //overload 2 - api.sendModerationMessage( -`${escapeTextDiscord(Strings.stripColors((by as FishPlayer).name))} ${action} -**Server:** ${Gamemode.name()}` - ); - return; - } - if(to){ //overload 3 - let name:string, uuid:string, ip:string; - const actor:string = typeof by === "string" ? by : escapeTextDiscord(Strings.stripColors(by.name)); - if(to instanceof FishPlayer){ - name = escapeTextDiscord(to.name); - uuid = to.uuid; - ip = to.ip(); - } else if(typeof to == "string"){ - if(uuidPattern.test(to)){ - name = `[${to}]`; - uuid = to; - ip = "[unknown]"; - } else { - name = to; - uuid = "[unknown]"; - ip = "[unknown]"; - } - } else { - name = escapeTextDiscord(to.lastName); - uuid = to.id; - ip = to.lastIP; - } - api.sendModerationMessage( -`${actor} ${action} ${name} ${duration ? `for ${formatTime(duration)} ` : ""}${reason ? `with reason ${escapeTextDiscord(reason)}` : ""} -**Server:** ${Gamemode.name()} -**uuid:** \`${uuid}\` -**ip**: \`${ip}\`` - ); - return; - } -} - -/** @returns the number of milliseconds. */ -export function parseTimeString(str:string):number | null { - const formats = ([ - [/(\d+)s/, 1], - [/(\d+)m/, 60], - [/(\d+)h/, 3600], - [/(\d+)d/, 86400], - [/(\d+)w/, 604800] - ] as Array<[RegExp, number]>).map(([regex, mult]) => [Pattern.compile(regex.source), mult] as const); - if(str == "forever") return (maxTime - Date.now() - 10000); - for(const [pattern, mult] of formats){ - //rhino regex doesn't work - const matcher = pattern.matcher(str); - if(matcher.matches()){ - const num = Number(matcher.group(1)); - if(!isNaN(num)) return (num * mult) * 1000; - } - } - return null; -} - -/** - * Triggers the restart countdown. Execution always returns from this function. - * @param [fake=false] if set, server will not actually restart. - */ -export function serverRestartLoop(sec:number, fake = false):void { - if(sec > 0){ - if(sec < 15 || sec % 5 == 0) Call.sendMessage(`[scarlet]Server restarting in: ${sec}`); - fishState.restartLoopTask = Timer.schedule(() => serverRestartLoop(sec - 1), 1); - } else if(!fake){ - restartNow(); - } -} -/** - * Actually restarts. Kicks all players. Execution always returns from this function. - * @param [removeSave=false] If set, save will be deleted instead of saved. Used to start a new game after the restart. - */ -export function restartNow(removeSave = false){ - Log.info(`Restarting...`); - Vars.netServer.kickAll(Packets.KickReason.serverRestarting); - Vars.net.closeServer(); - Vars.state.set(GameState.State.menu); - const file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); - if(removeSave){ - Core.app.post(() => { - file.delete(); - Core.app.exit(); - }); - } else { - Core.app.post(() => { - SaveIO.save(file); - Core.app.exit(); - }); - } -} - -export function isBuildable(block:Block){ - return block == Blocks.powerVoid || (block.buildType != Blocks.air.buildType && !(block instanceof ConstructBlock)); -} - -export const getUnitType = searchFixed( - () => Vars.content.units().select((u:UnitType) => !(u instanceof MissileUnitType || u.internal)).toArray(), [ - (u, q) => u.name == q, - (u, q) => u.name.includes(q.toLowerCase()), - ] -); - -/** The vanilla validation code doesn't work on servers */ -export function isMapValidForGamemode(map:MMap):boolean { - if(map.custom) return true; //we assume that all custom maps are appropriate for the selected gamemode - const pvpMaps = ["Veins", "Glacier", "Passage"]; //Maps.pvpMaps - switch(Vars.state.rules.mode().name()){ - case "sandbox": case "editor": return true; //sandbox can be played on any map - case "attack": case "pvp": return pvpMaps.includes(map.name()); //technically the pvp maps are valid attack maps, since they have an (undefended) enemy core - case "survival": return !pvpMaps.includes(map.name()); - default: return false; //unreachable - } -} - -export const getMap = searchFixed(() => Vars.maps.all().select(isMapValidForGamemode).toArray(), [ - (m, name) => m.name().replace(/ /g, "_") === name, //exact match with spaces replaced - (m, name) => m.name().replace(/ /g, "_").toLowerCase() === name.toLowerCase(), //exact match with spaces replaced ignoring case - (m, name) => m.plainName().replace(/ /g, "_").toLowerCase() === name.toLowerCase(), //exact match with spaces replaced ignoring case and colors - (m, name) => m.plainName().toLowerCase().includes(name.toLowerCase()), //partial match ignoring case and colors - (m, name) => m.plainName().replace(/ /g, "_").toLowerCase().includes(name.toLowerCase()), //partial match with spaces replaced ignoring case and colors - (m, name) => m.plainName().replace(/ /g, "").toLowerCase().includes(name.toLowerCase()), //partial match with spaces removed ignoring case and colors - (m, name) => m.plainName().replace(/[^a-zA-Z]/gi, "").toLowerCase().includes(name.toLowerCase()), //partial match with non-alphabetic characters removed ignoring case and colors -], "recomputeOptions"); - - -//static cache -let buildableBlocks:Seq | null = null; - -export function getBlock(block:string, filter:"buildable" | "air" | "all"):Block | string { - buildableBlocks ??= Vars.content.blocks().select(isBuildable); - const check = ({ - buildable: b => isBuildable(b), - air: b => b == Blocks.air || isBuildable(b), - all: b => true - } satisfies Record boolean>)[filter]; - let out:Block | null; - if(block in Blocks && Blocks[block] instanceof Block && check(Blocks[block])) return Blocks[block]; - else if((out = Vars.content.blocks().find(t => t.name.includes(block.toLowerCase()) && check(t)))) return out; - else if((out = Vars.content.blocks().find(t => t.name.replace(/-/g, "").includes(block.toLowerCase().replace(/ /g, "")) && check(t)))) return out; - else if(block.includes("airblast")) return Blocks.blastDrill; - return `"${block}" is not a valid block.`; -} - -export function teleportPlayer(player:mindustryPlayer, to:mindustryPlayer){ - Timer.schedule(() => { - const p = player.unit(); - const t = to.unit(); - if(p && t){ - p.set(t.x, t.y); - Call.setPosition(player.con, t.x, t.y); - Call.setCameraPosition(player.con, t.x, t.y); - } - }, 0, 0.016, 10); -} - -export function logErrors unknown>(message:string, func:T):T { - return function(...args:any[]){ - try { - return func(...args); - } catch(err){ - Log.err(message); - Log.err(parseError(err)); - } - } as T; -} - -export function definitelyRealMemoryCorruption(){ - Log.info(`Triggering a prank: this will cause players to see two error messages claiming to be from a memory corruption, and cause a flickering amount of fissile matter and dormant cysts to be put in the core.`); - FishPlayer.messageStaff(`[gray]<[cyan]staff[gray]> [white]Activating memory corruption prank! (please don't ruin it by telling players what is happening, pretend you dont know)`); - api.sendModerationMessage(`Activated memory corruption prank on server ${Vars.state.rules.mode().name()}`); - let t1f = false; - let t2f = false; - fishState.corruption_t1 = Timer.schedule(() => { - t1f = !t1f; - Vars.state.rules.defaultTeam.items()?.set(Items.dormantCyst, t1f ? 69 : 420); - }, 0, 0.4, 600); - fishState.corruption_t2 = Timer.schedule(() => { - t2f = !t2f; - Vars.state.rules.defaultTeam.items()?.set(Items.fissileMatter, t2f ? 999 : 123); - }, 0, 1.5, 200); - const hexString = Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, "0"); - Call.sendMessage("[scarlet]Error: internal server error."); - Call.sendMessage(`[scarlet]Error: memory corruption: mindustry.world.modules.ItemModule@${hexString}`); - FishEvents.fire("memoryCorruption", []); -} - -export function getEnemyTeam():Team { - if(Gamemode.pvp()) return Team.derelict; - else return Vars.state.rules.waveTeam; -} - -export function neutralGameover(){ - FishPlayer.ignoreGameover(() => { - Events.fire(new EventType.GameOverEvent(getEnemyTeam())); - }); -} - -/** Please validate requestedWaves to ensure it is not huge */ -export function skipWaves(requestedWaves: number, runIntermediateWaves: boolean){ - let winWave = Vars.state.rules.winWave; - if(winWave <= 0) winWave = Infinity; - const wavesToSkip = Math.min(requestedWaves, winWave - Vars.state.wave); - - if(runIntermediateWaves){ - for(let i = 0; i < wavesToSkip; i ++){ - Vars.logic.skipWave(); - } - } else { - Vars.state.wave += (wavesToSkip - 1); - Vars.logic.skipWave(); - } -} - - -export function logHTrip(player:FishPlayer, name:string, message?:string){ - Log.warn(`&yPlayer &b"${player.cleanedName}"&y (&b${player.uuid}&y/&b${player.ip()}&y) tripped &c${name}&y` + (message ? `: ${message}` : "")); - FishPlayer.messageStaff(`[yellow]Player [blue]"${player.cleanedName}"[] tripped [cyan]${name}[]` + (message ? `: ${message}` : "")); - api.sendModerationMessage(`Player \`${player.cleanedName}\` (\`${player.uuid}\`/\`${player.ip()}\`) tripped **${name}**${message ? `: ${message}` : ""}\n**Server:** ${Gamemode.name()}`); -} - -export function setType(input:unknown):asserts input is T { - //does not do any checking -} - -export function untilForever(){ - return (maxTime - Date.now() - 10000); -} - -export function colorNumber(number:number, getColor:(number:number) => string, side:"server" | "client" = "client"):string { - return getColor(number) + number.toString() + (side == "client" ? "[]" : "&fr"); -} - -export function formatRatekeeper(x:Ratekeeper):string { - if(x.lastTime <= 1) return "0"; - return `${x.occurences} / ${formatTimeRelative(x.lastTime, true)}`; -} - -export function getAntiBotInfo(side:"client" | "server"){ - const color = side == "client" ? "[acid]" : "&ly"; - const True = side == "client" ? "[red]true[]" : "&lrtrue"; - const False = side == "client" ? "[green]false[]" : "&gfalse"; - return ( -`${color}Flag count: ${formatRatekeeper(FishPlayer.autoflagRate)} -${color}Autobanning flagged players: ${FishPlayer.shouldWhackFlaggedPlayers() ? True : False} -${color}Kicking new players: ${FishPlayer.shouldKickNewPlayers() ? True : False} -${color}Recent connect packets: ${formatRatekeeper(FishPlayer.connectRate)} -${color}Reason: ${FishPlayer.lastAntibotReason}` - ); -} - -const failPrefix = "[scarlet]\u26A0 [yellow]"; -const successPrefix = "[#48e076]\uE800 "; - -export function outputFail(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer):void; -export function outputFail(message:string | PartialFormatString, sender:FishPlayer, ratelimit:number):void; -export function outputFail(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer, ratelimit?:number){ - const msg = failPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[yellow]") : message); - if(ratelimit) sender.sendMessage(msg, ratelimit); - else sender.sendMessage(msg); -} -export function outputSuccess(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer){ - sender.sendMessage(successPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[#48e076]") : message)); -} -export function outputMessage(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer){ - sender.sendMessage(((typeof message == "function" && "__partialFormatString" in message ? message(null) : message) + "").replace(/\t/g, " ".repeat(4))); -} -export function outputConsole(message:string | PartialFormatString, channel:(typeof Log)[LogLevelName] = Log.info){ - channel(typeof message == "function" && "__partialFormatString" in message ? message("") : message); -} - -export function updateBans(message?:(player:mindustryPlayer) => string){ - Groups.player.each(player => { - if(Vars.netServer.admins.isIDBanned(player.uuid())){ - player.con.kick(Packets.KickReason.banned); - if(message) - Call.sendMessage(message(player)); - } - }); -} - -export function processChat(player:mindustryPlayer, message:string, effects = false){ - const fishPlayer = FishPlayer.get(player); - let highlight = fishPlayer.highlight; - let filterTripText; - const suspicious = fishPlayer.suspicionLevel() == 3; - if( - (!fishPlayer.hasPerm("bypassChatFilter") || fishPlayer.chatStrictness == "strict") - && (filterTripText = matchFilter(message, fishPlayer.chatStrictness, suspicious)) - ){ - if(effects){ - if( - suspicious && removeFoosChars(message).split(" ") - .map(w => w.replace(/[-_.^*,]/g, "")) - .some(w => bannedWords.autoWhack.includes(w)) - ){ - if(!fishPlayer.muted){ - logHTrip(fishPlayer, "bad words in chat", `message: \`${message}\``); - fishPlayer.muted = true; - void fishPlayer.stop("automod", maxTime, `Automatic stop due to suspicious activity`, false); - } - } - Log.info(`Censored message from player ${player.name}: "${escapeStringColorsServer(message)}"; contained "${filterTripText}"`); - FishPlayer.messageStaff(`[yellow]Censored message from player ${fishPlayer.cleanedName}: "${message}" contained "${filterTripText}"`); - } - message = text.chatFilterReplacement.message(); - highlight ??= text.chatFilterReplacement.highlight(); - } - - if(message.startsWith("./")) message = message.replace("./", "/"); - - if(!fishPlayer.hasPerm("chat")){ - if(effects){ - FishPlayer.messageMuted(player.name, message); - Log.info(`${player.name}: ${message}`); - } - return null; - } - - return (highlight ?? "") + message; -} - - -const replacements = ([ - //Serpulo units - ["dagger", "mace", "fortress", "scepter", "reign", "nova", "pulsar", "quasar", "vela", "corvus", "crawler", "atrax", "spiroct", "arkyid", "toxopid", "flare", "horizon", "zenith", "antumbra", "eclipse", "mono", "poly", "mega", "quad", "oct", "risso", "minke", "bryde", "sei", "omura", "retusa", "oxynoe", "cyerce", "aegires", "navanax", "fort", "toxo", "flarogus"], - //Erekir units - ["stell", "locus", "precept", "vanquish", "conquer", "merui", "cleroi", "anthicus", "tecta", "collaris", "elude", "avert", "obviate", "quell", "disrupt", "vanq", "crab", "anthi", "larry", "obvi"], - - //Items, full form - ["copper", "lead", "metaglass", "graphite", "sand", "coal", "titanium", "thorium", "scrap", "silicon", "plastanium", "phase fabric", "surge alloy", "spore pod", "blast compound", "pyratite", "beryllium", "tungsten", "oxide", "carbide"], - //Items, short form - ["coppa", "meta", "graph", "tita", "titan", "thor", "scrap", "sili", "plast", "phase", "surge", "spore", "blast", "pyra", "beryl", "tung", "oxide", "carb"], - - //Liquids - ["water", "slag", "oil", "cryo", "cryofluid"], - - //Liquids/gases (erekir) - ["hydrogen", "ozone", "nitrogen", "cyanogen", "cyan", "nitro", "hydro", "arky", "arkycite", "neoplasm"], - - //Gamemodes - ["attack", "sandbox", "pvp", "hexed", "survival"], - - //teams - ["crux", "sharded", "malis", "neoplastic"], - - //maps - ["rampant", "harbor war", "cave canal", "acheron", "wolframfestung", "avast", "fallen omura", "assault"], - - //aquatic animals - ["fish", "shark", "whale", "dolphin", "salmon", "tuna", "squid", "jellyfish", "turtle"], - - //antonym adjectives - ["fast", "slow"], ["big", "little"], ["hot", "cold"], ["hard", "easy", "difficult", "ez"], ["hello", "bye"], -] satisfies string[][]).map(set => [set, new RegExp(`\\b(?:${set.join("|")})(e?s?(?:i?gone)?)\\b`, 'g')] as const); - -let foolCounter = 0; -export const foolifyChat = memoizeChatFilter(function foolifyChat(message:string){ - const cleanedMessage = removeFoosChars(message); - setShuffle: { - if(foolCounter < 8){ - //Skip the next 5 messages no matter what - foolCounter ++; - break setShuffle; - } - let replacedMessage = cleanedMessage; - for(const [set, regex] of replacements){ - replacedMessage = replacedMessage.replace(regex, (_, plural) => random(set) + plural); - //This code has a "feature": - //if it replaces a long item name to "blast compound", - //it will then replace "blast" to something else on the next pass - //this was unintended but it's funny so I'm keeping it - } - if(replacedMessage !== cleanedMessage){ - if(foolCounter < 11){ - //Skip the next 2 messages that would get altered - foolCounter ++; - break setShuffle; - } - foolCounter = 0; - return replacedMessage; - } else { - break setShuffle; - } - } - if(Math.random() < 0.01){ - return cleanedMessage.split("").reverse().join(""); - // eslint-disable-next-line no-dupe-else-if - } else if(Math.random() < 0.01){ - return "[scarlet]I really hope everyone is having a fun time :} <3"; - } else if(Math.random() < 0.005){ - return "[cyan]AMOGUS"; - } else { - return message; - } -}); - -export const addToTileHistory = logErrors("Error while saving a tilelog entry", (e:any) => { - - // eslint-disable-next-line prefer-const - let tile:Tile, uuid:string, action:string, type:string, time:number = Date.now(); - if(e instanceof EventType.BlockBuildBeginEvent){ - tile = e.tile; - uuid = e.unit?.player?.uuid() ?? e.unit?.type.name ?? "unknown"; - if(e.breaking){ - action = "broke"; - type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.previous.name : "unknown"; - if(e.unit?.player?.uuid() && e.tile.build?.team != Team.derelict){ - const fishP = FishPlayer.get(e.unit.player); - //TODO move this code - fishP.tstats.blocksBroken ++; - fishP.tstats.blockInteractionsThisMap ++; - fishP.updateStats(stats => stats.blocksBroken ++); - } - } else { - action = "built"; - type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.current.name : "unknown"; - if(e.unit?.player?.uuid()){ - const fishP = FishPlayer.get(e.unit.player); - //TODO move this code - fishP.updateStats(stats => stats.blocksPlaced ++); - fishP.tstats.blockInteractionsThisMap ++; - } - } - } else if(e instanceof EventType.ConfigEvent){ - tile = e.tile.tile; - uuid = e.player?.uuid() ?? "unknown"; - if(uuid != "unknown"){ - const fishP = FishPlayer.getById(uuid); - if(fishP) fishP.tstats.blockInteractionsThisMap ++; - } - action = "configured"; - type = e.tile.block.name; - } else if(e instanceof EventType.BuildRotateEvent){ - tile = e.build.tile; - uuid = e.unit?.player?.uuid() ?? e.unit?.type.name ?? "unknown"; - if(uuid != "unknown"){ - const fishP = FishPlayer.getById(uuid); - if(fishP) fishP.tstats.blockInteractionsThisMap ++; - } - action = "rotated"; - type = e.build.block.name; - } else if(e instanceof EventType.UnitDestroyEvent){ - tile = e.unit.tileOn(); - if(!tile) return; - if(!e.unit.type.playerControllable) return; - uuid = e.unit.isPlayer() ? e.unit.getPlayer().uuid() : e.unit.lastCommanded ?? "unknown"; - action = "killed"; - type = e.unit.type.name; - } else if(e instanceof EventType.BlockDestroyEvent){ - if(Gamemode.attack() && e.tile.build?.team != Vars.state.rules.defaultTeam) return; //Don't log destruction of enemy blocks - tile = e.tile; - uuid = "[[something]"; - action = "killed"; - type = e.tile.block()?.name ?? "air"; - } else if(e instanceof EventType.PayloadDropEvent){ - action = "pay-dropped"; - const controller = e.carrier.controller(); - uuid = e.carrier.player?.uuid() ?? (controller instanceof LogicAI && controller.controller ? - `${e.carrier.type.name} controlled by ${controller.controller.block.name} at ${controller.controller.tileX()},${controller.controller.tileY()} last accessed by ${e.carrier.getControllerName()}` - : null) ?? e.carrier.type.name; - if(e.build){ - tile = e.build.tile; - type = e.build.block.name; - } else if(e.unit){ - tile = e.unit.tileOn(); - if(!tile) return; - type = e.unit.type.name; - } else return; - } else if(e instanceof EventType.PickupEvent){ - action = "picked up"; - if(e.carrier.isPlayer()) return; //This event would have been handled by actionfilter - const controller = e.carrier.controller(); - if(!(controller instanceof LogicAI && controller.controller != null)) return; - uuid = `${e.carrier.type.name} controlled by ${controller.controller.block.name} at ${controller.controller.tileX()},${controller.controller.tileY()} last accessed by ${e.carrier.getControllerName()}`; - if(e.build){ - tile = e.build.tile; - type = e.build.block.name; - } else if(e.unit){ - tile = e.unit.tileOn(); - if(!tile) return; - type = e.unit.type.name; - } else return; - } else if(e instanceof EventType.UnitControlEvent){ - if(e.unit instanceof Packages.mindustry.gen.BlockUnitUnit){ - action = "controlled"; - tile = e.unit?.tile().tile; - if(!tile) return; - type = tile.block()?.name ?? "air"; - uuid = (e.player as mindustryPlayer).uuid(); - } else return; - } else if(e instanceof Object && "pos" in e && "uuid" in e && "action" in e && "type" in e){ - let pos; - ({pos, uuid, action, type} = e); - tile = Vars.world.tile(pos.split(",")[0], pos.split(",")[1]) ?? crash(`Cannot log ${action} at ${pos}: Nonexistent tile`); - } else return; - if(tile == null) return; - [tile, uuid, action, type, time] satisfies [Tile, string, string, string, number]; - - tile.getLinkedTiles(t => { - const pos = `${t.x},${t.y}`; - let existingData = tileHistory[pos] ? StringIO.read(tileHistory[pos], str => str.readArray(d => ({ - action: d.readString(2), - uuid: d.readString(3), - time: d.readNumber(16), - type: d.readString(2), - }), 1)) : []; - - existingData.push({ - action, uuid, time, type - }); - existingData = existingData.slice(-9); - //Write - tileHistory[t.x + ',' + t.y] = StringIO.write(existingData, (str, data) => str.writeArray(data, el => { - str.writeString(el.action, 2); - str.writeString(el.uuid, 3); - str.writeNumber(el.time, 16); - str.writeString(el.type, 2); - }, 1)); - }); - -}); - -export const tilelogAndResetAfk = logErrors("Error while saving a tilelog entry and resetting afk", (e:any) => { - addToTileHistory(e); - FishPlayer.get(e.unit.player).lastActive = Date.now(); -}); - -export function getIPRange(input:string, error?:(message:string) => never):string | null { - if(ipRangeCIDRPattern.test(input)){ - const [ip, maskLength] = input.split("/"); - switch(maskLength){ - case "24": - return ip.split(".").slice(0, 3).join(".") + "."; - case "16": - return ip.split(".").slice(0, 2).join(".") + "."; - default: - error?.(`Mindustry does not currently support netmasks other than /16 and /24`); - return null; - } - } else if(ipRangeWildcardPattern.test(input)){ - //1.2.3.* - //1.2.* - const [a, b, c, d] = input.split("."); - if(c !== "*") return `${a}.${b}.${c}.`; - return `${a}.${b}.`; - } else return null; -} - -//this brings me physical pain -export function getHash(file: Fi, algorithm: string = "SHA-1"): string | undefined { - try { - const header = `blob ${file.length()}\0`; - const fileSHAHeader = Packages.java.nio.charset.StandardCharsets.UTF_8.encode(header); - const contents = file.readBytes(); - const buffer = Packages.java.nio.ByteBuffer.allocate(fileSHAHeader.remaining() + contents.length) as ByteBuffer; - buffer.put(fileSHAHeader); - buffer.put(contents); - buffer.flip(); - const digest = Packages.java.security.MessageDigest.getInstance(algorithm) as MessageDigest; - digest.update(buffer); - return digest.digest().map(byte => - (byte & 0xFF).toString(16).padStart(2, "0") - ).join(""); - } catch (e) { - Log.err(`Cannot generate ${algorithm}, ${String(e)}`); - return undefined; - } -} - -export function match>(value:K, clauses:O):K extends keyof O ? O[K] : (O[K & keyof O] | undefined); -export function match>, D>(value:K, clauses:O, defaultValue:D):O[K & keyof O] | D; -export function match(value:PropertyKey, clauses:Record, defaultValue?:unknown):unknown { - return Object.prototype.hasOwnProperty.call(clauses, value) ? clauses[value] : defaultValue; -} - -/** @throws CommandError */ -export function fishCommandsRootDirPath():Path { - const commandsDir = Vars.modDirectory.child("fish-commands"); - if(!commandsDir.exists()) - fail(`Fish commands directory at path ${commandsDir.absolutePath()} does not exist!`); - let fishCommandsRootDirPath = Paths.get(commandsDir.file().path); - if(Packages.java.nio.file.Files.isSymbolicLink(fishCommandsRootDirPath)){ - //fish-commands is linked to the build directory of somewhere else - //resolve and get the parent directory of the build directory - fishCommandsRootDirPath = fishCommandsRootDirPath.toRealPath().getParent(); - } - return fishCommandsRootDirPath; -} - -/** Fails if "mode" is invalid. */ -export function applyEffectMode(mode:string, unit:Unit, ticks:number){ - const modes = { - fast: [StatusEffects.fast], - fast2: [StatusEffects.fast, StatusEffects.overdrive, StatusEffects.overclock], - boss: [StatusEffects.boss], - health: [StatusEffects.boss, StatusEffects.shielded], - slow: [StatusEffects.slow], - slow2: [ - StatusEffects.slow, - StatusEffects.freezing, - StatusEffects.wet, - StatusEffects.muddy, - StatusEffects.sapped, - StatusEffects.sporeSlowed, - StatusEffects.electrified, - StatusEffects.tarred, - ], - freeze: [StatusEffects.unmoving], - disarm: [StatusEffects.disarmed], - invincible: [StatusEffects.invincible], - boost: [ - StatusEffects.fast, - StatusEffects.overdrive, - StatusEffects.overclock, - StatusEffects.boss, - StatusEffects.shielded, - ], - damage: [ - StatusEffects.burning, - StatusEffects.freezing, - StatusEffects.wet, - StatusEffects.muddy, - StatusEffects.melting, - StatusEffects.sapped, - StatusEffects.tarred, - StatusEffects.shocked, - StatusEffects.blasted, - StatusEffects.corroded, - StatusEffects.sporeSlowed, - StatusEffects.electrified, - StatusEffects.fast, - ], - clear(unit){ - unit.clearStatuses(); - unit.maxHealth = unit.type.health; - }, - paper(unit){ - unit.health = 1; - unit.maxHealth = 1; - unit.apply(StatusEffects.disarmed, Number.MAX_VALUE / 2); - }, - heal(unit){ - unit.health = unit.maxHealth; - }, - overheal(unit){ - unit.maxHealth = unit.health = 1e15; - }, - shield(unit){ - unit.shield = 1e15; - } - } satisfies Record void)>; - const effects = match(mode, modes, null) ?? fail(`Invalid mode. Supported modes: ${Object.keys(modes).join(", ")}`); - if(typeof effects === "function"){ - effects(unit); - } else { - for(const effect of effects){ - unit.apply(effect, ticks); - } - } -} - -export function handleError(err:unknown, sender:FishPlayer, outputFail: (message: string | PartialFormatString, sender: FishPlayer) => void, context?: string){ - if(err instanceof CommandError){ - //If the error is a command error, then just outputFail - outputFail(err.data, sender); - } else if(err === Cancel){ - //Menu cancelled, do nothing - return; - } else { - sender.sendMessage(`[scarlet]\u274C An error occurred while executing the command!`); - if(sender.hasPerm("seeErrorMessages")) sender.sendMessage(parseError(err)); - Log.err(context ? - `Unhandled error in command execution: ${context}` - : `Unhandled error in command execution.`); - Log.err(err); - if(typeof err == "object" && err != null && "stack" in err) Log.err(err.stack); - } -} - -const sources = [ - Packages.mindustry.gen.UnitEntity, - Packages.mindustry.gen.MechUnit, - Packages.mindustry.gen.LegsUnit, - Packages.mindustry.gen.CrawlUnit, - Packages.mindustry.gen.UnitWaterMove, - Packages.mindustry.gen.BlockUnitUnit, - Packages.mindustry.gen.ElevationMoveUnit, - Packages.mindustry.gen.BuildingTetherPayloadUnit, - Packages.mindustry.gen.TimedKillUnit, - Packages.mindustry.gen.PayloadUnit, - Packages.mindustry.gen.TankUnit, -]; -export function getStatuses(unit:Unit):Seq<{ effect: StatusEffect }> { - for(const clazz of sources){ - if(unit instanceof clazz) - return ArcReflect.get(clazz, unit, "statuses") as Seq<{ effect: StatusEffect }>; - } - return new Seq(); -} +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains many utility functions that need access to any values from other files. +For functions that don't need values from other files, see funcs.ts. +*/ + +import * as api from "/api"; +import { adminNames, bannedWords, Gamemode, GamemodeName, multiCharSubstitutions, substitutions, text } from "/config"; +import { CommandError, fail, PartialFormatString } from "/frameworks/commands"; +import { Cancel } from "/frameworks/menus"; +import { crash, escapeStringColorsServer, escapeTextDiscord, parseError, random, searchFixed, StringIO } from "/funcs"; +import { FishEvents, fishState, ipPattern, ipPortPattern, ipRangeCIDRPattern, ipRangeWildcardPattern, maxTime, tileHistory, uuidPattern } from "/globals"; +import { FishPlayer } from "/players"; +import { SelectEnumClassKeys } from "/types"; + + +export function memoizeChatFilter(impl:(arg:string) => string){ + let lastCleanedInput:string | null = null; + let lastOutput:string | null = null; + return function memoized(input:string):string { + const cleanedInput = removeFoosChars(input); + if(cleanedInput === lastCleanedInput) return lastOutput!; + lastCleanedInput = cleanedInput; + return lastOutput = impl(input); + }; +} + +export function formatTime(time:number){ + + if(maxTime - (time + Date.now()) < 20_000) return "forever"; + if(isNaN(time)) return "N/A"; + + const months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); + const days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); + const hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + const minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); + const seconds = Math.floor((time % (60 * 1000)) / (1000)); + + return [ + months && `${months} month${months != 1 ? "s" : ""}`, + days && `${days} day${days != 1 ? "s" : ""}`, + hours && `${hours} hour${hours != 1 ? "s" : ""}`, + minutes && `${minutes} minute${minutes != 1 ? "s" : ""}`, + (seconds || time < 1000) && `${seconds} second${seconds != 1 ? "s" : ""}`, + ].filter(Boolean).join(", "); +} + +export function formatTimeShort(time:number){ + + if(maxTime - (time + Date.now()) < 20000) return "forever"; + if(isNaN(time)) return "N/A"; + + const months = Math.floor(time / (30 * 24 * 60 * 60 * 1000)); + const days = Math.floor((time % (30 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); + const hours = Math.floor((time % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + const minutes = Math.floor((time % (60 * 60 * 1000)) / (60 * 1000)); + const seconds = Math.floor((time % (60 * 1000)) / (1000)); + + return [ + months && `${months}mo`, + days && `${days}d`, + hours && `${hours}h`, + minutes && `${minutes}m`, + (seconds || time < 1000) && `${seconds}s`, + ].filter(Boolean).join(" "); +} + +//TODO move this data to be right next to Mode +export function formatModeName(name:GamemodeName){ + return { + "attack": "Attack", + "survival": "Survival", + "hexed": "Hexed", + "pvp": "PVP", + "sandbox": "Sandbox", + "hardcore": "Hardcore", + "testsrv": "Testing Server", + "minigame": "Minigames", + }[name]; +} + +export function formatTimestampFull(time:number){ + const date = new Date(time); + return `${date.toDateString()}, ${date.toTimeString()}`; +} + +export function formatTimestamp(time:number){ + return new Date(time).toLocaleString(); +} + +export function formatTimestampShort(time:number){ + const date = new Date(time); + return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`; +} + +export function formatTimeRelative(time:number, raw?:boolean){ + const difference = Math.abs(time - Date.now()); + + if(difference < 1000) + return "just now"; + else if(time > Date.now()) + return (raw ? "" : "in ") + formatTime(difference); + else + return formatTime(difference) + (raw ? "" : " ago"); +} + +/** Attempts to parse a Color from the input. */ +export function getColor(input:string):Color | null { + try { + if(input.includes(',')){ + const formattedColor = input.split(','); + const col = { + r: Number(formattedColor[0]), + g: Number(formattedColor[1]), + b: Number(formattedColor[2]), + a: 255, + }; + return new Color(col.r, col.g, col.b, col.a); + } else if(input.includes('#')){ + return Color.valueOf(input); + } else if(((input):input is SelectEnumClassKeys => input in Color)(input)){ + return Color[input]; + } else { + return null; + } + } catch(e){ + return null; + } +} + +/** Searches for an enemy tile near a unit. */ +export function nearbyEnemyTile(unit:Unit, dist:number):Building | null { + //because the indexer is buggy + if(dist > 10) crash(`nearbyEnemyTile(): dist (${dist}) is too high!`); + + const x = Math.floor(unit.x / Vars.tilesize); + const y = Math.floor(unit.y / Vars.tilesize); + for(let i = -dist; i <= dist; i ++){ + for(let j = -dist; j <= dist; j ++){ + const build = Vars.world.build(x + i, y + j); + if(build && build.team != unit.team && build.team != Team.derelict) return build; + } + } + return null; +} + +/** Attempts to parse a Team from the input. */ +export function getTeam(team:string):Team | string { + if(team in Team && Team[team as keyof typeof Team] instanceof Team) return Team[team as keyof typeof Team] as Team; + else if(Team.baseTeams.find(t => t.name.includes(team.toLowerCase()))) return Team.baseTeams.find(t => t.name.includes(team.toLowerCase()))!; + else if(!isNaN(Number(team))) return `"${team}" is not a valid team string. Did you mean "#${team}"?`; + else if(!isNaN(Number(team.slice(1)))){ + const num = Number(team.slice(1)); + if(num <= 255 && num >= 0 && Number.isInteger(num)) + return Team.all[Number(team.slice(1))]; + else + return `Team ${team} is outside the valid range (integers 0-255).`; + } + return `"${team}" is not a valid team string.`; +} + +/** Attempts to parse an Item from the input. */ +export const getItem = searchFixed(Vars.content.items().toArray(), [ + (i, s) => i.name == s, + (i, s) => i.name == s.toLowerCase(), + (i, s) => i.name.includes(s.toLowerCase()), + (i, s) => i.name.includes(s.toLowerCase().replace(" ", "-")), + (i, s) => i.emoji() == s, +]); + + +/** + * @param wordList "chat" is least strict, followed by "strict", and "name" is most strict. + * @returns a + */ +export function matchFilter(input:string, wordList = "chat" as "chat" | "strict" | "name", aggressive = false):false | string { + const currentBannedWords = [ + wordList == "name" ? bannedWords.normal.filter(w => w[0] !== "uwu") : bannedWords.normal, + (wordList == "strict" || wordList == "name") && bannedWords.strict, + wordList == "name" && bannedWords.names, + ].filter(Boolean).flat(); + if(aggressive) currentBannedWords.push(["hitler", []]); + //Replace substitutions + const variations = [input, cleanText(input, false)]; + if(aggressive) variations.push(cleanText(input, true)); + for(const [banned, whitelist] of currentBannedWords){ + for(const text of variations){ + if(banned instanceof RegExp ? banned.test(text) : text.includes(banned)){ + let modifiedText = text; + whitelist.forEach(w => modifiedText = modifiedText.replace(new RegExp(w, "g"), "")); //Replace whitelisted words with nothing + if(banned instanceof RegExp ? banned.test(modifiedText) : modifiedText.includes(banned)) //If the text still matches, fail + return ( + banned === uuidPattern ? `a Mindustry UUID` : + banned === ipPattern || banned === ipPortPattern ? `an IP address` : + //parsing regex with regex, massive hack + banned instanceof RegExp ? banned.source.replace(/\\b|\(\? acc.replace(from, to), + Strings.stripColors(removeFoosChars(text)) + .split("").map(c => substitutions[c] ?? c).join("") + ).toLowerCase().trim(); + if(applyAntiEvasion){ + replacedText = replacedText.replace(new RegExp(`[^a-zA-Z0-9]`, "gi"), ""); + } + return replacedText; +} + +export function isImpersonator(name:string, isAdmin:boolean):false | string { + const replacedText = cleanText(name); + const antiEvasionText = cleanText(name, true); + //very clean code i know + const filters:Array<[check: (value:string) => boolean, message:string]> = ( + (input: Array boolean), string]>) => + input.map(i => + Array.isArray(i) ? [ + typeof i[0] == "string" ? replacedText => replacedText.includes((i[0] as string)) : + i[0] instanceof RegExp ? replacedText => (i[0] as RegExp).test(replacedText) : + i[0], + i[1] + ] : [ + replacedText => replacedText.includes(i), + `Name contains disallowed ${i.length == 1 ? "icon" : "word"} '${i}'` + ] + ) + )([ + [/\bserver\b/, "Name contains disallowed word 'server'"], + "admin", "moderator", "staff", "owner", + [">|||>", "Name contains >|||> which is reserved for the server owner"], + "\uE817", "\uE82C", "\uE88E", "\uE813", + [/^[<\uE825].{1,3}[>\uE83A]/, "Name contains a prefix such as which is used for role prefixes"], + [(replacedText) => !isAdmin && adminNames.includes(replacedText.replace(/ /g, "")), "One of our admins uses this name"] + ]); + for(const [check, message] of filters){ + if(check(replacedText)) return message; + if(check(antiEvasionText)) return message; + } + return false; +} + +export function logAction(action:string):void; +export function logAction(action:string, by:FishPlayer):void; +export function logAction(action:string, by:FishPlayer | string, to:FishPlayer | PlayerInfo | string, reason?:string, duration?:number):void; +export function logAction(action:string, by?:FishPlayer | string, to?:FishPlayer | PlayerInfo | string, reason?:string, duration?:number) { + if(by === undefined){ //overload 1 + api.sendModerationMessage( +`${action} +**Server:** ${Gamemode.name()}` + ); + return; + } + if(to === undefined){ //overload 2 + api.sendModerationMessage( +`${escapeTextDiscord(Strings.stripColors((by as FishPlayer).name))} ${action} +**Server:** ${Gamemode.name()}` + ); + return; + } + if(to){ //overload 3 + let name:string, uuid:string, ip:string; + const actor:string = typeof by === "string" ? by : escapeTextDiscord(Strings.stripColors(by.name)); + if(to instanceof FishPlayer){ + name = escapeTextDiscord(to.name); + uuid = to.uuid; + ip = to.ip(); + } else if(typeof to == "string"){ + if(uuidPattern.test(to)){ + name = `[${to}]`; + uuid = to; + ip = "[unknown]"; + } else { + name = to; + uuid = "[unknown]"; + ip = "[unknown]"; + } + } else { + name = escapeTextDiscord(to.lastName); + uuid = to.id; + ip = to.lastIP; + } + api.sendModerationMessage( +`${actor} ${action} ${name} ${duration ? `for ${formatTime(duration)} ` : ""}${reason ? `with reason ${escapeTextDiscord(reason)}` : ""} +**Server:** ${Gamemode.name()} +**uuid:** \`${uuid}\` +**ip**: \`${ip}\`` + ); + return; + } +} + +/** @returns the number of milliseconds. */ +export function parseTimeString(str:string):number | null { + const formats = ([ + [/(\d+)s/, 1], + [/(\d+)m/, 60], + [/(\d+)h/, 3600], + [/(\d+)d/, 86400], + [/(\d+)w/, 604800] + ] as Array<[RegExp, number]>).map(([regex, mult]) => [Pattern.compile(regex.source), mult] as const); + if(str == "forever") return (maxTime - Date.now() - 10000); + for(const [pattern, mult] of formats){ + //rhino regex doesn't work + const matcher = pattern.matcher(str); + if(matcher.matches()){ + const num = Number(matcher.group(1)); + if(!isNaN(num)) return (num * mult) * 1000; + } + } + return null; +} + +/** + * Triggers the restart countdown. Execution always returns from this function. + * @param [fake=false] if set, server will not actually restart. + */ +export function serverRestartLoop(sec:number, fake = false):void { + if(sec > 0){ + if(sec < 15 || sec % 5 == 0) Call.sendMessage(`[scarlet]Server restarting in: ${sec}`); + fishState.restartLoopTask = Timer.schedule(() => serverRestartLoop(sec - 1), 1); + } else if(!fake){ + restartNow(); + } +} +/** + * Actually restarts. Kicks all players. Execution always returns from this function. + * @param [removeSave=false] If set, save will be deleted instead of saved. Used to start a new game after the restart. + */ +export function restartNow(removeSave = false){ + Log.info(`Restarting...`); + Vars.netServer.kickAll(Packets.KickReason.serverRestarting); + Vars.net.closeServer(); + Vars.state.set(GameState.State.menu); + const file = Vars.saveDirectory.child('1' + '.' + Vars.saveExtension); + if(removeSave){ + Core.app.post(() => { + file.delete(); + Core.app.exit(); + }); + } else { + Core.app.post(() => { + SaveIO.save(file); + Core.app.exit(); + }); + } +} + +export function isBuildable(block:Block){ + return block == Blocks.powerVoid || (block.buildType != Blocks.air.buildType && !(block instanceof ConstructBlock)); +} + +export const getUnitType = searchFixed( + () => Vars.content.units().select((u:UnitType) => !(u instanceof MissileUnitType || u.internal)).toArray(), [ + (u, q) => u.name == q, + (u, q) => u.name.includes(q.toLowerCase()), + ] +); + +/** The vanilla validation code doesn't work on servers */ +export function isMapValidForGamemode(map:MMap):boolean { + if(map.custom) return true; //we assume that all custom maps are appropriate for the selected gamemode + const pvpMaps = ["Veins", "Glacier", "Passage"]; //Maps.pvpMaps + switch(Vars.state.rules.mode().name()){ + case "sandbox": case "editor": return true; //sandbox can be played on any map + case "attack": case "pvp": return pvpMaps.includes(map.name()); //technically the pvp maps are valid attack maps, since they have an (undefended) enemy core + case "survival": return !pvpMaps.includes(map.name()); + default: return false; //unreachable + } +} + +export const getMap = searchFixed(() => Vars.maps.all().select(isMapValidForGamemode).toArray(), [ + (m, name) => m.name().replace(/ /g, "_") === name, //exact match with spaces replaced + (m, name) => m.name().replace(/ /g, "_").toLowerCase() === name.toLowerCase(), //exact match with spaces replaced ignoring case + (m, name) => m.plainName().replace(/ /g, "_").toLowerCase() === name.toLowerCase(), //exact match with spaces replaced ignoring case and colors + (m, name) => m.plainName().toLowerCase().includes(name.toLowerCase()), //partial match ignoring case and colors + (m, name) => m.plainName().replace(/ /g, "_").toLowerCase().includes(name.toLowerCase()), //partial match with spaces replaced ignoring case and colors + (m, name) => m.plainName().replace(/ /g, "").toLowerCase().includes(name.toLowerCase()), //partial match with spaces removed ignoring case and colors + (m, name) => m.plainName().replace(/[^a-zA-Z]/gi, "").toLowerCase().includes(name.toLowerCase()), //partial match with non-alphabetic characters removed ignoring case and colors +], "recomputeOptions"); + + +//static cache +let buildableBlocks:Seq | null = null; + +export function getBlock(block:string, filter:"buildable" | "air" | "all"):Block | string { + buildableBlocks ??= Vars.content.blocks().select(isBuildable); + const check = ({ + buildable: b => isBuildable(b), + air: b => b == Blocks.air || isBuildable(b), + all: b => true + } satisfies Record boolean>)[filter]; + let out:Block | null; + if(block in Blocks && Blocks[block] instanceof Block && check(Blocks[block])) return Blocks[block]; + else if((out = Vars.content.blocks().find(t => t.name.includes(block.toLowerCase()) && check(t)))) return out; + else if((out = Vars.content.blocks().find(t => t.name.replace(/-/g, "").includes(block.toLowerCase().replace(/ /g, "")) && check(t)))) return out; + else if(block.includes("airblast")) return Blocks.blastDrill; + return `"${block}" is not a valid block.`; +} + +export function teleportPlayer(player:mindustryPlayer, to:mindustryPlayer){ + Timer.schedule(() => { + const p = player.unit(); + const t = to.unit(); + if(p && t){ + p.set(t.x, t.y); + Call.setPosition(player.con, t.x, t.y); + Call.setCameraPosition(player.con, t.x, t.y); + } + }, 0, 0.016, 10); +} + +export function logErrors unknown>(message:string, func:T):T { + return function(...args:any[]){ + try { + return func(...args); + } catch(err){ + Log.err(message); + Log.err(parseError(err)); + } + } as T; +} + +export function definitelyRealMemoryCorruption(){ + Log.info(`Triggering a prank: this will cause players to see two error messages claiming to be from a memory corruption, and cause a flickering amount of fissile matter and dormant cysts to be put in the core.`); + FishPlayer.messageStaff(`[gray]<[cyan]staff[gray]> [white]Activating memory corruption prank! (please don't ruin it by telling players what is happening, pretend you dont know)`); + api.sendModerationMessage(`Activated memory corruption prank on server ${Vars.state.rules.mode().name()}`); + let t1f = false; + let t2f = false; + fishState.corruption_t1 = Timer.schedule(() => { + t1f = !t1f; + Vars.state.rules.defaultTeam.items()?.set(Items.dormantCyst, t1f ? 69 : 420); + }, 0, 0.4, 600); + fishState.corruption_t2 = Timer.schedule(() => { + t2f = !t2f; + Vars.state.rules.defaultTeam.items()?.set(Items.fissileMatter, t2f ? 999 : 123); + }, 0, 1.5, 200); + const hexString = Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, "0"); + Call.sendMessage("[scarlet]Error: internal server error."); + Call.sendMessage(`[scarlet]Error: memory corruption: mindustry.world.modules.ItemModule@${hexString}`); + FishEvents.fire("memoryCorruption", []); +} + +export function getEnemyTeam():Team { + if(Gamemode.pvp()) return Team.derelict; + else return Vars.state.rules.waveTeam; +} + +export function neutralGameover(){ + FishPlayer.ignoreGameover(() => { + Events.fire(new EventType.GameOverEvent(getEnemyTeam())); + }); +} + +/** Please validate requestedWaves to ensure it is not huge */ +export function skipWaves(requestedWaves: number, runIntermediateWaves: boolean){ + let winWave = Vars.state.rules.winWave; + if(winWave <= 0) winWave = Infinity; + const wavesToSkip = Math.min(requestedWaves, winWave - Vars.state.wave); + + if(runIntermediateWaves){ + for(let i = 0; i < wavesToSkip; i ++){ + Vars.logic.skipWave(); + } + } else { + Vars.state.wave += (wavesToSkip - 1); + Vars.logic.skipWave(); + } +} + + +export function logHTrip(player:FishPlayer, name:string, message?:string){ + Log.warn(`&yPlayer &b"${player.cleanedName}"&y (&b${player.uuid}&y/&b${player.ip()}&y) tripped &c${name}&y` + (message ? `: ${message}` : "")); + FishPlayer.messageStaff(`[yellow]Player [blue]"${player.cleanedName}"[] tripped [cyan]${name}[]` + (message ? `: ${message}` : "")); + api.sendModerationMessage(`Player \`${player.cleanedName}\` (\`${player.uuid}\`/\`${player.ip()}\`) tripped **${name}**${message ? `: ${message}` : ""}\n**Server:** ${Gamemode.name()}`); +} + +export function setType(input:unknown):asserts input is T { + //does not do any checking +} + +export function untilForever(){ + return (maxTime - Date.now() - 10000); +} + +export function colorNumber(number:number, getColor:(number:number) => string, side:"server" | "client" = "client"):string { + return getColor(number) + number.toString() + (side == "client" ? "[]" : "&fr"); +} + +export function formatRatekeeper(x:Ratekeeper):string { + if(x.lastTime <= 1) return "0"; + return `${x.occurences} / ${formatTimeRelative(x.lastTime, true)}`; +} + +export function getAntiBotInfo(side:"client" | "server"){ + const color = side == "client" ? "[acid]" : "&ly"; + const True = side == "client" ? "[red]true[]" : "&lrtrue"; + const False = side == "client" ? "[green]false[]" : "&gfalse"; + return ( +`${color}Flag count: ${formatRatekeeper(FishPlayer.autoflagRate)} +${color}Autobanning flagged players: ${FishPlayer.shouldWhackFlaggedPlayers() ? True : False} +${color}Kicking new players: ${FishPlayer.shouldKickNewPlayers() ? True : False} +${color}Recent connect packets: ${formatRatekeeper(FishPlayer.connectRate)} +${color}Reason: ${FishPlayer.lastAntibotReason}` + ); +} + +const failPrefix = "[scarlet]\u26A0 [yellow]"; +const successPrefix = "[#48e076]\uE800 "; + +export function outputFail(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer):void; +export function outputFail(message:string | PartialFormatString, sender:FishPlayer, ratelimit:number):void; +export function outputFail(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer, ratelimit?:number){ + const msg = failPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[yellow]") : message); + if(ratelimit) sender.sendMessage(msg, ratelimit); + else sender.sendMessage(msg); +} +export function outputSuccess(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer){ + sender.sendMessage(successPrefix + (typeof message == "function" && "__partialFormatString" in message ? message("[#48e076]") : message)); +} +export function outputMessage(message:string | PartialFormatString, sender:mindustryPlayer | FishPlayer){ + sender.sendMessage(((typeof message == "function" && "__partialFormatString" in message ? message(null) : message) + "").replace(/\t/g, " ".repeat(4))); +} +export function outputConsole(message:string | PartialFormatString, channel:(typeof Log)[LogLevelName] = Log.info){ + channel(typeof message == "function" && "__partialFormatString" in message ? message("") : message); +} + +export function updateBans(message?:(player:mindustryPlayer) => string){ + Groups.player.each(player => { + if(Vars.netServer.admins.isIDBanned(player.uuid())){ + player.con.kick(Packets.KickReason.banned); + if(message) + Call.sendMessage(message(player)); + } + }); +} + +export function processChat(player:mindustryPlayer, message:string, effects = false){ + const fishPlayer = FishPlayer.get(player); + let highlight = fishPlayer.highlight; + let filterTripText; + const suspicious = fishPlayer.suspicionLevel() == 3; + if( + (!fishPlayer.hasPerm("bypassChatFilter") || fishPlayer.chatStrictness == "strict") + && (filterTripText = matchFilter(message, fishPlayer.chatStrictness, suspicious)) + ){ + if(effects){ + if( + suspicious && removeFoosChars(message).split(" ") + .map(w => w.replace(/[-_.^*,]/g, "")) + .some(w => bannedWords.autoWhack.includes(w)) + ){ + if(!fishPlayer.muted){ + logHTrip(fishPlayer, "bad words in chat", `message: \`${message}\``); + fishPlayer.muted = true; + void fishPlayer.stop("automod", maxTime, `Automatic stop due to suspicious activity`, false); + } + } + Log.info(`Censored message from player ${player.name}: "${escapeStringColorsServer(message)}"; contained "${filterTripText}"`); + FishPlayer.messageStaff(`[yellow]Censored message from player ${fishPlayer.cleanedName}: "${message}" contained "${filterTripText}"`); + } + message = text.chatFilterReplacement.message(); + highlight ??= text.chatFilterReplacement.highlight(); + } + + if(message.startsWith("./")) message = message.replace("./", "/"); + + if(!fishPlayer.hasPerm("chat")){ + if(effects){ + FishPlayer.messageMuted(player.name, message); + Log.info(`${player.name}: ${message}`); + } + return null; + } + + return (highlight ?? "") + message; +} + + +const replacements = ([ + //Serpulo units + ["dagger", "mace", "fortress", "scepter", "reign", "nova", "pulsar", "quasar", "vela", "corvus", "crawler", "atrax", "spiroct", "arkyid", "toxopid", "flare", "horizon", "zenith", "antumbra", "eclipse", "mono", "poly", "mega", "quad", "oct", "risso", "minke", "bryde", "sei", "omura", "retusa", "oxynoe", "cyerce", "aegires", "navanax", "fort", "toxo", "flarogus"], + //Erekir units + ["stell", "locus", "precept", "vanquish", "conquer", "merui", "cleroi", "anthicus", "tecta", "collaris", "elude", "avert", "obviate", "quell", "disrupt", "vanq", "crab", "anthi", "larry", "obvi"], + + //Items, full form + ["copper", "lead", "metaglass", "graphite", "sand", "coal", "titanium", "thorium", "scrap", "silicon", "plastanium", "phase fabric", "surge alloy", "spore pod", "blast compound", "pyratite", "beryllium", "tungsten", "oxide", "carbide"], + //Items, short form + ["coppa", "meta", "graph", "tita", "titan", "thor", "scrap", "sili", "plast", "phase", "surge", "spore", "blast", "pyra", "beryl", "tung", "oxide", "carb"], + + //Liquids + ["water", "slag", "oil", "cryo", "cryofluid"], + + //Liquids/gases (erekir) + ["hydrogen", "ozone", "nitrogen", "cyanogen", "cyan", "nitro", "hydro", "arky", "arkycite", "neoplasm"], + + //Gamemodes + ["attack", "sandbox", "pvp", "hexed", "survival"], + + //teams + ["crux", "sharded", "malis", "neoplastic"], + + //maps + ["rampant", "harbor war", "cave canal", "acheron", "wolframfestung", "avast", "fallen omura", "assault"], + + //aquatic animals + ["fish", "shark", "whale", "dolphin", "salmon", "tuna", "squid", "jellyfish", "turtle"], + + //antonym adjectives + ["fast", "slow"], ["big", "little"], ["hot", "cold"], ["hard", "easy", "difficult", "ez"], ["hello", "bye"], +] satisfies string[][]).map(set => [set, new RegExp(`\\b(?:${set.join("|")})(e?s?(?:i?gone)?)\\b`, 'g')] as const); + +let foolCounter = 0; +export const foolifyChat = memoizeChatFilter(function foolifyChat(message:string){ + const cleanedMessage = removeFoosChars(message); + setShuffle: { + if(foolCounter < 8){ + //Skip the next 5 messages no matter what + foolCounter ++; + break setShuffle; + } + let replacedMessage = cleanedMessage; + for(const [set, regex] of replacements){ + replacedMessage = replacedMessage.replace(regex, (_, plural) => random(set) + plural); + //This code has a "feature": + //if it replaces a long item name to "blast compound", + //it will then replace "blast" to something else on the next pass + //this was unintended but it's funny so I'm keeping it + } + if(replacedMessage !== cleanedMessage){ + if(foolCounter < 11){ + //Skip the next 2 messages that would get altered + foolCounter ++; + break setShuffle; + } + foolCounter = 0; + return replacedMessage; + } else { + break setShuffle; + } + } + if(Math.random() < 0.01){ + return cleanedMessage.split("").reverse().join(""); + // eslint-disable-next-line no-dupe-else-if + } else if(Math.random() < 0.01){ + return "[scarlet]I really hope everyone is having a fun time :} <3"; + } else if(Math.random() < 0.005){ + return "[cyan]AMOGUS"; + } else { + return message; + } +}); + +export const addToTileHistory = logErrors("Error while saving a tilelog entry", (e:any) => { + + // eslint-disable-next-line prefer-const + let tile:Tile, uuid:string, action:string, type:string, time:number = Date.now(); + if(e instanceof EventType.BlockBuildBeginEvent){ + tile = e.tile; + uuid = e.unit?.player?.uuid() ?? e.unit?.type.name ?? "unknown"; + if(e.breaking){ + action = "broke"; + type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.previous.name : "unknown"; + if(e.unit?.player?.uuid() && e.tile.build?.team != Team.derelict){ + const fishP = FishPlayer.get(e.unit.player); + //TODO move this code + fishP.tstats.blocksBroken ++; + fishP.tstats.blockInteractionsThisMap ++; + fishP.updateStats(stats => stats.blocksBroken ++); + } + } else { + action = "built"; + type = (e.tile.build instanceof ConstructBlock.ConstructBuild) ? e.tile.build.current.name : "unknown"; + if(e.unit?.player?.uuid()){ + const fishP = FishPlayer.get(e.unit.player); + //TODO move this code + fishP.updateStats(stats => stats.blocksPlaced ++); + fishP.tstats.blockInteractionsThisMap ++; + } + } + } else if(e instanceof EventType.ConfigEvent){ + tile = e.tile.tile; + uuid = e.player?.uuid() ?? "unknown"; + if(uuid != "unknown"){ + const fishP = FishPlayer.getById(uuid); + if(fishP) fishP.tstats.blockInteractionsThisMap ++; + } + action = "configured"; + type = e.tile.block.name; + } else if(e instanceof EventType.BuildRotateEvent){ + tile = e.build.tile; + uuid = e.unit?.player?.uuid() ?? e.unit?.type.name ?? "unknown"; + if(uuid != "unknown"){ + const fishP = FishPlayer.getById(uuid); + if(fishP) fishP.tstats.blockInteractionsThisMap ++; + } + action = "rotated"; + type = e.build.block.name; + } else if(e instanceof EventType.UnitDestroyEvent){ + tile = e.unit.tileOn(); + if(!tile) return; + if(!e.unit.type.playerControllable) return; + uuid = e.unit.isPlayer() ? e.unit.getPlayer().uuid() : e.unit.lastCommanded ?? "unknown"; + action = "killed"; + type = e.unit.type.name; + } else if(e instanceof EventType.BlockDestroyEvent){ + if(Gamemode.attack() && e.tile.build?.team != Vars.state.rules.defaultTeam) return; //Don't log destruction of enemy blocks + tile = e.tile; + uuid = "[[something]"; + action = "killed"; + type = e.tile.block()?.name ?? "air"; + } else if(e instanceof EventType.PayloadDropEvent){ + action = "pay-dropped"; + const controller = e.carrier.controller(); + uuid = e.carrier.player?.uuid() ?? (controller instanceof LogicAI && controller.controller ? + `${e.carrier.type.name} controlled by ${controller.controller.block.name} at ${controller.controller.tileX()},${controller.controller.tileY()} last accessed by ${e.carrier.getControllerName()}` + : null) ?? e.carrier.type.name; + if(e.build){ + tile = e.build.tile; + type = e.build.block.name; + } else if(e.unit){ + tile = e.unit.tileOn(); + if(!tile) return; + type = e.unit.type.name; + } else return; + } else if(e instanceof EventType.PickupEvent){ + action = "picked up"; + if(e.carrier.isPlayer()) return; //This event would have been handled by actionfilter + const controller = e.carrier.controller(); + if(!(controller instanceof LogicAI && controller.controller != null)) return; + uuid = `${e.carrier.type.name} controlled by ${controller.controller.block.name} at ${controller.controller.tileX()},${controller.controller.tileY()} last accessed by ${e.carrier.getControllerName()}`; + if(e.build){ + tile = e.build.tile; + type = e.build.block.name; + } else if(e.unit){ + tile = e.unit.tileOn(); + if(!tile) return; + type = e.unit.type.name; + } else return; + } else if(e instanceof EventType.UnitControlEvent){ + if(e.unit instanceof Packages.mindustry.gen.BlockUnitUnit){ + action = "controlled"; + tile = e.unit?.tile().tile; + if(!tile) return; + type = tile.block()?.name ?? "air"; + uuid = (e.player as mindustryPlayer).uuid(); + } else return; + } else if(e instanceof Object && "pos" in e && "uuid" in e && "action" in e && "type" in e){ + let pos; + ({pos, uuid, action, type} = e); + tile = Vars.world.tile(pos.split(",")[0], pos.split(",")[1]) ?? crash(`Cannot log ${action} at ${pos}: Nonexistent tile`); + } else return; + if(tile == null) return; + [tile, uuid, action, type, time] satisfies [Tile, string, string, string, number]; + + tile.getLinkedTiles(t => { + const pos = `${t.x},${t.y}`; + let existingData = tileHistory[pos] ? StringIO.read(tileHistory[pos], str => str.readArray(d => ({ + action: d.readString(2), + uuid: d.readString(3), + time: d.readNumber(16), + type: d.readString(2), + }), 1)) : []; + + existingData.push({ + action, uuid, time, type + }); + existingData = existingData.slice(-9); + //Write + tileHistory[t.x + ',' + t.y] = StringIO.write(existingData, (str, data) => str.writeArray(data, el => { + str.writeString(el.action, 2); + str.writeString(el.uuid, 3); + str.writeNumber(el.time, 16); + str.writeString(el.type, 2); + }, 1)); + }); + +}); + +export const tilelogAndResetAfk = logErrors("Error while saving a tilelog entry and resetting afk", (e:any) => { + addToTileHistory(e); + FishPlayer.get(e.unit.player).lastActive = Date.now(); +}); + +export function getIPRange(input:string, error?:(message:string) => never):string | null { + if(ipRangeCIDRPattern.test(input)){ + const [ip, maskLength] = input.split("/"); + switch(maskLength){ + case "24": + return ip.split(".").slice(0, 3).join(".") + "."; + case "16": + return ip.split(".").slice(0, 2).join(".") + "."; + default: + error?.(`Mindustry does not currently support netmasks other than /16 and /24`); + return null; + } + } else if(ipRangeWildcardPattern.test(input)){ + //1.2.3.* + //1.2.* + const [a, b, c, d] = input.split("."); + if(c !== "*") return `${a}.${b}.${c}.`; + return `${a}.${b}.`; + } else return null; +} + +//this brings me physical pain +export function getHash(file: Fi, algorithm: string = "SHA-1"): string | undefined { + try { + const header = `blob ${file.length()}\0`; + const fileSHAHeader = Packages.java.nio.charset.StandardCharsets.UTF_8.encode(header); + const contents = file.readBytes(); + const buffer = Packages.java.nio.ByteBuffer.allocate(fileSHAHeader.remaining() + contents.length) as ByteBuffer; + buffer.put(fileSHAHeader); + buffer.put(contents); + buffer.flip(); + const digest = Packages.java.security.MessageDigest.getInstance(algorithm) as MessageDigest; + digest.update(buffer); + return digest.digest().map(byte => + (byte & 0xFF).toString(16).padStart(2, "0") + ).join(""); + } catch (e) { + Log.err(`Cannot generate ${algorithm}, ${String(e)}`); + return undefined; + } +} + +export function match>(value:K, clauses:O):K extends keyof O ? O[K] : (O[K & keyof O] | undefined); +export function match>, D>(value:K, clauses:O, defaultValue:D):O[K & keyof O] | D; +export function match(value:PropertyKey, clauses:Record, defaultValue?:unknown):unknown { + return Object.prototype.hasOwnProperty.call(clauses, value) ? clauses[value] : defaultValue; +} + +/** @throws CommandError */ +export function fishCommandsRootDirPath():Path { + const commandsDir = Vars.modDirectory.child("fish-commands"); + if(!commandsDir.exists()) + fail(`Fish commands directory at path ${commandsDir.absolutePath()} does not exist!`); + let fishCommandsRootDirPath = Paths.get(commandsDir.file().path); + if(Packages.java.nio.file.Files.isSymbolicLink(fishCommandsRootDirPath)){ + //fish-commands is linked to the build directory of somewhere else + //resolve and get the parent directory of the build directory + fishCommandsRootDirPath = fishCommandsRootDirPath.toRealPath().getParent(); + } + return fishCommandsRootDirPath; +} + +/** Fails if "mode" is invalid. */ +export function applyEffectMode(mode:string, unit:Unit, ticks:number){ + const modes = { + fast: [StatusEffects.fast], + fast2: [StatusEffects.fast, StatusEffects.overdrive, StatusEffects.overclock], + boss: [StatusEffects.boss], + health: [StatusEffects.boss, StatusEffects.shielded], + slow: [StatusEffects.slow], + slow2: [ + StatusEffects.slow, + StatusEffects.freezing, + StatusEffects.wet, + StatusEffects.muddy, + StatusEffects.sapped, + StatusEffects.sporeSlowed, + StatusEffects.electrified, + StatusEffects.tarred, + ], + freeze: [StatusEffects.unmoving], + disarm: [StatusEffects.disarmed], + invincible: [StatusEffects.invincible], + boost: [ + StatusEffects.fast, + StatusEffects.overdrive, + StatusEffects.overclock, + StatusEffects.boss, + StatusEffects.shielded, + ], + damage: [ + StatusEffects.burning, + StatusEffects.freezing, + StatusEffects.wet, + StatusEffects.muddy, + StatusEffects.melting, + StatusEffects.sapped, + StatusEffects.tarred, + StatusEffects.shocked, + StatusEffects.blasted, + StatusEffects.corroded, + StatusEffects.sporeSlowed, + StatusEffects.electrified, + StatusEffects.fast, + ], + clear(unit){ + unit.clearStatuses(); + unit.maxHealth = unit.type.health; + }, + paper(unit){ + unit.health = 1; + unit.maxHealth = 1; + unit.apply(StatusEffects.disarmed, Number.MAX_VALUE / 2); + }, + heal(unit){ + unit.health = unit.maxHealth; + }, + overheal(unit){ + unit.maxHealth = unit.health = 1e15; + }, + shield(unit){ + unit.shield = 1e15; + } + } satisfies Record void)>; + const effects = match(mode, modes, null) ?? fail(`Invalid mode. Supported modes: ${Object.keys(modes).join(", ")}`); + if(typeof effects === "function"){ + effects(unit); + } else { + for(const effect of effects){ + unit.apply(effect, ticks); + } + } +} + +export function handleError(err:unknown, sender:FishPlayer, outputFail: (message: string | PartialFormatString, sender: FishPlayer) => void, context?: string){ + if(err instanceof CommandError){ + //If the error is a command error, then just outputFail + outputFail(err.data, sender); + } else if(err === Cancel){ + //Menu cancelled, do nothing + return; + } else { + sender.sendMessage(`[scarlet]\u274C An error occurred while executing the command!`); + if(sender.hasPerm("seeErrorMessages")) sender.sendMessage(parseError(err)); + Log.err(context ? + `Unhandled error in command execution: ${context}` + : `Unhandled error in command execution.`); + Log.err(err); + if(typeof err == "object" && err != null && "stack" in err) Log.err(err.stack); + } +} + +const sources = [ + Packages.mindustry.gen.UnitEntity, + Packages.mindustry.gen.MechUnit, + Packages.mindustry.gen.LegsUnit, + Packages.mindustry.gen.CrawlUnit, + Packages.mindustry.gen.UnitWaterMove, + Packages.mindustry.gen.BlockUnitUnit, + Packages.mindustry.gen.ElevationMoveUnit, + Packages.mindustry.gen.BuildingTetherPayloadUnit, + Packages.mindustry.gen.TimedKillUnit, + Packages.mindustry.gen.PayloadUnit, + Packages.mindustry.gen.TankUnit, +]; +export function getStatuses(unit:Unit):Seq<{ effect: StatusEffect }> { + for(const clazz of sources){ + if(unit instanceof clazz) + return ArcReflect.get(clazz, unit, "statuses") as Seq<{ effect: StatusEffect }>; + } + return new Seq(); +} diff --git a/src/votes.ts b/src/votes.ts index c693b5c1..6fa52b5b 100644 --- a/src/votes.ts +++ b/src/votes.ts @@ -1,149 +1,149 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains the voting system. -Some contributions: @author Jurorno9 -*/ - -import { fail } from "/frameworks/commands"; -import { crash, EventEmitter } from "/funcs"; -import { fishState } from "/globals"; -import { FishPlayer } from "/players"; - -/** Event data for each voting event. */ -export type VoteEventMapping = { - "success": [forced:boolean]; - "fail": [forced:boolean]; - "vote passed": [votes:number, required:number]; - "vote failed": [votes:number, required:number]; - "player vote": [player:FishPlayer, current:number]; - "player vote change": [player:FishPlayer, previous:number, current:number]; - "player vote removed": [player:FishPlayer, previous:number]; -}; -export type VoteEvent = keyof VoteEventMapping; -export type VoteEventData = VoteEventMapping[T]; - -/** Manages a vote. */ -export class VoteManager extends EventEmitter { - - /** The ongoing voting session, if there is one. */ - session: { - data: SessionData; - votes: Map; - timer: TimerTask; - } | null = null; - - constructor( - public voteTime:number, - public goal:["fractionOfVoters", number] | ["absolute", number] = ["fractionOfVoters", 0.50001], - public isEligible:(fishP:FishPlayer, data: SessionData) => boolean = () => true, - public isCounted:(fishP:FishPlayer, data: SessionData) => boolean = (fishP) => !fishP.afk(), - ){ - super(); - if(goal[0] == "fractionOfVoters"){ - if(goal[1] < 0 || goal[1] > 1) crash(`Invalid goal: fractionOfVoters must be between 0 and 1 inclusive`); - } else if(goal[0] == "absolute"){ - if(goal[1] < 0) crash(`Invalid goal: absolute must be greater than 0`); - } - Events.on(EventType.PlayerLeave, ({player}) => { - //Run once the player has been removed, but resolve the player first in case the connection gets nulled - const fishP = FishPlayer.get(player); - Core.app.post(() => this.unvote(fishP)); - }); - Events.on(EventType.GameOverEvent, () => this.resetVote()); - } - - /** @throws CommandError */ - start(player:FishPlayer, newVote:number, data:SessionData){ - if(data === null) crash(`Cannot start vote: data not provided`); - if(!this.isEligible(player, data)) fail(`You are not eligible for this vote.`); - this.session = { - timer: Timer.schedule(() => this._checkVote(false), this.voteTime / 1000), - votes: new Map(), - data, - }; - this.vote(player, newVote, data); - } - - /** @throws CommandError */ - vote(player:FishPlayer, newVote:number, data:SessionData | null){ - if(!this.session) return this.start(player, newVote, data!); - if(!this.isEligible(player, this.session.data)) fail(`You are not eligible for this vote.`); - const oldVote = this.session.votes.get(player.uuid); - this.session.votes.set(player.uuid, newVote); - if(oldVote == null) this.fire("player vote", [player, newVote]); - this.fire("player vote change", [player, oldVote ?? 0, newVote]); - if(Date.now() - fishState.startTime < 3000) Timer.schedule(() => this._checkVote(false), 3); - else this._checkVote(false); - } - - unvote(player:FishPlayer){ - if(!this.session) return; - const fishP = FishPlayer.resolve(player); - const vote = this.session.votes.get(fishP.uuid); - if(vote){ - this.session.votes.delete(fishP.uuid); - this.fire("player vote removed", [player, vote]); - this._checkVote(false); - } - } - - /** Does not fire the events used to display messages, please print one before calling this */ - forceVote(outcome:boolean){ - if(outcome){ - this.fire("success", [true]); - } else { - this.fire("fail", [true]); - } - this.resetVote(); - } - - resetVote(){ - if(this.session == null) return; - this.session.timer.cancel(); - this.session = null; - } - - requiredVotes():number { - if(this.goal[0] == "absolute"){ - return this.goal[1]; - } else { - const numVoters = FishPlayer.getAllOnline().filter(p => - this.isEligible(p, this.session!.data) && (this.isCounted(p, this.session!.data) || this.session!.votes.has(p.uuid)) - ).length; - return Math.max(Math.ceil(this.goal[1] * numVoters), 1); - } - } - - currentVotes():number { - if(this.session){ - for(const key of this.session.votes.keys()){ - const fishP = FishPlayer.getById(key)!; - if(!this.isEligible(fishP, this.session.data)) this.session.votes.delete(key); - } - return [...this.session.votes].reduce((acc, [k, v]) => acc + v, 0); - } else return 0; - } - - getEligibleVoters():FishPlayer[] { - if(!this.session) return []; - return FishPlayer.getAllOnline().filter(p => - this.isEligible(p, this.session!.data) - ); - } - messageEligibleVoters(message:string){ - this.getEligibleVoters().forEach(p => p.sendMessage(message)); - } - _checkVote(end:boolean){ - const votes = this.currentVotes(); - const required = this.requiredVotes(); - if(votes >= required){ - this.fire("success", [false]); - this.fire("vote passed", [votes, required]); - this.resetVote(); - } else if(end){ - this.fire("fail", [false]); - this.fire("vote failed", [votes, required]); - this.resetVote(); - } - } +/* +Copyright © BalaM314, 2026. All Rights Reserved. +This file contains the voting system. +Some contributions: @author Jurorno9 +*/ + +import { fail } from "/frameworks/commands"; +import { crash, EventEmitter } from "/funcs"; +import { fishState } from "/globals"; +import { FishPlayer } from "/players"; + +/** Event data for each voting event. */ +export type VoteEventMapping = { + "success": [forced:boolean]; + "fail": [forced:boolean]; + "vote passed": [votes:number, required:number]; + "vote failed": [votes:number, required:number]; + "player vote": [player:FishPlayer, current:number]; + "player vote change": [player:FishPlayer, previous:number, current:number]; + "player vote removed": [player:FishPlayer, previous:number]; +}; +export type VoteEvent = keyof VoteEventMapping; +export type VoteEventData = VoteEventMapping[T]; + +/** Manages a vote. */ +export class VoteManager extends EventEmitter { + + /** The ongoing voting session, if there is one. */ + session: { + data: SessionData; + votes: Map; + timer: TimerTask; + } | null = null; + + constructor( + public voteTime:number, + public goal:["fractionOfVoters", number] | ["absolute", number] = ["fractionOfVoters", 0.50001], + public isEligible:(fishP:FishPlayer, data: SessionData) => boolean = () => true, + public isCounted:(fishP:FishPlayer, data: SessionData) => boolean = (fishP) => !fishP.afk(), + ){ + super(); + if(goal[0] == "fractionOfVoters"){ + if(goal[1] < 0 || goal[1] > 1) crash(`Invalid goal: fractionOfVoters must be between 0 and 1 inclusive`); + } else if(goal[0] == "absolute"){ + if(goal[1] < 0) crash(`Invalid goal: absolute must be greater than 0`); + } + Events.on(EventType.PlayerLeave, ({player}) => { + //Run once the player has been removed, but resolve the player first in case the connection gets nulled + const fishP = FishPlayer.get(player); + Core.app.post(() => this.unvote(fishP)); + }); + Events.on(EventType.GameOverEvent, () => this.resetVote()); + } + + /** @throws CommandError */ + start(player:FishPlayer, newVote:number, data:SessionData){ + if(data === null) crash(`Cannot start vote: data not provided`); + if(!this.isEligible(player, data)) fail(`You are not eligible for this vote.`); + this.session = { + timer: Timer.schedule(() => this._checkVote(false), this.voteTime / 1000), + votes: new Map(), + data, + }; + this.vote(player, newVote, data); + } + + /** @throws CommandError */ + vote(player:FishPlayer, newVote:number, data:SessionData | null){ + if(!this.session) return this.start(player, newVote, data!); + if(!this.isEligible(player, this.session.data)) fail(`You are not eligible for this vote.`); + const oldVote = this.session.votes.get(player.uuid); + this.session.votes.set(player.uuid, newVote); + if(oldVote == null) this.fire("player vote", [player, newVote]); + this.fire("player vote change", [player, oldVote ?? 0, newVote]); + if(Date.now() - fishState.startTime < 3000) Timer.schedule(() => this._checkVote(false), 3); + else this._checkVote(false); + } + + unvote(player:FishPlayer){ + if(!this.session) return; + const fishP = FishPlayer.resolve(player); + const vote = this.session.votes.get(fishP.uuid); + if(vote){ + this.session.votes.delete(fishP.uuid); + this.fire("player vote removed", [player, vote]); + this._checkVote(false); + } + } + + /** Does not fire the events used to display messages, please print one before calling this */ + forceVote(outcome:boolean){ + if(outcome){ + this.fire("success", [true]); + } else { + this.fire("fail", [true]); + } + this.resetVote(); + } + + resetVote(){ + if(this.session == null) return; + this.session.timer.cancel(); + this.session = null; + } + + requiredVotes():number { + if(this.goal[0] == "absolute"){ + return this.goal[1]; + } else { + const numVoters = FishPlayer.getAllOnline().filter(p => + this.isEligible(p, this.session!.data) && (this.isCounted(p, this.session!.data) || this.session!.votes.has(p.uuid)) + ).length; + return Math.max(Math.ceil(this.goal[1] * numVoters), 1); + } + } + + currentVotes():number { + if(this.session){ + for(const key of this.session.votes.keys()){ + const fishP = FishPlayer.getById(key)!; + if(!this.isEligible(fishP, this.session.data)) this.session.votes.delete(key); + } + return [...this.session.votes].reduce((acc, [k, v]) => acc + v, 0); + } else return 0; + } + + getEligibleVoters():FishPlayer[] { + if(!this.session) return []; + return FishPlayer.getAllOnline().filter(p => + this.isEligible(p, this.session!.data) + ); + } + messageEligibleVoters(message:string){ + this.getEligibleVoters().forEach(p => p.sendMessage(message)); + } + _checkVote(end:boolean){ + const votes = this.currentVotes(); + const required = this.requiredVotes(); + if(votes >= required){ + this.fire("success", [false]); + this.fire("vote passed", [votes, required]); + this.resetVote(); + } else if(end){ + this.fire("fail", [false]); + this.fire("vote failed", [votes, required]); + this.resetVote(); + } + } } \ No newline at end of file