Skip to content

Commit

Permalink
feat: implement numArgs, allowing for a variable number of inputs f…
Browse files Browse the repository at this point in the history
…or a single argument
  • Loading branch information
paradoxuum committed Aug 2, 2024
1 parent 81d54db commit b51e86d
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 36 deletions.
4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/decorators.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ ban() {}
## `ArgumentOptions`

- **name**: `string` - The name of the argument.
- **description**: `string` - The argument's description.
- **type**: `string` - The argument's type. Must be the name of a registered type.
- **numArgs**: `number | "rest"` - The number of inputs the argument should accept. This will produce
an array of values. If set to `rest`, it will accept all remaining arguments, but can only be used
on the last argument.
- **optional**: `boolean?` - Whether the argument is optional.
- **suggestions**: `string[]?` - An array of suggestions for the argument.
These will be added to the suggestions returned by the argument's type.
78 changes: 64 additions & 14 deletions packages/core/src/shared/core/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,25 @@ import {
GroupOptions,
SharedConfig,
} from "../types";
import { ObjectUtil, ReadonlyDeep, ReadonlyDeepObject } from "../util/data";
import {
ArrayUtil,
ObjectUtil,
ReadonlyDeep,
ReadonlyDeepObject,
} from "../util/data";
import { CenturionLogger } from "../util/log";
import { TransformResult } from "../util/type";
import { CommandContext } from "./context";
import { ImmutableRegistryPath } from "./path";
import { BaseRegistry } from "./registry";

interface ArgumentData {
options: ArgumentOptions;
type: ArgumentType<unknown>;
}

export abstract class BaseCommand {
protected readonly argTypes: ArgumentType<unknown>[] = [];
protected readonly arguments: ArgumentData[] = [];
protected readonly path: ImmutableRegistryPath;
protected readonly logger: CenturionLogger;
readonly options: ReadonlyDeepObject<CommandOptions>;
Expand All @@ -33,6 +43,7 @@ export abstract class BaseCommand {
if (options.arguments === undefined) return;

let hadOptional = false;
const lastIndex = options.arguments.size() - 1;
for (const i of $range(0, options.arguments.size() - 1)) {
const arg: ArgumentOptions | undefined = options.arguments[i];
if (arg.optional === true) {
Expand All @@ -43,12 +54,25 @@ export abstract class BaseCommand {
);
}

if (typeIs(arg.numArgs, "number") && arg.numArgs < 1) {
this.logger.error(
`Command '${options.name}' has an argument that requires less than 1 argument (arg ${arg.name} at position ${i + 1})`,
);
} else if (arg.numArgs === "rest" && i !== lastIndex) {
this.logger.error(
`Command '${options.name}' has a rest argument that is not the last argument (arg ${arg.name} at position ${i + 1})`,
);
}

const argType = registry.getType(arg.type);
this.logger.assert(
argType !== undefined,
`Argument '${arg.name}' uses a type that is unregistered: ${arg.type}`,
);
this.argTypes.push(argType);
this.arguments.push({
options: arg,
type: argType,
});
}
}

Expand Down Expand Up @@ -116,23 +140,49 @@ export class ExecutableCommand extends BaseCommand {
return TransformResult.ok([]);
}

const endIndex = args.size() - 1;
const endIndex = this.arguments.size() - 1;
const transformedArgs: unknown[] = [];
for (const i of $range(0, this.argTypes.size() - 1)) {
const argType = this.argTypes[i];
if (argType === undefined) continue;

const argData = argOptions[i];
if (i > endIndex) {
if (argData.optional) break;
let argIndex = 0;
for (const arg of this.arguments) {
const argument = this.arguments[math.min(argIndex, endIndex)];
const numArgs = argument.options.numArgs ?? 1;
const isNum = numArgs !== "rest";
const argInputs = ArrayUtil.slice(
args,
argIndex,
isNum ? argIndex + numArgs : undefined,
);

if (argInputs.isEmpty()) {
if (arg.options.optional) break;
return TransformResult.err(
`Missing required argument: <b>${arg.options.name}</b>`,
);
}

if (isNum && argInputs.size() < numArgs) {
return TransformResult.err(
`Missing required argument: <b>${argData.name}</b>`,
`Argument <b>${arg.options.name}</b> requires ${numArgs} argument(s), but only ${argInputs.size()} were provided`,
);
}

const transformedArg = argType.transform(args[i], context.executor);
if (!transformedArg.ok) return TransformResult.err(transformedArg.value);
transformedArgs[i] = transformedArg.value;
const argValues: unknown[] = [];
for (const i of $range(0, argInputs.size() - 1)) {
const transformedArg = arg.type.transform(
argInputs[i],
context.executor,
);

if (!transformedArg.ok) {
return TransformResult.err(transformedArg.value);
}
argValues[i] = transformedArg.value;
}

transformedArgs[argIndex] =
arg.options.numArgs !== undefined ? argValues : argValues[0];
argIndex += 1;
}

return TransformResult.ok(transformedArgs);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface ArgumentOptions {
name: string;
description: string;
type: string;
numArgs?: number | "rest";
optional?: boolean;
suggestions?: string[];
}
Expand Down
31 changes: 23 additions & 8 deletions packages/ui/src/components/terminal/terminal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ArrayUtil } from "@rbxts/centurion/out/shared/util/data";
import { ArgumentOptions } from "@rbxts/centurion";
import { ArrayUtil, ReadonlyDeep } from "@rbxts/centurion/out/shared/util/data";
import { endsWithSpace } from "@rbxts/centurion/out/shared/util/string";
import Vide, { effect, source } from "@rbxts/vide";
import { HISTORY_TEXT_SIZE } from "../../constants/text";
Expand All @@ -8,7 +9,6 @@ import { useHistory } from "../../hooks/use-history";
import { useMotion } from "../../hooks/use-motion";
import { px } from "../../hooks/use-px";
import {
argText,
currentArgIndex,
currentCommandPath,
currentSuggestion,
Expand Down Expand Up @@ -134,25 +134,40 @@ export function Terminal() {
if (command === undefined || argIndex === -1) return;

const args = command.options.arguments;
if (args === undefined || argIndex >= args.size()) {
if (args === undefined || argIndex === -1) {
currentSuggestion(undefined);
return;
}

const arg = args[argIndex];
const argType = api.registry.getType(arg.type);
if (argType === undefined) {
let index = 0;
let currentArg: ReadonlyDeep<ArgumentOptions> | undefined;
let endIndex: number | undefined = -1;
for (const i of $range(0, argIndex)) {
if (endIndex === undefined || i <= endIndex) continue;
if (index >= args.size()) {
currentArg = undefined;
break;
}

currentArg = args[index];
const numArgs = currentArg.numArgs ?? 1;
endIndex = numArgs !== "rest" ? i + (numArgs - 1) : undefined;
index += 1;
}

const argType = api.registry.getType(currentArg?.type ?? "");
if (currentArg === undefined || argType === undefined) {
currentArgIndex(undefined);
currentSuggestion(undefined);
return;
}

const suggestion = getArgumentSuggestion(
arg,
currentArg,
argType,
currentTextPart,
);

argText(currentTextPart ?? "");
currentArgIndex(argIndex);
currentSuggestion(suggestion);
if (suggestion?.error !== undefined) terminalTextValid(false);
Expand Down
17 changes: 3 additions & 14 deletions test/src/server/commands/echo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,12 @@ export class EchoCommand {
name: "text",
description: "The text to print",
type: CenturionType.String,
optional: true,
},
],
shortcuts: [
Enum.KeyCode.H,
{
keys: [Enum.KeyCode.LeftAlt, Enum.KeyCode.H],
arguments: ["Alt + H pressed"],
numArgs: "rest",
},
],
aliases: ["print"],
})
run(_: CommandContext, text?: string) {
if (text !== undefined) {
print(text);
} else {
print("Hello World!");
}
run(_: CommandContext, text: string[]) {
print(text.join(" "));
}
}
31 changes: 31 additions & 0 deletions test/src/server/commands/kick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
CenturionType,
Command,
CommandContext,
Register,
} from "@rbxts/centurion";

@Register()
export class KickCommand {
@Command({
name: "kick",
description: "Kicks a player",
arguments: [
{
name: "player",
description: "Player to kick",
type: CenturionType.Player,
},
{
name: "reason",
description: "Reason for kicking the player",
type: CenturionType.String,
numArgs: "rest",
},
],
})
kick(_: CommandContext, player: Player, reason: string[]) {
const reasonText = reason.join(" ");
print(`Kicked ${player.Name} for ${reasonText}`);
}
}
27 changes: 27 additions & 0 deletions test/src/server/commands/teleport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
CenturionType,
Command,
CommandContext,
Register,
} from "@rbxts/centurion";

@Register()
export class TeleportCommand {
@Command({
name: "teleport",
description: "Teleports to a position",
arguments: [
{
name: "position",
description: "Coordinates to teleport to",
type: CenturionType.Number,
numArgs: 3,
},
],
})
teleport(ctx: CommandContext, coords: number[]) {
const pos = new Vector3(coords[0], coords[1], coords[2]);
ctx.executor.Character?.PivotTo(new CFrame(pos));
ctx.reply(`Teleported to ${pos}`);
}
}

0 comments on commit b51e86d

Please sign in to comment.