Class CommandTabCompletionDispatch

java.lang.Object
com.ultikits.ultitools.abstracts.command.CommandTabCompletionDispatch

public final class CommandTabCompletionDispatch extends Object
Single tab-completion dispatch implementation originally shared by both command-executor generations (WIRE-01 / D-06); AbstractCommandExecutor was removed in 6.3.0, so BaseCommandExecutor is now this class's only caller.

BaseCommandExecutor and the removed AbstractCommandExecutor each scanned their own @CmdMapping methods into an independent BiMap<String, Method> and did not share a class hierarchy, so neither could simply inherit a single suggest implementation from a common supertype. This class was the alternative both delegated to: a stateless dispatch entered with the caller's own mapping table and this.

Two responsibilities live here that were previously either duplicated or entirely absent:

  • The mapping-level @CmdMapping(permission=)/requireOp() guard (checkPermission(CommandSender, Method)/checkOp(CommandSender, Method)), relocated -- not copied -- from the removed (6.3.0) AbstractCommandExecutor's private methods of the same name (T-05-20 / T-05-21). BaseCommandExecutor.suggest had ZERO permission checks before this class existed, so migrating a command onto the current generation leaked the entire sub-command table to unprivileged players. In tab completion the silent counterpart (isVisible) filters every path before a mapping contributes anything: first-token literals and parameter slots directly, later-argument literals and parameter slots through matchCandidates.
  • Argument-index -> matched-method -> parameter-name resolution for a multi-token argument vector (suggest(BiMap, Player, Command, String[], Object)), which is absent from commands/tabcomplete/TabCompletionManager (its createContext leaves matchedMethod/parameterName null).
Reflective suggestion-method invocation itself is deliberately NOT reimplemented here -- the resolved <param> slot is handed to TabCompletionManager.suggest(TabCompletionContext), which reaches MethodInvocationCompleter. That completer walks the class hierarchy (issue #190), so an AOP-proxied executor's suggestion method resolves correctly; the removed (6.3.0) AbstractCommandExecutor's own eight private reflection helpers did not.

@key notation (a suggest() value starting with @) resolves through TabCompletionManager.resolveSuggestValue(Method, String) + TabCompletionManager.suggest(TabCompletionContext, String) (05-06 / D-07) -- driven by the parameter's actual @CmdParam.suggest(), never by parameterName (the display name), so a display name that happens to start with @ is never mistaken for a completer key.

Since:
6.3.0
Author:
wisdomme
  • Constructor Details

    • CommandTabCompletionDispatch

      private CommandTabCompletionDispatch()
  • Method Details

    • suggest

      public static List<String> suggest(com.google.common.collect.BiMap<String,Method> mappings, org.bukkit.entity.Player player, org.bukkit.command.Command command, String[] args, Object executorInstance)
      The single tab-completion dispatch entry point for both base-class generations.

      Both generations' suggest(Player, Command, String[]) reduce to a one-line delegation to this method -- see BaseCommandExecutor.suggest(Player, Command, String[]) and the removed (6.3.0) AbstractCommandExecutor's own former shell.

      Parameters:
      mappings - the executor's own format-to-method mapping table
      player - the player requesting completion
      command - the command being completed
      args - the current arguments, including the partial final token
      executorInstance - the executor instance (this from the caller) -- carried through to TabCompletionContext for reflective suggestion invocation
      Returns:
      the suggestions; never null
    • suggestFirstToken

      private static List<String> suggestFirstToken(com.google.common.collect.BiMap<String,Method> mappings, org.bukkit.entity.Player player, org.bukkit.command.Command command, String[] args, Object executorInstance)
      First-token completion: literal command formats, permission-filtered before they enter the candidate map, then delegated to TabCompletionManager.suggestFirstArgs(Map, TabCompletionContext) for the package's existing sort-and-dedup semantics. The removed (6.3.0) AbstractCommandExecutor's own first-token branch neither sorted nor deduplicated -- adopting the package's behaviour here is a deliberate, named change (see plan 05-05's SUMMARY).
    • firstTokenParameterSuggestions

      private static List<String> firstTokenParameterSuggestions(Map<String,Method> visible, org.bukkit.entity.Player player, org.bukkit.command.Command command, String[] args, Object executorInstance)
      Resolves the <param> slot of every visible format whose first token is a parameter rather than a literal.

      TabCompletionManager.suggestFirstArgs skips parameter placeholders by construction -- it collects literal subcommand names -- and until 6.3.0 nothing else covered position 0, so a format like "<name>" contributed no candidates at all and its @CmdParam(suggest = ...) method was never invoked. That silently disabled completion for the first argument of roughly forty mappings across nine modules, including /home <name>, /warp <name>, /tpa <player> and /pay; two of them had written a suggester that never ran. See #398.

      Position 1 and later were unaffected, because suggestSubsequentToken has always resolved parameter slots. UltiMenu declares the same suggester on "<name>" and on "open <name>", and only the second worked -- same class, same parameter, same method, differing only in token position.

      Since:
      6.3.0
    • suggestSubsequentToken

      private static List<String> suggestSubsequentToken(com.google.common.collect.BiMap<String,Method> mappings, org.bukkit.entity.Player player, org.bukkit.command.Command command, String[] args, Object executorInstance)
      Argument-position resolution for a multi-token vector: find every mapping whose format's literal/parameter prefix matches the already-typed tokens, then for each candidate resolve the token at the position being completed -- either a <param> slot (delegated to commands/tabcomplete/) or a literal token.

      A literal comes from the matched candidate itself and from nothing else: matchCandidates(BiMap, Player, String[], int) has already filtered every candidate by the typed prefix, by format length and by visibility (permission and OP), so a player who typed alpha is offered only the literals of the alpha ... mappings they may see. Until 6.3.0 the literal branch instead rescanned every visible mapping at this position, ignoring the typed branch, and repeated that list once per matching candidate (case 61).

      Each literal is offered once: several candidates can share the literal at this position (alpha enable and alpha enable <target>), so a literal is added only when the result does not already hold it, in candidate order. Parameter-slot suggestions are a different source and are appended exactly as their completer returns them.

    • matchCandidates

      private static List<Method> matchCandidates(com.google.common.collect.BiMap<String,Method> mappings, org.bukkit.entity.Player player, String[] args, int targetIndex)
    • prefixMatches

      private static boolean prefixMatches(String[] formatArgs, String[] args, int targetIndex)
    • suggestParameterSlot

      private static List<String> suggestParameterSlot(org.bukkit.entity.Player player, org.bukkit.command.Command command, String[] args, int targetIndex, Method method, String arg, Object executorInstance)
    • splitFormat

      private static String[] splitFormat(String format)
    • argAt

      private static String argAt(String format, int index)
    • isParameterToken

      private static boolean isParameterToken(String token)
    • isVisible

      private static boolean isVisible(org.bukkit.entity.Player player, Method method)
      Tab-completion visibility filter: silent counterpart to checkPermission(CommandSender, Method) / checkOp(CommandSender, Method) (T-05-fix Part 3 / SILENT-25). Real-machine UAT observed a player without permission repeatedly receiving the "no permission" message while typing unrelated sub-commands, because every mapping this class's first-token and later-argument paths evaluate for visibility went through the SAME messaging checkPermission/checkOp used by actual command dispatch (the removed (6.3.0) AbstractCommandExecutor.onCommand's call site). Tab completion is not a command invocation: filtering an unprivileged sender's candidates out silently is correct, but re-sending the rejection notice on every keystroke that happens to re-evaluate a gated mapping is not. checkPermission(CommandSender, Method) and checkOp(CommandSender, Method) themselves are UNCHANGED and still message -- only this tab-completion-only visibility filter switches to the silent predicates below.
      Parameters:
      player - the player requesting completion
      method - the candidate @CmdMapping method
      Returns:
      true if the mapping is visible to player
    • checkPermission

      public static boolean checkPermission(org.bukkit.command.CommandSender sender, Method method)
      Mapping-level permission guard: gates a matched @CmdMapping method out of tab completion (or dispatch) entirely before it can contribute anything, rather than filtering an already-assembled list afterwards.

      Relocated -- not copied -- from the removed (6.3.0) AbstractCommandExecutor's private method of the same name and signature (T-05-20). Behaviour is byte-for-byte identical, including the sendMessage on denial, which both base-class generations inherited unchanged from the pre-existing class this predicate was ported from, back when both generations coexisted. This method remains BaseCommandExecutor's actual-dispatch guard (its call site is unchanged); tab completion's own visibility filter goes through the silent isPermissionSatisfied(CommandSender, Method) instead (T-05-fix Part 3).

      Parameters:
      sender - the command sender being checked
      method - the matched @CmdMapping method
      Returns:
      true if the mapping declares no permission or the sender holds it
    • checkOp

      public static boolean checkOp(org.bukkit.command.CommandSender sender, Method method)
      Mapping-level OP guard, the requireOp() counterpart to checkPermission(CommandSender, Method). Relocated -- not copied -- from the removed (6.3.0) AbstractCommandExecutor's private method of the same name and signature (T-05-21). Remains the actual-dispatch guard, unchanged; tab completion's visibility filter goes through the silent isOpSatisfied(CommandSender, Method) instead (T-05-fix Part 3).
      Parameters:
      sender - the command sender being checked
      method - the matched @CmdMapping method
      Returns:
      true if the mapping does not require OP or the sender is an OP
    • isPermissionSatisfied

      private static boolean isPermissionSatisfied(org.bukkit.command.CommandSender sender, Method method)
      The permission predicate checkPermission(CommandSender, Method) messages on, and isVisible(Player, Method) does not -- the single source of truth for BOTH, so the two can never independently drift into disagreeing about whether a mapping is permitted.
      Parameters:
      sender - the command sender being checked
      method - the matched @CmdMapping method
      Returns:
      true if the mapping declares no permission or the sender holds it
    • isOpSatisfied

      private static boolean isOpSatisfied(org.bukkit.command.CommandSender sender, Method method)
      The OP predicate checkOp(CommandSender, Method) messages on, and isVisible(Player, Method) does not -- the single source of truth for both.
      Parameters:
      sender - the command sender being checked
      method - the matched @CmdMapping method
      Returns:
      true if the mapping does not require OP or the sender is an OP