Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/world/bentobox/bentobox/BentoBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import world.bentobox.bentobox.managers.AddonsManager;
import world.bentobox.bentobox.managers.BlueprintsManager;
import world.bentobox.bentobox.managers.CommandsManager;
import world.bentobox.bentobox.suggestions.SuggestionsManager;
import world.bentobox.bentobox.managers.FlagsManager;
import world.bentobox.bentobox.managers.HooksManager;
import world.bentobox.bentobox.managers.ChunkPregenManager;
Expand Down Expand Up @@ -65,6 +66,7 @@ public class BentoBox extends JavaPlugin implements Listener {

// Managers
private CommandsManager commandsManager;
private SuggestionsManager suggestionsManager;
private LocalesManager localesManager;
private AddonsManager addonsManager;
private FlagsManager flagsManager;
Expand Down Expand Up @@ -164,6 +166,9 @@ public void onEnable(){
// Set up command manager
commandsManager = new CommandsManager();

// Did-you-mean command suggestions
suggestionsManager = new SuggestionsManager(this);

// Load BentoBox commands
commandsManager.registerDefaultCommands();

Expand Down Expand Up @@ -394,6 +399,14 @@ public CommandsManager getCommandsManager() {
return commandsManager;
}

/**
* @return the did-you-mean command suggestions manager
* @since 3.20.0
*/
public SuggestionsManager getSuggestionsManager() {
return suggestionsManager;
}

/**
* @return the Locales manager
*/
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/world/bentobox/bentobox/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ public class Settings implements ConfigObject {
@ConfigEntry(path = "general.teleport-remove-mobs")
private boolean teleportRemoveMobs = false;

@ConfigComment("Did-you-mean command suggestions.")
@ConfigComment("When a player types an unknown command, e.g. /teams instead of /oneblock team,")
@ConfigComment("BentoBox suggests the closest matching BentoBox command. The player can click the")
@ConfigComment("suggestion or type 'yes' to run it.")
@ConfigEntry(path = "general.did-you-mean.unknown-commands", since = "3.20.0")
private boolean didYouMeanUnknownCommands = true;

@ConfigComment("When a player types an unknown subcommand, e.g. /island invit instead of /island team invite,")
@ConfigComment("BentoBox suggests the closest matching subcommand instead of showing an error.")
@ConfigEntry(path = "general.did-you-mean.subcommands", since = "3.20.0")
private boolean didYouMeanSubcommands = true;

/* PANELS */
@ConfigComment("Panel click cooldown. Value is in milliseconds. Prevents players spamming button presses in GUIs.")
@ConfigEntry(path = "panel.click-cooldown-ms")
Expand Down Expand Up @@ -1787,4 +1799,36 @@ public void setDisabledStructures(List<String> disabledStructures) {
this.disabledStructures = disabledStructures;
}

/**
* @return whether unknown root commands trigger did-you-mean suggestions
* @since 3.20.0
*/
public boolean isDidYouMeanUnknownCommands() {
return didYouMeanUnknownCommands;
}

/**
* @param didYouMeanUnknownCommands the didYouMeanUnknownCommands to set
* @since 3.20.0
*/
public void setDidYouMeanUnknownCommands(boolean didYouMeanUnknownCommands) {
this.didYouMeanUnknownCommands = didYouMeanUnknownCommands;
}

/**
* @return whether unknown subcommands trigger did-you-mean suggestions
* @since 3.20.0
*/
public boolean isDidYouMeanSubcommands() {
return didYouMeanSubcommands;
}

/**
* @param didYouMeanSubcommands the didYouMeanSubcommands to set
* @since 3.20.0
*/
public void setDidYouMeanSubcommands(boolean didYouMeanSubcommands) {
this.didYouMeanSubcommands = didYouMeanSubcommands;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,18 @@ public boolean execute(@NonNull CommandSender sender, @NonNull String label, Str
CompositeCommand cmd = getCommandFromArgs(args);
String cmdLabel = (cmd.subCommandLevel > 0) ? args[cmd.subCommandLevel - 1] : label;
List<String> cmdArgs = Arrays.asList(args).subList(cmd.subCommandLevel, args.length);
// Did-you-mean: the walk stopped at a command that has subcommands but with
// unconsumed args, i.e. the first leftover arg matched none of them. Suggest
// what the player probably meant instead of dead-ending into an error.
if (!cmdArgs.isEmpty() && cmd.hasSubCommands() && user.isPlayer()
&& plugin.getSettings().isDidYouMeanSubcommands() && plugin.getSuggestionsManager() != null) {
String typedPrefix = "/" + label + (cmd.subCommandLevel > 0
? " " + String.join(" ", Arrays.asList(args).subList(0, cmd.subCommandLevel))
: "");
if (plugin.getSuggestionsManager().suggestSubcommand(user, cmd, typedPrefix, cmdArgs)) {
return true;
}
}
return cmd.call(user, cmdLabel, cmdArgs);
}

Expand Down
42 changes: 40 additions & 2 deletions src/main/java/world/bentobox/bentobox/api/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import com.google.common.cache.CacheBuilder;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.title.Title;
import world.bentobox.bentobox.BentoBox;
Expand Down Expand Up @@ -804,13 +806,49 @@ public void sendRawMessage(String message) {
/**
* Parses a message string into an Adventure Component, handling inline commands and
* auto-detecting legacy or MiniMessage format.
* <p>
* Inline [run_command]/[hover]-style commands are applied to the parsed Component
* programmatically, not by wrapping the text in MiniMessage click/hover tags: the
* text routinely contains a {@code §r} from the legacy round-trip (any closed color
* or decoration emits one), and the {@code <reset>} it maps to would close the
* wrapper tags, killing the events and leaking literal {@code </click></hover>}
* text into chat.
*
* @param text the message text
* @return the parsed Component
*/
private Component parseToComponent(String text) {
String mmMessage = Util.convertInlineCommandsToMiniMessage(text);
return Util.parseMiniMessageOrLegacy(mmMessage);
Util.InlineCommands inline = Util.extractInlineCommands(text);
Component component = Util.parseMiniMessageOrLegacy(inline.cleanText());
ClickEvent clickEvent = toClickEvent(inline.clickAction(), inline.clickValue());
if (clickEvent != null) {
component = component.clickEvent(clickEvent);
}
String hoverText = inline.hoverText();
if (hoverText != null) {
component = component.hoverEvent(HoverEvent.showText(Util.parseMiniMessageOrLegacy(hoverText)));
}
return component;
}

/**
* Maps an inline command action to an Adventure {@link ClickEvent}.
*
* @param action the inline command action name, may be null
* @param value the action's value, may be null
* @return the click event, or null if the action is unknown or absent
*/
private static ClickEvent toClickEvent(String action, String value) {
if (action == null || value == null) {
return null;
}
return switch (action) {
case "run_command" -> ClickEvent.runCommand(value);
case "suggest_command" -> ClickEvent.suggestCommand(value);
case "copy_to_clipboard" -> ClickEvent.copyToClipboard(value);
case "open_url" -> ClickEvent.openUrl(value);
default -> null;
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import world.bentobox.bentobox.listeners.teleports.PlayerTeleportListener;
import world.bentobox.bentobox.managers.ChunkPregenManager;
import world.bentobox.bentobox.managers.IslandDeletionManager;
import world.bentobox.bentobox.suggestions.DidYouMeanListener;

/**
* Registers all BentoBox event listeners with the Bukkit plugin manager.
Expand Down Expand Up @@ -35,6 +36,7 @@ public void register() {
manager.registerEvents(new BlockEndDragon(plugin), plugin);
manager.registerEvents(new BannedCommands(plugin), plugin);
manager.registerEvents(new DeathListener(plugin), plugin);
manager.registerEvents(new DidYouMeanListener(plugin), plugin);
// Register the plugin itself for any listeners it implements (e.g. MV unregister)
manager.registerEvents(plugin, plugin);
islandDeletionManager = new IslandDeletionManager(plugin);
Expand Down
Loading
Loading