433 lines
18 KiB
Java
433 lines
18 KiB
Java
// 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<CommandSourceStack> 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.<CommandSourceStack, ComputerSelector>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<ServerComputer> 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<ServerComputer> 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<ServerComputer> 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<ServerComputer> computers, List<String> 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<AggregatedMetric> 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<AggregatedMetric> 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<ComputerMetrics> timings, AggregatedMetric sortField, List<AggregatedMetric> fields) throws CommandSyntaxException {
|
|
if (timings.isEmpty()) throw NO_TIMINGS_EXCEPTION.create();
|
|
|
|
timings.sort(Comparator.<ComputerMetrics, Long>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<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputerSelector> suppliers) {
|
|
Set<ServerComputer> computers = new HashSet<>();
|
|
for (var supplier : suppliers) supplier.find(source).forEach(computers::add);
|
|
return computers;
|
|
}
|
|
}
|