mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-23 15:36:54 +00:00
Rewrite computer selectors
This adds support for computer selectors, in the style of entity selectors. The long-term goal here is to replace our existing ad-hoc selectors. However, to aid migration, we currently support both - the previous one will most likely be removed in MC 1.21. Computer selectors take the form @c[<key>=<value>,...]. Currently we support filtering by id, instance id, label, family (as before) and distance from the player (new!). The code also supports computers within a bounding box, but there's no parsing support for that yet. This commit also (finally) documents the /computercraft command. Well, sort of - it's definitely not my best word, but I couldn't find better words.
This commit is contained in:
parent
cb8e06af2a
commit
b7df91349a
@ -21,5 +21,5 @@ SPDX-License-Identifier: MPL-2.0
|
||||
<suppress checks="PackageName" files=".*[\\/]T[A-Za-z]+.java" />
|
||||
|
||||
<!-- Allow underscores in our test classes. -->
|
||||
<suppress checks="MethodName" files=".*Contract.java" />
|
||||
<suppress checks="MethodName" files=".*(Contract|Test).java" />
|
||||
</suppressions>
|
||||
|
BIN
doc/images/computercraft-dump.png
Normal file
BIN
doc/images/computercraft-dump.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 254 KiB |
BIN
doc/images/computercraft-track.png
Normal file
BIN
doc/images/computercraft-track.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 304 KiB |
140
doc/reference/command.md
Normal file
140
doc/reference/command.md
Normal file
@ -0,0 +1,140 @@
|
||||
---
|
||||
module: [kind=reference] computercraft_command
|
||||
---
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# The `/computercraft` command
|
||||
CC: Tweaked provides a `/computercraft` command for server owners to manage running computers on a server.
|
||||
|
||||
## Permissions {#permissions}
|
||||
As the `/computercraft` command is mostly intended for debugging and administrative purposes, its sub-commands typically
|
||||
require you to have op (or similar).
|
||||
|
||||
- All players have access to the [`queue`] sub-command.
|
||||
- On a multi-player server, all other commands require op.
|
||||
- On a single-player world, the player can run the [`dump`], [`turn-on`]/[`shutdown`], and [`track`] sub-commands, even
|
||||
when cheats are not enabled. The [`tp`] and [`view`] commands require cheats.
|
||||
|
||||
If a permission mod such as [LuckPerms] is installed[^permission], you can configure access to the individual
|
||||
sub-commands. Each sub-command creates a `computercraft.command.NAME` permission node to control which players can
|
||||
execute it.
|
||||
|
||||
[LuckPerms]: https://github.com/LuckPerms/LuckPerms/ "A permissions plugin for Minecraft servers."
|
||||
[fabric-permission-api]: https://github.com/lucko/fabric-permissions-api "A simple permissions API for Fabric"
|
||||
|
||||
[^permission]: This supports any mod which uses Forge's permission API or [fabric-permission-api].
|
||||
|
||||
## Computer selectors {#computer-selectors}
|
||||
Some commands (such as [`tp`] or [`turn-on`]) target a specific computer, or a list of computers. To specify which
|
||||
computers to operate on, you must use "computer selectors".
|
||||
|
||||
Computer selectors are similar to Minecraft's [entity target selectors], but targeting computers instead. They allow
|
||||
you to select one or more computers, based on a set of predicates.
|
||||
|
||||
The following predicates are supported:
|
||||
- `id=<id>`: Select computer(s) with a specific id.
|
||||
- `instance=<id>`: Select the computer with the given instance id.
|
||||
- `family=<normal|advanced|command>`: Select computers based on their type.
|
||||
- `label=<label>`: Select computers with the given label.
|
||||
- `distance=<distance>`: Select computers within a specific distance of the player executing the command. This uses
|
||||
Minecraft's [float range] syntax.
|
||||
|
||||
`#<id>` may also be used as a shorthand for `@c[id=<id>]`, to select computer(s) with a specific id.
|
||||
|
||||
### Examples:
|
||||
- `/computercraft turn-on #12`: Turn on the computer(s) with an id of 12.
|
||||
- `/computercraft shutdown @c[distance=..100]`: Shut down all computers with 100 blocks of the player.
|
||||
|
||||
[entity target selectors]: https://minecraft.wiki/w/Target_selectors "Target Selectors on the Minecraft wiki"
|
||||
[Float range]: https://minecraft.wiki/w/Argument_types#minecraft:float_range
|
||||
|
||||
## Commands {#commands}
|
||||
### `/computercraft dump` {#dump}
|
||||
`/computercraft dump` prints a table of currently loaded computers, including their id, position, and whether they're
|
||||
running. It can also be run with a single computer argument to dump more detailed information about a computer.
|
||||
|
||||
![A screenshot of a Minecraft world. In the chat box, there is a table listing 5 computers, with columns labelled
|
||||
"Computer", "On" and "Position". Below that, is a more detailed list of information about Computer 0, including its
|
||||
label ("My computer") and that it has a monitor on the right hand side](../images/computercraft-dump.png "An example of
|
||||
running '/computercraft dump'")
|
||||
|
||||
Next to the computer id, there are several buttons to either [teleport][`tp`] to the computer, or [open its terminal
|
||||
][`view`].
|
||||
|
||||
Computers are sorted by distance to the player, so nearby computers will appear earlier.
|
||||
|
||||
### `/computercraft turn-on [computers...]` {#turn-on}
|
||||
Turn on one or more computers or, if no run with no arguments, all loaded computers.
|
||||
|
||||
#### Examples
|
||||
- `/computercraft turn-on #0 #2`: Turn on computers with id 0 and 2.
|
||||
- `/computercraft turn-on @c[family=command]`: Turn on all command computers.
|
||||
|
||||
### `/computercraft shutdown [computers...]` {#shutdown}
|
||||
Shutdown one or more computers or, if no run with no arguments, all loaded computers.
|
||||
|
||||
This is sometimes useful when dealing with lag, as a way to ensure that ComputerCraft is not causing problems.
|
||||
|
||||
#### Examples
|
||||
- `/computercraft shutdown`: Shut down all loaded computers.
|
||||
- `/computercraft shutdown @c[distance=..10]`: Shut down all computers in a block radius.
|
||||
|
||||
### `/computercraft tp [computer]` {#tp}
|
||||
Teleport to the given computer.
|
||||
|
||||
This is normally used from via the [`dump`] command interface rather than being invoked directly.
|
||||
|
||||
### `/computercraft view [computer]` {#view}
|
||||
Open a terminal for the specified computer. This allows remotely viewing computers without having to interact with the
|
||||
block.
|
||||
|
||||
This is normally used from via the [`dump`] command interface rather than being invoked directly.
|
||||
|
||||
### `/computercraft track` {#track}
|
||||
The `/computercraft track` command allows you to enable profiling of computers. When a computer runs code, or interacts
|
||||
with the Minecraft world, we time how long that takes. This timing information may then be queried, and used to find
|
||||
computers which may be causing lag.
|
||||
|
||||
To enable the profiler, run `/computercraft track start`. Computers will then start recording metrics. Once enough data
|
||||
has been gathered, run `/computercraft track stop` to stop profiling and display the recorded data.
|
||||
|
||||
![](../images/computercraft-track.png)
|
||||
|
||||
The table by default shows the number of times each computer has run, and how long it ran for (in total, and on
|
||||
average). In the above screenshot, we can see one computer was particularly badly behaved, and ran for 7 seconds. The
|
||||
buttons may be used to [teleport][`tp`] to the computer, or [open its terminal ][`view`], and inspect it further.
|
||||
|
||||
`/computercraft track dump` can be used to display this table at any point (including while profiling is still running).
|
||||
|
||||
Computers also record other information, such as how much server-thread time they consume, or their HTTP bandwidth
|
||||
usage. The `dump` subcommand accepts a list of other fields to display, instead of the default timings.
|
||||
|
||||
#### Examples
|
||||
- `/computercraft track dump server_tasks_count server_tasks`: Print the number of server-thread tasks each computer
|
||||
executed, and how long they took in total.
|
||||
- `/computercraft track dump http_upload http_download`: Print the number of bytes uploaded and downloaded by each
|
||||
computer.
|
||||
|
||||
|
||||
### `/computercraft queue` {#queue}
|
||||
The queue subcommand allows non-operator players to queue a `computer_command` event on *command* computers.
|
||||
|
||||
This has a similar purpose to vanilla's [`/trigger`] command. Command computers may choose to listen to this event, and
|
||||
then perform some action.
|
||||
|
||||
[`/trigger`]: https://minecraft.wiki/w/Commands/trigger "/trigger on the Minecraft wiki"
|
||||
|
||||
|
||||
[`dump`]: #dump "/computercraft dump"
|
||||
[`queue`]: #queue "/computercraft queue"
|
||||
[`shutdown`]: #shutdown "/computercraft shutdown"
|
||||
[`tp`]: #tp "/computercraft tp"
|
||||
[`track`]: #track "/computercraft track"
|
||||
[`turn-on`]: #turn-on "/computercraft turn-on"
|
||||
[`view`]: #view "/computercraft view"
|
||||
[computer selectors]: #computer-selectors "Computer selectors"
|
@ -13,6 +13,7 @@ import dan200.computercraft.api.upgrades.UpgradeBase;
|
||||
import dan200.computercraft.core.metrics.Metric;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerSelector;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.Aggregate;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
|
||||
import dan200.computercraft.shared.config.ConfigFile;
|
||||
@ -166,10 +167,19 @@ public final class LanguageProvider implements DataProvider {
|
||||
add("commands.computercraft.generic.exception", "Unhandled exception (%s)");
|
||||
add("commands.computercraft.generic.additional_rows", "%d additional rows…");
|
||||
|
||||
// Argument types
|
||||
add("argument.computercraft.computer.instance", "Unique instance ID");
|
||||
add("argument.computercraft.computer.id", "Computer ID");
|
||||
add("argument.computercraft.computer.label", "Computer label");
|
||||
add("argument.computercraft.computer.distance", "Distance to entity");
|
||||
add("argument.computercraft.computer.family", "Computer family");
|
||||
|
||||
// Exceptions
|
||||
add("argument.computercraft.computer.no_matching", "No computers matching '%s'");
|
||||
add("argument.computercraft.computer.many_matching", "Multiple computers matching '%s' (instances %s)");
|
||||
add("argument.computercraft.tracking_field.no_field", "Unknown field '%s'");
|
||||
add("argument.computercraft.argument_expected", "Argument expected");
|
||||
add("argument.computercraft.unknown_computer_family", "Unknown computer family '%s'");
|
||||
|
||||
// Metrics
|
||||
add(Metrics.COMPUTER_TASKS, "Tasks");
|
||||
@ -282,7 +292,8 @@ public final class LanguageProvider implements DataProvider {
|
||||
pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
|
||||
Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"),
|
||||
ConfigSpec.serverSpec.entries().map(ConfigFile.Entry::translationKey),
|
||||
ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey)
|
||||
ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey),
|
||||
ComputerSelector.options().values().stream().map(ComputerSelector.Option::translationKey)
|
||||
).flatMap(x -> x);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@ import dan200.computercraft.impl.PocketUpgrades;
|
||||
import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.shared.command.UserLevel;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType;
|
||||
import dan200.computercraft.shared.common.ClearColourRecipe;
|
||||
@ -337,8 +336,7 @@ public final class ModRegistry {
|
||||
|
||||
static {
|
||||
register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric());
|
||||
register("computer", ComputerArgumentType.class, ComputerArgumentType.oneComputer());
|
||||
register("computers", ComputersArgumentType.class, new ComputersArgumentType.Info());
|
||||
register("computer", ComputerArgumentType.class, ComputerArgumentType.get());
|
||||
registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.Info());
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerSelector;
|
||||
import dan200.computercraft.shared.command.text.TableBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
@ -23,6 +24,7 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@ -42,9 +44,6 @@ import java.util.*;
|
||||
import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.NOT_TRACKING_EXCEPTION;
|
||||
import static dan200.computercraft.shared.command.Exceptions.NO_TIMINGS_EXCEPTION;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
|
||||
import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.metric;
|
||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.args;
|
||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
|
||||
@ -70,37 +69,37 @@ public final class CommandComputerCraft {
|
||||
.requires(ModRegistry.Permissions.PERMISSION_DUMP)
|
||||
.executes(c -> dump(c.getSource()))
|
||||
.then(args()
|
||||
.arg("computer", oneComputer())
|
||||
.executes(c -> dumpComputer(c.getSource(), getComputerArgument(c, "computer")))))
|
||||
.arg("computer", ComputerArgumentType.get())
|
||||
.executes(c -> dumpComputer(c.getSource(), ComputerArgumentType.getOne(c, "computer")))))
|
||||
|
||||
.then(command("shutdown")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
|
||||
.executes((c, a) -> shutdown(c.getSource(), unwrap(c.getSource(), a))))
|
||||
|
||||
.then(command("turn-on")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.argManyValue("computers", ComputerArgumentType.get(), ComputerSelector.all())
|
||||
.executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a))))
|
||||
|
||||
.then(command("tp")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TP)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(c -> teleport(c.getSource(), getComputerArgument(c, "computer"))))
|
||||
.arg("computer", ComputerArgumentType.get())
|
||||
.executes(c -> teleport(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
|
||||
|
||||
.then(command("queue")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_QUEUE)
|
||||
.arg(
|
||||
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
|
||||
RequiredArgumentBuilder.<CommandSourceStack, ComputerSelector>argument("computer", ComputerArgumentType.get())
|
||||
.suggests((context, builder) -> Suggestions.empty())
|
||||
)
|
||||
.argManyValue("args", StringArgumentType.string(), List.of())
|
||||
.executes((c, a) -> queue(getComputersArgument(c, "computer"), a)))
|
||||
.executes((c, a) -> queue(ComputerArgumentType.getMany(c, "computer"), a)))
|
||||
|
||||
.then(command("view")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_VIEW)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(c -> view(c.getSource(), getComputerArgument(c, "computer"))))
|
||||
.arg("computer", ComputerArgumentType.get())
|
||||
.executes(c -> view(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))
|
||||
|
||||
.then(choice("track")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TRACK)
|
||||
@ -332,29 +331,26 @@ public final class CommandComputerCraft {
|
||||
|
||||
// Additional helper functions.
|
||||
|
||||
private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer serverComputer, int computerId) {
|
||||
private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer computer, int computerId) {
|
||||
var out = Component.literal("");
|
||||
|
||||
// Append the computer instance
|
||||
if (serverComputer == null) {
|
||||
out.append(text("?"));
|
||||
// And instance
|
||||
if (computer == null) {
|
||||
out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY));
|
||||
} else {
|
||||
out.append(link(
|
||||
text(Integer.toString(serverComputer.getInstanceID())),
|
||||
"/computercraft dump " + serverComputer.getInstanceID(),
|
||||
text("#" + computerId + " ").append(coloured("(instance " + computer.getInstanceID() + ")", ChatFormatting.GRAY)),
|
||||
makeComputerCommand("dump", computer),
|
||||
Component.translatable("commands.computercraft.dump.action")
|
||||
));
|
||||
}
|
||||
|
||||
// And ID
|
||||
out.append(" (id " + computerId + ")");
|
||||
|
||||
// And, if we're a player, some useful links
|
||||
if (serverComputer != null && isPlayer(source)) {
|
||||
if (computer != null && isPlayer(source)) {
|
||||
if (ModRegistry.Permissions.PERMISSION_TP.test(source)) {
|
||||
out.append(" ").append(link(
|
||||
text("\u261b"),
|
||||
"/computercraft tp " + serverComputer.getInstanceID(),
|
||||
makeComputerCommand("tp", computer),
|
||||
Component.translatable("commands.computercraft.tp.action")
|
||||
));
|
||||
}
|
||||
@ -362,7 +358,7 @@ public final class CommandComputerCraft {
|
||||
if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) {
|
||||
out.append(" ").append(link(
|
||||
text("\u20e2"),
|
||||
"/computercraft view " + serverComputer.getInstanceID(),
|
||||
makeComputerCommand("view", computer),
|
||||
Component.translatable("commands.computercraft.view.action")
|
||||
));
|
||||
}
|
||||
@ -376,11 +372,15 @@ public final class CommandComputerCraft {
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String makeComputerCommand(String command, ServerComputer computer) {
|
||||
return String.format("/computercraft %s @c[instance=%d]", command, computer.getInstanceID());
|
||||
}
|
||||
|
||||
private static Component linkPosition(CommandSourceStack context, ServerComputer computer) {
|
||||
if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
|
||||
return link(
|
||||
position(computer.getPosition()),
|
||||
"/computercraft tp " + computer.getInstanceID(),
|
||||
makeComputerCommand("tp", computer),
|
||||
Component.translatable("commands.computercraft.tp.action")
|
||||
);
|
||||
} else {
|
||||
@ -431,4 +431,10 @@ public final class CommandComputerCraft {
|
||||
table.display(source);
|
||||
return timings.size();
|
||||
}
|
||||
|
||||
public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputerSelector> suppliers) {
|
||||
Set<ServerComputer> computers = new HashSet<>();
|
||||
for (var supplier : suppliers) supplier.find(source).forEach(computers::add);
|
||||
return computers;
|
||||
}
|
||||
}
|
||||
|
@ -28,12 +28,12 @@ public final class CommandUtils {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static CompletableFuture<Suggestions> suggestOnServer(CommandContext<?> context, Function<CommandContext<CommandSourceStack>, CompletableFuture<Suggestions>> supplier) {
|
||||
var source = context.getSource();
|
||||
if (!(source instanceof SharedSuggestionProvider)) {
|
||||
if (!(source instanceof SharedSuggestionProvider shared)) {
|
||||
return Suggestions.empty();
|
||||
} else if (source instanceof CommandSourceStack) {
|
||||
return supplier.apply((CommandContext<CommandSourceStack>) context);
|
||||
} else {
|
||||
return ((SharedSuggestionProvider) source).customSuggestion(context);
|
||||
return shared.customSuggestion(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ package dan200.computercraft.shared.command;
|
||||
import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType;
|
||||
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import net.minecraft.commands.arguments.selector.EntitySelectorParser;
|
||||
import net.minecraft.commands.arguments.selector.options.EntitySelectorOptions;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class Exceptions {
|
||||
@ -20,6 +22,13 @@ public final class Exceptions {
|
||||
|
||||
public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated("argument.computercraft.argument_expected");
|
||||
|
||||
public static final DynamicCommandExceptionType UNKNOWN_FAMILY = translated1("argument.computercraft.unknown_computer_family");
|
||||
|
||||
public static final DynamicCommandExceptionType ERROR_EXPECTED_OPTION_VALUE = EntitySelectorParser.ERROR_EXPECTED_OPTION_VALUE;
|
||||
public static final SimpleCommandExceptionType ERROR_EXPECTED_END_OF_OPTIONS = EntitySelectorParser.ERROR_EXPECTED_END_OF_OPTIONS;
|
||||
public static final DynamicCommandExceptionType ERROR_UNKNOWN_OPTION = EntitySelectorOptions.ERROR_UNKNOWN_OPTION;
|
||||
public static final DynamicCommandExceptionType ERROR_INAPPLICABLE_OPTION = EntitySelectorOptions.ERROR_INAPPLICABLE_OPTION;
|
||||
|
||||
private static SimpleCommandExceptionType translated(String key) {
|
||||
return new SimpleCommandExceptionType(Component.translatable(key));
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.mojang.brigadier.StringReader;
|
||||
|
||||
final class ArgumentParserUtils {
|
||||
private ArgumentParserUtils() {
|
||||
}
|
||||
|
||||
public static boolean consume(StringReader reader, char lookahead) {
|
||||
if (!reader.canRead() || reader.peek() != lookahead) return false;
|
||||
|
||||
reader.skip();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean consume(StringReader reader, String lookahead) {
|
||||
if (!reader.canRead(lookahead.length())) return false;
|
||||
for (var i = 0; i < lookahead.length(); i++) {
|
||||
if (reader.peek(i) != lookahead.charAt(i)) return false;
|
||||
}
|
||||
|
||||
reader.setCursor(reader.getCursor() + lookahead.length());
|
||||
return true;
|
||||
}
|
||||
}
|
@ -14,66 +14,58 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY;
|
||||
|
||||
public final class ComputerArgumentType implements ArgumentType<ComputerArgumentType.ComputerSupplier> {
|
||||
public final class ComputerArgumentType implements ArgumentType<ComputerSelector> {
|
||||
private static final ComputerArgumentType INSTANCE = new ComputerArgumentType();
|
||||
|
||||
public static ComputerArgumentType oneComputer() {
|
||||
return INSTANCE;
|
||||
}
|
||||
private static final List<String> EXAMPLES = List.of(
|
||||
"0", "123", "@c[instance_id=123]"
|
||||
);
|
||||
|
||||
public static ServerComputer getComputerArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputerSupplier.class).unwrap(context.getSource());
|
||||
public static ComputerArgumentType get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private ComputerArgumentType() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a list of computers from a {@link CommandContext} argument.
|
||||
*
|
||||
* @param context The current command context.
|
||||
* @param name The name of the argument.
|
||||
* @return The found computer(s).
|
||||
*/
|
||||
public static List<ServerComputer> getMany(CommandContext<CommandSourceStack> context, String name) {
|
||||
return context.getArgument(name, ComputerSelector.class).find(context.getSource()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a single computer from a {@link CommandContext} argument.
|
||||
*
|
||||
* @param context The current command context.
|
||||
* @param name The name of the argument.
|
||||
* @return The found computer.
|
||||
* @throws CommandSyntaxException If exactly one computer could not be found.
|
||||
*/
|
||||
public static ServerComputer getOne(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputerSelector.class).findOne(context.getSource());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputerSupplier parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var supplier = ComputersArgumentType.someComputers().parse(reader);
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
|
||||
return s -> {
|
||||
var computers = supplier.unwrap(s);
|
||||
|
||||
if (computers.size() == 1) return computers.iterator().next();
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var first = true;
|
||||
for (var computer : computers) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(computer.getInstanceID());
|
||||
}
|
||||
|
||||
|
||||
// We have an incorrect number of computers: reset and throw an error
|
||||
reader.setCursor(start);
|
||||
throw COMPUTER_ARG_MANY.createWithContext(reader, selector, builder.toString());
|
||||
};
|
||||
public ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
|
||||
return ComputerSelector.parse(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
return ComputersArgumentType.someComputers().listSuggestions(context, builder);
|
||||
return ComputerSelector.suggest(context, builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return ComputersArgumentType.someComputers().getExamples();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ComputerSupplier {
|
||||
ServerComputer unwrap(CommandSourceStack source) throws CommandSyntaxException;
|
||||
return EXAMPLES;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,374 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.mojang.brigadier.Message;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.advancements.critereon.MinMaxBounds;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.*;
|
||||
import static dan200.computercraft.shared.command.arguments.ArgumentParserUtils.consume;
|
||||
|
||||
public record ComputerSelector(
|
||||
String selector,
|
||||
OptionalInt instanceId,
|
||||
OptionalInt computerId,
|
||||
@Nullable String label,
|
||||
@Nullable ComputerFamily family,
|
||||
@Nullable AABB bounds,
|
||||
@Nullable MinMaxBounds.Doubles range
|
||||
) {
|
||||
private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), OptionalInt.empty(), null, null, null, null);
|
||||
|
||||
/**
|
||||
* A {@link ComputerSelector} which matches all computers.
|
||||
*
|
||||
* @return A {@link ComputerSelector} instance.
|
||||
*/
|
||||
public static ComputerSelector all() {
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all computers matching this selector.
|
||||
*
|
||||
* @param source The source requesting these computers.
|
||||
* @return The stream of matching computers.
|
||||
*/
|
||||
public Stream<ServerComputer> find(CommandSourceStack source) {
|
||||
var context = ServerContext.get(source.getServer());
|
||||
if (instanceId.isPresent()) {
|
||||
var computer = context.registry().get(instanceId.getAsInt());
|
||||
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
|
||||
}
|
||||
|
||||
return context.registry().getComputers().stream().filter(c -> matches(source, c));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find exactly one computer which matches this selector.
|
||||
*
|
||||
* @param source The source requesting this computer.
|
||||
* @return The computer.
|
||||
* @throws CommandSyntaxException If no or multiple computers could be found.
|
||||
*/
|
||||
public ServerComputer findOne(CommandSourceStack source) throws CommandSyntaxException {
|
||||
var computers = find(source).toList();
|
||||
if (computers.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
|
||||
if (computers.size() == 1) return computers.iterator().next();
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var first = true;
|
||||
for (var computer : computers) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(computer.getInstanceID());
|
||||
}
|
||||
|
||||
|
||||
// We have an incorrect number of computers: throw an error
|
||||
throw COMPUTER_ARG_MANY.create(selector, builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this selector matches a given computer.
|
||||
*
|
||||
* @param source The command source, used for distance comparisons.
|
||||
* @param computer The computer to check.
|
||||
* @return If this computer is matched by the selector.
|
||||
*/
|
||||
public boolean matches(CommandSourceStack source, ServerComputer computer) {
|
||||
return (instanceId().isEmpty() || computer.getInstanceID() == instanceId().getAsInt())
|
||||
&& (computerId().isEmpty() || computer.getID() == computerId().getAsInt())
|
||||
&& (label == null || Objects.equals(computer.getLabel(), label))
|
||||
&& (family == null || computer.getFamily() == family)
|
||||
&& (bounds == null || (source.getLevel() == computer.getLevel() && bounds.contains(Vec3.atCenterOf(computer.getPosition()))))
|
||||
&& (range == null || (source.getLevel() == computer.getLevel() && range.matchesSqr(source.getPosition().distanceToSqr(Vec3.atCenterOf(computer.getPosition())))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an input string.
|
||||
*
|
||||
* @param reader The reader to parse from.
|
||||
* @return The parsed selector.
|
||||
* @throws CommandSyntaxException If the selector was incomplete or malformed.
|
||||
*/
|
||||
public static ComputerSelector parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
|
||||
var builder = new Builder();
|
||||
|
||||
if (consume(reader, "@c[")) {
|
||||
parseSelector(builder, reader);
|
||||
} else {
|
||||
var kind = reader.peek();
|
||||
if (kind == '@') {
|
||||
reader.skip();
|
||||
builder.label = reader.readString();
|
||||
} else if (kind == '~') {
|
||||
reader.skip();
|
||||
builder.family = parseFamily(reader);
|
||||
} else if (kind == '#') {
|
||||
reader.skip();
|
||||
builder.computerId = OptionalInt.of(reader.readInt());
|
||||
} else {
|
||||
builder.instanceId = OptionalInt.of(reader.readInt());
|
||||
}
|
||||
}
|
||||
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
return new ComputerSelector(selector, builder.instanceId, builder.computerId, builder.label, builder.family, builder.bounds, builder.range);
|
||||
}
|
||||
|
||||
private static void parseSelector(Builder builder, StringReader reader) throws CommandSyntaxException {
|
||||
Set<Option> seenOptions = new HashSet<>();
|
||||
while (true) {
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (!reader.canRead()) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
|
||||
if (consume(reader, ']')) break;
|
||||
|
||||
// Read the option and validate it.
|
||||
var option = parseOption(reader, seenOptions);
|
||||
reader.skipWhitespace();
|
||||
if (!consume(reader, '=')) throw ERROR_EXPECTED_OPTION_VALUE.createWithContext(reader, option.name());
|
||||
reader.skipWhitespace();
|
||||
option.parser.parse(reader, builder);
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (consume(reader, ']')) break;
|
||||
if (!consume(reader, ',')) throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private static Option parseOption(StringReader reader, Set<Option> seen) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var name = reader.readUnquotedString();
|
||||
var option = options.get(name);
|
||||
if (option == null) {
|
||||
reader.setCursor(start);
|
||||
throw ERROR_UNKNOWN_OPTION.createWithContext(reader, name);
|
||||
} else if (!seen.add(option)) {
|
||||
throw ERROR_INAPPLICABLE_OPTION.createWithContext(reader, name);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
private static ComputerFamily parseFamily(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var name = reader.readUnquotedString();
|
||||
var family = Arrays.stream(ComputerFamily.values()).filter(x -> x.name().equalsIgnoreCase(name)).findFirst().orElse(null);
|
||||
if (family == null) {
|
||||
reader.setCursor(start);
|
||||
throw UNKNOWN_FAMILY.createWithContext(reader, name);
|
||||
}
|
||||
|
||||
return family;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest completions for a selector argument.
|
||||
*
|
||||
* @param context The current command context.
|
||||
* @param builder The builder containing the current input.
|
||||
* @return The possible suggestions.
|
||||
*/
|
||||
public static CompletableFuture<Suggestions> suggest(CommandContext<?> context, SuggestionsBuilder builder) {
|
||||
var remaining = builder.getRemaining();
|
||||
|
||||
if (remaining.startsWith("@")) {
|
||||
var reader = new StringReader(builder.getInput());
|
||||
reader.setCursor(builder.getStart());
|
||||
return suggestSelector(context, reader);
|
||||
} else if (remaining.startsWith("#")) {
|
||||
return suggestComputers(c -> "#" + c.getID()).suggest(context, builder);
|
||||
} else {
|
||||
return suggestComputers(c -> Integer.toString(c.getInstanceID())).suggest(context, builder);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestSelector(CommandContext<?> context, StringReader reader) {
|
||||
Set<Option> seenOptions = new HashSet<>();
|
||||
var builder = new Builder();
|
||||
|
||||
if (!consume(reader, "@c[")) return suggestions(reader).suggest("@c[").buildFuture();
|
||||
|
||||
while (true) {
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (!reader.canRead()) return suggestOptions(reader);
|
||||
if (consume(reader, ']')) break;
|
||||
|
||||
// Read the option and validate it.
|
||||
Option option;
|
||||
try {
|
||||
option = parseOption(reader, seenOptions);
|
||||
} catch (CommandSyntaxException e) {
|
||||
return suggestOptions(reader);
|
||||
}
|
||||
reader.skipWhitespace();
|
||||
if (!consume(reader, '=')) return suggestions(reader).suggest("=").buildFuture();
|
||||
reader.skipWhitespace();
|
||||
try {
|
||||
option.parser.parse(reader, builder);
|
||||
} catch (CommandSyntaxException e) {
|
||||
return option.suggest.suggest(context, suggestions(reader));
|
||||
}
|
||||
reader.skipWhitespace();
|
||||
|
||||
if (consume(reader, ']')) break;
|
||||
if (!consume(reader, ',')) return suggestions(reader).suggest(",").buildFuture();
|
||||
}
|
||||
|
||||
return Suggestions.empty();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestOptions(StringReader reader) {
|
||||
return SharedSuggestionProvider.suggest(options().values(), suggestions(reader), Option::name, Option::tooltip);
|
||||
}
|
||||
|
||||
private static SuggestionsBuilder suggestions(StringReader reader) {
|
||||
return new SuggestionsBuilder(reader.getString(), reader.getCursor());
|
||||
}
|
||||
|
||||
private static final class Builder {
|
||||
private OptionalInt instanceId = OptionalInt.empty();
|
||||
private OptionalInt computerId = OptionalInt.empty();
|
||||
private @Nullable String label;
|
||||
private @Nullable ComputerFamily family;
|
||||
private @Nullable AABB bounds;
|
||||
private @Nullable MinMaxBounds.Doubles range;
|
||||
}
|
||||
|
||||
private static final Map<String, Option> options;
|
||||
|
||||
/**
|
||||
* Get a map of individual selector options.
|
||||
*
|
||||
* @return The available options.
|
||||
*/
|
||||
public static Map<String, Option> options() {
|
||||
return options;
|
||||
}
|
||||
|
||||
static {
|
||||
var optionList = new Option[]{
|
||||
new Option(
|
||||
"instance",
|
||||
(reader, builder) -> builder.instanceId = OptionalInt.of(reader.readInt()),
|
||||
suggestComputers(c -> Integer.toString(c.getInstanceID()))
|
||||
),
|
||||
new Option(
|
||||
"id",
|
||||
(reader, builder) -> builder.computerId = OptionalInt.of(reader.readInt()),
|
||||
suggestComputers(c -> Integer.toString(c.getID()))
|
||||
),
|
||||
new Option(
|
||||
"label",
|
||||
(reader, builder) -> builder.label = reader.readQuotedString(),
|
||||
suggestComputers(ServerComputer::getLabel)
|
||||
),
|
||||
new Option(
|
||||
"family",
|
||||
(reader, builder) -> builder.family = parseFamily(reader),
|
||||
(source, builder) -> SharedSuggestionProvider.suggest(Arrays.stream(ComputerFamily.values()).map(x -> x.name().toLowerCase(Locale.ROOT)), builder)
|
||||
),
|
||||
new Option(
|
||||
"distance",
|
||||
(reader, builder) -> builder.range = MinMaxBounds.Doubles.fromReader(reader),
|
||||
(source, builder) -> Suggestions.empty()
|
||||
),
|
||||
};
|
||||
|
||||
options = Arrays.stream(optionList).collect(Collectors.toUnmodifiableMap(Option::name, x -> x));
|
||||
}
|
||||
|
||||
/**
|
||||
* A single option to filter a computer by.
|
||||
*/
|
||||
public static final class Option {
|
||||
private final String name;
|
||||
private final Parser parser;
|
||||
private final SuggestionProvider suggest;
|
||||
private final String translationKey;
|
||||
private final Message tooltip;
|
||||
|
||||
|
||||
Option(String name, Parser parser, SuggestionProvider suggest) {
|
||||
this.name = name;
|
||||
this.parser = parser;
|
||||
this.suggest = suggest;
|
||||
tooltip = Component.translatable(translationKey = "argument.computercraft.computer." + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of this selector.
|
||||
*
|
||||
* @return The selector's name.
|
||||
*/
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Message tooltip() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* The translation key for this selector.
|
||||
*
|
||||
* @return The selector's translation key.
|
||||
*/
|
||||
public String translationKey() {
|
||||
return translationKey;
|
||||
}
|
||||
}
|
||||
|
||||
private interface Parser {
|
||||
void parse(StringReader reader, Builder builder) throws CommandSyntaxException;
|
||||
}
|
||||
|
||||
private interface SuggestionProvider {
|
||||
CompletableFuture<Suggestions> suggest(CommandContext<?> source, SuggestionsBuilder builder);
|
||||
}
|
||||
|
||||
private static SuggestionProvider suggestComputers(Function<ServerComputer, String> renderer) {
|
||||
return (anyContext, builder) -> suggestOnServer(anyContext, context -> {
|
||||
var remaining = builder.getRemaining();
|
||||
for (var computer : ServerContext.get(context.getSource().getServer()).registry().getComputers()) {
|
||||
var converted = renderer.apply(computer);
|
||||
if (converted != null && converted.startsWith(remaining)) {
|
||||
builder.suggest(converted);
|
||||
}
|
||||
}
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggest;
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_NONE;
|
||||
|
||||
public final class ComputersArgumentType implements ArgumentType<ComputersArgumentType.ComputersSupplier> {
|
||||
private static final ComputersArgumentType MANY = new ComputersArgumentType(false);
|
||||
private static final ComputersArgumentType SOME = new ComputersArgumentType(true);
|
||||
|
||||
private static final List<String> EXAMPLES = List.of(
|
||||
"0", "#0", "@Label", "~Advanced"
|
||||
);
|
||||
|
||||
public static ComputersArgumentType manyComputers() {
|
||||
return MANY;
|
||||
}
|
||||
|
||||
public static ComputersArgumentType someComputers() {
|
||||
return SOME;
|
||||
}
|
||||
|
||||
public static Collection<ServerComputer> getComputersArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputersSupplier.class).unwrap(context.getSource());
|
||||
}
|
||||
|
||||
private final boolean requireSome;
|
||||
|
||||
private ComputersArgumentType(boolean requireSome) {
|
||||
this.requireSome = requireSome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersSupplier parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var kind = reader.peek();
|
||||
ComputersSupplier computers;
|
||||
if (kind == '@') {
|
||||
reader.skip();
|
||||
var label = reader.readUnquotedString();
|
||||
computers = getComputers(x -> Objects.equals(label, x.getLabel()));
|
||||
} else if (kind == '~') {
|
||||
reader.skip();
|
||||
var family = reader.readUnquotedString();
|
||||
computers = getComputers(x -> x.getFamily().name().equalsIgnoreCase(family));
|
||||
} else if (kind == '#') {
|
||||
reader.skip();
|
||||
var id = reader.readInt();
|
||||
computers = getComputers(x -> x.getID() == id);
|
||||
} else {
|
||||
var instance = reader.readInt();
|
||||
computers = s -> {
|
||||
var computer = ServerContext.get(s.getServer()).registry().get(instance);
|
||||
return computer == null ? List.of() : List.of(computer);
|
||||
};
|
||||
}
|
||||
|
||||
if (requireSome) {
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
return source -> {
|
||||
var matched = computers.unwrap(source);
|
||||
if (matched.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
|
||||
return matched;
|
||||
};
|
||||
} else {
|
||||
return computers;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
var remaining = builder.getRemaining();
|
||||
|
||||
// We can run this one on the client, for obvious reasons.
|
||||
if (remaining.startsWith("~")) {
|
||||
return suggest(builder, ComputerFamily.values(), x -> "~" + x.name());
|
||||
}
|
||||
|
||||
// Verify we've a command source and we're running on the server
|
||||
return suggestOnServer(context, s -> {
|
||||
if (remaining.startsWith("@")) {
|
||||
suggestComputers(s.getSource(), builder, remaining, x -> {
|
||||
var label = x.getLabel();
|
||||
return label == null ? null : "@" + label;
|
||||
});
|
||||
} else if (remaining.startsWith("#")) {
|
||||
suggestComputers(s.getSource(), builder, remaining, c -> "#" + c.getID());
|
||||
} else {
|
||||
suggestComputers(s.getSource(), builder, remaining, c -> Integer.toString(c.getInstanceID()));
|
||||
}
|
||||
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return EXAMPLES;
|
||||
}
|
||||
|
||||
private static void suggestComputers(CommandSourceStack source, SuggestionsBuilder builder, String remaining, Function<ServerComputer, String> renderer) {
|
||||
remaining = remaining.toLowerCase(Locale.ROOT);
|
||||
for (var computer : ServerContext.get(source.getServer()).registry().getComputers()) {
|
||||
var converted = renderer.apply(computer);
|
||||
if (converted != null && converted.toLowerCase(Locale.ROOT).startsWith(remaining)) {
|
||||
builder.suggest(converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ComputersSupplier getComputers(Predicate<ServerComputer> predicate) {
|
||||
return s -> ServerContext.get(s.getServer()).registry()
|
||||
.getComputers()
|
||||
.stream()
|
||||
.filter(predicate)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static class Info implements ArgumentTypeInfo<ComputersArgumentType, Template> {
|
||||
@Override
|
||||
public void serializeToNetwork(ComputersArgumentType.Template arg, FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(arg.requireSome());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersArgumentType.Template deserializeFromNetwork(FriendlyByteBuf buf) {
|
||||
var requiresSome = buf.readBoolean();
|
||||
return new ComputersArgumentType.Template(this, requiresSome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serializeToJson(ComputersArgumentType.Template arg, JsonObject json) {
|
||||
json.addProperty("requireSome", arg.requireSome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersArgumentType.Template unpack(ComputersArgumentType argumentType) {
|
||||
return new ComputersArgumentType.Template(this, argumentType.requireSome);
|
||||
}
|
||||
}
|
||||
|
||||
public record Template(Info info, boolean requireSome) implements ArgumentTypeInfo.Template<ComputersArgumentType> {
|
||||
@Override
|
||||
public ComputersArgumentType instantiate(CommandBuildContext context) {
|
||||
return requireSome ? SOME : MANY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Info type() {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ComputersSupplier {
|
||||
Collection<ServerComputer> unwrap(CommandSourceStack source) throws CommandSyntaxException;
|
||||
}
|
||||
|
||||
public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputersSupplier> suppliers) throws CommandSyntaxException {
|
||||
Set<ServerComputer> computers = new HashSet<>();
|
||||
for (var supplier : suppliers) computers.addAll(supplier.unwrap(source));
|
||||
return computers;
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.context.StringRange;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestion;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.command.Exceptions;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
|
||||
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
|
||||
class ComputerSelectorTest {
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("getArgumentTestCases")
|
||||
public void Parse_basic_inputs(String input, ComputerSelector expected) throws CommandSyntaxException {
|
||||
assertEquals(expected, ComputerSelector.parse(new StringReader(input)));
|
||||
}
|
||||
|
||||
public static Arguments[] getArgumentTestCases() {
|
||||
return new Arguments[]{
|
||||
// Legacy selectors
|
||||
Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), OptionalInt.empty(), "some_label", null, null, null)),
|
||||
Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
|
||||
Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)),
|
||||
Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), OptionalInt.empty(), null, null, null, null)),
|
||||
// New selectors
|
||||
Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), OptionalInt.empty(), null, null, null, null)),
|
||||
Arguments.of("@c[instance=123]", new ComputerSelector("@c[instance=123]", OptionalInt.of(123), OptionalInt.empty(), null, null, null, null)),
|
||||
Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)),
|
||||
Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), OptionalInt.empty(), "foo", null, null, null)),
|
||||
Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
|
||||
// Complex selectors
|
||||
Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)),
|
||||
Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)),
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Fails_on_repeated_options() {
|
||||
var error = assertThrows(CommandSyntaxException.class, () -> ComputerSelector.parse(new StringReader("@c[id=1, id=2]")));
|
||||
assertEquals(Exceptions.ERROR_INAPPLICABLE_OPTION, error.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Complete_selector_components() {
|
||||
assertEquals(List.of(new Suggestion(StringRange.between(0, 1), "@c[")), suggest("@"));
|
||||
assertThat(suggest("@c["), hasItem(
|
||||
new Suggestion(StringRange.at(3), "family", ComputerSelector.options().get("family").tooltip())
|
||||
));
|
||||
assertEquals(List.of(new Suggestion(StringRange.at(9), "=")), suggest("@c[family"));
|
||||
assertEquals(List.of(new Suggestion(StringRange.at(16), ",")), suggest("@c[family=normal"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Complete_selector_family() {
|
||||
assertThat(suggest("@c[family="), containsInAnyOrder(
|
||||
new Suggestion(StringRange.at(10), "normal"),
|
||||
new Suggestion(StringRange.at(10), "advanced"),
|
||||
new Suggestion(StringRange.at(10), "command")
|
||||
));
|
||||
assertThat(suggest("@c[family=n"), contains(
|
||||
new Suggestion(StringRange.between(10, 11), "normal")
|
||||
));
|
||||
}
|
||||
|
||||
private List<Suggestion> suggest(String input) {
|
||||
var context = new CommandContext<>(new Object(), "", Map.of(), null, null, List.of(), StringRange.at(0), null, null, false);
|
||||
return ComputerSelector.suggest(context, new SuggestionsBuilder(input, 0)).getNow(null).getList();
|
||||
}
|
||||
}
|
@ -1,8 +1,14 @@
|
||||
{
|
||||
"argument.computercraft.argument_expected": "Argument expected",
|
||||
"argument.computercraft.computer.distance": "Distance to entity",
|
||||
"argument.computercraft.computer.family": "Computer family",
|
||||
"argument.computercraft.computer.id": "Computer ID",
|
||||
"argument.computercraft.computer.instance": "Unique instance ID",
|
||||
"argument.computercraft.computer.label": "Computer label",
|
||||
"argument.computercraft.computer.many_matching": "Multiple computers matching '%s' (instances %s)",
|
||||
"argument.computercraft.computer.no_matching": "No computers matching '%s'",
|
||||
"argument.computercraft.tracking_field.no_field": "Unknown field '%s'",
|
||||
"argument.computercraft.unknown_computer_family": "Unknown computer family '%s'",
|
||||
"block.computercraft.cable": "Networking Cable",
|
||||
"block.computercraft.computer_advanced": "Advanced Computer",
|
||||
"block.computercraft.computer_command": "Command Computer",
|
||||
|
@ -1,8 +1,14 @@
|
||||
{
|
||||
"argument.computercraft.argument_expected": "Argument expected",
|
||||
"argument.computercraft.computer.distance": "Distance to entity",
|
||||
"argument.computercraft.computer.family": "Computer family",
|
||||
"argument.computercraft.computer.id": "Computer ID",
|
||||
"argument.computercraft.computer.instance": "Unique instance ID",
|
||||
"argument.computercraft.computer.label": "Computer label",
|
||||
"argument.computercraft.computer.many_matching": "Multiple computers matching '%s' (instances %s)",
|
||||
"argument.computercraft.computer.no_matching": "No computers matching '%s'",
|
||||
"argument.computercraft.tracking_field.no_field": "Unknown field '%s'",
|
||||
"argument.computercraft.unknown_computer_family": "Unknown computer family '%s'",
|
||||
"block.computercraft.cable": "Networking Cable",
|
||||
"block.computercraft.computer_advanced": "Advanced Computer",
|
||||
"block.computercraft.computer_command": "Command Computer",
|
||||
|
Loading…
Reference in New Issue
Block a user