001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.command; 020 021import com.plotsquared.core.configuration.caption.Caption; 022import com.plotsquared.core.configuration.caption.CaptionHolder; 023import com.plotsquared.core.configuration.caption.StaticCaption; 024import com.plotsquared.core.configuration.caption.TranslatableCaption; 025import com.plotsquared.core.permissions.PermissionHolder; 026import com.plotsquared.core.player.PlotPlayer; 027import com.plotsquared.core.util.MathMan; 028import com.plotsquared.core.util.StringComparison; 029import com.plotsquared.core.util.StringMan; 030import com.plotsquared.core.util.task.RunnableVal2; 031import com.plotsquared.core.util.task.RunnableVal3; 032import net.kyori.adventure.text.minimessage.MiniMessage; 033import net.kyori.adventure.text.minimessage.Template; 034import org.checkerframework.checker.nullness.qual.Nullable; 035 036import java.lang.reflect.InvocationTargetException; 037import java.lang.reflect.Method; 038import java.util.ArrayList; 039import java.util.Arrays; 040import java.util.Collection; 041import java.util.Collections; 042import java.util.HashMap; 043import java.util.HashSet; 044import java.util.List; 045import java.util.Map; 046import java.util.Set; 047import java.util.concurrent.CompletableFuture; 048 049public abstract class Command { 050 051 static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build(); 052 053 // May be none 054 private final ArrayList<Command> allCommands = new ArrayList<>(); 055 private final ArrayList<Command> dynamicCommands = new ArrayList<>(); 056 private final HashMap<String, Command> staticCommands = new HashMap<>(); 057 058 // Parent command (may be null) 059 private final Command parent; 060 private final boolean isStatic; 061 // The command ID 062 private String id; 063 private List<String> aliases; 064 private RequiredType required; 065 private String usage; 066 private Caption description; 067 private String permission; 068 private boolean confirmation; 069 private CommandCategory category; 070 private Argument<?>[] arguments; 071 072 public Command( 073 Command parent, boolean isStatic, String id, String permission, 074 RequiredType required, CommandCategory category 075 ) { 076 this.parent = parent; 077 this.isStatic = isStatic; 078 this.id = id; 079 this.permission = permission; 080 this.required = required; 081 this.category = category; 082 this.aliases = Collections.singletonList(id); 083 if (this.parent != null) { 084 this.parent.register(this); 085 } 086 } 087 088 public Command(Command parent, boolean isStatic) { 089 this.parent = parent; 090 this.isStatic = isStatic; 091 CommandDeclaration cdAnnotation = getClass().getAnnotation(CommandDeclaration.class); 092 if (cdAnnotation != null) { 093 init(cdAnnotation); 094 } 095 for (final Method method : getClass().getDeclaredMethods()) { 096 if (method.isAnnotationPresent(CommandDeclaration.class)) { 097 Class<?>[] types = method.getParameterTypes(); 098 // final PlotPlayer<?> player, String[] args, RunnableVal3<Command,Runnable,Runnable> confirm, RunnableVal2<Command, CommandResult> 099 // whenDone 100 if (types.length == 5 && types[0] == Command.class && types[1] == PlotPlayer.class 101 && types[2] == String[].class && types[3] == RunnableVal3.class 102 && types[4] == RunnableVal2.class) { 103 Command tmp = new Command(this, true) { 104 @Override 105 public CompletableFuture<Boolean> execute( 106 PlotPlayer<?> player, String[] args, 107 RunnableVal3<Command, Runnable, Runnable> confirm, 108 RunnableVal2<Command, CommandResult> whenDone 109 ) { 110 try { 111 method.invoke(Command.this, this, player, args, confirm, whenDone); 112 return CompletableFuture.completedFuture(true); 113 } catch (IllegalAccessException | InvocationTargetException e) { 114 e.printStackTrace(); 115 } 116 return CompletableFuture.completedFuture(false); 117 } 118 }; 119 tmp.init(method.getAnnotation(CommandDeclaration.class)); 120 } 121 } 122 } 123 } 124 125 public Command getParent() { 126 return this.parent; 127 } 128 129 public String getId() { 130 return this.id; 131 } 132 133 public String getFullId() { 134 if (this.parent != null && this.parent.getParent() != null) { 135 return this.parent.getFullId() + "." + this.id; 136 } 137 return this.id; 138 } 139 140 public List<Command> getCommands(PlotPlayer<?> player) { 141 List<Command> commands = new ArrayList<>(); 142 for (Command cmd : this.allCommands) { 143 if (cmd.canExecute(player, false)) { 144 commands.add(cmd); 145 } 146 } 147 return commands; 148 } 149 150 public List<Command> getCommands(CommandCategory category, PlotPlayer<?> player) { 151 List<Command> commands = getCommands(player); 152 if (category != null) { 153 commands.removeIf(command -> command.category != category); 154 } 155 return commands; 156 } 157 158 public List<Command> getCommands() { 159 return this.allCommands; 160 } 161 162 public boolean hasConfirmation(PermissionHolder player) { 163 return this.confirmation && !player.hasPermission(getPermission() + ".confirm.bypass"); 164 } 165 166 public List<String> getAliases() { 167 return this.aliases; 168 } 169 170 public Caption getDescription() { 171 return this.description; 172 } 173 174 public RequiredType getRequiredType() { 175 return this.required; 176 } 177 178 public Argument<?>[] getRequiredArguments() { 179 return this.arguments; 180 } 181 182 public void setRequiredArguments(Argument<?>[] arguments) { 183 this.arguments = arguments; 184 } 185 186 public void init(CommandDeclaration declaration) { 187 this.id = declaration.command(); 188 this.permission = declaration.permission(); 189 this.required = declaration.requiredType(); 190 this.category = declaration.category(); 191 192 List<String> aliasOptions = new ArrayList<>(); 193 aliasOptions.add(this.id); 194 aliasOptions.addAll(Arrays.asList(declaration.aliases())); 195 196 this.aliases = aliasOptions; 197 if (declaration.description().isEmpty()) { 198 Command parent = getParent(); 199 // we're collecting the "path" of the command 200 List<String> path = new ArrayList<>(); 201 path.add(this.id); 202 while (parent != null && !parent.equals(MainCommand.getInstance())) { 203 path.add(parent.getId()); 204 parent = parent.getParent(); 205 } 206 Collections.reverse(path); 207 String descriptionKey = String.join(".", path); 208 this.description = TranslatableCaption.of(String.format("commands.description.%s", descriptionKey)); 209 } else { 210 this.description = StaticCaption.of(declaration.description()); 211 } 212 this.usage = declaration.usage(); 213 this.confirmation = declaration.confirmation(); 214 215 if (this.parent != null) { 216 this.parent.register(this); 217 } 218 } 219 220 public void register(Command command) { 221 if (command.isStatic) { 222 for (String alias : command.aliases) { 223 this.staticCommands.put(alias.toLowerCase(), command); 224 } 225 } else { 226 this.dynamicCommands.add(command); 227 } 228 this.allCommands.add(command); 229 } 230 231 public String getPermission() { 232 if (this.permission != null && !this.permission.isEmpty()) { 233 return this.permission; 234 } 235 if (this.parent == null) { 236 return "plots.use"; 237 } 238 return "plots." + getFullId(); 239 } 240 241 public <T> void paginate( 242 PlotPlayer<?> player, List<T> c, int size, int page, 243 RunnableVal3<Integer, T, CaptionHolder> add, String baseCommand, Caption header 244 ) { 245 // Calculate pages & index 246 if (page < 0) { 247 page = 0; 248 } 249 int totalPages = (int) Math.floor((double) c.size() / size); 250 if (page > totalPages) { 251 page = totalPages; 252 } 253 int max = page * size + size; 254 if (max > c.size()) { 255 max = c.size(); 256 } 257 // Send the header 258 Template curTemplate = Template.of("cur", String.valueOf(page + 1)); 259 Template maxTemplate = Template.of("max", String.valueOf(totalPages + 1)); 260 Template amountTemplate = Template.of("amount", String.valueOf(c.size())); 261 player.sendMessage(header, curTemplate, maxTemplate, amountTemplate); 262 // Send the page content 263 List<T> subList = c.subList(page * size, max); 264 int i = page * size; 265 for (T obj : subList) { 266 i++; 267 final CaptionHolder msg = new CaptionHolder(); 268 add.run(i, obj, msg); 269 player.sendMessage(msg.get(), msg.getTemplates()); 270 } 271 // Send the footer 272 Template command1 = Template.of("command1", baseCommand + " " + page); 273 Template command2 = Template.of("command2", baseCommand + " " + (page + 2)); 274 Template clickable = Template.of("clickable", TranslatableCaption.of("list.clickable").getComponent(player)); 275 player.sendMessage(TranslatableCaption.of("list.page_turn"), command1, command2, clickable); 276 } 277 278 /** 279 * @param player Caller 280 * @param args Arguments 281 * @param confirm Instance, Success, Failure 282 * @param whenDone task to run when done 283 * @return CompletableFuture {@code true} if the command executed fully, {@code false} in 284 * any other case 285 */ 286 public CompletableFuture<Boolean> execute( 287 PlotPlayer<?> player, String[] args, 288 RunnableVal3<Command, Runnable, Runnable> confirm, 289 RunnableVal2<Command, CommandResult> whenDone 290 ) throws CommandException { 291 if (args.length == 0 || args[0] == null) { 292 if (this.parent == null) { 293 MainCommand.getInstance().help.displayHelp(player, null, 0); 294 } else { 295 sendUsage(player); 296 } 297 return CompletableFuture.completedFuture(false); 298 } 299 if (this.allCommands.isEmpty()) { 300 player.sendMessage( 301 StaticCaption.of("Not Implemented: https://github.com/IntellectualSites/PlotSquared/issues")); 302 return CompletableFuture.completedFuture(false); 303 } 304 Command cmd = getCommand(args[0]); 305 if (cmd == null) { 306 if (this.parent != null) { 307 sendUsage(player); 308 return CompletableFuture.completedFuture(false); 309 } 310 // Help command 311 try { 312 if (!MathMan.isInteger(args[0])) { 313 CommandCategory.valueOf(args[0].toUpperCase()); 314 } 315 // This will default certain syntax to the help command 316 // e.g. /plot, /plot 1, /plot claiming 317 MainCommand.getInstance().help.execute(player, args, null, null); 318 return CompletableFuture.completedFuture(false); 319 } catch (IllegalArgumentException ignored) { 320 } 321 // Command recommendation 322 player.sendMessage(TranslatableCaption.of("commandconfig.not_valid_subcommand")); 323 List<Command> commands = getCommands(player); 324 if (commands.isEmpty()) { 325 player.sendMessage( 326 TranslatableCaption.of("commandconfig.did_you_mean"), 327 Template.of("value", MainCommand.getInstance().help.getUsage()) 328 ); 329 return CompletableFuture.completedFuture(false); 330 } 331 HashSet<String> setArgs = new HashSet<>(args.length); 332 for (String arg : args) { 333 setArgs.add(arg.toLowerCase()); 334 } 335 String[] allArgs = setArgs.toArray(new String[0]); 336 int best = 0; 337 for (Command current : commands) { 338 int match = getMatch(allArgs, current, player); 339 if (match > best) { 340 cmd = current; 341 } 342 } 343 if (cmd == null) { 344 cmd = new StringComparison<>(args[0], this.allCommands).getMatchObject(); 345 } 346 player.sendMessage( 347 TranslatableCaption.of("commandconfig.did_you_mean"), 348 Template.of("value", cmd.getUsage()) 349 ); 350 return CompletableFuture.completedFuture(false); 351 } 352 String[] newArgs = Arrays.copyOfRange(args, 1, args.length); 353 if (!cmd.checkArgs(player, newArgs) || !cmd.canExecute(player, true)) { 354 return CompletableFuture.completedFuture(false); 355 } 356 try { 357 cmd.execute(player, newArgs, confirm, whenDone); 358 } catch (CommandException e) { 359 e.perform(player); 360 } 361 return CompletableFuture.completedFuture(true); 362 } 363 364 public boolean checkArgs(PlotPlayer<?> player, String[] args) { 365 Argument<?>[] reqArgs = getRequiredArguments(); 366 if (reqArgs != null && reqArgs.length > 0) { 367 boolean failed = args.length < reqArgs.length; 368 String[] baseSplit = getCommandString().split(" "); 369 String[] fullSplit = getUsage().split(" "); 370 if (fullSplit.length - baseSplit.length < reqArgs.length) { 371 String[] tmp = new String[baseSplit.length + reqArgs.length]; 372 System.arraycopy(fullSplit, 0, tmp, 0, fullSplit.length); 373 fullSplit = tmp; 374 } 375 for (int i = 0; i < reqArgs.length; i++) { 376 fullSplit[i + baseSplit.length] = reqArgs[i].getExample().toString(); 377 failed = failed || reqArgs[i].parse(args[i]) == null; 378 } 379 if (failed) { 380 // TODO improve or remove the Argument system 381 player.sendMessage( 382 TranslatableCaption.of("commandconfig.command_syntax"), 383 Template.of("value", StringMan.join(fullSplit, " ")) 384 ); 385 return false; 386 } 387 } 388 return true; 389 } 390 391 public int getMatch(String[] args, Command cmd, PlotPlayer<?> player) { 392 String perm = cmd.getPermission(); 393 int count = cmd.getAliases().stream().filter(alias -> alias.startsWith(args[0])) 394 .mapToInt(alias -> 5).sum(); 395 HashSet<String> desc = new HashSet<>(); 396 Collections.addAll(desc, cmd.getDescription().getComponent(player).split(" ")); 397 for (String arg : args) { 398 if (perm.startsWith(arg)) { 399 count++; 400 } 401 if (desc.contains(arg)) { 402 count++; 403 } 404 } 405 String[] usage = cmd.getUsage().split(" "); 406 for (int i = 0; i < Math.min(4, usage.length); i++) { 407 int require; 408 if (usage[i].startsWith("<")) { 409 require = 1; 410 } else { 411 require = 0; 412 } 413 String[] split = usage[i].split("\\|| |\\>|\\<|\\[|\\]|\\{|\\}|\\_|\\/"); 414 for (String aSplit : split) { 415 for (String arg : args) { 416 if (arg.equalsIgnoreCase(aSplit)) { 417 count += 5 - i + require; 418 } 419 } 420 } 421 } 422 count += StringMan.intersection(desc, args); 423 return count; 424 } 425 426 public Command getCommand(String arg) { 427 Command cmd = this.staticCommands.get(arg.toLowerCase()); 428 if (cmd == null) { 429 for (Command command : this.dynamicCommands) { 430 if (command.matches(arg)) { 431 return command; 432 } 433 } 434 } 435 return cmd; 436 } 437 438 public Command getCommand(Class<?> clazz) { 439 for (Command cmd : this.allCommands) { 440 if (cmd.getClass() == clazz) { 441 return cmd; 442 } 443 } 444 return null; 445 } 446 447 public Command getCommandById(String id) { 448 Command exact = this.staticCommands.get(id); 449 if (exact != null) { 450 return exact; 451 } 452 for (Command cmd : this.allCommands) { 453 if (cmd.getId().equals(id)) { 454 return cmd; 455 } 456 } 457 return null; 458 } 459 460 public boolean canExecute(PlotPlayer<?> player, boolean message) { 461 if (player == null) { 462 return true; 463 } 464 if (!this.required.allows(player)) { 465 if (message) { 466 player.sendMessage(this.required.getErrorMessage()); 467 } 468 } else if (!player.hasPermission(getPermission())) { 469 if (message) { 470 player.sendMessage( 471 TranslatableCaption.of("permission.no_permission"), 472 Template.of("node", getPermission()) 473 ); 474 } 475 } else { 476 return true; 477 } 478 return false; 479 } 480 481 public boolean matches(String arg) { 482 arg = arg.toLowerCase(); 483 return StringMan.isEqual(arg, this.id) || this.aliases.contains(arg); 484 } 485 486 public String getCommandString() { 487 if (this.parent == null) { 488 return "/" + toString(); 489 } else { 490 return this.parent.getCommandString() + " " + toString(); 491 } 492 } 493 494 public void sendUsage(PlotPlayer<?> player) { 495 player.sendMessage( 496 TranslatableCaption.of("commandconfig.command_syntax"), 497 Template.of("value", getUsage()) 498 ); 499 } 500 501 public String getUsage() { 502 if (this.usage != null && !this.usage.isEmpty()) { 503 if (this.usage.startsWith("/")) { 504 return this.usage; 505 } 506 return getCommandString() + " " + this.usage; 507 } 508 if (this.allCommands.isEmpty()) { 509 return getCommandString(); 510 } 511 StringBuilder args = new StringBuilder("["); 512 String prefix = ""; 513 for (Command cmd : this.allCommands) { 514 args.append(prefix).append(cmd.isStatic ? cmd.toString() : "<" + cmd + ">"); 515 prefix = "|"; 516 } 517 return getCommandString() + " " + args + "]"; 518 } 519 520 public Collection<Command> tabOf( 521 PlotPlayer<?> player, String[] input, boolean space, 522 String... args 523 ) { 524 if (!space) { 525 return null; 526 } 527 List<Command> result = new ArrayList<>(); 528 int index = input.length; 529 for (String arg : args) { 530 arg = arg.replace(getCommandString() + " ", ""); 531 String[] split = arg.split(" "); 532 if (split.length <= index) { 533 continue; 534 } 535 arg = StringMan.join(Arrays.copyOfRange(split, index, split.length), " "); 536 Command cmd = new Command(null, false, arg, getPermission(), getRequiredType(), null) { 537 }; 538 result.add(cmd); 539 } 540 return result; 541 } 542 543 public Collection<Command> tab(PlotPlayer<?> player, String[] args, boolean space) { 544 switch (args.length) { 545 case 0: 546 return this.allCommands; 547 case 1: 548 String arg = args[0].toLowerCase(); 549 if (space) { 550 Command cmd = getCommand(arg); 551 if (cmd != null && cmd.canExecute(player, false)) { 552 return cmd.tab(player, Arrays.copyOfRange(args, 1, args.length), space); 553 } else { 554 return null; 555 } 556 } else { 557 Set<Command> commands = new HashSet<>(); 558 for (Map.Entry<String, Command> entry : this.staticCommands.entrySet()) { 559 if (entry.getKey().startsWith(arg) && entry.getValue() 560 .canExecute(player, false)) { 561 commands.add(entry.getValue()); 562 } 563 } 564 return commands; 565 } 566 default: 567 Command cmd = getCommand(args[0]); 568 if (cmd != null) { 569 return cmd.tab(player, Arrays.copyOfRange(args, 1, args.length), space); 570 } else { 571 return null; 572 } 573 } 574 } 575 576 @Override 577 public String toString() { 578 return !this.aliases.isEmpty() ? this.aliases.get(0) : this.id; 579 } 580 581 @Override 582 public boolean equals(Object obj) { 583 if (this == obj) { 584 return true; 585 } 586 if (getClass() != obj.getClass()) { 587 return false; 588 } 589 Command other = (Command) obj; 590 if (this.hashCode() != other.hashCode()) { 591 return false; 592 } 593 return this.getFullId().equals(other.getFullId()); 594 } 595 596 @Override 597 public int hashCode() { 598 return this.getFullId().hashCode(); 599 } 600 601 public void checkTrue(boolean mustBeTrue, Caption message, Template... args) { 602 if (!mustBeTrue) { 603 throw new CommandException(message, args); 604 } 605 } 606 607 public <T> T check(T object, Caption message, Template... args) { 608 if (object == null) { 609 throw new CommandException(message, args); 610 } 611 return object; 612 } 613 614 615 public enum CommandResult { 616 FAILURE, 617 SUCCESS 618 } 619 620 621 public static class CommandException extends RuntimeException { 622 623 private final Template[] args; 624 private final Caption message; 625 626 public CommandException(final @Nullable Caption message, final Template... args) { 627 this.message = message; 628 this.args = args; 629 } 630 631 public void perform(final @Nullable PlotPlayer<?> player) { 632 if (player != null && message != null) { 633 player.sendMessage(message, args); 634 } 635 } 636 637 } 638 639}