001/* 002 * Copyright (c) 2016-2017 Daniel Ennis (Aikar) - MIT License 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining 005 * a copy of this software and associated documentation files (the 006 * "Software"), to deal in the Software without restriction, including 007 * without limitation the rights to use, copy, modify, merge, publish, 008 * distribute, sublicense, and/or sell copies of the Software, and to 009 * permit persons to whom the Software is furnished to do so, subject to 010 * the following conditions: 011 * 012 * The above copyright notice and this permission notice shall be 013 * included in all copies or substantial portions of the Software. 014 * 015 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 016 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 017 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 018 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 019 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 020 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 021 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 022 */ 023 024package co.aikar.commands; 025 026import co.aikar.commands.apachecommonslang.ApacheCommonsExceptionUtil; 027import co.aikar.timings.lib.MCTiming; 028import co.aikar.timings.lib.TimingManager; 029import com.google.common.collect.Maps; 030import org.bukkit.Bukkit; 031import org.bukkit.ChatColor; 032import org.bukkit.Server; 033import org.bukkit.command.Command; 034import org.bukkit.command.CommandMap; 035import org.bukkit.command.CommandSender; 036import org.bukkit.command.SimpleCommandMap; 037import org.bukkit.entity.Player; 038import org.bukkit.event.EventHandler; 039import org.bukkit.event.Listener; 040import org.bukkit.event.player.PlayerJoinEvent; 041import org.bukkit.event.player.PlayerQuitEvent; 042import org.bukkit.event.server.PluginDisableEvent; 043import org.bukkit.plugin.Plugin; 044import org.bukkit.scheduler.BukkitTask; 045import org.jetbrains.annotations.NotNull; 046 047import java.lang.reflect.Field; 048import java.lang.reflect.Method; 049import java.lang.reflect.Parameter; 050import java.util.HashMap; 051import java.util.List; 052import java.util.Locale; 053import java.util.Map; 054import java.util.Objects; 055import java.util.UUID; 056import java.util.logging.Level; 057import java.util.logging.Logger; 058 059@SuppressWarnings("WeakerAccess") 060public class BukkitCommandManager extends CommandManager<CommandSender, BukkitCommandIssuer, ChatColor, BukkitMessageFormatter> { 061 062 @SuppressWarnings("WeakerAccess") 063 protected final Plugin plugin; 064 private final CommandMap commandMap; 065 private final TimingManager timingManager; 066 private final BukkitTask localeTask; 067 private final Logger logger; 068 protected Map<String, Command> knownCommands = new HashMap<>(); 069 protected Map<String, BukkitRootCommand> registeredCommands = new HashMap<>(); 070 protected BukkitCommandContexts contexts; 071 protected BukkitCommandCompletions completions; 072 MCTiming commandTiming; 073 protected BukkitLocales locales; 074 private boolean cantReadLocale = false; 075 protected Map<UUID, Locale> issuersLocale = Maps.newConcurrentMap(); 076 077 @SuppressWarnings("JavaReflectionMemberAccess") 078 public BukkitCommandManager(Plugin plugin) { 079 this.plugin = plugin; 080 this.logger = Logger.getLogger(this.plugin.getName()); 081 this.timingManager = TimingManager.of(plugin); 082 this.commandTiming = this.timingManager.of("Commands"); 083 this.commandMap = hookCommandMap(); 084 this.formatters.put(MessageType.ERROR, defaultFormatter = new BukkitMessageFormatter(ChatColor.RED, ChatColor.YELLOW, ChatColor.RED)); 085 this.formatters.put(MessageType.SYNTAX, new BukkitMessageFormatter(ChatColor.YELLOW, ChatColor.GREEN, ChatColor.WHITE)); 086 this.formatters.put(MessageType.INFO, new BukkitMessageFormatter(ChatColor.BLUE, ChatColor.DARK_GREEN, ChatColor.GREEN)); 087 this.formatters.put(MessageType.HELP, new BukkitMessageFormatter(ChatColor.AQUA, ChatColor.GREEN, ChatColor.YELLOW)); 088 Bukkit.getPluginManager().registerEvents(new ACFBukkitListener(plugin), plugin); 089 getLocales(); // auto load locales 090 this.localeTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { 091 if (cantReadLocale) { 092 return; 093 } 094 Bukkit.getOnlinePlayers().forEach(this::readPlayerLocale); 095 }, 5, 5); 096 } 097 098 @NotNull private CommandMap hookCommandMap() { 099 CommandMap commandMap = null; 100 try { 101 Server server = Bukkit.getServer(); 102 Method getCommandMap = server.getClass().getDeclaredMethod("getCommandMap"); 103 getCommandMap.setAccessible(true); 104 commandMap = (CommandMap) getCommandMap.invoke(server); 105 if (!SimpleCommandMap.class.isAssignableFrom(commandMap.getClass())) { 106 this.log(LogLevel.ERROR, "ERROR: CommandMap has been hijacked! Offending command map is located at: " + commandMap.getClass().getName()); 107 this.log(LogLevel.ERROR, "We are going to try to hijack it back and resolve this, but you are now in dangerous territory."); 108 this.log(LogLevel.ERROR, "We can not guarantee things are going to work."); 109 Field cmField = server.getClass().getDeclaredField("commandMap"); 110 commandMap = new ProxyCommandMap(this, commandMap); 111 cmField.set(server, commandMap); 112 this.log(LogLevel.INFO, "Injected Proxy Command Map... good luck..."); 113 } 114 Field knownCommands = SimpleCommandMap.class.getDeclaredField("knownCommands"); 115 knownCommands.setAccessible(true); 116 //noinspection unchecked 117 this.knownCommands = (Map<String, Command>) knownCommands.get(commandMap); 118 } catch (Exception e) { 119 this.log(LogLevel.ERROR, "Failed to get Command Map. ACF will not function."); 120 ACFUtil.sneaky(e); 121 } 122 return commandMap; 123 } 124 125 public Plugin getPlugin() { 126 return this.plugin; 127 } 128 129 @Override 130 public boolean isCommandIssuer(Class<?> type) { 131 return CommandSender.class.isAssignableFrom(type); 132 } 133 134 @Override 135 public synchronized CommandContexts<BukkitCommandExecutionContext> getCommandContexts() { 136 if (this.contexts == null) { 137 this.contexts = new BukkitCommandContexts(this); 138 } 139 return contexts; 140 } 141 142 @Override 143 public synchronized CommandCompletions<BukkitCommandCompletionContext> getCommandCompletions() { 144 if (this.completions == null) { 145 this.completions = new BukkitCommandCompletions(this); 146 } 147 return completions; 148 } 149 150 151 @Override 152 public BukkitLocales getLocales() { 153 if (this.locales == null) { 154 this.locales = new BukkitLocales(this); 155 this.locales.loadLanguages(); 156 } 157 return locales; 158 } 159 160 161 @Override 162 public boolean hasRegisteredCommands() { 163 return !registeredCommands.isEmpty(); 164 } 165 166 public void registerCommand(BaseCommand command, boolean force) { 167 final String plugin = this.plugin.getName().toLowerCase(); 168 command.onRegister(this); 169 for (Map.Entry<String, RootCommand> entry : command.registeredCommands.entrySet()) { 170 String commandName = entry.getKey().toLowerCase(); 171 BukkitRootCommand bukkitCommand = (BukkitRootCommand) entry.getValue(); 172 if (!bukkitCommand.isRegistered) { 173 if (force && knownCommands.containsKey(commandName)) { 174 Command oldCommand = commandMap.getCommand(commandName); 175 knownCommands.remove(commandName); 176 for (Map.Entry<String, Command> ce : knownCommands.entrySet()) { 177 String key = ce.getKey(); 178 Command value = ce.getValue(); 179 if (key.contains(":") && oldCommand.equals(value)) { 180 String[] split = ACFPatterns.COLON.split(key, 2); 181 if (split.length > 1) { 182 oldCommand.unregister(commandMap); 183 oldCommand.setLabel(split[0] + ":" + command.getName()); 184 oldCommand.register(commandMap); 185 } 186 } 187 } 188 } 189 commandMap.register(commandName, plugin, bukkitCommand); 190 } 191 bukkitCommand.isRegistered = true; 192 registeredCommands.put(commandName, bukkitCommand); 193 } 194 } 195 196 @Override 197 public void registerCommand(BaseCommand command) { 198 registerCommand(command, false); 199 } 200 201 public void unregisterCommand(BaseCommand command) { 202 for (RootCommand rootcommand : command.registeredCommands.values()) { 203 BukkitRootCommand bukkitCommand = (BukkitRootCommand) rootcommand; 204 bukkitCommand.getSubCommands().values().removeAll(command.subCommands.values()); 205 if (bukkitCommand.isRegistered && bukkitCommand.getSubCommands().isEmpty()) { 206 unregisterCommand(bukkitCommand); 207 bukkitCommand.isRegistered = false; 208 } 209 } 210 } 211 212 /** 213 * @deprecated Use unregisterCommand(BaseCommand) - this will be visibility reduced later. 214 * @param command 215 */ 216 @Deprecated 217 public void unregisterCommand(BukkitRootCommand command) { 218 final String plugin = this.plugin.getName().toLowerCase(); 219 command.unregister(commandMap); 220 String key = command.getName(); 221 Command registered = knownCommands.get(key); 222 if (command.equals(registered)) { 223 knownCommands.remove(key); 224 } 225 knownCommands.remove(plugin + ":" + key); 226 } 227 228 public void unregisterCommands() { 229 for (Map.Entry<String, BukkitRootCommand> entry : registeredCommands.entrySet()) { 230 unregisterCommand(entry.getValue()); 231 } 232 this.registeredCommands.clear(); 233 } 234 235 236 private Field getEntityField(Player player) throws NoSuchFieldException { 237 Class cls = player.getClass(); 238 while (cls != Object.class) { 239 if (cls.getName().endsWith("CraftEntity")) { 240 Field field = cls.getDeclaredField("entity"); 241 field.setAccessible(true); 242 return field; 243 } 244 cls = cls.getSuperclass(); 245 } 246 return null; 247 } 248 249 private void readPlayerLocale(Player player) { 250 if (!player.isOnline() || cantReadLocale) { 251 return; 252 } 253 try { 254 Field entityField = getEntityField(player); 255 if (entityField == null) { 256 return; 257 } 258 Object nmsPlayer = entityField.get(player); 259 if (nmsPlayer != null) { 260 Field localeField = nmsPlayer.getClass().getField("locale"); 261 Object localeString = localeField.get(nmsPlayer); 262 if (localeString != null && localeString instanceof String) { 263 String[] split = ACFPatterns.UNDERSCORE.split((String) localeString); 264 Locale locale = split.length > 1 ? new Locale(split[0], split[1]) : new Locale(split[0]); 265 Locale prev = issuersLocale.put(player.getUniqueId(), locale); 266 if (!Objects.equals(locale, prev)) { 267 this.notifyLocaleChange(getCommandIssuer(player), prev, locale); 268 } 269 } 270 } 271 } catch (Exception e) { 272 cantReadLocale = true; 273 this.localeTask.cancel(); 274 this.log(LogLevel.INFO, "Can't read players locale, you will be unable to automatically detect players language. Only Bukkit 1.7+ is supported for this.", e); 275 } 276 } 277 278 private class ACFBukkitListener implements Listener { 279 private final Plugin plugin; 280 281 public ACFBukkitListener(Plugin plugin) { 282 this.plugin = plugin; 283 } 284 285 @EventHandler 286 public void onPluginDisable(PluginDisableEvent event) { 287 if (!(plugin.getName().equalsIgnoreCase(event.getPlugin().getName()))) { 288 return; 289 } 290 unregisterCommands(); 291 } 292 @EventHandler 293 public void onPlayerJoin(PlayerJoinEvent event) { 294 Player player = event.getPlayer(); 295 readPlayerLocale(player); 296 this.plugin.getServer().getScheduler().runTaskLater(this.plugin, () -> readPlayerLocale(player), 20); 297 } 298 299 @EventHandler 300 public void onPlayerJoin(PlayerQuitEvent event) { 301 issuersLocale.remove(event.getPlayer().getUniqueId()); 302 } 303 } 304 305 public TimingManager getTimings() { 306 return timingManager; 307 } 308 309 @Override 310 public RootCommand createRootCommand(String cmd) { 311 return new BukkitRootCommand(this, cmd); 312 } 313 314 @Override 315 public BukkitCommandIssuer getCommandIssuer(Object issuer) { 316 if (!(issuer instanceof CommandSender)) { 317 throw new IllegalArgumentException(issuer.getClass().getName() + " is not a Command Issuer."); 318 } 319 return new BukkitCommandIssuer(this, (CommandSender) issuer); 320 } 321 322 @Override 323 public <R extends CommandExecutionContext> R createCommandContext(RegisteredCommand command, Parameter parameter, CommandIssuer sender, List<String> args, int i, Map<String, Object> passedArgs) { 324 //noinspection unchecked 325 return (R) new BukkitCommandExecutionContext(command, parameter, (BukkitCommandIssuer) sender, args, i, passedArgs); 326 } 327 328 @Override 329 public CommandCompletionContext createCompletionContext(RegisteredCommand command, CommandIssuer sender, String input, String config, String[] args) { 330 return new BukkitCommandCompletionContext(command, sender, input, config, args); 331 } 332 333 @Override 334 public RegisteredCommand createRegisteredCommand(BaseCommand command, String cmdName, Method method, String prefSubCommand) { 335 return new BukkitRegisteredCommand(command, cmdName, method, prefSubCommand); 336 } 337 338 @Override 339 public void log(LogLevel level, String message, Throwable throwable) { 340 Level logLevel = level == LogLevel.INFO ? Level.INFO : Level.SEVERE; 341 logger.log(logLevel, LogLevel.LOG_PREFIX + message); 342 if (throwable != null) { 343 for (String line : ACFPatterns.NEWLINE.split(ApacheCommonsExceptionUtil.getFullStackTrace(throwable))) { 344 logger.log(logLevel, LogLevel.LOG_PREFIX + line); 345 } 346 } 347 } 348 349 @Override 350 public Locale getIssuerLocale(CommandIssuer issuer) { 351 if (usingPerIssuerLocale() && issuer.getIssuer() instanceof Player) { 352 UUID uniqueId = ((Player) issuer.getIssuer()).getUniqueId(); 353 Locale locale = issuersLocale.get(uniqueId); 354 if (locale != null) { 355 return locale; 356 } 357 } 358 return super.getIssuerLocale(issuer); 359 } 360}