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