diff --git a/build/scripts/commands.js b/build/scripts/commands.js index ea7f9b1..58aa069 100644 --- a/build/scripts/commands.js +++ b/build/scripts/commands.js @@ -19,6 +19,42 @@ var __assign = (this && this.__assign) || function () { }; 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; @@ -633,6 +669,7 @@ function register(commands, clientHandler, serverHandler) { 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) { + var _this = this; if (!initialized) (0, funcs_4.crash)("Commands not initialized!"); var fishSender = players_1.FishPlayer.get(sender); @@ -658,66 +695,77 @@ function register(commands, clientHandler, serverHandler) { return; } //Recursively resolve unresolved args (such as players that need to be determined through a menu) - resolveArgsRecursive(output.processedArgs, output.unresolvedArgs, fishSender, function () { + resolveArgsRecursive(output.processedArgs, output.unresolvedArgs, fishSender).then(function (resolvedArgs) { return __awaiter(_this, void 0, void 0, function () { + var usageData, failed, args_1, err_1; var _a, _b; - //Run the command handler - var usageData = fishSender.getUsageData(name); - var failed = false; - try { - var args_1 = { - rawArgs: rawArgs, - args: output.processedArgs, - 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: outputFormatter_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_4.crash)("No tap handler to activate: command \"".concat(name, "\"")); - if (mode == "off") { - fishSender.tapInfo.commandName = null; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + usageData = fishSender.getUsageData(name); + failed = false; + _c.label = 1; + case 1: + _c.trys.push([1, 3, 4, 5]); + 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: outputFormatter_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_4.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; + }, + }; + (_b = data.requirements) === null || _b === void 0 ? void 0 : _b.forEach(function (r) { return r(args_1); }); + return [4 /*yield*/, data.handler(args_1)]; + case 2: + _c.sent(); + //Update usage data + if (!failed) { + usageData.lastUsedSuccessfully = globalUsageData[name].lastUsedSuccessfully = Date.now(); + } + return [3 /*break*/, 5]; + case 3: + err_1 = _c.sent(); + if (err_1 instanceof exports.CommandError) { + //If the error is a command error, then just outputFail + (0, utils_1.outputFail)(err_1.data, sender); } else { - fishSender.tapInfo.commandName = name; - fishSender.tapInfo.mode = mode; + sender.sendMessage("[scarlet]\u274C An error occurred while executing the command!"); + if (fishSender.hasPerm("seeErrorMessages")) + sender.sendMessage((0, funcs_2.parseError)(err_1)); + Log.err("Unhandled error in command execution: ".concat(fishSender.cleanedName, " ran /").concat(name)); + Log.err(err_1); + Log.err(err_1.stack); } - fishSender.tapInfo.lastArgs = output.processedArgs; - }, - }; - (_b = data.requirements) === null || _b === void 0 ? void 0 : _b.forEach(function (r) { return r(args_1); }); - data.handler(args_1); - //Update usage data - if (!failed) { - usageData.lastUsedSuccessfully = globalUsageData[name].lastUsedSuccessfully = Date.now(); - } - } - catch (err) { - if (err instanceof exports.CommandError) { - //If the error is a command error, then just outputFail - (0, utils_1.outputFail)(err.data, sender); + return [3 /*break*/, 5]; + case 4: + usageData.lastUsed = globalUsageData[name].lastUsed = Date.now(); + return [7 /*endfinally*/]; + case 5: return [2 /*return*/]; } - else { - sender.sendMessage("[scarlet]\u274C An error occurred while executing the command!"); - if (fishSender.hasPerm("seeErrorMessages")) - sender.sendMessage((0, funcs_2.parseError)(err)); - Log.err("Unhandled error in command execution: ".concat(fishSender.cleanedName, " ran /").concat(name)); - Log.err(err); - Log.err(err.stack); - } - } - finally { - usageData.lastUsed = globalUsageData[name].lastUsed = Date.now(); - } - }); + }); + }); }); } })); exports.allCommands[name] = data; }; @@ -791,26 +839,38 @@ function registerConsole(commands, serverHandler) { } } /** Recursively resolves args. This function is necessary to handle cases such as a command that accepts multiple players that all need to be selected through menus. */ -function resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback) { - if (unresolvedArgs.length == 0) { - callback(processedArgs); - } - else { - var argToResolve_1 = unresolvedArgs.shift(); - var optionsList_1 = []; - //TODO Dubious implementation - switch (argToResolve_1.type) { - case "player": - Groups.player.each(function (player) { return optionsList_1.push(player); }); - break; - default: (0, funcs_4.crash)("Unable to resolve arg of type ".concat(argToResolve_1.type)); - } - (0, menus_1.menu)("Select a player", "Select a player for the argument \"".concat(argToResolve_1.name, "\""), optionsList_1, sender, function (_a) { - var option = _a.option; - processedArgs[argToResolve_1.name] = players_1.FishPlayer.get(option); - resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback); - }, true, function (player) { return Strings.stripColors(player.name).length >= 3 ? Strings.stripColors(player.name) : (0, funcs_3.escapeStringColorsClient)(player.name); }); - } +function resolveArgsRecursive(processedArgs, unresolvedArgs, sender) { + return __awaiter(this, void 0, void 0, function () { + var argToResolve, optionsList_1, option; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(unresolvedArgs.length == 0)) return [3 /*break*/, 1]; + return [2 /*return*/, processedArgs]; + case 1: + argToResolve = unresolvedArgs.shift(); + optionsList_1 = []; + //TODO Dubious implementation + switch (argToResolve.type) { + case "player": + Groups.player.each(function (player) { return optionsList_1.push(player); }); + break; + default: (0, funcs_4.crash)("Unable to resolve arg of type ".concat(argToResolve.type)); + } + return [4 /*yield*/, menus_1.Menu.menu("Select a player", "Select a player for the argument \"".concat(argToResolve.name, "\""), optionsList_1, sender, { + includeCancel: true, + optionStringifier: function (player) { return Strings.stripColors(player.name).length >= 3 ? + player.name + : (0, funcs_3.escapeStringColorsClient)(player.name); } + })]; + case 2: + option = _a.sent(); + processedArgs[argToResolve.name] = players_1.FishPlayer.get(option); + return [4 /*yield*/, resolveArgsRecursive(processedArgs, unresolvedArgs, sender)]; + case 3: return [2 /*return*/, _a.sent()]; + } + }); + }); } function initialize() { var e_5, _a, e_6, _b; diff --git a/build/scripts/fjsContext.js b/build/scripts/fjsContext.js index 346ecc3..884c1e3 100644 --- a/build/scripts/fjsContext.js +++ b/build/scripts/fjsContext.js @@ -25,7 +25,7 @@ var Promise = require("./promise").Promise; var Perm = commands.Perm, allCommands = commands.allCommands; var FishPlayer = players.FishPlayer; var Rank = ranks.Rank, RoleFlag = ranks.RoleFlag; -var menu = menus.menu; +var Menu = menus.Menu; Object.assign(this, utils); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever var Ranks = null; var $ = Object.assign(function $(input) { diff --git a/build/scripts/main.js b/build/scripts/main.js index 1082d06..fbf1bcf 100644 --- a/build/scripts/main.js +++ b/build/scripts/main.js @@ -33,12 +33,12 @@ Array.prototype.flat = function(depth){ } 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(''); + 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(''); } //Fix rhino regex if(/ae?a/.test("aeea")){ @@ -48,4 +48,5 @@ if(/ae?a/.test("aeea")){ }; } -require("index"); \ No newline at end of file +this.Promise = require('promise').Promise; +require("index"); diff --git a/build/scripts/menus.js b/build/scripts/menus.js index a274f5b..349e78d 100644 --- a/build/scripts/menus.js +++ b/build/scripts/menus.js @@ -3,6 +3,28 @@ Copyright © BalaM314, 2025. All Rights Reserved. This file contains the menu system. */ +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); @@ -30,36 +52,44 @@ var __read = (this && this.__read) || function (o, n) { } 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 = void 0; +exports.listeners = exports.Menu = void 0; exports.registerListeners = registerListeners; -exports.menu = menu; var commands_1 = require("./commands"); var players_1 = require("./players"); var utils_1 = require("./utils"); var funcs_1 = require("./funcs"); var funcs_2 = require("./funcs"); +var promise_1 = require("./promise"); +/** 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 = (function (d) { return d; })({ +var listeners = { generic: function (player, option) { - var _a, _b; var fishSender = players_1.FishPlayer.get(player); - if (option === -1 || option === fishSender.activeMenu.cancelOptionId) - return; - var prevCallback = fishSender.activeMenu.callback; - (_b = (_a = fishSender.activeMenu).callback) === null || _b === void 0 ? void 0 : _b.call(_a, fishSender, option); - //if the callback wasn't modified, then clear it - if (fishSender.activeMenu.callback === prevCallback) - fishSender.activeMenu.callback = undefined; - //otherwise, the menu spawned another menu that needs to be handled + 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 } -}); +}; /** Registers all listeners, should be called on server load. */ function registerListeners() { var e_1, _a; @@ -78,59 +108,143 @@ function registerListeners() { finally { if (e_1) throw e_1.error; } } } -//this is a minor abomination but theres no good way to do overloads in typescript -function menu(title, description, options, target, callback, includeCancel, optionStringifier, //this is dubious -columns) { - if (includeCancel === void 0) { includeCancel = true; } - if (optionStringifier === void 0) { optionStringifier = function (t) { return t; }; } - if (columns === void 0) { columns = 3; } - if (!callback) { - //overload 1, just display a menu with no callback - Call.menu(target.con, registeredListeners.none, title, description, options.length == 0 ? [[""]] : (0, funcs_2.to2DArray)(options.map(optionStringifier), columns)); - } - else { - //overload 2, display a menu with callback - //Set up the 2D array of options, and add cancel - //Use "" as a fallback, because Call.menu with an empty array of options causes a client crash - var arrangedOptions = (options.length == 0 && !includeCancel) ? [[""]] : (0, funcs_2.to2DArray)(options.map(optionStringifier), columns); - if (includeCancel) { - arrangedOptions.push(["Cancel"]); - target.activeMenu.cancelOptionId = options.length; - } - else { - target.activeMenu.cancelOptionId = -1; - } +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 ? "ignore" : _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. - target.activeMenu.callback = function (fishSender, 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. - //We do need to validate option though, as it can be any number. - if (!(option in options)) - return; - try { - callback({ - option: options[option], - sender: target, - outputFail: function (message) { return (0, utils_1.outputFail)(message, target); }, - outputSuccess: function (message) { return (0, utils_1.outputSuccess)(message, target); }, - }); - } - catch (err) { - if (err instanceof commands_1.CommandError) { - //If the error is a command error, then just outputFail - (0, utils_1.outputFail)(err.data, target); + //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("cancel"); + else + return; + } + else { + resolve(options[option]); + } } + catch (err) { + if (err instanceof commands_1.CommandError) { + //If the error is a command error, then just outputFail + (0, utils_1.outputFail)(err.data, target); + } + else { + target.sendMessage("[scarlet]\u274C An error occurred while executing the command!"); + if (target.hasPerm("seeErrorMessages")) + target.sendMessage((0, funcs_1.parseError)(err)); + Log.err("Unhandled error in menu callback: ".concat(target.cleanedName, " submitted menu \"").concat(title, "\" \"").concat(description, "\"")); + Log.err(err); + } + } + } }); + 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 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 ? "ignore" : _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_2.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 === "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)); + }, + 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; }); + }, + 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); + exports.Menu.buttons(target, title, description, opts, cfg).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 { - target.sendMessage("[scarlet]\u274C An error occurred while executing the command!"); - if (target.hasPerm("seeErrorMessages")) - target.sendMessage((0, funcs_1.parseError)(err)); - Log.err("Unhandled error in menu callback: ".concat(target.cleanedName, " submitted menu \"").concat(title, "\" \"").concat(description, "\"")); - Log.err(err); + //Treat numbers as cancel + if (cfg.onCancel == "null") + resolve(null); + else if (cfg.onCancel == "reject") + reject("cancel"); + //otherwise, just let the promise hang } - } - }; - Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions); + }); + } + showPage(0); + return promise; + }, + 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_2.to2DArray)((0, funcs_2.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); + }, + 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_2.to2DArray)((0, funcs_2.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/playerCommands.js b/build/scripts/playerCommands.js index 90a364d..19a6ca5 100644 --- a/build/scripts/playerCommands.js +++ b/build/scripts/playerCommands.js @@ -18,6 +18,42 @@ var __assign = (this && this.__assign) || function () { }; 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; @@ -565,11 +601,10 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { if (target.hasPerm("blockTrolling")) (0, commands_1.fail)(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); } - (0, menus_1.menu)("Rules for [#0000ff]>|||> FISH [white]servers", config_1.rules.join("\n\n"), ["[green]I agree to abide by these rules[]", "No"], target, function (_a) { - var option = _a.option; + 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).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); - }, false); + }); if (target !== sender) outputSuccess(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Reminded ", " of the rules."], ["Reminded ", " of the rules."])), target)); }, @@ -587,7 +622,7 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { (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."); - (0, menus_1.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); + 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); (0, utils_1.logAction)("showed void warning", sender, args.player); outputSuccess(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Warned ", " about power voids with a popup message."], ["Warned ", " about power voids with a popup message."])), args.player)); } @@ -665,26 +700,37 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { }); }, requirements: [commands_1.Req.cooldown(3000), commands_1.Req.mode("survival"), commands_1.Req.gameRunning], handler: function (_a) { - var sender = _a.sender, manager = _a.data.manager; - if (!manager.session) { - (0, menus_1.menu)("Start a Next Wave Vote", "Select the amount of waves you would like to skip.", [1, 5, 10], sender, function (_a) { - var option = _a.option; - if (manager.session) { - //Someone else started a vote - if (manager.session.data != option) - (0, commands_1.fail)("Someone else started a vote with a different number of waves to skip."); - else - manager.vote(sender, sender.voteWeight(), option); + return __awaiter(this, arguments, void 0, function (_b) { + var option; + var sender = _b.sender, manager = _b.data.manager; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!!manager.session) return [3 /*break*/, 2]; + 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 1: + option = _c.sent(); + if (manager.session) { + //Someone else started a vote + if (manager.session.data != option) + (0, commands_1.fail)("Someone else started a vote with a different number of waves to skip."); + else + manager.vote(sender, sender.voteWeight(), option); + } + else { + manager.start(sender, sender.voteWeight(), option); + } + return [3 /*break*/, 3]; + case 2: + manager.vote(sender, sender.voteWeight(), null); + _c.label = 3; + case 3: return [2 /*return*/]; } - else { - //this is still a race condition technically... shouldn't be that bad right? - manager.start(sender, sender.voteWeight(), option); - } - }, true, function (n) { return "".concat(n, " waves"); }); - } - else { - manager.vote(sender, sender.voteWeight(), null); - } + }); + }); } }), forcertv: { args: ["force:boolean?"], diff --git a/build/scripts/players.js b/build/scripts/players.js index 0697957..0c0424e 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -68,7 +68,8 @@ var FishPlayer = /** @class */ (function () { this.player = null; this.pet = ""; this.watch = false; - this.activeMenu = { cancelOptionId: -1 }; + /** Front-to-back queue of menus to show. */ + this.activeMenus = []; this.tileId = false; this.tilelog = null; this.trail = null; @@ -352,7 +353,7 @@ var FishPlayer = /** @class */ (function () { }); //I think this is a better spot for this if (fishPlayer.firstJoin()) - (0, menus_1.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); + 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. */ @@ -412,7 +413,7 @@ var FishPlayer = /** @class */ (function () { } } //Clear temporary states such as menu and taphandler - fishP.activeMenu.callback = undefined; + fishP.activeMenus = []; fishP.tapInfo.commandName = null; fishP.stats.timeInGame += (Date.now() - fishP.lastJoined); //Time between joining and leaving fishP.lastJoined = Date.now(); @@ -479,7 +480,7 @@ var FishPlayer = /** @class */ (function () { var _this = this; this.forEachPlayer(function (fishPlayer) { //Clear temporary states such as menu and taphandler - fishPlayer.activeMenu.callback = undefined; + fishPlayer.activeMenus = []; fishPlayer.tapInfo.commandName = null; //Update stats if (!_this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict) { @@ -676,15 +677,14 @@ var FishPlayer = /** @class */ (function () { api.sendStaffMessage("Autoflagged player ".concat(_this.name, "[cyan] for suspected vpn!"), "AntiVPN"); 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.")); - (0, menus_1.menu)("[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."), ["Close", "Discord"], _this, function (_a) { - var option = _a.option, sender = _a.sender; + 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(sender.con, config_1.text.discordURL); + Call.openURI(_this.con, config_1.text.discordURL); } - }, false, function (str) { return ({ - "Close": "Close", - "Discord": config_1.FColor.discord("Discord") - }[str]); }); + }); _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.")); } } diff --git a/build/scripts/staffCommands.js b/build/scripts/staffCommands.js index ad61e72..b687ade 100644 --- a/build/scripts/staffCommands.js +++ b/build/scripts/staffCommands.js @@ -7,6 +7,42 @@ var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cook if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } return cooked; }; +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; @@ -74,7 +110,7 @@ exports.commands = (0, commands_1.commandList)({ 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"; - (0, menus_1.menu)('Warning', message, ["[green]Accept"], args.player); + menus_1.Menu.menu('Warning', message, ["[green]Accept"], args.player); (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)); } @@ -228,72 +264,88 @@ exports.commands = (0, commands_1.commandList)({ description: "Stops an offline player.", perm: commands_1.Perm.mod, handler: function (_a) { - var _b; - var args = _a.args, sender = _a.sender, outputFail = _a.outputFail, outputSuccess = _a.outputSuccess, f = _a.f, admins = _a.admins; - var maxPlayers = 60; - function stop(option, time) { - var fishP = players_1.FishPlayer.getFromInfo(option); - if (sender.canModerate(fishP, true)) { - (0, utils_1.logAction)(fishP.marked() ? time == 1000 ? "freed" : "updated stop time of" : "stopped", sender, option, undefined, time); - fishP.stop(sender, time); - outputSuccess(f(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Player ", " was marked for ", "."], ["Player ", " was marked for ", "."])), option, (0, utils_1.formatTime)(time))); - } - else { - outputFail("You do not have permission to stop this player."); - } - } - if (args.name && globals_2.uuidPattern.test(args.name)) { - var info = admins.getInfoOptional(args.name); - if (info != null) { - stop(info, (_b = args.time) !== null && _b !== void 0 ? _b : (0, utils_1.untilForever)()); - } - else { - outputFail(f(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); - } - return; - } - var possiblePlayers; - if (args.name) { - possiblePlayers = (0, funcs_5.setToArray)(admins.searchNames(args.name)); - if (possiblePlayers.length > maxPlayers) { - var exactPlayers = (0, funcs_5.setToArray)(admins.findByName(args.name)); - if (exactPlayers.length > 0) { - possiblePlayers = exactPlayers; + return __awaiter(this, arguments, void 0, function (_b) { + function stop(option, time) { + var fishP = players_1.FishPlayer.getFromInfo(option); + if (sender.canModerate(fishP, true)) { + (0, utils_1.logAction)(fishP.marked() ? time == 1000 ? "freed" : "updated stop time of" : "stopped", sender, option, undefined, time); + fishP.stop(sender, time); + outputSuccess(f(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Player ", " was marked for ", "."], ["Player ", " was marked for ", "."])), option, (0, utils_1.formatTime)(time))); } else { - (0, commands_1.fail)("Too many players with that name."); + outputFail("You do not have permission to stop this player."); } } - else if (possiblePlayers.length == 0) { - (0, commands_1.fail)("No players with that name were found."); - } - var 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(); }); - } - (0, menus_1.menu)("Stop", "Choose a player to mark", possiblePlayers, sender, function (_a) { - var optionPlayer = _a.option, sender = _a.sender; - if (args.time == null) { - (0, menus_1.menu)("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, function (_a) { - var optionTime = _a.option, sender = _a.sender; - var time = optionTime == "2 days" ? 172800000 : - optionTime == "7 days" ? 604800000 : - optionTime == "30 days" ? 2592000000 : - (globals_1.maxTime - Date.now() - 10000); - stop(optionPlayer, time); - }, false); - } - else { - stop(optionPlayer, args.time); - } - }, true, function (p) { return p.lastName; }); + 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_2.uuidPattern.test(args.name)) { + info = admins.getInfoOptional(args.name); + if (info != null) { + stop(info, (_f = args.time) !== null && _f !== void 0 ? _f : (0, utils_1.untilForever)()); + } + else { + outputFail(f(templateObject_22 || (templateObject_22 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); + } + return [2 /*return*/]; + } + if (args.name) { + possiblePlayers = (0, funcs_5.setToArray)(admins.searchNames(args.name)); + if (possiblePlayers.length > maxPlayers) { + exactPlayers = (0, funcs_5.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 1: + optionPlayer = _h.sent(); + if (!((_g = args.time) !== null && _g !== void 0)) return [3 /*break*/, 2]; + _c = _g; + return [3 /*break*/, 4]; + case 2: + _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 3: + _c = (_d.time = _e.apply(void 0, [_h.sent(), { + "2 days": 172800000, + "7 days": 604800000, + "30 days": 2592000000, + "forever": globals_1.maxTime - Date.now() - 10000, + }])); + _h.label = 4; + case 4: + _c; + stop(optionPlayer, args.time); + return [2 /*return*/]; + } + }); + }); } }, mute_offline: { @@ -301,60 +353,80 @@ exports.commands = (0, commands_1.commandList)({ description: "Mutes an offline player.", perm: commands_1.Perm.mod, handler: function (_a) { - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f, admins = _a.admins; - var maxPlayers = 60; - function mute(option) { - var fishP = players_1.FishPlayer.getFromInfo(option); - if (!sender.canModerate(fishP, true)) - (0, commands_1.fail)("You do not have permission to mute this player."); - (0, menus_1.menu)("Mute Offine Confirmation", "Are you sure you want to ".concat(fishP.muted ? "unmute" : "mute", " player ").concat(option.lastName, "?"), [true, false], sender, function (res) { - if (res.option) { - (0, utils_1.logAction)(fishP.muted ? "unmuted" : "muted", sender, fishP); - if (fishP.muted) - fishP.unmute(sender); - else - fishP.mute(sender); - outputSuccess("".concat(fishP.muted ? "Muted" : "Unmuted", " ").concat(option.lastName, ".")); - } - }, false, function (opt) { return opt ? "[green]Yes, ".concat(fishP.muted ? "unmute" : "mute", " them") : "[red]Cancel"; }); - } - if (args.name && globals_2.uuidPattern.test(args.name)) { - var info = admins.getInfoOptional(args.name); - if (!info) - (0, commands_1.fail)(f(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); - mute(info); - return; - } - var possiblePlayers; - if (args.name) { - possiblePlayers = (0, funcs_5.setToArray)(admins.searchNames(args.name)); - if (possiblePlayers.length > maxPlayers) { - var exactPlayers = (0, funcs_5.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."); + 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) + fishP.unmute(sender); + else + fishP.mute(sender); + outputSuccess("".concat(fishP.muted ? "Muted" : "Unmuted", " ").concat(option.lastName, ".")); + return [2 /*return*/]; + } + }); + }); } - var 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(); }); - } - (0, menus_1.menu)("Mute", "Choose a player to mute", possiblePlayers, sender, function (_a) { - var optionPlayer = _a.option; - mute(optionPlayer); - }, true, function (p) { return p.lastName; }); + 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_2.uuidPattern.test(args.name)) { + info = (_c = admins.getInfoOptional(args.name)) !== null && _c !== void 0 ? _c : (0, commands_1.fail)(f(templateObject_23 || (templateObject_23 = __makeTemplateObject(["Unknown UUID ", ""], ["Unknown UUID ", ""])), args.name)); + mute(info); + return [2 /*return*/]; + } + if (args.name) { + possiblePlayers = (0, funcs_5.setToArray)(admins.searchNames(args.name)); + if (possiblePlayers.length > maxPlayers) { + exactPlayers = (0, funcs_5.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 1: + option = _d.sent(); + mute(option); + return [2 /*return*/]; + } + }); + }); } }, restart: { @@ -472,79 +544,81 @@ exports.commands = (0, commands_1.commandList)({ description: "Bans a player by UUID and IP.", perm: commands_1.Perm.admin, handler: function (_a) { - var args = _a.args, sender = _a.sender, outputSuccess = _a.outputSuccess, f = _a.f, admins = _a.admins; - if (args.uuid_or_ip && globals_2.uuidPattern.test(args.uuid_or_ip)) { - //Overload 1: ban by uuid - var uuid_1 = args.uuid_or_ip; - var data_1; - if ((data_1 = admins.getInfoOptional(uuid_1)) != null && data_1.admin) - (0, commands_1.fail)("Cannot ban an admin."); - var name = data_1 ? "".concat((0, funcs_2.escapeStringColorsClient)(data_1.lastName), " (").concat(uuid_1, "/").concat(data_1.lastIP, ")") : uuid_1; - (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(name, "?"), ["[red]Yes", "[green]Cancel"], sender, function (_a) { - var confirm = _a.option; - if (confirm != "[red]Yes") - (0, commands_1.fail)("Cancelled."); - admins.banPlayerID(uuid_1); - if (data_1) { - var ip = data_1.lastIP; - admins.banPlayerIP(ip); - api.ban({ ip: ip, uuid: uuid_1 }); - Log.info("".concat(uuid_1, "/").concat(ip, " was banned.")); - (0, utils_1.logAction)("banned", sender, data_1); - outputSuccess(f(templateObject_29 || (templateObject_29 = __makeTemplateObject(["Banned player ", " (", "/", ")"], ["Banned player ", " (", "/", ")"])), (0, funcs_2.escapeStringColorsClient)(data_1.lastName), uuid_1, ip)); - //TODO add way to specify whether to activate or escape color tags - } - else { - api.ban({ uuid: uuid_1 }); - Log.info("".concat(uuid_1, " was banned.")); - (0, utils_1.logAction)("banned", sender, uuid_1); - outputSuccess(f(templateObject_30 || (templateObject_30 = __makeTemplateObject(["Banned player ", ". [yellow]Unable to determine IP.[]"], ["Banned player ", ". [yellow]Unable to determine IP.[]"])), uuid_1)); - } - (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - }, false); - return; - } - else if (args.uuid_or_ip && globals_2.ipPattern.test(args.uuid_or_ip)) { - //Overload 2: ban by uuid - var ip_1 = args.uuid_or_ip; - (0, menus_1.menu)("Confirm", "Are you sure you want to ban IP ".concat(ip_1, "?"), ["[red]Yes", "[green]Cancel"], sender, function (_a) { - var confirm = _a.option; - if (confirm != "[red]Yes") - (0, commands_1.fail)("Cancelled."); - api.ban({ ip: ip_1 }); - var info = admins.findByIP(ip_1); - if (info) - (0, utils_1.logAction)("banned", sender, info); - else - (0, utils_1.logAction)("banned ".concat(ip_1), sender); - var alreadyBanned = admins.banPlayerIP(ip_1); - if (alreadyBanned) { - outputSuccess(f(templateObject_31 || (templateObject_31 = __makeTemplateObject(["IP ", " is already banned. Ban was synced to other servers."], ["IP ", " is already banned. Ban was synced to other servers."])), ip_1)); + 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_2.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_2.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_29 || (templateObject_29 = __makeTemplateObject(["Banned player ", " (", "/", ")"], ["Banned player ", " (", "/", ")"])), (0, funcs_2.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_30 || (templateObject_30 = __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_2.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_31 || (templateObject_31 = __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_32 || (templateObject_32 = __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_5.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_33 || (templateObject_33 = __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*/]; } - else { - outputSuccess(f(templateObject_32 || (templateObject_32 = __makeTemplateObject(["IP ", " has been banned. Ban was synced to other servers."], ["IP ", " has been banned. Ban was synced to other servers."])), ip_1)); - } - (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - }, false); - return; - } - //Overload 3: ban by menu - (0, menus_1.menu)("[scarlet]BAN[]", "Choose a player to ban.", (0, funcs_5.setToArray)(Groups.player), sender, function (_a) { - var option = _a.option; - if (option.admin) - (0, commands_1.fail)("Cannot ban an admin."); - (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(option.name, "?"), ["[red]Yes", "[green]Cancel"], sender, function (_a) { - var confirm = _a.option; - if (confirm != "[red]Yes") - (0, commands_1.fail)("Cancelled."); - 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_33 || (templateObject_33 = __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, "."); }); - }, false); - }, true, function (opt) { return opt.name; }); + }); + }); } }, kill: { @@ -569,51 +643,51 @@ exports.commands = (0, commands_1.commandList)({ description: "Kills all units, optionally specifying a team and unit type.", perm: commands_1.Perm.massKill, handler: function (_a) { - var _b = _a.args, team = _b.team, unit = _b.unit, sender = _a.sender, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, f = _a.f; - if (team) { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every ".concat(unit ? unit.localizedName : "unit", "[] on the team ").concat(team.coloredName(), "."), ["[orange]Kill units[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill units[]") { - if (unit) { - var i_1 = 0; - team.data().units.each(function (u) { return u.type == unit; }, function (u) { - u.kill(); - i_1++; - }); - outputSuccess(f(templateObject_36 || (templateObject_36 = __makeTemplateObject(["Killed ", " units on ", "."], ["Killed ", " units on ", "."])), i_1, team)); - } - else { - var before = team.data().units.size; - team.data().units.each(function (u) { return u.kill(); }); - outputSuccess(f(templateObject_37 || (templateObject_37 = __makeTemplateObject(["Killed ", " units on ", "."], ["Killed ", " units on ", "."])), before, team)); - } - } - else - outputFail("Cancelled."); - }, false); - } - else { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every single ".concat(unit ? unit.localizedName : "unit", "[]."), ["[orange]Kill all units[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill all units[]") { - if (unit) { - var i_2 = 0; - Groups.unit.each(function (u) { return u.type == unit; }, function (u) { - u.kill(); - i_2++; - }); - outputSuccess(f(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), i_2)); - } - else { - var before = Groups.unit.size(); - Groups.unit.each(function (u) { return u.kill(); }); - outputSuccess(f(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), before)); - } + 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, outputFail = _b.outputFail, 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_36 || (templateObject_36 = __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_37 || (templateObject_37 = __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_38 || (templateObject_38 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), i_2)); + } + else { + before = Groups.unit.size(); + Groups.unit.each(function (u) { return u.kill(); }); + outputSuccess(f(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Killed ", " units."], ["Killed ", " units."])), before)); + } + _d.label = 4; + case 4: return [2 /*return*/]; } - else - outputFail("Cancelled."); - }, false); - } + }); + }); } }, killbuildings: { @@ -621,31 +695,31 @@ exports.commands = (0, commands_1.commandList)({ description: "Kills all buildings (except cores), optionally specifying a team.", perm: commands_1.Perm.massKill, handler: function (_a) { - var team = _a.args.team, sender = _a.sender, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, f = _a.f; - if (team) { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every building[] on the team ".concat(team.coloredName(), ", except cores."), ["[orange]Kill buildings[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill buildings[]") { - var 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_40 || (templateObject_40 = __makeTemplateObject(["Killed ", " buildings on ", ""], ["Killed ", " buildings on ", ""])), count, team)); + return __awaiter(this, arguments, void 0, function (_b) { + var count, count; + var team = _b.args.team, sender = _b.sender, outputSuccess = _b.outputSuccess, outputFail = _b.outputFail, 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_40 || (templateObject_40 = __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_41 || (templateObject_41 = __makeTemplateObject(["Killed ", " buildings."], ["Killed ", " buildings."])), count)); + _c.label = 4; + case 4: return [2 /*return*/]; } - else - outputFail("Cancelled."); - }, false); - } - else { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every building[] except cores.", ["[orange]Kill buildings[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill buildings[]") { - var count = Groups.build.size(); - Groups.build.each(function (b) { return !(b.block instanceof CoreBlock); }, function (b) { return b.tile.remove(); }); - outputSuccess(f(templateObject_41 || (templateObject_41 = __makeTemplateObject(["Killed ", " buildings."], ["Killed ", " buildings."])), count)); - } - else - outputFail("Cancelled."); - }, false); - } + }); + }); } }, respawn: { @@ -971,39 +1045,52 @@ exports.commands = (0, commands_1.commandList)({ description: "Searches playerinfo by name, IP, or UUID.", perm: commands_1.Perm.admin, handler: function (_a) { - var input = _a.args.input, admins = _a.admins, output = _a.output, f = _a.f, sender = _a.sender; - if (globals_2.uuidPattern.test(input)) { - var fishP = players_1.FishPlayer.getById(input); - var info = admins.getInfoOptional(input); - if (fishP == null && info == null) - (0, commands_1.fail)(f(templateObject_55 || (templateObject_55 = __makeTemplateObject(["No stored data matched uuid ", "."], ["No stored data matched uuid ", "."])), input)); - else if (fishP == null && info) - output(f(templateObject_56 || (templateObject_56 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); - else if (fishP && info) - output(f(templateObject_57 || (templateObject_57 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); - else - (0, commands_1.fail)(f(templateObject_58 || (templateObject_58 = __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)); - } - else if (globals_2.ipPattern.test(input)) { - var matches = admins.findByIPs(input); - if (matches.isEmpty()) - (0, commands_1.fail)(f(templateObject_59 || (templateObject_59 = __makeTemplateObject(["No stored data matched IP ", ""], ["No stored data matched IP ", ""])), input)); - output(f(templateObject_60 || (templateObject_60 = __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_61 || (templateObject_61 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); }); - } - else { - var matches_1 = Vars.netServer.admins.searchNames(input); - if (matches_1.isEmpty()) - (0, commands_1.fail)(f(templateObject_62 || (templateObject_62 = __makeTemplateObject(["No stored data matched name ", ""], ["No stored data matched name ", ""])), input)); - output(f(templateObject_63 || (templateObject_63 = __makeTemplateObject(["[accent]Found ", " match", " for search \"", "\"."], ["[accent]Found ", " match", " for search \"", "\"."])), matches_1.size, matches_1.size == 1 ? "" : "es", input)); - var displayMatches = function () { - matches_1.each(function (info) { return output(f(templateObject_64 || (templateObject_64 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); }); - }; - if (matches_1.size > 20) - (0, menus_1.menu)("Confirm", "Are you sure you want to view all ".concat(matches_1.size, " matches?"), ["Yes"], sender, displayMatches); - else - displayMatches(); - } + 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_2.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_55 || (templateObject_55 = __makeTemplateObject(["No stored data matched uuid ", "."], ["No stored data matched uuid ", "."])), input)); + else if (fishP == null && info) + output(f(templateObject_56 || (templateObject_56 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); + else if (fishP && info) + output(f(templateObject_57 || (templateObject_57 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); + else + (0, commands_1.fail)(f(templateObject_58 || (templateObject_58 = __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_2.ipPattern.test(input)) return [3 /*break*/, 2]; + matches = admins.findByIPs(input); + if (matches.isEmpty()) + (0, commands_1.fail)(f(templateObject_59 || (templateObject_59 = __makeTemplateObject(["No stored data matched IP ", ""], ["No stored data matched IP ", ""])), input)); + output(f(templateObject_60 || (templateObject_60 = __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_61 || (templateObject_61 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.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_62 || (templateObject_62 = __makeTemplateObject(["No stored data matched name ", ""], ["No stored data matched name ", ""])), input)); + output(f(templateObject_63 || (templateObject_63 = __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_64 || (templateObject_64 = __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_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.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: { diff --git a/src/commands.ts b/src/commands.ts index 90808e5..f0ee838 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -6,7 +6,7 @@ This file contains the commands system. import { FColor, Gamemode, GamemodeName, text } from "./config"; import { ipPattern, uuidPattern } from "./globals"; -import { menu } from "./menus"; +import { Menu } from "./menus"; import { FishPlayer } from "./players"; import { Rank, RankName, RoleFlag } from "./ranks"; import type { ClientCommandHandler, CommandArg, FishCommandArgType, FishCommandData, FishCommandHandlerData, FishCommandHandlerUtils, FishConsoleCommandData, Formattable, PartialFormatString, SelectEnumClassKeys, ServerCommandHandler } from "./types"; @@ -548,14 +548,14 @@ export function register(commands:Record | } //Recursively resolve unresolved args (such as players that need to be determined through a menu) - resolveArgsRecursive(output.processedArgs, output.unresolvedArgs, fishSender, () => { + resolveArgsRecursive(output.processedArgs, output.unresolvedArgs, fishSender).then(async (resolvedArgs) => { //Run the command handler const usageData = fishSender.getUsageData(name); let failed = false; try { const args:FishCommandHandlerData & FishCommandHandlerUtils = { rawArgs, - args: output.processedArgs, + args: resolvedArgs, sender: fishSender, data: data.data, outputFail: message => {outputFail(message, sender); failed = true;}, @@ -577,11 +577,11 @@ export function register(commands:Record | fishSender.tapInfo.commandName = name; fishSender.tapInfo.mode = mode; } - fishSender.tapInfo.lastArgs = output.processedArgs; + fishSender.tapInfo.lastArgs = resolvedArgs; }, }; data.requirements?.forEach(r => r(args)); - data.handler(args); + await data.handler(args); //Update usage data if(!failed){ usageData.lastUsedSuccessfully = globalUsageData[name].lastUsedSuccessfully = Date.now(); @@ -665,9 +665,9 @@ export function registerConsole(commands:Record, unresolvedArgs:CommandArg[], sender:FishPlayer, callback:(args:Record) => void){ +async function resolveArgsRecursive(processedArgs: Record, unresolvedArgs:CommandArg[], sender:FishPlayer){ if(unresolvedArgs.length == 0){ - callback(processedArgs); + return processedArgs; } else { const argToResolve = unresolvedArgs.shift()!; let optionsList:mindustryPlayer[] = []; @@ -676,13 +676,15 @@ function resolveArgsRecursive(processedArgs: Record, case "player": Groups.player.each(player => optionsList.push(player)); break; default: crash(`Unable to resolve arg of type ${argToResolve.type}`); } - menu(`Select a player`, `Select a player for the argument "${argToResolve.name}"`, optionsList, sender, ({option}) => { - processedArgs[argToResolve.name] = FishPlayer.get(option); - resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback); - }, true, player => Strings.stripColors(player.name).length >= 3 ? Strings.stripColors(player.name) : escapeStringColorsClient(player.name)) - + const option = await Menu.menu(`Select a player`, `Select a player for the argument "${argToResolve.name}"`, optionsList, sender, { + includeCancel: true, + optionStringifier: player => Strings.stripColors(player.name).length >= 3 ? + player.name + : escapeStringColorsClient(player.name) + }); + processedArgs[argToResolve.name] = FishPlayer.get(option); + return await resolveArgsRecursive(processedArgs, unresolvedArgs, sender); } - } export function initialize(){ diff --git a/src/fjsContext.ts b/src/fjsContext.ts index c6ae6a7..0a3415d 100644 --- a/src/fjsContext.ts +++ b/src/fjsContext.ts @@ -24,9 +24,9 @@ const { Promise } = require("./promise"); const { Perm, allCommands } = commands; const { FishPlayer } = players; const { Rank, RoleFlag } = ranks; -const { menu } = menus; +const { Menu } = menus; -Object.assign(this as never, utils); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever +Object.assign(this as never as typeof globalThis, utils); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever const Ranks = null!; diff --git a/src/main.js b/src/main.js index 5f16d83..b4278d1 100644 --- a/src/main.js +++ b/src/main.js @@ -33,12 +33,12 @@ Array.prototype.flat = function(depth){ } 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(''); + 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(''); } //Fix rhino regex if(/ae?a/.test("aeea")){ @@ -48,4 +48,5 @@ if(/ae?a/.test("aeea")){ }; } -require("index"); \ No newline at end of file +this.Promise = require('promise').Promise; +require("index"); diff --git a/src/menus.ts b/src/menus.ts index 69eac78..a0ac961 100644 --- a/src/menus.ts +++ b/src/menus.ts @@ -3,35 +3,32 @@ Copyright © BalaM314, 2025. All Rights Reserved. This file contains the menu system. */ -import { CommandError } from "./commands"; +import { CommandError, fail } from "./commands"; import { FishPlayer } from "./players"; -import { outputFail, outputSuccess } from "./utils"; +import { outputFail } from "./utils"; import { parseError } from './funcs'; import { to2DArray } from './funcs'; +import { Promise } from "./promise"; +/** Used to change the behavior of adding another menu when being run in a menu callback. */ +let isInMenuCallback = false; /** Stores a mapping from name to the numeric id of a listener that has been registered. */ -const registeredListeners:{ - [index:string]: number; -} = {}; +const registeredListeners: Record = {}; /** Stores all listeners in use by fish-commands. */ -const listeners = ( - void>>(d:T) => d -)({ +const listeners = { generic(player, option){ const fishSender = FishPlayer.get(player); - if(option === -1 || option === fishSender.activeMenu.cancelOptionId) return; - const prevCallback = fishSender.activeMenu.callback; - fishSender.activeMenu.callback?.(fishSender, option); - //if the callback wasn't modified, then clear it - if(fishSender.activeMenu.callback === prevCallback) - fishSender.activeMenu.callback = undefined; - //otherwise, the menu spawned another menu that needs to be handled + const prevCallback = fishSender.activeMenus.shift(); + if(!prevCallback) return; //No menu to process, do nothing + isInMenuCallback = true; + prevCallback.callback(option); + isInMenuCallback = false; }, none(player, option){ //do nothing } -}); +} satisfies Record void>; /** Registers all listeners, should be called on server load. */ export function registerListeners(){ @@ -40,59 +37,71 @@ export function registerListeners(){ } } -/** Displays a menu to a player. */ -function menu(title:string, description:string, options:string[], target:FishPlayer):void; -/** Displays a menu to a player with callback. */ -function menu( - title:string, description:string, options:T[], target:FishPlayer, - callback: (opts: { - option:T, sender:FishPlayer, outputSuccess:(message:string) => void, outputFail:(message:string) => void; - }) => void, - includeCancel?:boolean, optionStringifier?:(opt:T) => string, columns?:number -):void; -//this is a minor abomination but theres no good way to do overloads in typescript -function menu( - title:string, description:string, options:T[], target:FishPlayer, - callback?: (opts: { - option:T, sender:FishPlayer, outputSuccess:(message:string) => void, outputFail:(message:string) => void; - }) => void, - includeCancel:boolean = true, - optionStringifier:(opt:T) => string = t => t as unknown as string, //this is dubious - columns:number = 3, -){ +type MenuConfirmProps = { + /** This message is sent to the user (prefixed with /!\) if they cancel. */ + cancelOutput?: string; + title?: string; + confirmText?: string; + cancelText?: string; +}; - if(!callback){ - //overload 1, just display a menu with no callback - Call.menu(target.con, registeredListeners.none, title, description, options.length == 0 ? [[""]] : to2DArray(options.map(optionStringifier), columns)); - } else { - //overload 2, display a menu with callback +type MenuCancelOption = "ignore" | "reject" | "null"; +type MenuOptions = { + /** [red]Cancel[] will be added to the list of options. */ + includeCancel?: boolean; + optionStringifier?: (opt: TOption) => string; + columns?: number; + /** + * Specifies the behavior when the player cancels the menu (by clicking Cancel, or by pressing Escape). + * @default "ignore" + */ + onCancel?: TCancelBehavior; + cancelOptionId?: number; +}; - //Set up the 2D array of options, and add cancel - //Use "" as a fallback, because Call.menu with an empty array of options causes a client crash - const arrangedOptions = (options.length == 0 && !includeCancel) ? [[""]] : to2DArray(options.map(optionStringifier), columns); - if(includeCancel){ - arrangedOptions.push(["Cancel"]); - target.activeMenu.cancelOptionId = options.length; - } else { - target.activeMenu.cancelOptionId = -1; - } +export const Menu = { + /** Displays a menu to a player, returning a Promise. */ + raw( + this:void, title:string, description:string, arrangedOptions:TOption[][], target:FishPlayer, + { + optionStringifier = String, + onCancel = "ignore" as never, + cancelOptionId = -1, + }:{ + optionStringifier?:(opt:TOption) => string; + /** + * Specifies the behavior when the player cancels the menu (by clicking Cancel, or by pressing Escape). + * @default "ignore" + */ + onCancel?: TCancelBehavior; + cancelOptionId?: number; + } = {} + ){ + const { promise, reject, resolve } = Promise.withResolvers< + (TCancelBehavior extends "null" ? null : never) | TOption, + TCancelBehavior extends "reject" ? "cancel" : never + >(); //The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered. - target.activeMenu.callback = (fishSender, option) => { + //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(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. - - //We do need to validate option though, as it can be any number. - if(!(option in options)) return; + try { - callback({ - option: options[option], - sender: target, - outputFail: message => outputFail(message, target), - outputSuccess: message => outputSuccess(message, target), - }); + const 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 as never); + else if(onCancel == "reject") reject("cancel" as never); + else return; + } else { + resolve(options[option]); + } } catch(err){ if(err instanceof CommandError){ //If the error is a command error, then just outputFail @@ -104,14 +113,137 @@ function menu( Log.err(err as Error); } } - }; + }}); - Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions); - } + let i = 0; + const stringifiedOptions = arrangedOptions.map(r => r.map(item => { + if(i === cancelOptionId) return item as string; + 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 options into a 2D array, and can add a Cancel option. */ + menu( + this:void, title:string, description:string, options:TOption[], target:FishPlayer, + { + includeCancel = false, + optionStringifier = String, + columns = 3, + onCancel = "ignore" as never, + cancelOptionId = -1, + }:MenuOptions = {} + ){ + //Set up the 2D array of options, and maybe add cancel + //Call.menu() with [[]] will cause a client crash, make sure to pass [] instead + const arrangedOptions = (options.length == 0 && !includeCancel) ? [] : to2DArray(options, columns); + if(includeCancel){ + arrangedOptions.push(["[red]Cancel[]" as never]); + //This is safe because cancelOptionId is set, + //so the handler will never get called with "Cancel". + cancelOptionId = options.length; + } + + return Menu.raw(title, description, arrangedOptions, target, { + cancelOptionId, onCancel, optionStringifier + }); + }, + /** Rejects with a CommandError if the user chooses to cancel. */ + confirm(target:FishPlayer, description:string, { + cancelOutput = "Cancelled.", + title = "Confirm", + confirmText = "[green]Confirm", + cancelText = "[red]Cancel", + }:MenuConfirmProps = {}){ + return Menu.menu( + title, description, [confirmText, cancelText], target, + { onCancel: "reject", cancelOptionId: 1 } + ).catch(e => { + if(e === "cancel") fail(cancelOutput); + throw e; //some random error, rethrow it + }); + }, + /** Same as confirm(), but with inverted colors, for potentially dangerous actions. */ + confirmDangerous(target:FishPlayer, description:string, { + confirmText = "[red]Confirm", + cancelText = "[green]Cancel", + ...rest + }:MenuConfirmProps = {}){ + return Menu.confirm(target, description, { cancelText, confirmText, ...rest }); + }, + buttons( + this:void, target:FishPlayer, title:string, description:string, + options:{ data: TButtonData; text: string; }[][], + cfg: Omit, "optionStringifier" | "columns"> = {}, + ){ + return Menu.raw(title, description, options, target, { + ...cfg, + optionStringifier: o => o.text, + }).then(o => o?.data as TButtonData | (TCancelBehavior extends "null" ? null : never)); + }, + pages( + this:void, target:FishPlayer, title:string, description:string, + options:{ data: TOption; text: string; }[][][], + cfg: Pick, "onCancel">, + ){ + const { promise, reject, resolve } = Promise.withResolvers< + (TCancelBehavior extends "null" ? null : never) | TOption, + TCancelBehavior extends "reject" ? "cancel" : never + >(); + function showPage(index:number){ + const opts:{ data: "left" | "numbers" | "right" | readonly [TOption]; text: string; }[][] = [ + ...options[index].map(r => r.map(d => ({ text: d.text, data: [d.data] as const }))), + [ + { data: "left", text: `[${index == 0 ? "gray" : "accent"}]<--` }, + { data: "numbers", text: `[accent]${index + 1}/${options.length}` }, + { data: "right", text: `[${index == options.length - 1 ? "gray" : "accent"}]-->` } + ] + ]; + Menu.buttons(target, title, description, opts, cfg).then(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 as never); + else if(cfg.onCancel == "reject") reject("cancel" as never); + //otherwise, just let the promise hang + } + }); + } + showPage(0); + return promise; + }, + pagedListButtons( + this:void, target:FishPlayer, title:string, description:string, + options:{ data: TButtonData; text: string; }[], + { rowsPerPage = 10, columns = 3, ...cfg }: Pick, "columns" | "onCancel"> & { + /** @default 10 */ + rowsPerPage?:number; + }, + ){ + //Generate pages + const pages = to2DArray(to2DArray(options, columns), rowsPerPage); + if(pages.length <= 1) return Menu.buttons(target, title, description, pages[0] ?? [], cfg); + return Menu.pages(target, title, description, pages, cfg); + }, + pagedList( + this:void, target:FishPlayer, title:string, description:string, + options:TButtonData[], + { rowsPerPage = 10, columns = 3, optionStringifier = String, ...cfg }: Pick, "columns" | "onCancel" | "optionStringifier"> & { + /** @default 10 */ + rowsPerPage?:number; + } = {}, + ){ + //Generate pages + const pages = to2DArray(to2DArray(options.map( + o => ({ data: o, get text(){ return optionStringifier(o); }}) + ), columns), rowsPerPage); + if(pages.length <= 1) return Menu.buttons(target, title, description, pages[0] ?? [], cfg); + return Menu.pages(target, title, description, pages, cfg); + } } -export { - registeredListeners as listeners, - menu -}; +export { registeredListeners as listeners }; diff --git a/src/playerCommands.ts b/src/playerCommands.ts index 7011281..f6815a1 100644 --- a/src/playerCommands.ts +++ b/src/playerCommands.ts @@ -7,7 +7,7 @@ import * as api from './api'; import { command, commandList, fail, formatArg, Perm, Req } from './commands'; import { FishServer, Gamemode, rules, text } from './config'; import { FishEvents, fishPlugin, fishState, ipPortPattern, recentWhispers, tileHistory, uuidPattern } from './globals'; -import { menu } from './menus'; +import { Menu } from './menus'; import { FishPlayer } from './players'; import { Rank, RoleFlag } from './ranks'; import type { FishCommandData } from './types'; @@ -540,13 +540,12 @@ Available types:[yellow] if(!sender.hasPerm("warn")) fail(`You do not have permission to show rules to other players.`); if(target.hasPerm("blockTrolling")) fail(f`Player ${args.player!} is insufficiently trollable.`); } - menu( + Menu.menu( "Rules for [#0000ff]>|||> FISH [white]servers", rules.join("\n\n"), ["[green]I agree to abide by these rules[]", "No"], target, - ({option}) => { - if(option == "No") target.kick("You must agree to the rules to play on this server. Rejoin to agree to the rules.", 1); - }, false - ); + ).then((option) => { + if(option == "No") target.kick("You must agree to the rules to play on this server. Rejoin to agree to the rules.", 1); + }); if(target !== sender) outputSuccess(f`Reminded ${target} of the rules.`); }, }, @@ -561,7 +560,7 @@ Available types:[yellow] if(Date.now() - lastUsedSuccessfullySender < 20000) fail(`This command was used recently and is on cooldown.`); if(!sender.hasPerm("trusted")) 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")) fail(`Target player is insufficiently trollable.`); - menu("\uf83f [scarlet]WARNING[] \uf83f", + Menu.menu("\uf83f [scarlet]WARNING[] \uf83f", `[white]Don't break the Power Void (\uf83f), it's a trap! Power voids disable anything they are connected to. If you break it, [scarlet]you will get attacked[] by enemy units. @@ -641,27 +640,26 @@ Please stop attacking and [lime]build defenses[] first!` .on("player vote removed", (t, player) => Call.sendMessage(`VNW: ${player.name} [white] has left. [green]${t.currentVotes()}[white] votes, [green]${t.requiredVotes()}[white] required.`)) }), requirements: [Req.cooldown(3000), Req.mode("survival"), Req.gameRunning], - handler({sender, data:{manager}}){ + async handler({sender, data:{manager}}){ - if(!manager.session){ - menu( + if(!manager.session as boolean){ //Disable narrowing + const option = await Menu.menu( "Start a Next Wave Vote", "Select the amount of waves you would like to skip.", [1, 5, 10], sender, - ({option}) => { - if(manager.session){ - //Someone else started a vote - if(manager.session.data != option) fail(`Someone else started a vote with a different number of waves to skip.`); - else manager.vote(sender, sender.voteWeight(), option); - } else { - //this is still a race condition technically... shouldn't be that bad right? - manager.start(sender, sender.voteWeight(), option); - } - }, - true, - n => `${n} waves` - ); + { + includeCancel: true, + optionStringifier: n => `${n} waves` + } + ) + if(manager.session){ + //Someone else started a vote + if(manager.session.data != option) fail(`Someone else started a vote with a different number of waves to skip.`); + else manager.vote(sender, sender.voteWeight(), option); + } else { + manager.start(sender, sender.voteWeight(), option); + } } else { manager.vote(sender, sender.voteWeight(), null); } diff --git a/src/players.ts b/src/players.ts index d076666..20f79ac 100644 --- a/src/players.ts +++ b/src/players.ts @@ -8,7 +8,7 @@ import { Perm, PermType } from "./commands"; import * as globals from "./globals"; import { FColor, Gamemode, heuristics, Mode, prefixes, rules, stopAntiEvadeTime, text, tips } from "./config"; import { uuidPattern } from "./globals"; -import { menu } from "./menus"; +import { Menu } from "./menus"; import { Rank, RankName, RoleFlag, RoleFlagName } from "./ranks"; import type { FishCommandArgType, FishPlayerData, PlayerHistoryEntry } from "./types"; import { cleanText, formatTime, formatTimeRelative, isImpersonator, logAction, logHTrip, matchFilter } from "./utils"; @@ -53,10 +53,10 @@ export class FishPlayer { player:mindustryPlayer | null = null; pet:string = ""; watch:boolean = false; - activeMenu: { - cancelOptionId: number; - callback?: (sender:FishPlayer, option:number) => void; - } = {cancelOptionId: -1}; + /** Front-to-back queue of menus to show. */ + activeMenus: { + callback: (option:number) => void; + }[] = []; tileId = false; tilelog:null | "once" | "persist" = null; trail: { @@ -320,7 +320,12 @@ export class FishPlayer { fishPlayer.updateName(); }); //I think this is a better spot for this - if(fishPlayer.firstJoin()) 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); + if(fishPlayer.firstJoin()) 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 + ); } } @@ -377,7 +382,7 @@ export class FishPlayer { } } //Clear temporary states such as menu and taphandler - fishP.activeMenu.callback = undefined; + fishP.activeMenus = []; fishP.tapInfo.commandName = null; fishP.stats.timeInGame += (Date.now() - fishP.lastJoined); //Time between joining and leaving fishP.lastJoined = Date.now(); @@ -449,7 +454,7 @@ export class FishPlayer { static onGameOver(winningTeam:Team){ this.forEachPlayer((fishPlayer) => { //Clear temporary states such as menu and taphandler - fishPlayer.activeMenu.callback = undefined; + fishPlayer.activeMenus = []; fishPlayer.tapInfo.commandName = null; //Update stats if(!this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict){ @@ -608,22 +613,19 @@ Previously used UUID \`${uuid}\`(${Vars.netServer.admins.getInfoOptional(uuid)?. api.sendStaffMessage(`Autoflagged player ${this.name}[cyan] for suspected vpn!`, "AntiVPN"); 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.`); - menu( + 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.`, - ["Close", "Discord"], - this, - ({option, sender}) => { - if(option == "Discord"){ - Call.openURI(sender.con, text.discordURL); - } - }, - false, - str => ({ - "Close": "Close", - "Discord": FColor.discord("Discord") - }[str]) - ); + [[ + { 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){ diff --git a/src/staffCommands.ts b/src/staffCommands.ts index 16c8091..e884ec7 100644 --- a/src/staffCommands.ts +++ b/src/staffCommands.ts @@ -10,7 +10,7 @@ import { maxTime } from "./globals"; import { updateMaps } from "./files"; import * as fjsContext from "./fjsContext"; import { fishState, ipPattern, uuidPattern } from "./globals"; -import { menu } from './menus'; +import { Menu } from './menus'; import { FishPlayer } from "./players"; import { Rank } from "./ranks"; import { addToTileHistory, colorBadBoolean, formatTime, formatTimeRelative, getAntiBotInfo, logAction, match, serverRestartLoop, untilForever, updateBans } from "./utils"; @@ -32,7 +32,7 @@ export const commands = commandList({ if(args.player.hasPerm("blockTrolling")) fail(f`Player ${args.player} is insufficiently trollable.`); const message = args.message ?? "You have been warned. I suggest you stop what you're doing"; - menu('Warning', message, ["[green]Accept"], args.player); + Menu.menu('Warning', message, ["[green]Accept"], args.player); logAction('warned', sender, args.player, message); outputSuccess(f`Warned player ${args.player} for "${message}"`); } @@ -175,7 +175,7 @@ export const commands = commandList({ args: ["time:time?", "name:string?"], description: "Stops an offline player.", perm: Perm.mod, - handler({args, sender, outputFail, outputSuccess, f, admins}){ + async handler({args, sender, outputFail, outputSuccess, f, admins}){ const maxPlayers = 60; function stop(option:PlayerInfo, time:number){ @@ -223,20 +223,20 @@ export const commands = commandList({ } - menu("Stop", "Choose a player to mark", possiblePlayers, sender, ({option: optionPlayer, sender}) => { - if(args.time == null){ - menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, ({option: optionTime, sender}) => { - const time = - optionTime == "2 days" ? 172800000 : - optionTime == "7 days" ? 604800000 : - optionTime == "30 days" ? 2592000000 : - (maxTime - Date.now() - 10000); - stop(optionPlayer, time); - }, false); - } else { - stop(optionPlayer, args.time); + const optionPlayer = await Menu.menu("Stop", "Choose a player to mark", possiblePlayers, sender, { + includeCancel: true, + optionStringifier: p => p.lastName + }); + args.time ??= match( + await Menu.menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender), + { + "2 days": 172800000, + "7 days": 604800000, + "30 days": 2592000000, + "forever": maxTime - Date.now() - 10000, } - }, true, p => p.lastName); + ); + stop(optionPlayer, args.time); } }, @@ -244,29 +244,24 @@ export const commands = commandList({ args: ["name:string?"], description: "Mutes an offline player.", perm: Perm.mod, - handler({args, sender, outputSuccess, f, admins}){ - const maxPlayers = 60; + async handler({args, sender, outputSuccess, f, admins}){ + const maxPlayers = 300; - function mute(option:PlayerInfo){ + async function mute(option:PlayerInfo){ const fishP = FishPlayer.getFromInfo(option); if(!sender.canModerate(fishP, true)) fail(`You do not have permission to mute this player.`); - menu( - "Mute Offine Confirmation", - `Are you sure you want to ${fishP.muted ? "unmute" : "mute"} player ${option.lastName}?`, - [true, false], - sender, (res) => { - if(res.option){ - logAction(fishP.muted ? "unmuted" : "muted", sender, fishP); - if(fishP.muted) fishP.unmute(sender) - else fishP.mute(sender); - outputSuccess(`${fishP.muted ? "Muted" : "Unmuted"} ${option.lastName}.`); - } - }, false, opt => opt ? `[green]Yes, ${fishP.muted ? "unmute" : "mute"} them` : `[red]Cancel`); + await Menu.confirm(sender, `Are you sure you want to ${fishP.muted ? "unmute" : "mute"} player ${option.lastName}?`, { + title: "Mute Offine Confirmation", + confirmText: `[green]Yes, ${fishP.muted ? "unmute" : "mute"} them`, + }); + logAction(fishP.muted ? "unmuted" : "muted", sender, fishP); + if(fishP.muted) fishP.unmute(sender) + else fishP.mute(sender); + outputSuccess(`${fishP.muted ? "Muted" : "Unmuted"} ${option.lastName}.`); } if(args.name && uuidPattern.test(args.name)){ - const info:PlayerInfo | null = admins.getInfoOptional(args.name); - if(!info) fail(f`Unknown UUID ${args.name}`); + const info = admins.getInfoOptional(args.name) ?? fail(f`Unknown UUID ${args.name}`); mute(info); return; } @@ -295,9 +290,10 @@ export const commands = commandList({ } - menu("Mute", "Choose a player to mute", possiblePlayers, sender, ({option: optionPlayer}) => { - mute(optionPlayer); - }, true, p => p.lastName); + const option = await Menu.pagedList(sender, "Mute", "Choose a player to mute", possiblePlayers, { + optionStringifier: p => p.lastName + }); + mute(option); } }, @@ -418,68 +414,64 @@ export const commands = commandList({ args: ["uuid_or_ip:string?"], description: "Bans a player by UUID and IP.", perm: Perm.admin, - handler({args, sender, outputSuccess, f, admins}){ + async handler({args, sender, outputSuccess, f, admins}){ if(args.uuid_or_ip && uuidPattern.test(args.uuid_or_ip)){ //Overload 1: ban by uuid const uuid = args.uuid_or_ip; let data:PlayerInfo | null; if((data = admins.getInfoOptional(uuid)) != null && data.admin) fail(`Cannot ban an admin.`); const name = data ? `${escapeStringColorsClient(data.lastName)} (${uuid}/${data.lastIP})` : uuid; - menu("Confirm", `Are you sure you want to ban ${name}?`, ["[red]Yes", "[green]Cancel"], sender, ({option:confirm}) => { - if(confirm != "[red]Yes") fail("Cancelled."); - admins.banPlayerID(uuid); - if(data){ - const ip = data.lastIP; - admins.banPlayerIP(ip); - api.ban({ip, uuid}); - Log.info(`${uuid}/${ip} was banned.`); - logAction("banned", sender, data); - outputSuccess(f`Banned player ${escapeStringColorsClient(data.lastName)} (${uuid}/${ip})`); - //TODO add way to specify whether to activate or escape color tags - } else { - api.ban({uuid}); - Log.info(`${uuid} was banned.`); - logAction("banned", sender, uuid); - outputSuccess(f`Banned player ${uuid}. [yellow]Unable to determine IP.[]`); - } - updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); - }, false); + await Menu.confirmDangerous(sender, `Are you sure you want to ban ${name}?`); + admins.banPlayerID(uuid); + if(data){ + const ip = data.lastIP; + admins.banPlayerIP(ip); + api.ban({ip, uuid}); + Log.info(`${uuid}/${ip} was banned.`); + logAction("banned", sender, data); + outputSuccess(f`Banned player ${escapeStringColorsClient(data.lastName)} (${uuid}/${ip})`); + //TODO add way to specify whether to activate or escape color tags + } else { + api.ban({uuid}); + Log.info(`${uuid} was banned.`); + logAction("banned", sender, uuid); + outputSuccess(f`Banned player ${uuid}. [yellow]Unable to determine IP.[]`); + } + updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); return; } else if(args.uuid_or_ip && ipPattern.test(args.uuid_or_ip)){ //Overload 2: ban by uuid const ip = args.uuid_or_ip; - menu("Confirm", `Are you sure you want to ban IP ${ip}?`, ["[red]Yes", "[green]Cancel"], sender, ({option:confirm}) => { - if(confirm != "[red]Yes") fail("Cancelled."); + await Menu.confirmDangerous(sender, `Are you sure you want to ban IP ${ip}?`); - api.ban({ip}); - const info = admins.findByIP(ip); - if(info) logAction("banned", sender, info); - else logAction(`banned ${ip}`, sender); + api.ban({ip}); + const info = admins.findByIP(ip); + if(info) logAction("banned", sender, info); + else logAction(`banned ${ip}`, sender); - const alreadyBanned = admins.banPlayerIP(ip); - if(alreadyBanned){ - outputSuccess(f`IP ${ip} is already banned. Ban was synced to other servers.`); - } else { - outputSuccess(f`IP ${ip} has been banned. Ban was synced to other servers.`); - } - - updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); - }, false); + const alreadyBanned = admins.banPlayerIP(ip); + if(alreadyBanned){ + outputSuccess(f`IP ${ip} is already banned. Ban was synced to other servers.`); + } else { + outputSuccess(f`IP ${ip} has been banned. Ban was synced to other servers.`); + } + + updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); return; } //Overload 3: ban by menu - menu(`[scarlet]BAN[]`, "Choose a player to ban.", setToArray(Groups.player), sender, ({option}) => { - if(option.admin) fail(`Cannot ban an admin.`); - menu("Confirm", `Are you sure you want to ban ${option.name}?`, ["[red]Yes", "[green]Cancel"], sender, ({option:confirm}) => { - if(confirm != "[red]Yes") fail("Cancelled."); - admins.banPlayerIP(option.ip()); //this also bans the UUID - api.ban({ip: option.ip(), uuid: option.uuid()}); - Log.info(`${option.ip()}/${option.uuid()} was banned.`); - logAction("banned", sender, option.getInfo()); - outputSuccess(f`Banned player ${option}.`); - updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); - }, false); - }, true, opt => opt.name); + const option = await Menu.menu(`[scarlet]BAN[]`, "Choose a player to ban.", setToArray(Groups.player), sender, { + includeCancel: true, + optionStringifier: opt => opt.name + }); + if(option.admin) fail(`Cannot ban an admin.`); + await Menu.confirmDangerous(sender, `Are you sure you want to ban ${option.name}?`); + admins.banPlayerIP(option.ip()); //this also bans the UUID + api.ban({ip: option.ip(), uuid: option.uuid()}); + Log.info(`${option.ip()}/${option.uuid()} was banned.`); + logAction("banned", sender, option.getInfo()); + outputSuccess(f`Banned player ${option}.`); + updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); } }, @@ -503,53 +495,41 @@ export const commands = commandList({ args: ["team:team?", "unit:unittype?"], description: "Kills all units, optionally specifying a team and unit type.", perm: Perm.massKill, - handler({args:{team, unit}, sender, outputSuccess, outputFail, f}){ + async handler({args:{team, unit}, sender, outputSuccess, outputFail, f}){ if(team){ - menu( - `Confirm`, + await Menu.confirmDangerous(sender, `This will kill [scarlet]every ${unit ? unit.localizedName : "unit"}[] on the team ${team.coloredName()}.`, - ["[orange]Kill units[]", "[green]Cancel[]"], - sender, - ({option}) => { - if(option == "[orange]Kill units[]"){ - if(unit){ - let i = 0; - team.data().units.each(u => u.type == unit, u => { - u.kill(); - i ++; - }); - outputSuccess(f`Killed ${i} units on ${team}.`); - } else { - const before = team.data().units.size; - team.data().units.each(u => u.kill()); - outputSuccess(f`Killed ${before} units on ${team}.`); - } - } else outputFail(`Cancelled.`); - }, false + { confirmText: "[orange]Kill units[]" }, ); + if(unit){ + let i = 0; + team.data().units.each(u => u.type == unit, u => { + u.kill(); + i ++; + }); + outputSuccess(f`Killed ${i} units on ${team}.`); + } else { + const before = team.data().units.size; + team.data().units.each(u => u.kill()); + outputSuccess(f`Killed ${before} units on ${team}.`); + } } else { - menu( - `Confirm`, + await Menu.confirmDangerous(sender, `This will kill [scarlet]every single ${unit ? unit.localizedName : "unit"}[].`, - ["[orange]Kill all units[]", "[green]Cancel[]"], - sender, - ({option}) => { - if(option == "[orange]Kill all units[]"){ - if(unit){ - let i = 0; - Groups.unit.each(u => u.type == unit, u => { - u.kill(); - i ++; - }); - outputSuccess(f`Killed ${i} units.`); - } else { - const before = Groups.unit.size(); - Groups.unit.each(u => u.kill()); - outputSuccess(f`Killed ${before} units.`); - } - } else outputFail(`Cancelled.`); - }, false + { confirmText: "[orange]Kill all units[]" }, ); + if(unit){ + let i = 0; + Groups.unit.each(u => u.type == unit, u => { + u.kill(); + i ++; + }); + outputSuccess(f`Killed ${i} units.`); + } else { + const before = Groups.unit.size(); + Groups.unit.each(u => u.kill()); + outputSuccess(f`Killed ${before} units.`); + } } } }, @@ -557,35 +537,23 @@ export const commands = commandList({ args: ["team:team?"], description: "Kills all buildings (except cores), optionally specifying a team.", perm: Perm.massKill, - handler({args:{team}, sender, outputSuccess, outputFail, f}){ + async handler({args:{team}, sender, outputSuccess, outputFail, f}){ if(team){ - menu( - `Confirm`, + await Menu.confirmDangerous(sender, `This will kill [scarlet]every building[] on the team ${team.coloredName()}, except cores.`, - ["[orange]Kill buildings[]", "[green]Cancel[]"], - sender, - ({option}) => { - if(option == "[orange]Kill buildings[]"){ - const count = team.data().buildings.size; - team.data().buildings.each(b => !(b.block instanceof CoreBlock), b => b.tile.remove()); - outputSuccess(f`Killed ${count} buildings on ${team}`); - } else outputFail(`Cancelled.`); - }, false + { confirmText: "[orange]Kill buildings[]" }, ); + const count = team.data().buildings.size; + team.data().buildings.each(b => !(b.block instanceof CoreBlock), b => b.tile.remove()); + outputSuccess(f`Killed ${count} buildings on ${team}.`); } else { - menu( - `Confirm`, + await Menu.confirmDangerous(sender, `This will kill [scarlet]every building[] except cores.`, - ["[orange]Kill buildings[]", "[green]Cancel[]"], - sender, - ({option}) => { - if(option == "[orange]Kill buildings[]"){ - const count = Groups.build.size(); - Groups.build.each(b => !(b.block instanceof CoreBlock), b => b.tile.remove()); - outputSuccess(f`Killed ${count} buildings.`); - } else outputFail(`Cancelled.`); - }, false + { confirmText: "[orange]Kill buildings[]" }, ); + const count = Groups.build.size(); + Groups.build.each(b => !(b.block instanceof CoreBlock), b => b.tile.remove()); + outputSuccess(f`Killed ${count} buildings.`); } } }, @@ -895,7 +863,7 @@ ${getAntiBotInfo("client")}` args: ["input:string"], description: "Searches playerinfo by name, IP, or UUID.", perm: Perm.admin, - handler({args:{input}, admins, output, f, sender}){ + async handler({args:{input}, admins, output, f, sender}){ if(uuidPattern.test(input)){ const fishP = FishPlayer.getById(input); const info = admins.getInfoOptional(input); @@ -931,8 +899,9 @@ Last name used: "${info.plainLastName()}" [gray](${escapeStringColorsClient(info IPs used: ${info.ips.map(i => `[blue]${i}[]`).toString(", ")}` )); }; - if(matches.size > 20) menu("Confirm", `Are you sure you want to view all ${matches.size} matches?`, ["Yes"], sender, displayMatches); - else displayMatches(); + if(matches.size > 20) + await Menu.confirm(sender, `Are you sure you want to view all ${matches.size} matches?`); + displayMatches(); } } }, diff --git a/src/types.ts b/src/types.ts index 8517b53..2897344 100644 --- a/src/types.ts +++ b/src/types.ts @@ -120,7 +120,7 @@ export type FishCommandHandlerUtils = { handleTaps(mode:TapHandleMode):void; }; export type FishCommandHandler = - (fish:FishCommandHandlerData & FishCommandHandlerUtils) => unknown; + (fish:FishCommandHandlerData & FishCommandHandlerUtils) => void | Promise; export interface FishConsoleCommandRunner { (_:{