// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers // // SPDX-License-Identifier: MPL-2.0 package dan200.computercraft.shared.command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.exceptions.CommandSyntaxException; 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.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; import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; import dan200.computercraft.shared.computer.metrics.basic.Aggregate; 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; import net.minecraft.world.MenuProvider; import net.minecraft.world.entity.RelativeMovement; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import javax.annotation.Nullable; import java.io.File; 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.TrackingFieldArgumentType.metric; import static dan200.computercraft.shared.command.builder.CommandBuilder.args; import static dan200.computercraft.shared.command.builder.CommandBuilder.command; import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice; import static dan200.computercraft.shared.command.text.ChatHelpers.*; import static net.minecraft.commands.Commands.literal; public final class CommandComputerCraft { public static final UUID SYSTEM_UUID = new UUID(0, 0); /** * The client-side command to open the folder. Ideally this would live under the main {@code computercraft} * namespace, but unfortunately that overrides commands, rather than merging them. */ public static final String CLIENT_OPEN_FOLDER = "computercraft-computer-folder"; private CommandComputerCraft() { } public static void register(CommandDispatcher dispatcher) { dispatcher.register(choice("computercraft") .then(literal("dump") .requires(ModRegistry.Permissions.PERMISSION_DUMP) .executes(c -> dump(c.getSource())) .then(args() .arg("computer", ComputerArgumentType.get()) .executes(c -> dumpComputer(c.getSource(), ComputerArgumentType.getOne(c, "computer"))))) .then(command("shutdown") .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN) .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", ComputerArgumentType.get(), ComputerSelector.all()) .executes((c, a) -> turnOn(c.getSource(), unwrap(c.getSource(), a)))) .then(command("tp") .requires(ModRegistry.Permissions.PERMISSION_TP) .arg("computer", ComputerArgumentType.get()) .executes(c -> teleport(c.getSource(), ComputerArgumentType.getOne(c, "computer")))) .then(command("queue") .requires(ModRegistry.Permissions.PERMISSION_QUEUE) .arg( RequiredArgumentBuilder.argument("computer", ComputerArgumentType.get()) .suggests((context, builder) -> Suggestions.empty()) ) .argManyValue("args", StringArgumentType.string(), List.of()) .executes((c, a) -> queue(ComputerArgumentType.getMany(c, "computer"), a))) .then(command("view") .requires(ModRegistry.Permissions.PERMISSION_VIEW) .arg("computer", ComputerArgumentType.get()) .executes(c -> view(c.getSource(), ComputerArgumentType.getOne(c, "computer")))) .then(choice("track") .requires(ModRegistry.Permissions.PERMISSION_TRACK) .then(command("start").executes(c -> trackStart(c.getSource()))) .then(command("stop").executes(c -> trackStop(c.getSource()))) .then(command("dump") .argManyValue("fields", metric(), DEFAULT_FIELDS) .executes((c, f) -> trackDump(c.getSource(), f)))) ); } /** * Display loaded computers to a table. * * @param source The thing that executed this command. * @return The number of loaded computers. */ private static int dump(CommandSourceStack source) { var table = new TableBuilder("DumpAll", "Computer", "On", "Position"); List computers = new ArrayList<>(ServerContext.get(source.getServer()).registry().getComputers()); Level world = source.getLevel(); var pos = BlockPos.containing(source.getPosition()); // Sort by nearby computers. computers.sort((a, b) -> { if (a.getLevel() == b.getLevel() && a.getLevel() == world) { return Double.compare(a.getPosition().distSqr(pos), b.getPosition().distSqr(pos)); } else if (a.getLevel() == world) { return -1; } else if (b.getLevel() == world) { return 1; } else { return a.getInstanceUUID().compareTo(b.getInstanceUUID()); } }); for (var computer : computers) { table.row( linkComputer(source, computer, computer.getID()), bool(computer.isOn()), linkPosition(source, computer) ); } table.display(source); return computers.size(); } /** * Display additional information about a single computer. * * @param source The thing that executed this command. * @param computer The computer we're dumping. * @return The constant {@code 1}. */ private static int dumpComputer(CommandSourceStack source, ServerComputer computer) { var table = new TableBuilder("Dump"); table.row(header("Instance UUID"), text(computer.getInstanceUUID().toString())); table.row(header("Id"), text(Integer.toString(computer.getID()))); table.row(header("Label"), text(computer.getLabel())); table.row(header("On"), bool(computer.isOn())); table.row(header("Position"), linkPosition(source, computer)); table.row(header("Family"), text(computer.getFamily().toString())); for (var side : ComputerSide.values()) { var peripheral = computer.getPeripheral(side); if (peripheral != null) { table.row(header("Peripheral " + side.getName()), text(peripheral.getType())); } } table.display(source); return 1; } /** * Shutdown a list of computers. * * @param source The thing that executed this command. * @param computers The computers to shutdown. * @return The constant {@code 1}. */ private static int shutdown(CommandSourceStack source, Collection computers) { var shutdown = 0; for (var computer : computers) { if (computer.isOn()) shutdown++; computer.shutdown(); } var didShutdown = shutdown; source.sendSuccess(() -> Component.translatable("commands.computercraft.shutdown.done", didShutdown, computers.size()), false); return shutdown; } /** * Turn on a list of computers. * * @param source The thing that executed this command. * @param computers The computers to turn on. * @return The constant {@code 1}. */ private static int turnOn(CommandSourceStack source, Collection computers) { var on = 0; for (var computer : computers) { if (!computer.isOn()) on++; computer.turnOn(); } var didOn = on; source.sendSuccess(() -> Component.translatable("commands.computercraft.turn_on.done", didOn, computers.size()), false); return on; } /** * Teleport to a computer. * * @param source The thing that executed this command. This must be an entity, other types will throw an exception. * @param computer The computer to teleport to. * @return The constant {@code 1}. */ private static int teleport(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException { var world = computer.getLevel(); var pos = Vec3.atBottomCenterOf(computer.getPosition()); source.getEntityOrException().teleportTo(world, pos.x(), pos.y(), pos.z(), EnumSet.noneOf(RelativeMovement.class), 0, 0); return 1; } /** * Queue a {@code computer_command} event on a command computer. * * @param computers The list of computers to queue on. * @param args The arguments for this event. * @return The number of computers this event was queued on. */ private static int queue(Collection computers, List args) { var rest = args.toArray(); var queued = 0; for (var computer : computers) { if (computer.getFamily() == ComputerFamily.COMMAND && computer.isOn()) { computer.queueEvent("computer_command", rest); queued++; } } return queued; } /** * Open a terminal for a computer. * * @param source The thing that executed this command. * @param computer The computer to view. * @return The constant {@code 1}. */ private static int view(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException { var player = source.getPlayerOrException(); new ComputerContainerData(computer, new ItemStack(ModRegistry.Items.COMPUTER_NORMAL.get())).open(player, new MenuProvider() { @Override public Component getDisplayName() { return Component.translatable("gui.computercraft.view_computer"); } @Override public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) { return new ViewComputerMenu(id, player, computer); } }); return 1; } /** * Start tracking metrics for the current player. * * @param source The thing that executed this command. * @return The constant {@code 1}. */ private static int trackStart(CommandSourceStack source) { getMetricsInstance(source).start(); var stopCommand = "/computercraft track stop"; source.sendSuccess(() -> Component.translatable( "commands.computercraft.track.start.stop", link(text(stopCommand), stopCommand, Component.translatable("commands.computercraft.track.stop.action")) ), false); return 1; } /** * Stop tracking metrics for the current player, displaying a table with the results. * * @param source The thing that executed this command. * @return The constant {@code 1}. */ private static int trackStop(CommandSourceStack source) throws CommandSyntaxException { var metrics = getMetricsInstance(source); if (!metrics.stop()) throw NOT_TRACKING_EXCEPTION.create(); displayTimings(source, metrics.getSnapshot(), new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG), DEFAULT_FIELDS); return 1; } private static final List DEFAULT_FIELDS = List.of( new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.COUNT), new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.NONE), new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG) ); /** * Display the latest metrics for the current player. * * @param source The thing that executed this command. * @param fields The fields to display in this table, defaulting to {@link #DEFAULT_FIELDS}. * @return The constant {@code 1}. */ private static int trackDump(CommandSourceStack source, List fields) throws CommandSyntaxException { AggregatedMetric sort; if (fields.size() == 1 && DEFAULT_FIELDS.contains(fields.get(0))) { sort = fields.get(0); fields = DEFAULT_FIELDS; } else { sort = fields.get(0); } return displayTimings(source, getMetricsInstance(source).getTimings(), sort, fields); } // Additional helper functions. private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer computer, int computerId) { var out = Component.literal(""); // And instance if (computer == null) { out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY)); } else { out.append(makeComputerDumpCommand(computer)); } // And, if we're a player, some useful links if (computer != null && isPlayer(source)) { if (ModRegistry.Permissions.PERMISSION_TP.test(source)) { out.append(" ").append(link( text("\u261b"), makeComputerCommand("tp", computer), Component.translatable("commands.computercraft.tp.action") )); } if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) { out.append(" ").append(link( text("\u20e2"), makeComputerCommand("view", computer), Component.translatable("commands.computercraft.view.action") )); } } if (isPlayer(source) && UserLevel.isOwner(source)) { var linkPath = linkStorage(source, computerId); if (linkPath != null) out.append(" ").append(linkPath); } return out; } private static Component linkPosition(CommandSourceStack context, ServerComputer computer) { if (ModRegistry.Permissions.PERMISSION_TP.test(context)) { return link( position(computer.getPosition()), makeComputerCommand("tp", computer), Component.translatable("commands.computercraft.tp.action") ); } else { return position(computer.getPosition()); } } private static @Nullable Component linkStorage(CommandSourceStack source, int id) { var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id); if (!file.isDirectory()) return null; return clientLink( text("\u270E"), "/" + CLIENT_OPEN_FOLDER + " " + id, Component.translatable("commands.computercraft.dump.open_path") ); } private static BasicComputerMetricsObserver getMetricsInstance(CommandSourceStack source) { var entity = source.getEntity(); return ServerContext.get(source.getServer()).metrics().getMetricsInstance(entity instanceof Player ? entity.getUUID() : SYSTEM_UUID); } private static int displayTimings(CommandSourceStack source, List timings, AggregatedMetric sortField, List fields) throws CommandSyntaxException { if (timings.isEmpty()) throw NO_TIMINGS_EXCEPTION.create(); timings.sort(Comparator.comparing(x -> x.get(sortField.metric(), sortField.aggregate())).reversed()); var headers = new Component[1 + fields.size()]; headers[0] = Component.translatable("commands.computercraft.track.dump.computer"); for (var i = 0; i < fields.size(); i++) headers[i + 1] = fields.get(i).displayName(); var table = new TableBuilder("Metrics", headers); for (var entry : timings) { var serverComputer = entry.computer(); var computerComponent = linkComputer(source, serverComputer, entry.computerId()); var row = new Component[1 + fields.size()]; row[0] = computerComponent; for (var i = 0; i < fields.size(); i++) { var metric = fields.get(i); row[i + 1] = text(entry.getFormatted(metric.metric(), metric.aggregate())); } table.row(row); } table.display(source); return timings.size(); } public static Set unwrap(CommandSourceStack source, Collection suppliers) { Set computers = new HashSet<>(); for (var supplier : suppliers) supplier.find(source).forEach(computers::add); return computers; } }