Geyser/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java

306 lines
15 KiB
Java

/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.command;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.AllArgsConstructor;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.api.command.Command;
import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent;
import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand;
import org.geysermc.geyser.command.defaults.AdvancementsCommand;
import org.geysermc.geyser.command.defaults.ConnectionTestCommand;
import org.geysermc.geyser.command.defaults.DumpCommand;
import org.geysermc.geyser.command.defaults.ExtensionsCommand;
import org.geysermc.geyser.command.defaults.HelpCommand;
import org.geysermc.geyser.command.defaults.ListCommand;
import org.geysermc.geyser.command.defaults.OffhandCommand;
import org.geysermc.geyser.command.defaults.ReloadCommand;
import org.geysermc.geyser.command.defaults.SettingsCommand;
import org.geysermc.geyser.command.defaults.StatisticsCommand;
import org.geysermc.geyser.command.defaults.StopCommand;
import org.geysermc.geyser.command.defaults.VersionCommand;
import org.geysermc.geyser.event.GeyserEventRegistrar;
import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl;
import org.geysermc.geyser.extension.command.GeyserExtensionCommand;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.exception.ArgumentParseException;
import org.incendo.cloud.exception.CommandExecutionException;
import org.incendo.cloud.exception.InvalidCommandSenderException;
import org.incendo.cloud.exception.InvalidSyntaxException;
import org.incendo.cloud.exception.NoPermissionException;
import org.incendo.cloud.exception.NoSuchCommandException;
import org.incendo.cloud.exception.handling.ExceptionContext;
import org.incendo.cloud.exception.handling.ExceptionController;
import org.incendo.cloud.exception.handling.ExceptionHandler;
import org.incendo.cloud.execution.ExecutionCoordinator;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
public class CommandRegistry {
private final GeyserImpl geyser;
private final CommandManager<GeyserCommandSource> cloud;
/**
* Map of Geyser subcommands to their Commands
*/
private final Map<String, Command> commands = new Object2ObjectOpenHashMap<>(13);
/**
* Map of Extensions to maps of their subcommands
*/
private final Map<Extension, Map<String, Command>> extensionCommands = new Object2ObjectOpenHashMap<>(0);
/**
* Map of root commands (that are for extensions) to Extensions
*/
private final Map<String, Extension> extensionRootCommands = new Object2ObjectOpenHashMap<>(0);
/**
* Map containing only permissions that have been registered with a default value
*/
private final Map<String, TriState> permissionDefaults = new Object2ObjectOpenHashMap<>(13);
public CommandRegistry(GeyserImpl geyser, CommandManager<GeyserCommandSource> cloud) {
this.geyser = geyser;
this.cloud = cloud;
// Yeet the default exception handlers that the typical cloud implementations provide so that we can perform localization.
// This is kind of meaningless for our Geyser-Standalone implementation since these handlers are the default exception handlers in that case.
cloud.exceptionController().clearHandlers();
List<GeyserExceptionHandler<?>> exceptionHandlers = List.of(
new GeyserExceptionHandler<>(InvalidSyntaxException.class, (src, e) -> src.sendLocaleString("geyser.command.invalid_syntax", e.correctSyntax())),
new GeyserExceptionHandler<>(InvalidCommandSenderException.class, (src, e) -> src.sendLocaleString("geyser.command.invalid_sender", e.commandSender().getClass().getSimpleName(), e.requiredSender())),
new GeyserExceptionHandler<>(NoPermissionException.class, this::handleNoPermission),
new GeyserExceptionHandler<>(NoSuchCommandException.class, (src, e) -> src.sendLocaleString("geyser.command.not_found")),
new GeyserExceptionHandler<>(ArgumentParseException.class, (src, e) -> src.sendLocaleString("geyser.command.invalid_argument", e.getCause().getMessage())),
new GeyserExceptionHandler<>(CommandExecutionException.class, (src, e) -> handleUnexpectedThrowable(src, e.getCause())),
new GeyserExceptionHandler<>(Throwable.class, (src, e) -> handleUnexpectedThrowable(src, e.getCause()))
);
for (GeyserExceptionHandler<?> handler : exceptionHandlers) {
handler.register(cloud);
}
// begin command registration
registerBuiltInCommand(new HelpCommand(geyser, "help", "geyser.commands.help.desc", "geyser.command.help", "geyser", "geyser.command", this.commands));
registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list"));
registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload"));
registerBuiltInCommand(new OffhandCommand(geyser, "offhand", "geyser.commands.offhand.desc", "geyser.command.offhand"));
registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump"));
registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version"));
registerBuiltInCommand(new SettingsCommand(geyser, "settings", "geyser.commands.settings.desc", "geyser.command.settings"));
registerBuiltInCommand(new StatisticsCommand(geyser, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics"));
registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements"));
registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips"));
registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest"));
if (this.geyser.getPlatformType() == PlatformType.STANDALONE) {
registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
}
if (!this.geyser.extensionManager().extensions().isEmpty()) {
registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions"));
}
GeyserDefineCommandsEvent defineCommandsEvent = new GeyserDefineCommandsEventImpl(this.commands) {
@Override
public void register(@NonNull Command command) {
if (!(command instanceof GeyserExtensionCommand extensionCommand)) {
throw new IllegalArgumentException("Expected GeyserExtensionCommand as part of command registration but got " + command + "! Did you use the Command builder properly?");
}
registerExtensionCommand(extensionCommand.extension(), extensionCommand);
}
};
this.geyser.eventBus().fire(defineCommandsEvent);
for (Map.Entry<Extension, Map<String, Command>> entry : this.extensionCommands.entrySet()) {
Extension extension = entry.getKey();
// Register this extension's root command
extensionRootCommands.put(extension.rootCommand(), extension);
// Register help commands for all extensions with commands
String id = extension.description().id();
registerExtensionCommand(extension, new HelpCommand(
this.geyser,
"help",
"geyser.commands.exthelp.desc",
"geyser.command.exthelp." + id,
extension.rootCommand(),
extension.description().id() + ".command",
entry.getValue()));
}
// wait for the right moment (depends on the platform) to register permissions
geyser.eventBus().subscribe(new GeyserEventRegistrar(this), GeyserRegisterPermissionsEvent.class, this::onRegisterPermissions);
}
@NonNull
public CommandManager<GeyserCommandSource> cloud() {
return cloud;
}
@NonNull
public Map<String, Command> commands() {
return Collections.unmodifiableMap(this.commands);
}
/**
* For internal Geyser commands
*/
public void registerBuiltInCommand(GeyserCommand command) {
register(command, this.commands);
}
public void registerExtensionCommand(@NonNull Extension extension, @NonNull GeyserCommand command) {
register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>()));
}
public boolean hasPermission(GeyserCommandSource source, String permission) {
return cloud.hasPermission(source, permission);
}
private void register(GeyserCommand command, Map<String, Command> commands) {
command.register(cloud);
commands.put(command.name(), command);
geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", command.name()));
for (String alias : command.aliases()) {
commands.put(alias, command);
}
if (!command.permission().isBlank() && command.permissionDefault() != null) {
permissionDefaults.put(command.permission(), command.permissionDefault());
}
if (command instanceof HelpCommand helpCommand) {
permissionDefaults.put(helpCommand.rootCommand(), helpCommand.permissionDefault());
}
}
private void onRegisterPermissions(GeyserRegisterPermissionsEvent event) {
for (Map.Entry<String, TriState> permission : permissionDefaults.entrySet()) {
event.register(permission.getKey(), permission.getValue());
}
// Register other various Geyser permissions
event.register(Constants.UPDATE_PERMISSION, TriState.NOT_SET);
event.register(Constants.SERVER_SETTINGS_PERMISSION, TriState.NOT_SET);
event.register(Constants.SETTINGS_GAMERULES_PERMISSION, TriState.NOT_SET);
}
/**
* Returns the description of the given command
*
* @param command the root command node
* @param locale the ideal locale that the description should be in
* @return a description if found, otherwise an empty string. The locale is not guaranteed.
*/
@NonNull
public String description(@NonNull String command, @NonNull String locale) {
if (command.equals(GeyserCommand.DEFAULT_ROOT_COMMAND)) {
return GeyserLocale.getPlayerLocaleString("geyser.command.root.geyser", locale);
}
Extension extension = extensionRootCommands.get(command);
if (extension != null) {
return GeyserLocale.getPlayerLocaleString("geyser.command.root.extension", locale, extension.name());
}
return "";
}
/**
* Dispatches a command into cloud and handles any thrown exceptions.
* This method may or may not be blocking, depending on the {@link ExecutionCoordinator} in use by cloud.
*/
public void runCommand(@NonNull GeyserCommandSource source, @NonNull String command) {
cloud.commandExecutor().executeCommand(source, command);
}
private void handleNoPermission(GeyserCommandSource source, NoPermissionException exception) {
// we basically recheck bedrock-only and player-only to see if they were the cause of this
if (exception.missingPermission() instanceof GeyserPermission permission) {
GeyserPermission.Result result = permission.check(source);
if (result == GeyserPermission.Result.NOT_BEDROCK) {
source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.bedrock_only", source.locale()));
return;
}
if (result == GeyserPermission.Result.NOT_PLAYER) {
source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.player_only", source.locale()));
return;
}
} else {
GeyserLogger logger = GeyserImpl.getInstance().getLogger();
if (logger.isDebug()) {
logger.debug("Expected a GeyserPermission for %s but instead got %s".formatted(exception.currentChain(), exception.missingPermission()));
}
}
// Result.NO_PERMISSION, or we're unable to recheck
source.sendLocaleString("geyser.command.permission_fail");
}
private void handleUnexpectedThrowable(GeyserCommandSource source, Throwable throwable) {
source.sendMessage(MinecraftLocale.getLocaleString("command.failed", source.locale())); // java edition translation key
GeyserImpl.getInstance().getLogger().error("Exception while executing command handler", throwable);
}
@AllArgsConstructor
private static class GeyserExceptionHandler<E extends Throwable> implements ExceptionHandler<GeyserCommandSource, E> {
final Class<E> type;
final BiConsumer<GeyserCommandSource, E> handler;
void register(CommandManager<GeyserCommandSource> manager) {
manager.exceptionController().registerHandler(type, this);
}
@Override
public void handle(@NonNull ExceptionContext context) throws Throwable {
Throwable unwrapped = ExceptionController.unwrapCompletionException(context.exception());
handler.accept((GeyserCommandSource) context.context().sender(), type.cast(unwrapped));
}
}
}