Path: blob/master/src/library/classes/commandBuilder.ts
1784 views
/* eslint-disable @typescript-eslint/no-explicit-any */1import { Player, Vector3 } from "@minecraft/server";2import { configuration } from "../configurations.js";3import {4storedRegisterInformation,5registerInformation,6commandArgList,7commandFlag,8commandArg,9commandSubDef,10commandSyntaxError,11argParseResult,12commandNum,13} from "../@types/classes/CommandBuilder";14import { Player as playerHandler } from "./playerBuilder.js";15import { contentLog, RawText, Thread } from "../utils/index.js";16import { EventEmitter } from "./eventEmitter.js";1718export class CustomArgType {19static parseArgs: (args: Array<string>, argIndex: number) => argParseResult<unknown>;20clone: () => CustomArgType;21}2223export class CommandPosition implements CustomArgType {24x = 0;25y = 0;26z = 0;27xRelative = true;28yRelative = true;29zRelative = true;3031clone() {32const pos = new CommandPosition();33pos.x = this.x;34pos.y = this.y;35pos.z = this.z;36pos.xRelative = this.xRelative;37pos.yRelative = this.yRelative;38pos.zRelative = this.zRelative;39return pos;40}4142relativeTo(player: Player, isBlockLoc = false): Vector3 {43const loc = { x: 0, y: 0, z: 0 };44const x = this.x + (this.xRelative ? player.location.x : 0);45const y = this.y + (this.yRelative ? player.location.y : 0);46const z = this.z + (this.zRelative ? player.location.z : 0);4748loc.x = isBlockLoc ? Math.floor(x) : x;49loc.y = isBlockLoc ? Math.floor(y) : y;50loc.z = isBlockLoc ? Math.floor(z) : z;51return loc;52}5354static parseArgs(args: Array<string>, index: number, is3d = true) {55const pos = new CommandPosition();56for (let i = 0; i < (is3d ? 3 : 2); i++) {57let arg = args[index];58if (!args) {59const err: commandSyntaxError = {60isSyntaxError: true,61stack: contentLog.stack(),62idx: -1,63};64throw err;65}6667let relative = false;68if (arg.includes("~")) {69arg = arg.slice(1);70relative = true;71}72const val = arg == "" ? 0 : parseFloat(arg);73if (val != val || isNaN(val)) {74throw RawText.translate("commands.generic.num.invalid").with(arg);75}7677if (i == 0) {78pos.x = val;79pos.xRelative = relative;80} else if (i == 1 && is3d) {81pos.y = val;82pos.yRelative = relative;83} else {84pos.z = val;85pos.zRelative = relative;86}87index++;88}89return { result: pos, argIndex: index };90}91}9293export class CommandBuilder extends EventEmitter<{ runCommand: [player: Player, command: string, args: Array<string>, result: any] }> {94public prefix: string = configuration.prefix;95private _registrationInformation: Array<storedRegisterInformation> = [];96private customArgTypes: Map<string, typeof CustomArgType> = new Map();9798/**99* Register a command with a callback100* @param {registerInformation} register An object of information needed to register the custom command101* @param {(data: ChatSendBeforeEvent, args: Array<string>) => void}callback Code you want to execute when the command is executed102* @example import { Server } from "../../Minecraft";103* const server = new Server();104* server.commands.register({ name: 'ping' }, (data, args) => {105* server.broadcast('Pong!', player.nameTag);106* });107*/108register(register: registerInformation, callback: storedRegisterInformation["callback"]): void {109this._registrationInformation.push({110name: register.name.toLowerCase(),111aliases: register.aliases ? register.aliases.map((v) => v.toLowerCase()) : null,112description: register.description,113usage: register.usage ?? ([] as commandArgList),114permission: register.permission,115callback,116});117}118/**119* Get a list of registered commands120* @returns {Array<string>}121* @example getAll();122*/123getAll(): Array<string> {124const commands: Array<string> = [];125this._registrationInformation.forEach((element) => {126commands.push(element.name);127});128return commands;129}130/**131* Get a list of all registered information132* @returns {Array<storedRegisterInformation>}133* @example getAllRegistration();134*/135getAllRegistration(): Array<storedRegisterInformation> {136return this._registrationInformation;137}138/**139* Get registration information on a specific command140* @param name The command name or alias you want to get information on141* @returns {storedRegisterInformation}142* @example getRegistration('ping');143*/144getRegistration(name: string): storedRegisterInformation {145const command = this._registrationInformation.some((element) => element.name.toLowerCase() === name || (element.aliases && element.aliases.includes(name)));146if (!command) return;147let register;148this._registrationInformation.forEach((element) => {149const eachCommand = element.name.toLowerCase() === name || (element.aliases && element.aliases.includes(name));150if (!eachCommand) return;151register = element;152});153return register;154}155156addCustomArgType(name: string, argType: typeof CustomArgType) {157this.customArgTypes.set(name, argType);158}159160printCommandArguments(name: string, player?: Player): Array<string> {161const register = this.getRegistration(name);162if (!register) return;163164const usages: Array<string> = [];165166function accumulate(base: Array<string>, args: commandArgList, subName = "_") {167const text = [...base];168let hasSubCommand = false;169let flagText: string;170171if (subName.charAt(0) != "_") {172text.push(subName);173}174175args?.forEach((arg) => {176if ((!("flag" in arg) || (arg.flag && arg.name)) && flagText) {177text.push(flagText + "]");178flagText = null;179}180181if ("subName" in arg) {182hasSubCommand = true;183if (player && !playerHandler.hasPermission(player, arg.permission)) {184return;185}186accumulate(text, arg.args, arg.subName);187} else if ("flag" in arg) {188if (!flagText) flagText = "[-";189190flagText += arg.flag;191if (arg.name) {192text.push(flagText + ` <${arg.name}: ${arg.type}>]`);193flagText = null;194}195} else {196let argText = arg.default ? "[" : "<";197argText += arg.name + ": ";198if ("range" in arg && typeof arg.range[0] == "number" && typeof arg.range[1] == "number") argText += arg.range[0] + ".." + arg.range[1];199else argText += arg.type;200201text.push(argText + (arg.default ? "]" : ">"));202}203});204205if (flagText) {206text.push(flagText + "]");207flagText = null;208}209210if (!hasSubCommand) {211usages.push(text.join(" "));212}213}214215if (player && !playerHandler.hasPermission(player, register.permission)) {216return [];217}218219if (!register.usage.length) return [""];220accumulate([], register.usage);221222return usages;223}224225parseArgs(comnand: string, args: Array<string>): Map<string, any> {226const result = new Map<string, any>();227const argDefs = this.getRegistration(comnand)?.usage;228if (argDefs == undefined) return;229230const processArg = (idx: number, def: commandArg, result: Map<string, any>) => {231let type = def.type;232let value: unknown;233if (type.endsWith("...")) type = type.replace("...", "");234235if (type === "int" || type === "float") {236const val = (type === "int" ? parseInt : parseFloat)(args[idx]);237if (val != val || isNaN(val)) throw RawText.translate("commands.generic.num.invalid").with(args[idx]);238239const range = (<commandNum>def).range;240if (range) {241const less = val < (range[0] ?? -Infinity);242const greater = val > (range[1] ?? Infinity);243244if (less) throw RawText.translate("commands.generic.wedit:tooSmall").with(val).with(range[0]);245else if (greater) throw RawText.translate("commands.generic.wedit:tooBig").with(val).with(range[1]);246}247248idx++;249value = val;250} else if (type == "xz") {251const parse = CommandPosition.parseArgs(args, idx, false);252idx = parse.argIndex;253value = parse.result;254} else if (type == "xyz") {255const parse = CommandPosition.parseArgs(args, idx, true);256idx = parse.argIndex;257value = parse.result;258} else if (type == "CommandName") {259const cmdBaseInfo = this.getRegistration(args[idx]);260if (!cmdBaseInfo) throw RawText.translate("commands.generic.unknown").with(args[idx]);261idx++;262value = cmdBaseInfo.name;263} else if (type == "string") {264value = args[idx++];265} else if (this.customArgTypes.has(type)) {266try {267const parse = this.customArgTypes.get(type).parseArgs(args, idx);268idx = parse.argIndex;269value = parse.result;270} catch (error) {271if (error.isSyntaxError) error.idx = idx;272throw error;273}274} else {275throw `Unknown argument type: ${type}`;276}277278if (def.type.endsWith("...")) {279if (!result.has(def.name)) result.set(def.name, []);280(result.get(def.name) as Array<unknown>).push(value);281} else {282result.set(def.name, value);283}284return idx;285};286287const processList = (currIdx: number, argDefs: commandArgList, result: Map<string, any>, flagDefs?: Map<string, commandFlag>) => {288let defIdx = 0;289let hasNamedSubCmd = false;290let invalidFlags: string[] = [];291flagDefs = new Map<string, commandFlag>(flagDefs);292argDefs?.forEach((argDef) => {293if ("flag" in argDef && !flagDefs.has(argDef.flag)) {294flagDefs.set(argDef.flag, argDef);295}296});297298function processSubCmd(idx: number, arg: string) {299let processed = false;300let unnamedSubs: Array<commandSubDef> = [];301302// process named sub-commands and collect unnamed ones303while (defIdx < argDefs.length && "subName" in argDefs[defIdx]) {304const argDef = <commandSubDef>argDefs[defIdx];305if (!processed) {306if (argDef.subName.startsWith("_")) {307unnamedSubs.push(argDef);308} else {309hasNamedSubCmd = true;310if (argDef.subName == arg) {311idx = processList(idx + 1, argDef.args, result, flagDefs);312result.set(argDef.subName, true);313processed = true;314unnamedSubs = [];315}316}317}318defIdx++;319}320321// Unknown subcommand322if (!processed && hasNamedSubCmd && !unnamedSubs.length) {323const err: commandSyntaxError = {324isSyntaxError: true,325stack: contentLog.stack(),326idx: i,327};328throw err;329}330331// process unnamed sub-commands332const fails: Array<string> = [];333for (const sub of unnamedSubs) {334try {335const subResult = new Map<string, any>();336idx = processList(i, sub.args, subResult, flagDefs);337result.set(sub.subName, true);338subResult.forEach((v, k) => result.set(k, v));339invalidFlags.forEach((f, i) => {340if (sub.args.map((argDef) => ("flag" in argDef ? argDef.flag : null)).includes(f)) {341result.set(f, true);342invalidFlags[i] = "";343}344});345invalidFlags = invalidFlags.filter((f) => f !== "");346break;347} catch (e) {348fails.push(e);349}350}351352if (fails.length != 0 && fails.length == unnamedSubs.length) {353throw fails[0];354}355356return idx;357}358359const numList = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];360let i: number;361for (i = currIdx; i < args.length; i++) {362const arg = args[i];363364if (arg.startsWith("-") && !numList.includes(arg.charAt(1))) {365for (const f of arg) {366if (f == "-") continue;367if (flagDefs.has(f)) {368result.set(f, true);369const argDef = flagDefs.get(f);370if (argDef.type != undefined) {371i =372processArg(373i + 1,374{375name: argDef.flag + "-" + argDef.name,376type: argDef.type,377},378result379) - 1;380}381} else {382invalidFlags.push(f);383}384}385continue;386}387388let argDef: commandArg | commandSubDef;389while (defIdx < (argDefs?.length ?? 0)) {390if (!("flag" in argDefs[defIdx])) {391argDef = <typeof argDef>argDefs[defIdx];392break;393}394defIdx++;395}396397// Leftover arguments398if (!argDef) {399const lastArg = argDefs[argDefs.length - 1] as commandArg;400if (lastArg?.type?.endsWith("...")) {401argDef = lastArg;402} else {403const err: commandSyntaxError = {404isSyntaxError: true,405stack: contentLog.stack(),406idx: i,407};408throw err;409}410}411412if ("type" in argDef && !("flag" in argDef)) {413i = processArg(i, argDef, result) - 1;414defIdx++;415} else if ("subName" in argDef) {416i = processSubCmd(i, arg) - 1;417}418}419420// Process optional arguments (and throw if some are required)421while (defIdx < (argDefs?.length ?? 0)) {422const argDef = argDefs[defIdx];423if (!("flag" in argDef)) {424if ("type" in argDef && argDef?.default != undefined && !("subName" in argDef)) {425const def = argDef.default.clone?.() ?? argDef.default;426result.set(argDef.name, def);427} else if ("subName" in argDef) {428processSubCmd(i, "");429} else {430// Required arguments not specified431const err: commandSyntaxError = {432isSyntaxError: true,433stack: contentLog.stack(),434idx: -1,435};436throw err;437}438}439defIdx++;440}441442if (invalidFlags.length != 0) {443throw RawText.translate("commands.generic.wedit:invalidFlag").with(invalidFlags[0]);444}445446return i;447};448449processList(0, argDefs, result);450return result;451}452453callCommand(player: Player, command: string, args: Array<string> | string = [], options?: { noCallback?: boolean }) {454function regexIndexOf(text: string, re: RegExp, index: number) {455const i = text.slice(index).search(re);456return i == -1 ? -1 : i + index;457}458459const getCommand = Command.getAllRegistration().some((element) => element.name === command || (element.aliases && element.aliases.includes(command)));460if (!getCommand) throw RawText.translate("commands.generic.unknown").with(`${command}`);461462let msg = "";463const offsets: Array<number> = [];464if (typeof args == "string") {465const arrArgs: Array<string> = [];466let i = 0;467while (i < args.length && i != -1) {468const quoted = args[i] == '"';469let idx: number;470if (quoted) {471i++;472idx = regexIndexOf(args, /"/, i);473} else {474idx = regexIndexOf(args, /\s/, i);475}476477if (idx == -1) {478arrArgs.push(args.slice(i));479offsets.push(i);480break;481} else {482arrArgs.push(args.slice(i, idx));483offsets.push(i);484i = regexIndexOf(args, /[^\s]/, idx + (quoted ? 1 : 0));485}486}487msg = args;488args = arrArgs;489} else {490let offset = 0;491for (const arg of args) {492offsets.push(offset);493msg += arg + " ";494offset = msg.length;495}496msg = args.join(" ");497}498499offsets.forEach((v, i) => (offsets[i] = v + this.prefix.length + command.length + 1));500msg = this.prefix + command + " " + msg;501502for (const element of Command.getAllRegistration()) {503if (!(element.name == command || element.aliases?.includes(command))) continue;504505/**506* Registration callback507*/508let result;509try {510if (element.permission && !playerHandler.hasPermission(player, element.permission)) throw RawText.translate("commands.generic.wedit:noPermission");511const parsedArgs = this.parseArgs(command, args);512result = options?.noCallback ? new Thread() : element.callback(player, msg, parsedArgs);513this.emit("runCommand", player, command, args, result);514} catch (e) {515if (e.isSyntaxError) {516if (e.idx == -1 || e.idx >= args.length) {517throw RawText.translate("commands.generic.syntax").with(msg).with("").with("");518} else {519let start = offsets[e.idx];520if (e.start) start += e.start;521let end = start + args[e.idx].length;522if (e.end) end = start + e.end;523throw RawText.translate("commands.generic.syntax").with(msg.slice(0, start)).with(msg.slice(start, end)).with(msg.slice(end));524}525} else {526if (e instanceof RawText) {527throw e;528} else {529contentLog.error(e, e.stack);530throw RawText.text(e);531}532}533}534return result;535}536}537}538export const Command = new CommandBuilder();539540541