Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/src/library/classes/commandBuilder.ts
1784 views
1
/* eslint-disable @typescript-eslint/no-explicit-any */
2
import { Player, Vector3 } from "@minecraft/server";
3
import { configuration } from "../configurations.js";
4
import {
5
storedRegisterInformation,
6
registerInformation,
7
commandArgList,
8
commandFlag,
9
commandArg,
10
commandSubDef,
11
commandSyntaxError,
12
argParseResult,
13
commandNum,
14
} from "../@types/classes/CommandBuilder";
15
import { Player as playerHandler } from "./playerBuilder.js";
16
import { contentLog, RawText, Thread } from "../utils/index.js";
17
import { EventEmitter } from "./eventEmitter.js";
18
19
export class CustomArgType {
20
static parseArgs: (args: Array<string>, argIndex: number) => argParseResult<unknown>;
21
clone: () => CustomArgType;
22
}
23
24
export class CommandPosition implements CustomArgType {
25
x = 0;
26
y = 0;
27
z = 0;
28
xRelative = true;
29
yRelative = true;
30
zRelative = true;
31
32
clone() {
33
const pos = new CommandPosition();
34
pos.x = this.x;
35
pos.y = this.y;
36
pos.z = this.z;
37
pos.xRelative = this.xRelative;
38
pos.yRelative = this.yRelative;
39
pos.zRelative = this.zRelative;
40
return pos;
41
}
42
43
relativeTo(player: Player, isBlockLoc = false): Vector3 {
44
const loc = { x: 0, y: 0, z: 0 };
45
const x = this.x + (this.xRelative ? player.location.x : 0);
46
const y = this.y + (this.yRelative ? player.location.y : 0);
47
const z = this.z + (this.zRelative ? player.location.z : 0);
48
49
loc.x = isBlockLoc ? Math.floor(x) : x;
50
loc.y = isBlockLoc ? Math.floor(y) : y;
51
loc.z = isBlockLoc ? Math.floor(z) : z;
52
return loc;
53
}
54
55
static parseArgs(args: Array<string>, index: number, is3d = true) {
56
const pos = new CommandPosition();
57
for (let i = 0; i < (is3d ? 3 : 2); i++) {
58
let arg = args[index];
59
if (!args) {
60
const err: commandSyntaxError = {
61
isSyntaxError: true,
62
stack: contentLog.stack(),
63
idx: -1,
64
};
65
throw err;
66
}
67
68
let relative = false;
69
if (arg.includes("~")) {
70
arg = arg.slice(1);
71
relative = true;
72
}
73
const val = arg == "" ? 0 : parseFloat(arg);
74
if (val != val || isNaN(val)) {
75
throw RawText.translate("commands.generic.num.invalid").with(arg);
76
}
77
78
if (i == 0) {
79
pos.x = val;
80
pos.xRelative = relative;
81
} else if (i == 1 && is3d) {
82
pos.y = val;
83
pos.yRelative = relative;
84
} else {
85
pos.z = val;
86
pos.zRelative = relative;
87
}
88
index++;
89
}
90
return { result: pos, argIndex: index };
91
}
92
}
93
94
export class CommandBuilder extends EventEmitter<{ runCommand: [player: Player, command: string, args: Array<string>, result: any] }> {
95
public prefix: string = configuration.prefix;
96
private _registrationInformation: Array<storedRegisterInformation> = [];
97
private customArgTypes: Map<string, typeof CustomArgType> = new Map();
98
99
/**
100
* Register a command with a callback
101
* @param {registerInformation} register An object of information needed to register the custom command
102
* @param {(data: ChatSendBeforeEvent, args: Array<string>) => void}callback Code you want to execute when the command is executed
103
* @example import { Server } from "../../Minecraft";
104
* const server = new Server();
105
* server.commands.register({ name: 'ping' }, (data, args) => {
106
* server.broadcast('Pong!', player.nameTag);
107
* });
108
*/
109
register(register: registerInformation, callback: storedRegisterInformation["callback"]): void {
110
this._registrationInformation.push({
111
name: register.name.toLowerCase(),
112
aliases: register.aliases ? register.aliases.map((v) => v.toLowerCase()) : null,
113
description: register.description,
114
usage: register.usage ?? ([] as commandArgList),
115
permission: register.permission,
116
callback,
117
});
118
}
119
/**
120
* Get a list of registered commands
121
* @returns {Array<string>}
122
* @example getAll();
123
*/
124
getAll(): Array<string> {
125
const commands: Array<string> = [];
126
this._registrationInformation.forEach((element) => {
127
commands.push(element.name);
128
});
129
return commands;
130
}
131
/**
132
* Get a list of all registered information
133
* @returns {Array<storedRegisterInformation>}
134
* @example getAllRegistration();
135
*/
136
getAllRegistration(): Array<storedRegisterInformation> {
137
return this._registrationInformation;
138
}
139
/**
140
* Get registration information on a specific command
141
* @param name The command name or alias you want to get information on
142
* @returns {storedRegisterInformation}
143
* @example getRegistration('ping');
144
*/
145
getRegistration(name: string): storedRegisterInformation {
146
const command = this._registrationInformation.some((element) => element.name.toLowerCase() === name || (element.aliases && element.aliases.includes(name)));
147
if (!command) return;
148
let register;
149
this._registrationInformation.forEach((element) => {
150
const eachCommand = element.name.toLowerCase() === name || (element.aliases && element.aliases.includes(name));
151
if (!eachCommand) return;
152
register = element;
153
});
154
return register;
155
}
156
157
addCustomArgType(name: string, argType: typeof CustomArgType) {
158
this.customArgTypes.set(name, argType);
159
}
160
161
printCommandArguments(name: string, player?: Player): Array<string> {
162
const register = this.getRegistration(name);
163
if (!register) return;
164
165
const usages: Array<string> = [];
166
167
function accumulate(base: Array<string>, args: commandArgList, subName = "_") {
168
const text = [...base];
169
let hasSubCommand = false;
170
let flagText: string;
171
172
if (subName.charAt(0) != "_") {
173
text.push(subName);
174
}
175
176
args?.forEach((arg) => {
177
if ((!("flag" in arg) || (arg.flag && arg.name)) && flagText) {
178
text.push(flagText + "]");
179
flagText = null;
180
}
181
182
if ("subName" in arg) {
183
hasSubCommand = true;
184
if (player && !playerHandler.hasPermission(player, arg.permission)) {
185
return;
186
}
187
accumulate(text, arg.args, arg.subName);
188
} else if ("flag" in arg) {
189
if (!flagText) flagText = "[-";
190
191
flagText += arg.flag;
192
if (arg.name) {
193
text.push(flagText + ` <${arg.name}: ${arg.type}>]`);
194
flagText = null;
195
}
196
} else {
197
let argText = arg.default ? "[" : "<";
198
argText += arg.name + ": ";
199
if ("range" in arg && typeof arg.range[0] == "number" && typeof arg.range[1] == "number") argText += arg.range[0] + ".." + arg.range[1];
200
else argText += arg.type;
201
202
text.push(argText + (arg.default ? "]" : ">"));
203
}
204
});
205
206
if (flagText) {
207
text.push(flagText + "]");
208
flagText = null;
209
}
210
211
if (!hasSubCommand) {
212
usages.push(text.join(" "));
213
}
214
}
215
216
if (player && !playerHandler.hasPermission(player, register.permission)) {
217
return [];
218
}
219
220
if (!register.usage.length) return [""];
221
accumulate([], register.usage);
222
223
return usages;
224
}
225
226
parseArgs(comnand: string, args: Array<string>): Map<string, any> {
227
const result = new Map<string, any>();
228
const argDefs = this.getRegistration(comnand)?.usage;
229
if (argDefs == undefined) return;
230
231
const processArg = (idx: number, def: commandArg, result: Map<string, any>) => {
232
let type = def.type;
233
let value: unknown;
234
if (type.endsWith("...")) type = type.replace("...", "");
235
236
if (type === "int" || type === "float") {
237
const val = (type === "int" ? parseInt : parseFloat)(args[idx]);
238
if (val != val || isNaN(val)) throw RawText.translate("commands.generic.num.invalid").with(args[idx]);
239
240
const range = (<commandNum>def).range;
241
if (range) {
242
const less = val < (range[0] ?? -Infinity);
243
const greater = val > (range[1] ?? Infinity);
244
245
if (less) throw RawText.translate("commands.generic.wedit:tooSmall").with(val).with(range[0]);
246
else if (greater) throw RawText.translate("commands.generic.wedit:tooBig").with(val).with(range[1]);
247
}
248
249
idx++;
250
value = val;
251
} else if (type == "xz") {
252
const parse = CommandPosition.parseArgs(args, idx, false);
253
idx = parse.argIndex;
254
value = parse.result;
255
} else if (type == "xyz") {
256
const parse = CommandPosition.parseArgs(args, idx, true);
257
idx = parse.argIndex;
258
value = parse.result;
259
} else if (type == "CommandName") {
260
const cmdBaseInfo = this.getRegistration(args[idx]);
261
if (!cmdBaseInfo) throw RawText.translate("commands.generic.unknown").with(args[idx]);
262
idx++;
263
value = cmdBaseInfo.name;
264
} else if (type == "string") {
265
value = args[idx++];
266
} else if (this.customArgTypes.has(type)) {
267
try {
268
const parse = this.customArgTypes.get(type).parseArgs(args, idx);
269
idx = parse.argIndex;
270
value = parse.result;
271
} catch (error) {
272
if (error.isSyntaxError) error.idx = idx;
273
throw error;
274
}
275
} else {
276
throw `Unknown argument type: ${type}`;
277
}
278
279
if (def.type.endsWith("...")) {
280
if (!result.has(def.name)) result.set(def.name, []);
281
(result.get(def.name) as Array<unknown>).push(value);
282
} else {
283
result.set(def.name, value);
284
}
285
return idx;
286
};
287
288
const processList = (currIdx: number, argDefs: commandArgList, result: Map<string, any>, flagDefs?: Map<string, commandFlag>) => {
289
let defIdx = 0;
290
let hasNamedSubCmd = false;
291
let invalidFlags: string[] = [];
292
flagDefs = new Map<string, commandFlag>(flagDefs);
293
argDefs?.forEach((argDef) => {
294
if ("flag" in argDef && !flagDefs.has(argDef.flag)) {
295
flagDefs.set(argDef.flag, argDef);
296
}
297
});
298
299
function processSubCmd(idx: number, arg: string) {
300
let processed = false;
301
let unnamedSubs: Array<commandSubDef> = [];
302
303
// process named sub-commands and collect unnamed ones
304
while (defIdx < argDefs.length && "subName" in argDefs[defIdx]) {
305
const argDef = <commandSubDef>argDefs[defIdx];
306
if (!processed) {
307
if (argDef.subName.startsWith("_")) {
308
unnamedSubs.push(argDef);
309
} else {
310
hasNamedSubCmd = true;
311
if (argDef.subName == arg) {
312
idx = processList(idx + 1, argDef.args, result, flagDefs);
313
result.set(argDef.subName, true);
314
processed = true;
315
unnamedSubs = [];
316
}
317
}
318
}
319
defIdx++;
320
}
321
322
// Unknown subcommand
323
if (!processed && hasNamedSubCmd && !unnamedSubs.length) {
324
const err: commandSyntaxError = {
325
isSyntaxError: true,
326
stack: contentLog.stack(),
327
idx: i,
328
};
329
throw err;
330
}
331
332
// process unnamed sub-commands
333
const fails: Array<string> = [];
334
for (const sub of unnamedSubs) {
335
try {
336
const subResult = new Map<string, any>();
337
idx = processList(i, sub.args, subResult, flagDefs);
338
result.set(sub.subName, true);
339
subResult.forEach((v, k) => result.set(k, v));
340
invalidFlags.forEach((f, i) => {
341
if (sub.args.map((argDef) => ("flag" in argDef ? argDef.flag : null)).includes(f)) {
342
result.set(f, true);
343
invalidFlags[i] = "";
344
}
345
});
346
invalidFlags = invalidFlags.filter((f) => f !== "");
347
break;
348
} catch (e) {
349
fails.push(e);
350
}
351
}
352
353
if (fails.length != 0 && fails.length == unnamedSubs.length) {
354
throw fails[0];
355
}
356
357
return idx;
358
}
359
360
const numList = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
361
let i: number;
362
for (i = currIdx; i < args.length; i++) {
363
const arg = args[i];
364
365
if (arg.startsWith("-") && !numList.includes(arg.charAt(1))) {
366
for (const f of arg) {
367
if (f == "-") continue;
368
if (flagDefs.has(f)) {
369
result.set(f, true);
370
const argDef = flagDefs.get(f);
371
if (argDef.type != undefined) {
372
i =
373
processArg(
374
i + 1,
375
{
376
name: argDef.flag + "-" + argDef.name,
377
type: argDef.type,
378
},
379
result
380
) - 1;
381
}
382
} else {
383
invalidFlags.push(f);
384
}
385
}
386
continue;
387
}
388
389
let argDef: commandArg | commandSubDef;
390
while (defIdx < (argDefs?.length ?? 0)) {
391
if (!("flag" in argDefs[defIdx])) {
392
argDef = <typeof argDef>argDefs[defIdx];
393
break;
394
}
395
defIdx++;
396
}
397
398
// Leftover arguments
399
if (!argDef) {
400
const lastArg = argDefs[argDefs.length - 1] as commandArg;
401
if (lastArg?.type?.endsWith("...")) {
402
argDef = lastArg;
403
} else {
404
const err: commandSyntaxError = {
405
isSyntaxError: true,
406
stack: contentLog.stack(),
407
idx: i,
408
};
409
throw err;
410
}
411
}
412
413
if ("type" in argDef && !("flag" in argDef)) {
414
i = processArg(i, argDef, result) - 1;
415
defIdx++;
416
} else if ("subName" in argDef) {
417
i = processSubCmd(i, arg) - 1;
418
}
419
}
420
421
// Process optional arguments (and throw if some are required)
422
while (defIdx < (argDefs?.length ?? 0)) {
423
const argDef = argDefs[defIdx];
424
if (!("flag" in argDef)) {
425
if ("type" in argDef && argDef?.default != undefined && !("subName" in argDef)) {
426
const def = argDef.default.clone?.() ?? argDef.default;
427
result.set(argDef.name, def);
428
} else if ("subName" in argDef) {
429
processSubCmd(i, "");
430
} else {
431
// Required arguments not specified
432
const err: commandSyntaxError = {
433
isSyntaxError: true,
434
stack: contentLog.stack(),
435
idx: -1,
436
};
437
throw err;
438
}
439
}
440
defIdx++;
441
}
442
443
if (invalidFlags.length != 0) {
444
throw RawText.translate("commands.generic.wedit:invalidFlag").with(invalidFlags[0]);
445
}
446
447
return i;
448
};
449
450
processList(0, argDefs, result);
451
return result;
452
}
453
454
callCommand(player: Player, command: string, args: Array<string> | string = [], options?: { noCallback?: boolean }) {
455
function regexIndexOf(text: string, re: RegExp, index: number) {
456
const i = text.slice(index).search(re);
457
return i == -1 ? -1 : i + index;
458
}
459
460
const getCommand = Command.getAllRegistration().some((element) => element.name === command || (element.aliases && element.aliases.includes(command)));
461
if (!getCommand) throw RawText.translate("commands.generic.unknown").with(`${command}`);
462
463
let msg = "";
464
const offsets: Array<number> = [];
465
if (typeof args == "string") {
466
const arrArgs: Array<string> = [];
467
let i = 0;
468
while (i < args.length && i != -1) {
469
const quoted = args[i] == '"';
470
let idx: number;
471
if (quoted) {
472
i++;
473
idx = regexIndexOf(args, /"/, i);
474
} else {
475
idx = regexIndexOf(args, /\s/, i);
476
}
477
478
if (idx == -1) {
479
arrArgs.push(args.slice(i));
480
offsets.push(i);
481
break;
482
} else {
483
arrArgs.push(args.slice(i, idx));
484
offsets.push(i);
485
i = regexIndexOf(args, /[^\s]/, idx + (quoted ? 1 : 0));
486
}
487
}
488
msg = args;
489
args = arrArgs;
490
} else {
491
let offset = 0;
492
for (const arg of args) {
493
offsets.push(offset);
494
msg += arg + " ";
495
offset = msg.length;
496
}
497
msg = args.join(" ");
498
}
499
500
offsets.forEach((v, i) => (offsets[i] = v + this.prefix.length + command.length + 1));
501
msg = this.prefix + command + " " + msg;
502
503
for (const element of Command.getAllRegistration()) {
504
if (!(element.name == command || element.aliases?.includes(command))) continue;
505
506
/**
507
* Registration callback
508
*/
509
let result;
510
try {
511
if (element.permission && !playerHandler.hasPermission(player, element.permission)) throw RawText.translate("commands.generic.wedit:noPermission");
512
const parsedArgs = this.parseArgs(command, args);
513
result = options?.noCallback ? new Thread() : element.callback(player, msg, parsedArgs);
514
this.emit("runCommand", player, command, args, result);
515
} catch (e) {
516
if (e.isSyntaxError) {
517
if (e.idx == -1 || e.idx >= args.length) {
518
throw RawText.translate("commands.generic.syntax").with(msg).with("").with("");
519
} else {
520
let start = offsets[e.idx];
521
if (e.start) start += e.start;
522
let end = start + args[e.idx].length;
523
if (e.end) end = start + e.end;
524
throw RawText.translate("commands.generic.syntax").with(msg.slice(0, start)).with(msg.slice(start, end)).with(msg.slice(end));
525
}
526
} else {
527
if (e instanceof RawText) {
528
throw e;
529
} else {
530
contentLog.error(e, e.stack);
531
throw RawText.text(e);
532
}
533
}
534
}
535
return result;
536
}
537
}
538
}
539
export const Command = new CommandBuilder();
540
541