From 92da624c33f6f1703c6fd014bd40911ed7c3a5af Mon Sep 17 00:00:00 2001
From: BalaM314 <71201189+BalaM314@users.noreply.github.com>
Date: Sun, 26 Jan 2025 13:04:02 +0530
Subject: [PATCH 1/7] remove unused code from menu handler

---
 build/scripts/commands.js       |  3 +--
 build/scripts/menus.js          | 15 +++++----------
 build/scripts/playerCommands.js |  6 ++----
 build/scripts/players.js        |  5 ++---
 build/scripts/staffCommands.js  | 30 ++++++++++-------------------
 src/commands.ts                 |  2 +-
 src/menus.ts                    | 34 +++++++++++----------------------
 src/playerCommands.ts           |  4 ++--
 src/players.ts                  |  4 ++--
 src/staffCommands.ts            | 20 +++++++++----------
 10 files changed, 46 insertions(+), 77 deletions(-)

diff --git a/build/scripts/commands.js b/build/scripts/commands.js
index ea7f9b1..dfea9ea 100644
--- a/build/scripts/commands.js
+++ b/build/scripts/commands.js
@@ -805,8 +805,7 @@ function resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback) {
                 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;
+        (0, menus_1.menu)("Select a player", "Select a player for the argument \"".concat(argToResolve_1.name, "\""), optionsList_1, sender, function (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); });
diff --git a/build/scripts/menus.js b/build/scripts/menus.js
index a274f5b..8e38f49 100644
--- a/build/scripts/menus.js
+++ b/build/scripts/menus.js
@@ -86,13 +86,13 @@ columns) {
     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 ? [["<no options>"]] : (0, funcs_2.to2DArray)(options.map(optionStringifier), columns));
+        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 "<no options>" as a fallback, because Call.menu with an empty array of options causes a client crash
-        var arrangedOptions = (options.length == 0 && !includeCancel) ? [["<no options>"]] : (0, funcs_2.to2DArray)(options.map(optionStringifier), columns);
+        //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.map(optionStringifier), columns);
         if (includeCancel) {
             arrangedOptions.push(["Cancel"]);
             target.activeMenu.cancelOptionId = options.length;
@@ -110,12 +110,7 @@ columns) {
             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); },
-                });
+                callback(options[option]);
             }
             catch (err) {
                 if (err instanceof commands_1.CommandError) {
diff --git a/build/scripts/playerCommands.js b/build/scripts/playerCommands.js
index 90a364d..d1b44f6 100644
--- a/build/scripts/playerCommands.js
+++ b/build/scripts/playerCommands.js
@@ -565,8 +565,7 @@ 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;
+            (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 (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);
@@ -667,8 +666,7 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: {
         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;
+                (0, menus_1.menu)("Start a Next Wave Vote", "Select the amount of waves you would like to skip.", [1, 5, 10], sender, function (option) {
                     if (manager.session) {
                         //Someone else started a vote
                         if (manager.session.data != option)
diff --git a/build/scripts/players.js b/build/scripts/players.js
index 0697957..a8c1675 100644
--- a/build/scripts/players.js
+++ b/build/scripts/players.js
@@ -676,10 +676,9 @@ 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;
+                        (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 (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",
diff --git a/build/scripts/staffCommands.js b/build/scripts/staffCommands.js
index ad61e72..57738ee 100644
--- a/build/scripts/staffCommands.js
+++ b/build/scripts/staffCommands.js
@@ -278,11 +278,9 @@ exports.commands = (0, commands_1.commandList)({
             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;
+            (0, menus_1.menu)("Stop", "Choose a player to mark", possiblePlayers, sender, function (optionPlayer) {
                 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;
+                    (0, menus_1.menu)("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, function (optionTime) {
                         var time = optionTime == "2 days" ? 172800000 :
                             optionTime == "7 days" ? 604800000 :
                                 optionTime == "30 days" ? 2592000000 :
@@ -480,8 +478,7 @@ exports.commands = (0, commands_1.commandList)({
                 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;
+                (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(name, "?"), ["[red]Yes", "[green]Cancel"], sender, function (confirm) {
                     if (confirm != "[red]Yes")
                         (0, commands_1.fail)("Cancelled.");
                     admins.banPlayerID(uuid_1);
@@ -507,8 +504,7 @@ exports.commands = (0, commands_1.commandList)({
             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;
+                (0, menus_1.menu)("Confirm", "Are you sure you want to ban IP ".concat(ip_1, "?"), ["[red]Yes", "[green]Cancel"], sender, function (confirm) {
                     if (confirm != "[red]Yes")
                         (0, commands_1.fail)("Cancelled.");
                     api.ban({ ip: ip_1 });
@@ -529,12 +525,10 @@ exports.commands = (0, commands_1.commandList)({
                 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;
+            (0, menus_1.menu)("[scarlet]BAN[]", "Choose a player to ban.", (0, funcs_5.setToArray)(Groups.player), sender, function (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;
+                (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(option.name, "?"), ["[red]Yes", "[green]Cancel"], sender, function (confirm) {
                     if (confirm != "[red]Yes")
                         (0, commands_1.fail)("Cancelled.");
                     admins.banPlayerIP(option.ip()); //this also bans the UUID
@@ -571,8 +565,7 @@ exports.commands = (0, commands_1.commandList)({
         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;
+                (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 (option) {
                     if (option == "[orange]Kill units[]") {
                         if (unit) {
                             var i_1 = 0;
@@ -593,8 +586,7 @@ exports.commands = (0, commands_1.commandList)({
                 }, 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;
+                (0, menus_1.menu)("Confirm", "This will kill [scarlet]every single ".concat(unit ? unit.localizedName : "unit", "[]."), ["[orange]Kill all units[]", "[green]Cancel[]"], sender, function (option) {
                     if (option == "[orange]Kill all units[]") {
                         if (unit) {
                             var i_2 = 0;
@@ -623,8 +615,7 @@ exports.commands = (0, commands_1.commandList)({
         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;
+                (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 (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(); });
@@ -635,8 +626,7 @@ exports.commands = (0, commands_1.commandList)({
                 }, 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;
+                (0, menus_1.menu)("Confirm", "This will kill [scarlet]every building[] except cores.", ["[orange]Kill buildings[]", "[green]Cancel[]"], sender, function (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(); });
diff --git a/src/commands.ts b/src/commands.ts
index 90808e5..18fe5bf 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -676,7 +676,7 @@ function resolveArgsRecursive(processedArgs: Record<string, FishCommandArgType>,
 			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}) => {
+		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))
diff --git a/src/menus.ts b/src/menus.ts
index 69eac78..68d0880 100644
--- a/src/menus.ts
+++ b/src/menus.ts
@@ -41,21 +41,17 @@ export function registerListeners(){
 }
 
 /** Displays a menu to a player. */
-function menu(title:string, description:string, options:string[], target:FishPlayer):void;
+export function menu(title:string, description:string, options:string[], target:FishPlayer):void;
 /** Displays a menu to a player with callback. */
-function menu<const T>(
+export function menu<const T>(
 	title:string, description:string, options:T[], target:FishPlayer,
-	callback: (opts: {
-		option:T, sender:FishPlayer, outputSuccess:(message:string) => void, outputFail:(message:string) => void;
-	}) => void,
+	callback: (option:T) => 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<T>(
+export function menu<T>(
 	title:string, description:string, options:T[], target:FishPlayer,
-	callback?: (opts: {
-		option:T, sender:FishPlayer, outputSuccess:(message:string) => void, outputFail:(message:string) => void;
-	}) => void,
+	callback?: (option:T) => void,
 	includeCancel:boolean = true,
 	optionStringifier:(opt:T) => string = t => t as unknown as string, //this is dubious
 	columns:number = 3,
@@ -63,13 +59,13 @@ function menu<T>(
 
 	if(!callback){
 		//overload 1, just display a menu with no callback
-		Call.menu(target.con, registeredListeners.none, title, description, options.length == 0 ? [["<no options>"]] : to2DArray(options.map(optionStringifier), columns));
+		Call.menu(target.con, registeredListeners.none, title, description, options.length == 0 ? [] : to2DArray(options.map(optionStringifier), columns));
 	} else {
 		//overload 2, display a menu with callback
 
-		//Set up the 2D array of options, and add cancel
-		//Use "<no options>" as a fallback, because Call.menu with an empty array of options causes a client crash
-		const arrangedOptions = (options.length == 0 && !includeCancel) ? [["<no options>"]] : to2DArray(options.map(optionStringifier), columns);
+		//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.map(optionStringifier), columns);
 		if(includeCancel){
 			arrangedOptions.push(["Cancel"]);
 			target.activeMenu.cancelOptionId = options.length;
@@ -87,12 +83,7 @@ function menu<T>(
 			//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),
-				});
+				callback(options[option]);
 			} catch(err){
 				if(err instanceof CommandError){
 					//If the error is a command error, then just outputFail
@@ -111,7 +102,4 @@ function menu<T>(
 
 }
 
-export {
-	registeredListeners as listeners,
-	menu
-};
+export { registeredListeners as listeners };
diff --git a/src/playerCommands.ts b/src/playerCommands.ts
index 7011281..6c4a18e 100644
--- a/src/playerCommands.ts
+++ b/src/playerCommands.ts
@@ -543,7 +543,7 @@ Available types:[yellow]
 			menu(
 				"Rules for [#0000ff]>|||> FISH [white]servers", rules.join("\n\n"),
 				["[green]I agree to abide by these rules[]", "No"], target,
-				({option}) => {
+				(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
 			);
@@ -649,7 +649,7 @@ Please stop attacking and [lime]build defenses[] first!`
 					"Select the amount of waves you would like to skip.",
 					[1, 5, 10],
 					sender,
-					({option}) => {
+					(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.`);
diff --git a/src/players.ts b/src/players.ts
index d076666..7e9a97a 100644
--- a/src/players.ts
+++ b/src/players.ts
@@ -613,9 +613,9 @@ Previously used UUID \`${uuid}\`(${Vars.netServer.admins.getInfoOptional(uuid)?.
 							`[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}) => {
+							(option) => {
 								if(option == "Discord"){
-									Call.openURI(sender.con, text.discordURL);
+									Call.openURI(this.con, text.discordURL);
 								}
 							},
 							false,
diff --git a/src/staffCommands.ts b/src/staffCommands.ts
index 16c8091..ff3d0d6 100644
--- a/src/staffCommands.ts
+++ b/src/staffCommands.ts
@@ -223,9 +223,9 @@ export const commands = commandList({
 			}
 
 
-			menu("Stop", "Choose a player to mark", possiblePlayers, sender, ({option: optionPlayer, sender}) => {
+			menu("Stop", "Choose a player to mark", possiblePlayers, sender, (optionPlayer) => {
 				if(args.time == null){
-					menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, ({option: optionTime, sender}) => {
+					menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, (optionTime) => {
 						const time =
 							optionTime == "2 days" ? 172800000 :
 							optionTime == "7 days" ? 604800000 :
@@ -425,7 +425,7 @@ export const commands = commandList({
 				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}) => {
+				menu("Confirm", `Are you sure you want to ban ${name}?`, ["[red]Yes", "[green]Cancel"], sender, (confirm) => {
 					if(confirm != "[red]Yes") fail("Cancelled.");
 					admins.banPlayerID(uuid);
 					if(data){
@@ -448,7 +448,7 @@ export const commands = commandList({
 			} 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}) => {
+				menu("Confirm", `Are you sure you want to ban IP ${ip}?`, ["[red]Yes", "[green]Cancel"], sender, (confirm) => {
 					if(confirm != "[red]Yes") fail("Cancelled.");
 
 					api.ban({ip});
@@ -468,9 +468,9 @@ export const commands = commandList({
 				return;
 			}
 			//Overload 3: ban by menu
-			menu(`[scarlet]BAN[]`, "Choose a player to ban.", setToArray(Groups.player), sender, ({option}) => {
+			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}) => {
+				menu("Confirm", `Are you sure you want to ban ${option.name}?`, ["[red]Yes", "[green]Cancel"], sender, (confirm) => {
 					if(confirm != "[red]Yes") fail("Cancelled.");
 					admins.banPlayerIP(option.ip()); //this also bans the UUID
 					api.ban({ip: option.ip(), uuid: option.uuid()});
@@ -510,7 +510,7 @@ export const commands = commandList({
 					`This will kill [scarlet]every ${unit ? unit.localizedName : "unit"}[] on the team ${team.coloredName()}.`,
 					["[orange]Kill units[]", "[green]Cancel[]"],
 					sender,
-					({option}) => {
+					(option) => {
 						if(option == "[orange]Kill units[]"){
 							if(unit){
 								let i = 0;
@@ -533,7 +533,7 @@ export const commands = commandList({
 					`This will kill [scarlet]every single ${unit ? unit.localizedName : "unit"}[].`,
 					["[orange]Kill all units[]", "[green]Cancel[]"],
 					sender,
-					({option}) => {
+					(option) => {
 						if(option == "[orange]Kill all units[]"){
 							if(unit){
 								let i = 0;
@@ -564,7 +564,7 @@ export const commands = commandList({
 					`This will kill [scarlet]every building[] on the team ${team.coloredName()}, except cores.`,
 					["[orange]Kill buildings[]", "[green]Cancel[]"],
 					sender,
-					({option}) => {
+					(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());
@@ -578,7 +578,7 @@ export const commands = commandList({
 					`This will kill [scarlet]every building[] except cores.`,
 					["[orange]Kill buildings[]", "[green]Cancel[]"],
 					sender,
-					({option}) => {
+					(option) => {
 						if(option == "[orange]Kill buildings[]"){
 							const count = Groups.build.size();
 							Groups.build.each(b => !(b.block instanceof CoreBlock), b => b.tile.remove());

From cdebabe432e5bae83b19364ab20680be6701c68e Mon Sep 17 00:00:00 2001
From: BalaM314 <71201189+BalaM314@users.noreply.github.com>
Date: Sun, 26 Jan 2025 15:44:54 +0530
Subject: [PATCH 2/7] create Menu.menu and Menu.confirm

---
 build/scripts/commands.js       |   9 +-
 build/scripts/menus.js          |  92 +++++--
 build/scripts/playerCommands.js |  90 +++++--
 build/scripts/players.js        |  14 +-
 build/scripts/staffCommands.js  | 456 ++++++++++++++++++--------------
 src/commands.ts                 |  11 +-
 src/menus.ts                    | 119 ++++++---
 src/playerCommands.ts           |  44 ++-
 src/players.ts                  |  33 ++-
 src/staffCommands.ts            | 234 ++++++++--------
 10 files changed, 640 insertions(+), 462 deletions(-)

diff --git a/build/scripts/commands.js b/build/scripts/commands.js
index dfea9ea..9381780 100644
--- a/build/scripts/commands.js
+++ b/build/scripts/commands.js
@@ -805,10 +805,15 @@ function resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback) {
                 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 (option) {
+        menus_1.Menu.menu("Select a player", "Select a player for the argument \"".concat(argToResolve_1.name, "\""), optionsList_1, sender, {
+            includeCancel: true,
+            optionStringifier: function (player) { return Strings.stripColors(player.name).length >= 3 ?
+                Strings.stripColors(player.name)
+                : (0, funcs_3.escapeStringColorsClient)(player.name); }
+        }).then(function (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 initialize() {
diff --git a/build/scripts/menus.js b/build/scripts/menus.js
index 8e38f49..515698d 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);
@@ -31,24 +53,23 @@ var __read = (this && this.__read) || function (o, n) {
     return ar;
 };
 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");
 /** 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;
+        //TODO replace with queue
         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
@@ -59,7 +80,7 @@ var listeners = (function (d) { return d; })({
     none: function (player, option) {
         //do nothing
     }
-});
+};
 /** Registers all listeners, should be called on server load. */
 function registerListeners() {
     var e_1, _a;
@@ -78,27 +99,17 @@ 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
+exports.Menu = {
+    /** Displays a menu to a player, returning a Promise. */
+    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;
+        var _h = promise_1.Promise.withResolvers(), promise = _h.promise, reject = _h.reject, resolve = _h.resolve;
         //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.map(optionStringifier), columns);
         if (includeCancel) {
             arrangedOptions.push(["Cancel"]);
-            target.activeMenu.cancelOptionId = options.length;
-        }
-        else {
-            target.activeMenu.cancelOptionId = -1;
+            cancelOptionId = options.length;
         }
         //The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered.
         target.activeMenu.callback = function (fishSender, option) {
@@ -106,11 +117,20 @@ columns) {
             //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(options[option]);
+                //We do need to validate option though, as it can be any number.
+                if (option === -1 || option === fishSender.activeMenu.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) {
@@ -127,5 +147,21 @@ columns) {
             }
         };
         Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions);
-    }
-}
+        return promise;
+    },
+    /** 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 this.confirm(target, description, __assign({ cancelText: cancelText, confirmText: confirmText }, rest));
+    },
+};
diff --git a/build/scripts/playerCommands.js b/build/scripts/playerCommands.js
index d1b44f6..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,10 +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 (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));
         },
@@ -586,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));
             }
@@ -664,25 +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 (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 a8c1675..db994f5 100644
--- a/build/scripts/players.js
+++ b/build/scripts/players.js
@@ -352,7 +352,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. */
@@ -676,14 +676,16 @@ 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 (option) {
+                        menus_1.Menu.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, {
+                            optionStringifier: function (str) { return ({
+                                "Close": "Close",
+                                "Discord": config_1.FColor.discord("Discord")
+                            }[str]); }
+                        }).then(function (option) {
                             if (option == "Discord") {
                                 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 57738ee..b15533d 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,70 +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 (optionPlayer) {
-                if (args.time == null) {
-                    (0, menus_1.menu)("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, function (optionTime) {
-                        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: {
@@ -305,7 +359,7 @@ exports.commands = (0, commands_1.commandList)({
                 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) {
+                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)
@@ -349,7 +403,7 @@ exports.commands = (0, commands_1.commandList)({
             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) {
+            menu("Mute", "Choose a player to mute", possiblePlayers, sender, function (_a) {
                 var optionPlayer = _a.option;
                 mute(optionPlayer);
             }, true, function (p) { return p.lastName; });
@@ -470,75 +524,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 (confirm) {
-                    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 (confirm) {
-                    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));
-                    }
-                    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));
+            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*/];
                     }
-                    (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 (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 (confirm) {
-                    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: {
@@ -563,49 +623,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 (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));
-                        }
+            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);
-            }
-            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 (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));
-                        }
-                    }
-                    else
-                        outputFail("Cancelled.");
-                }, false);
-            }
+                });
+            });
         }
     },
     killbuildings: {
@@ -613,29 +675,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 (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));
-                    }
-                    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 (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));
+            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);
-            }
+                });
+            });
         }
     },
     respawn: {
@@ -990,7 +1054,7 @@ exports.commands = (0, commands_1.commandList)({
                     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);
+                    menus_1.Menu.menu("Confirm", "Are you sure you want to view all ".concat(matches_1.size, " matches?"), ["Yes"], sender, { includeCancel: true }).then(displayMatches);
                 else
                     displayMatches();
             }
diff --git a/src/commands.ts b/src/commands.ts
index 18fe5bf..cdb6c11 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";
@@ -676,10 +676,15 @@ function resolveArgsRecursive(processedArgs: Record<string, FishCommandArgType>,
 			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) => {
+		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 ?
+				Strings.stripColors(player.name)
+			: escapeStringColorsClient(player.name)
+		}).then((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))
+		});
 
 	}
 
diff --git a/src/menus.ts b/src/menus.ts
index 68d0880..b7a9296 100644
--- a/src/menus.ts
+++ b/src/menus.ts
@@ -3,24 +3,21 @@ 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 { parseError } from './funcs';
+import { outputFail } from "./utils";
+import { crash, parseError } from './funcs';
 import { to2DArray } from './funcs';
+import { Promise } from "./promise";
 
 /** Stores a mapping from name to the numeric id of a listener that has been registered. */
-const registeredListeners:{
-	[index:string]: number;
-} = {};
+const registeredListeners: Record<string, number> = {};
 /** Stores all listeners in use by fish-commands. */
-const listeners = (
-	<T extends Record<string, (player:mindustryPlayer, option:number) => void>>(d:T) => d
-)({
+const listeners = {
 	generic(player, option){
 		const fishSender = FishPlayer.get(player);
-		if(option === -1 || option === fishSender.activeMenu.cancelOptionId) return;
 
+		//TODO replace with queue
 		const prevCallback = fishSender.activeMenu.callback;
 		fishSender.activeMenu.callback?.(fishSender, option);
 		//if the callback wasn't modified, then clear it
@@ -31,7 +28,7 @@ const listeners = (
 	none(player, option){
 		//do nothing
 	}
-});
+} satisfies Record<string, (player:mindustryPlayer, option:number) => void>;
 
 /** Registers all listeners, should be called on server load. */
 export function registerListeners(){
@@ -40,37 +37,48 @@ export function registerListeners(){
 	}
 }
 
-/** Displays a menu to a player. */
-export function menu(title:string, description:string, options:string[], target:FishPlayer):void;
-/** Displays a menu to a player with callback. */
-export function menu<const T>(
-	title:string, description:string, options:T[], target:FishPlayer,
-	callback: (option:T) => 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
-export function menu<T>(
-	title:string, description:string, options:T[], target:FishPlayer,
-	callback?: (option:T) => void,
-	includeCancel:boolean = true,
-	optionStringifier:(opt:T) => string = t => t as unknown as string, //this is dubious
-	columns:number = 3,
-){
+type MenuConfirmProps = {
+	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
+export const Menu = {
+	/** Displays a menu to a player, returning a Promise. */
+	menu<const TOption, TCancelBehavior extends "ignore" | "reject" | "null" = "ignore">(
+		this:void, title:string, description:string, options:TOption[], target:FishPlayer,
+		{
+			includeCancel = false,
+			optionStringifier = String,
+			columns = 3,
+			onCancel = "ignore" as never,
+			cancelOptionId = -1,
+		}:{
+			/** [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;
+		} = {}
+	){
+		const { promise, reject, resolve } = Promise.withResolvers<
+			(TCancelBehavior extends "null" ? null : never) | TOption,
+			TCancelBehavior extends "reject" ? "cancel" : never
+		>();
 
 		//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.map(optionStringifier), columns);
+
 		if(includeCancel){
 			arrangedOptions.push(["Cancel"]);
-			target.activeMenu.cancelOptionId = options.length;
-		} else {
-			target.activeMenu.cancelOptionId = -1;
+			cancelOptionId = options.length;
 		}
 	
 		//The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered.
@@ -79,11 +87,17 @@ export function menu<T>(
 			//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(options[option]);
+				//We do need to validate option though, as it can be any number.
+				if(option === -1 || option === fishSender.activeMenu.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
@@ -98,8 +112,31 @@ export function menu<T>(
 		};
 	
 		Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions);
-	}
-
+		return promise;
+	},
+	/** 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 this.confirm(target, description, { cancelText, confirmText, ...rest });
+	},
 }
 
 export { registeredListeners as listeners };
diff --git a/src/playerCommands.ts b/src/playerCommands.ts
index 6c4a18e..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 7e9a97a..ebcfa64 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";
@@ -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
+			);
 
 		}
 	}
@@ -608,22 +613,22 @@ 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.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. ${FColor.discord`Join our Discord`} to request a staff member come online if none are on.`,
 							["Close", "Discord"],
 							this,
-							(option) => {
-								if(option == "Discord"){
-									Call.openURI(this.con, text.discordURL);
-								}
-							},
-							false,
-							str => ({
-								"Close": "Close",
-								"Discord": FColor.discord("Discord")
-							}[str])
-						);
+							{
+								optionStringifier: str => ({
+									"Close": "Close",
+									"Discord": FColor.discord("Discord")
+								}[str])
+							}
+						).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 ff3d0d6..a7e194e 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, (optionPlayer) => {
-				if(args.time == null){
-					menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, (optionTime) => {
-						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);
 		}
 	},
 
@@ -418,68 +418,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, (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, (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, (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 +499,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 +541,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.`);
 			}
 		}
 	},
@@ -931,7 +903,13 @@ 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);
+				if(matches.size > 20) Menu.menu(
+					"Confirm",
+					`Are you sure you want to view all ${matches.size} matches?`,
+					["Yes"],
+					sender,
+					{ includeCancel: true }
+				).then(displayMatches);
 				else displayMatches();
 			}
 		}

From 61781affafcc9587cf56371bbbae13e72557e2bc Mon Sep 17 00:00:00 2001
From: BalaM314 <71201189+BalaM314@users.noreply.github.com>
Date: Sun, 26 Jan 2025 15:58:24 +0530
Subject: [PATCH 3/7] Use a queue for menus

---
 build/scripts/menus.js   | 82 ++++++++++++++++++++--------------------
 build/scripts/players.js |  7 ++--
 src/menus.ts             | 24 ++++++------
 src/players.ts           | 12 +++---
 4 files changed, 65 insertions(+), 60 deletions(-)

diff --git a/build/scripts/menus.js b/build/scripts/menus.js
index 515698d..08f7204 100644
--- a/build/scripts/menus.js
+++ b/build/scripts/menus.js
@@ -61,21 +61,21 @@ 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 = {
     generic: function (player, option) {
-        var _a, _b;
         var fishSender = players_1.FishPlayer.get(player);
-        //TODO replace with queue
-        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
@@ -112,40 +112,42 @@ exports.Menu = {
             cancelOptionId = options.length;
         }
         //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.
-            try {
-                //We do need to validate option though, as it can be any number.
-                if (option === -1 || option === fishSender.activeMenu.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;
+        //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 {
+                    //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]);
+                    }
                 }
-                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);
+                    }
                 }
-            }
-            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);
-                }
-            }
-        };
+            } });
         Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions);
         return promise;
     },
diff --git a/build/scripts/players.js b/build/scripts/players.js
index db994f5..e847d00 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;
@@ -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) {
diff --git a/src/menus.ts b/src/menus.ts
index b7a9296..f391bc5 100644
--- a/src/menus.ts
+++ b/src/menus.ts
@@ -6,10 +6,12 @@ This file contains the menu system.
 import { CommandError, fail } from "./commands";
 import { FishPlayer } from "./players";
 import { outputFail } from "./utils";
-import { crash, parseError } from './funcs';
+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: Record<string, number> = {};
 /** Stores all listeners in use by fish-commands. */
@@ -17,13 +19,11 @@ const listeners = {
 	generic(player, option){
 		const fishSender = FishPlayer.get(player);
 
-		//TODO replace with queue
-		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
@@ -82,7 +82,9 @@ export const Menu = {
 		}
 	
 		//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.
@@ -90,7 +92,7 @@ export const Menu = {
 	
 			try {
 				//We do need to validate option though, as it can be any number.
-				if(option === -1 || option === fishSender.activeMenu.cancelOptionId || !(option in options)){
+				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);
@@ -109,7 +111,7 @@ export const Menu = {
 					Log.err(err as Error);
 				}
 			}
-		};
+		}});
 	
 		Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions);
 		return promise;
diff --git a/src/players.ts b/src/players.ts
index ebcfa64..d2f9dfd 100644
--- a/src/players.ts
+++ b/src/players.ts
@@ -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: {
@@ -382,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();
@@ -454,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){

From 0218e805c0cfd2f40034d65942045ccc9bec54c2 Mon Sep 17 00:00:00 2001
From: BalaM314 <71201189+BalaM314@users.noreply.github.com>
Date: Sun, 26 Jan 2025 16:09:23 +0530
Subject: [PATCH 4/7] make Promise global

---
 build/scripts/main.js | 15 ++++++++-------
 src/fjsContext.ts     |  2 +-
 src/main.js           | 15 ++++++++-------
 3 files changed, 17 insertions(+), 15 deletions(-)

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/src/fjsContext.ts b/src/fjsContext.ts
index c6ae6a7..6cce3bf 100644
--- a/src/fjsContext.ts
+++ b/src/fjsContext.ts
@@ -26,7 +26,7 @@ const { FishPlayer } = players;
 const { Rank, RoleFlag } = ranks;
 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");

From e4b719178619c69302d4bad842aef06c79eb7845 Mon Sep 17 00:00:00 2001
From: BalaM314 <71201189+BalaM314@users.noreply.github.com>
Date: Sun, 26 Jan 2025 16:50:54 +0530
Subject: [PATCH 5/7] Use async for command processing

---
 build/scripts/commands.js | 212 ++++++++++++++++++++++++--------------
 src/commands.ts           |  21 ++--
 src/types.ts              |   2 +-
 3 files changed, 144 insertions(+), 91 deletions(-)

diff --git a/build/scripts/commands.js b/build/scripts/commands.js
index 9381780..6b31c9e 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);
-                        }
-                        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);
+                                return [3 /*break*/, 5];
+                            case 4:
+                                usageData.lastUsed = globalUsageData[name].lastUsed = Date.now();
+                                return [7 /*endfinally*/];
+                            case 5: return [2 /*return*/];
                         }
-                    }
-                    finally {
-                        usageData.lastUsed = globalUsageData[name].lastUsed = Date.now();
-                    }
-                });
+                    });
+                }); });
             } }));
         exports.allCommands[name] = data;
     };
@@ -791,30 +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));
-        }
-        menus_1.Menu.menu("Select a player", "Select a player for the argument \"".concat(argToResolve_1.name, "\""), optionsList_1, sender, {
-            includeCancel: true,
-            optionStringifier: function (player) { return Strings.stripColors(player.name).length >= 3 ?
-                Strings.stripColors(player.name)
-                : (0, funcs_3.escapeStringColorsClient)(player.name); }
-        }).then(function (option) {
-            processedArgs[argToResolve_1.name] = players_1.FishPlayer.get(option);
-            resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback);
+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 ?
+                                Strings.stripColors(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/src/commands.ts b/src/commands.ts
index cdb6c11..383fc3f 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -548,14 +548,14 @@ export function register(commands:Record<string, FishCommandData<string, any> |
 				}
 				
 				//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<string, any> & 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<string, FishCommandData<string, any> |
 									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<string, FishConsoleCommandData<s
 }
 
 /** 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: Record<string, FishCommandArgType>, unresolvedArgs:CommandArg[], sender:FishPlayer, callback:(args:Record<string, FishCommandArgType>) => void){
+async function resolveArgsRecursive(processedArgs: Record<string, FishCommandArgType>, unresolvedArgs:CommandArg[], sender:FishPlayer){
 	if(unresolvedArgs.length == 0){
-		callback(processedArgs);
+		return processedArgs;
 	} else {
 		const argToResolve = unresolvedArgs.shift()!;
 		let optionsList:mindustryPlayer[] = [];
@@ -676,18 +676,15 @@ function resolveArgsRecursive(processedArgs: Record<string, FishCommandArgType>,
 			case "player": Groups.player.each(player => optionsList.push(player)); break;
 			default: crash(`Unable to resolve arg of type ${argToResolve.type}`);
 		}
-		Menu.menu(`Select a player`, `Select a player for the argument "${argToResolve.name}"`, optionsList, sender, {
+		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 ?
 				Strings.stripColors(player.name)
 			: escapeStringColorsClient(player.name)
-		}).then((option) => {
-			processedArgs[argToResolve.name] = FishPlayer.get(option);
-			resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback);
 		});
-
+		processedArgs[argToResolve.name] = FishPlayer.get(option);
+		return await resolveArgsRecursive(processedArgs, unresolvedArgs, sender);
 	}
-
 }
 
 export function initialize(){
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<ArgType extends string, StoredData> =
-	(fish:FishCommandHandlerData<ArgType, StoredData> & FishCommandHandlerUtils) => unknown;
+	(fish:FishCommandHandlerData<ArgType, StoredData> & FishCommandHandlerUtils) => void | Promise<void>;
 
 export interface FishConsoleCommandRunner<ArgType extends string, StoredData> {
 	(_:{

From 79a8a89dc52e861f1357cb5e5f96e351e9aa8f4f Mon Sep 17 00:00:00 2001
From: BalaM314 <71201189+BalaM314@users.noreply.github.com>
Date: Mon, 10 Feb 2025 20:37:42 +0530
Subject: [PATCH 6/7] Multi-page menus

---
 build/scripts/fjsContext.js    |   2 +-
 build/scripts/menus.js         |  95 ++++++++++++++++++++---
 build/scripts/players.js       |  10 +--
 build/scripts/staffCommands.js | 128 ++++++++++++++++++-------------
 src/fjsContext.ts              |   2 +-
 src/menus.ts                   | 135 ++++++++++++++++++++++++++++-----
 src/players.ts                 |  15 ++--
 src/staffCommands.ts           |  40 +++++-----
 8 files changed, 304 insertions(+), 123 deletions(-)

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/menus.js b/build/scripts/menus.js
index 08f7204..6f1985a 100644
--- a/build/scripts/menus.js
+++ b/build/scripts/menus.js
@@ -52,6 +52,15 @@ 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 = exports.Menu = void 0;
 exports.registerListeners = registerListeners;
@@ -101,16 +110,9 @@ function registerListeners() {
 }
 exports.Menu = {
     /** Displays a menu to a player, returning a Promise. */
-    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;
-        var _h = promise_1.Promise.withResolvers(), promise = _h.promise, reject = _h.reject, resolve = _h.resolve;
-        //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.map(optionStringifier), columns);
-        if (includeCancel) {
-            arrangedOptions.push(["Cancel"]);
-            cancelOptionId = options.length;
-        }
+    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.
         //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.
@@ -120,6 +122,7 @@ exports.Menu = {
                 //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
@@ -148,9 +151,27 @@ exports.Menu = {
                     }
                 }
             } });
-        Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions);
+        Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions.map(function (r) { return r.map(optionStringifier); }));
         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(["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;
@@ -164,6 +185,56 @@ exports.Menu = {
     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 this.confirm(target, description, __assign({ cancelText: cancelText, confirmText: confirmText }, rest));
+        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 {
+                    //Treat numbers as cancel
+                    if (cfg.onCancel == "null")
+                        resolve(null);
+                    else if (cfg.onCancel == "reject")
+                        reject("cancel");
+                    //otherwise, just let the promise hang
+                }
+            });
+        }
+        showPage(0);
+        return promise;
+    },
+    pagedListButtons: function (target, title, description, options, _a) {
+        var _b = _a.rowsPerPage, rowsPerPage = _b === void 0 ? 10 : _b, _c = _a.columns, columns = _c === void 0 ? 3 : _c, 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, pages[0], cfg);
+        return exports.Menu.pages(target, title, description, pages, cfg);
+    },
+    pagedList: function (target, title, description, options, _a) {
+        var _b = _a.rowsPerPage, rowsPerPage = _b === void 0 ? 10 : _b, _c = _a.columns, columns = _c === void 0 ? 3 : _c, optionStringifier = _a.optionStringifier, 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, pages[0], cfg);
+        return exports.Menu.pages(target, title, description, pages, cfg);
+    }
 };
diff --git a/build/scripts/players.js b/build/scripts/players.js
index e847d00..0c0424e 100644
--- a/build/scripts/players.js
+++ b/build/scripts/players.js
@@ -677,12 +677,10 @@ 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."));
-                        menus_1.Menu.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, {
-                            optionStringifier: function (str) { return ({
-                                "Close": "Close",
-                                "Discord": config_1.FColor.discord("Discord")
-                            }[str]); }
-                        }).then(function (option) {
+                        menus_1.Menu.buttons(_this, "[gold]Welcome to Fish Community!", "[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on."), [[
+                                { data: "Close", text: "Close" },
+                                { data: "Discord", text: config_1.FColor.discord("Discord") },
+                            ]]).then(function (option) {
                             if (option == "Discord") {
                                 Call.openURI(_this.con, config_1.text.discordURL);
                             }
diff --git a/build/scripts/staffCommands.js b/build/scripts/staffCommands.js
index b15533d..a7383c5 100644
--- a/build/scripts/staffCommands.js
+++ b/build/scripts/staffCommands.js
@@ -349,64 +349,84 @@ exports.commands = (0, commands_1.commandList)({
         }
     },
     mute_offline: {
-        args: ["name:string?"],
+        args: ["name:uuid?"],
         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.");
-                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(); });
-            }
-            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) {
+                                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: {
diff --git a/src/fjsContext.ts b/src/fjsContext.ts
index 6cce3bf..0a3415d 100644
--- a/src/fjsContext.ts
+++ b/src/fjsContext.ts
@@ -24,7 +24,7 @@ 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 as typeof globalThis, utils); //global scope goes brrrrr, I'm sure this will not cause any bugs whatsoever
 
diff --git a/src/menus.ts b/src/menus.ts
index f391bc5..5928bee 100644
--- a/src/menus.ts
+++ b/src/menus.ts
@@ -44,21 +44,30 @@ type MenuConfirmProps = {
 	cancelText?: string;
 };
 
+type MenuCancelOption = "ignore" | "reject" | "null";
+type MenuOptions<TOption, TCancelBehavior extends MenuCancelOption> = {
+	/** [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;
+};
+
 export const Menu = {
 	/** Displays a menu to a player, returning a Promise. */
-	menu<const TOption, TCancelBehavior extends "ignore" | "reject" | "null" = "ignore">(
-		this:void, title:string, description:string, options:TOption[], target:FishPlayer,
+	raw<const TOption, TCancelBehavior extends MenuCancelOption = "ignore">(
+		this:void, title:string, description:string, arrangedOptions:TOption[][], target:FishPlayer,
 		{
-			includeCancel = false,
 			optionStringifier = String,
-			columns = 3,
 			onCancel = "ignore" as never,
 			cancelOptionId = -1,
 		}:{
-			/** [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"
@@ -71,15 +80,6 @@ export const Menu = {
 			(TCancelBehavior extends "null" ? null : never) | TOption,
 			TCancelBehavior extends "reject" ? "cancel" : never
 		>();
-
-		//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.map(optionStringifier), columns);
-
-		if(includeCancel){
-			arrangedOptions.push(["Cancel"]);
-			cancelOptionId = options.length;
-		}
 	
 		//The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered.
 		//If menu() is being called from a menu calback, add it to the front of the queue so it is processed before any other menus.
@@ -91,6 +91,7 @@ export const Menu = {
 			//Additionally, the callback is cleared by the generic menu listener after it is executed.
 	
 			try {
+				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
@@ -113,9 +114,35 @@ export const Menu = {
 			}
 		}});
 	
-		Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions);
+		Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions.map(r => r.map(optionStringifier)));
 		return promise;
 	},
+	/** Displays a menu to a player, returning a Promise. Arranges options into a 2D array, and can add a Cancel option. */
+	menu<const TOption, TCancelBehavior extends MenuCancelOption = "ignore">(
+		this:void, title:string, description:string, options:TOption[], target:FishPlayer,
+		{
+			includeCancel = false,
+			optionStringifier = String,
+			columns = 3,
+			onCancel = "ignore" as never,
+			cancelOptionId = -1,
+		}:MenuOptions<TOption, TCancelBehavior> = {}
+	){
+		//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(["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.",
@@ -137,8 +164,80 @@ export const Menu = {
 		cancelText = "[green]Cancel",
 		...rest
 	}:MenuConfirmProps = {}){
-		return this.confirm(target, description, { cancelText, confirmText, ...rest });
+		return Menu.confirm(target, description, { cancelText, confirmText, ...rest });
+	},
+	buttons<TButtonData extends unknown, TCancelBehavior extends MenuCancelOption>(
+		this:void, target:FishPlayer, title:string, description:string,
+		options:{ data: TButtonData; text: string; }[][],
+		cfg: Omit<MenuOptions<TButtonData, TCancelBehavior>, "optionStringifier" | "columns"> = {},
+	){
+		return Menu.raw(title, description, options, target, {
+			...cfg,
+			optionStringifier: o => o.text,
+		}).then(o => o?.data);
+	},
+	pages<TOption extends unknown, TCancelBehavior extends MenuCancelOption>(
+		this:void, target:FishPlayer, title:string, description:string,
+		options:{ data: TOption; text: string; }[][][],
+		cfg: Pick<MenuOptions<TOption, TCancelBehavior>, "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<unknown, never>(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<TButtonData extends unknown, MenuCancelBehavior extends MenuCancelOption>(
+		this:void, target:FishPlayer, title:string, description:string,
+		options:{ data: TButtonData; text: string; }[],
+		{ rowsPerPage = 10, columns = 3, ...cfg }: Pick<MenuOptions<TButtonData, MenuCancelBehavior>, "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<TButtonData extends unknown, MenuCancelBehavior extends MenuCancelOption>(
+		this:void, target:FishPlayer, title:string, description:string,
+		options:TButtonData[],
+		{ rowsPerPage = 10, columns = 3, optionStringifier, ...cfg }: Pick<MenuOptions<TButtonData, MenuCancelBehavior>, "columns" | "onCancel" | "optionStringifier"> & {
+			/** @default 10 */
+			rowsPerPage?:number;
+			optionStringifier: {};
+		},
+	){
+		//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 };
diff --git a/src/players.ts b/src/players.ts
index d2f9dfd..20f79ac 100644
--- a/src/players.ts
+++ b/src/players.ts
@@ -613,17 +613,14 @@ 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(
+						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,
-							{
-								optionStringifier: 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);
diff --git a/src/staffCommands.ts b/src/staffCommands.ts
index a7e194e..6766066 100644
--- a/src/staffCommands.ts
+++ b/src/staffCommands.ts
@@ -241,32 +241,27 @@ export const commands = commandList({
 	},
 
 	mute_offline: {
-		args: ["name:string?"],
+		args: ["name:uuid?"],
 		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}`);
+			if(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);
 		}
 	},
 

From 4a973965370654275b67bf00360f674ffcb26f61 Mon Sep 17 00:00:00 2001
From: BalaM314 <71201189+BalaM314@users.noreply.github.com>
Date: Mon, 10 Feb 2025 21:03:31 +0530
Subject: [PATCH 7/7] Bugfixes

---
 build/scripts/commands.js      |  2 +-
 build/scripts/menus.js         | 26 +++++++----
 build/scripts/staffCommands.js | 83 ++++++++++++++++++++--------------
 src/commands.ts                |  2 +-
 src/menus.ts                   | 28 +++++++-----
 src/staffCommands.ts           | 17 +++----
 6 files changed, 91 insertions(+), 67 deletions(-)

diff --git a/build/scripts/commands.js b/build/scripts/commands.js
index 6b31c9e..58aa069 100644
--- a/build/scripts/commands.js
+++ b/build/scripts/commands.js
@@ -860,7 +860,7 @@ function resolveArgsRecursive(processedArgs, unresolvedArgs, sender) {
                     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 ?
-                                Strings.stripColors(player.name)
+                                player.name
                                 : (0, funcs_3.escapeStringColorsClient)(player.name); }
                         })];
                 case 2:
diff --git a/build/scripts/menus.js b/build/scripts/menus.js
index 6f1985a..349e78d 100644
--- a/build/scripts/menus.js
+++ b/build/scripts/menus.js
@@ -151,7 +151,14 @@ exports.Menu = {
                     }
                 }
             } });
-        Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions.map(function (r) { return r.map(optionStringifier); }));
+        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. */
@@ -161,7 +168,7 @@ exports.Menu = {
         //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(["Cancel"]);
+            arrangedOptions.push(["[red]Cancel[]"]);
             //This is safe because cancelOptionId is set,
             //so the handler will never get called with "Cancel".
             cancelOptionId = options.length;
@@ -222,19 +229,22 @@ exports.Menu = {
         return promise;
     },
     pagedListButtons: function (target, title, description, options, _a) {
-        var _b = _a.rowsPerPage, rowsPerPage = _b === void 0 ? 10 : _b, _c = _a.columns, columns = _c === void 0 ? 3 : _c, cfg = __rest(_a, ["rowsPerPage", "columns"]);
+        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, pages[0], cfg);
+        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 = _a.rowsPerPage, rowsPerPage = _b === void 0 ? 10 : _b, _c = _a.columns, columns = _c === void 0 ? 3 : _c, optionStringifier = _a.optionStringifier, cfg = __rest(_a, ["rowsPerPage", "columns", "optionStringifier"]);
+        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, pages[0], cfg);
+        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/staffCommands.js b/build/scripts/staffCommands.js
index a7383c5..b687ade 100644
--- a/build/scripts/staffCommands.js
+++ b/build/scripts/staffCommands.js
@@ -349,7 +349,7 @@ exports.commands = (0, commands_1.commandList)({
         }
     },
     mute_offline: {
-        args: ["name:uuid?"],
+        args: ["name:string?"],
         description: "Mutes an offline player.",
         perm: commands_1.Perm.mod,
         handler: function (_a) {
@@ -387,7 +387,7 @@ exports.commands = (0, commands_1.commandList)({
                     switch (_d.label) {
                         case 0:
                             maxPlayers = 300;
-                            if (args.name) {
+                            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*/];
@@ -1045,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)
-                    menus_1.Menu.menu("Confirm", "Are you sure you want to view all ".concat(matches_1.size, " matches?"), ["Yes"], sender, { includeCancel: true }).then(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 383fc3f..f0ee838 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -679,7 +679,7 @@ async function resolveArgsRecursive(processedArgs: Record<string, FishCommandArg
 		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 ?
-				Strings.stripColors(player.name)
+				player.name
 			: escapeStringColorsClient(player.name)
 		});
 		processedArgs[argToResolve.name] = FishPlayer.get(option);
diff --git a/src/menus.ts b/src/menus.ts
index 5928bee..a0ac961 100644
--- a/src/menus.ts
+++ b/src/menus.ts
@@ -38,6 +38,7 @@ export function registerListeners(){
 }
 
 type MenuConfirmProps = {
+	/** This message is sent to the user (prefixed with /!\) if they cancel. */
 	cancelOutput?: string;
 	title?: string;
 	confirmText?: string;
@@ -69,7 +70,7 @@ export const Menu = {
 		}:{
 			optionStringifier?:(opt:TOption) => string;
 			/**
-			 * Specifies the behavior when the player cancels the menu (by clicking Cancel, or by pressing Escape). 
+			 * Specifies the behavior when the player cancels the menu (by clicking Cancel, or by pressing Escape).
 			 * @default "ignore"
 			 */
 			onCancel?: TCancelBehavior;
@@ -114,7 +115,13 @@ export const Menu = {
 			}
 		}});
 	
-		Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions.map(r => r.map(optionStringifier)));
+		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. */
@@ -133,7 +140,7 @@ export const Menu = {
 		const arrangedOptions = (options.length == 0 && !includeCancel) ? [] : to2DArray(options, columns);
 
 		if(includeCancel){
-			arrangedOptions.push(["Cancel" as never]);
+			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;
@@ -174,7 +181,7 @@ export const Menu = {
 		return Menu.raw(title, description, options, target, {
 			...cfg,
 			optionStringifier: o => o.text,
-		}).then(o => o?.data);
+		}).then(o => o?.data as TButtonData | (TCancelBehavior extends "null" ? null : never));
 	},
 	pages<TOption extends unknown, TCancelBehavior extends MenuCancelOption>(
 		this:void, target:FishPlayer, title:string, description:string,
@@ -209,7 +216,7 @@ export const Menu = {
 		showPage(0);
 		return promise;
 	},
-	pagedListButtons<TButtonData extends unknown, MenuCancelBehavior extends MenuCancelOption>(
+	pagedListButtons<TButtonData extends unknown, MenuCancelBehavior extends MenuCancelOption = "ignore">(
 		this:void, target:FishPlayer, title:string, description:string,
 		options:{ data: TButtonData; text: string; }[],
 		{ rowsPerPage = 10, columns = 3, ...cfg }: Pick<MenuOptions<TButtonData, MenuCancelBehavior>, "columns" | "onCancel"> & {
@@ -219,23 +226,22 @@ export const Menu = {
 	){
 		//Generate pages
 		const pages = to2DArray(to2DArray(options, columns), rowsPerPage);
-		if(pages.length == 1) return Menu.buttons(target, title, description, pages[0], cfg);
+		if(pages.length <= 1) return Menu.buttons(target, title, description, pages[0] ?? [], cfg);
 		return Menu.pages(target, title, description, pages, cfg);
 	},
-	pagedList<TButtonData extends unknown, MenuCancelBehavior extends MenuCancelOption>(
+	pagedList<TButtonData extends unknown, MenuCancelBehavior extends MenuCancelOption = "ignore">(
 		this:void, target:FishPlayer, title:string, description:string,
 		options:TButtonData[],
-		{ rowsPerPage = 10, columns = 3, optionStringifier, ...cfg }: Pick<MenuOptions<TButtonData, MenuCancelBehavior>, "columns" | "onCancel" | "optionStringifier"> & {
+		{ rowsPerPage = 10, columns = 3, optionStringifier = String, ...cfg }: Pick<MenuOptions<TButtonData, MenuCancelBehavior>, "columns" | "onCancel" | "optionStringifier"> & {
 			/** @default 10 */
 			rowsPerPage?:number;
-			optionStringifier: {};
-		},
+		} = {},
 	){
 		//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);
+		if(pages.length <= 1) return Menu.buttons(target, title, description, pages[0] ?? [], cfg);
 		return Menu.pages(target, title, description, pages, cfg);
 	}
 }
diff --git a/src/staffCommands.ts b/src/staffCommands.ts
index 6766066..e884ec7 100644
--- a/src/staffCommands.ts
+++ b/src/staffCommands.ts
@@ -241,7 +241,7 @@ export const commands = commandList({
 	},
 
 	mute_offline: {
-		args: ["name:uuid?"],
+		args: ["name:string?"],
 		description: "Mutes an offline player.",
 		perm: Perm.mod,
 		async handler({args, sender, outputSuccess, f, admins}){
@@ -260,7 +260,7 @@ export const commands = commandList({
 				outputSuccess(`${fishP.muted ? "Muted" : "Unmuted"} ${option.lastName}.`);
 			}
 			
-			if(args.name){
+			if(args.name && uuidPattern.test(args.name)){
 				const info = admins.getInfoOptional(args.name) ?? fail(f`Unknown UUID ${args.name}`);
 				mute(info);
 				return;
@@ -863,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);
@@ -899,14 +899,9 @@ Last name used: "${info.plainLastName()}" [gray](${escapeStringColorsClient(info
 IPs used: ${info.ips.map(i => `[blue]${i}[]`).toString(", ")}`
 					));
 				};
-				if(matches.size > 20) Menu.menu(
-					"Confirm",
-					`Are you sure you want to view all ${matches.size} matches?`,
-					["Yes"],
-					sender,
-					{ includeCancel: true }
-				).then(displayMatches);
-				else displayMatches();
+				if(matches.size > 20)
+					await Menu.confirm(sender, `Are you sure you want to view all ${matches.size} matches?`);
+				displayMatches();
 			}
 		}
 	},