001//
002// MIT License
003//
004// Copyright (c) 2021 Alexander Söderberg & Contributors
005//
006// Permission is hereby granted, free of charge, to any person obtaining a copy
007// of this software and associated documentation files (the "Software"), to deal
008// in the Software without restriction, including without limitation the rights
009// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010// copies of the Software, and to permit persons to whom the Software is
011// furnished to do so, subject to the following conditions:
012//
013// The above copyright notice and this permission notice shall be included in all
014// copies or substantial portions of the Software.
015//
016// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022// SOFTWARE.
023//
024package cloud.commandframework.bukkit;
025
026import cloud.commandframework.Command;
027import cloud.commandframework.CommandManager;
028import cloud.commandframework.arguments.CommandArgument;
029import cloud.commandframework.arguments.StaticArgument;
030import cloud.commandframework.internal.CommandRegistrationHandler;
031import org.bukkit.Bukkit;
032import org.bukkit.command.CommandMap;
033import org.bukkit.command.PluginIdentifiableCommand;
034import org.bukkit.command.SimpleCommandMap;
035import org.bukkit.help.GenericCommandHelpTopic;
036import org.checkerframework.checker.nullness.qual.NonNull;
037
038import java.lang.reflect.Field;
039import java.lang.reflect.Method;
040import java.util.ArrayList;
041import java.util.HashMap;
042import java.util.HashSet;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046import java.util.TreeSet;
047
048public class BukkitPluginRegistrationHandler<C> implements CommandRegistrationHandler {
049
050    private final Map<CommandArgument<?, ?>, org.bukkit.command.Command> registeredCommands = new HashMap<>();
051    private final Set<String> recognizedAliases = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
052
053    private Map<String, org.bukkit.command.Command> bukkitCommands;
054    private BukkitCommandManager<C> bukkitCommandManager;
055    private CommandMap commandMap;
056
057    BukkitPluginRegistrationHandler() {
058    }
059
060    final void initialize(final @NonNull BukkitCommandManager<C> bukkitCommandManager) throws Exception {
061        final Method getCommandMap = Bukkit.getServer().getClass().getDeclaredMethod("getCommandMap");
062        getCommandMap.setAccessible(true);
063        this.commandMap = (CommandMap) getCommandMap.invoke(Bukkit.getServer());
064        final Field knownCommands = SimpleCommandMap.class.getDeclaredField("knownCommands");
065        knownCommands.setAccessible(true);
066        @SuppressWarnings("unchecked") final Map<String, org.bukkit.command.Command> bukkitCommands =
067                (Map<String, org.bukkit.command.Command>) knownCommands.get(commandMap);
068        this.bukkitCommands = bukkitCommands;
069        this.bukkitCommandManager = bukkitCommandManager;
070        Bukkit.getHelpMap().registerHelpTopicFactory(BukkitCommand.class, GenericCommandHelpTopic::new);
071    }
072
073    @Override
074    public final boolean registerCommand(final @NonNull Command<?> command) {
075        /* We only care about the root command argument */
076        final CommandArgument<?, ?> commandArgument = command.getArguments().get(0);
077        if (!(this.bukkitCommandManager.getCommandRegistrationHandler() instanceof CloudCommodoreManager)
078                && this.registeredCommands.containsKey(commandArgument)) {
079            return false;
080        }
081        final String label = commandArgument.getName();
082        final String namespacedLabel = this.getNamespacedLabel(label);
083
084        @SuppressWarnings("unchecked")
085        final List<String> aliases = new ArrayList<>(((StaticArgument<C>) commandArgument).getAlternativeAliases());
086
087        @SuppressWarnings("unchecked") final BukkitCommand<C> bukkitCommand = new BukkitCommand<>(
088                label,
089                aliases,
090                (Command<C>) command,
091                (CommandArgument<C, ?>) commandArgument,
092                this.bukkitCommandManager
093        );
094
095        if (this.bukkitCommandManager.getSetting(CommandManager.ManagerSettings.OVERRIDE_EXISTING_COMMANDS)) {
096            this.bukkitCommands.remove(label);
097            aliases.forEach(alias -> this.bukkitCommands.remove(alias));
098        }
099
100        final Set<String> newAliases = new HashSet<>();
101
102        for (final String alias : aliases) {
103            final String namespacedAlias = this.getNamespacedLabel(alias);
104            newAliases.add(namespacedAlias);
105            if (!this.bukkitCommandOrAliasExists(alias)) {
106                newAliases.add(alias);
107            }
108        }
109
110        if (!this.bukkitCommandExists(label)) {
111            newAliases.add(label);
112        }
113        newAliases.add(namespacedLabel);
114
115        this.commandMap.register(
116                label,
117                this.bukkitCommandManager.getOwningPlugin().getName().toLowerCase(),
118                bukkitCommand
119        );
120
121        this.recognizedAliases.addAll(newAliases);
122        if (this.bukkitCommandManager.getSplitAliases()) {
123            newAliases.forEach(alias -> this.registerExternal(alias, command, bukkitCommand));
124        }
125
126        this.registeredCommands.put(commandArgument, bukkitCommand);
127        return true;
128    }
129
130    private @NonNull String getNamespacedLabel(final @NonNull String label) {
131        return String.format("%s:%s", this.bukkitCommandManager.getOwningPlugin().getName(), label).toLowerCase();
132    }
133
134    /**
135     * Check if the given alias is recognizable by this registration handler
136     *
137     * @param alias Alias
138     * @return {@code true} if the alias is recognized, else {@code false}
139     */
140    public boolean isRecognized(final @NonNull String alias) {
141        return this.recognizedAliases.contains(alias);
142    }
143
144    protected void registerExternal(
145            final @NonNull String label,
146            final @NonNull Command<?> command,
147            final @NonNull BukkitCommand<C> bukkitCommand
148    ) {
149    }
150
151    /**
152     * Returns true if a command exists in the Bukkit command map, is not an alias, and is not owned by us.
153     *
154     * @param commandLabel label to check
155     * @return whether the command exists and is not an alias
156     */
157    private boolean bukkitCommandExists(final String commandLabel) {
158        final org.bukkit.command.Command existingCommand = this.bukkitCommands.get(commandLabel);
159        if (existingCommand == null) {
160            return false;
161        }
162        if (existingCommand instanceof PluginIdentifiableCommand) {
163            return existingCommand.getLabel().equals(commandLabel)
164                    && !((PluginIdentifiableCommand) existingCommand).getPlugin().getName()
165                    .equalsIgnoreCase(this.bukkitCommandManager.getOwningPlugin().getName());
166        }
167        return existingCommand.getLabel().equals(commandLabel);
168    }
169
170    /**
171     * Returns true if a command exists in the Bukkit command map, and it is not owned by us, whether or not it is an alias.
172     *
173     * @param commandLabel label to check
174     * @return whether the command exists
175     */
176    private boolean bukkitCommandOrAliasExists(final String commandLabel) {
177        final org.bukkit.command.Command command = this.bukkitCommands.get(commandLabel);
178        if (command instanceof PluginIdentifiableCommand) {
179            return !((PluginIdentifiableCommand) command).getPlugin().getName()
180                    .equalsIgnoreCase(this.bukkitCommandManager.getOwningPlugin().getName());
181        }
182        return command != null;
183    }
184
185}