1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-11-01 06:03:00 +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:
Jonathan Coates
2024-03-12 20:12:13 +00:00
parent cb8e06af2a
commit b7df91349a
16 changed files with 737 additions and 264 deletions

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
});
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}