diff --git a/build.gradle b/build.gradle index 64f4736..ed6e6d6 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ java { PluginManifest pluginManifest = [ name : 'CommandPrompter', - version : new Version(major: 2, minor: 7, patch: 0, fix: 0, classifier: 'SNAPSHOT'), + version : new Version(major: 2, minor: 8, patch: 0, fix: 0, classifier: 'SNAPSHOT'), author : 'CyR1en', description: 'Perfect companion plugin for inventory UI menu.', entry : 'com.cyr1en.commandprompter.CommandPrompter' @@ -57,6 +57,7 @@ repositories { maven { url 'https://repo.codemc.io/repository/maven-snapshots/' } maven { url 'https://repo.extendedclip.com/content/repositories/placeholderapi/' } maven { url 'https://jitpack.io' } + maven { url 'https://repo.glaremasters.me/repository/towny/'} flatDir { dirs 'libs' } } @@ -74,12 +75,15 @@ dependencies { implementation 'com.github.stefvanschie.inventoryframework:IF:0.10.11' compileOnly 'me.clip:placeholderapi:2.11.2' - compileOnly "net.kyori:adventure-text-serializer-legacy:4.13.1" - compileOnly "net.kyori:adventure-text-serializer-plain:4.13.1" - compileOnly "org.spigotmc:spigot-api:1.20-R0.1-SNAPSHOT" + compileOnly "net.kyori:adventure-text-serializer-legacy:4.15.0" + compileOnly "net.kyori:adventure-text-serializer-plain:4.15.0" + compileOnly "net.kyori:adventure-text-minimessage:4.15.0" + compileOnly 'com.palmergames.bukkit.towny:towny:0.100.0.0' + compileOnly "org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT" compileOnly 'com.github.LeonMangler:SuperVanish:6.2.17' compileOnly 'com.github.mbax:VanishNoPacket:3.22' compileOnly 'org.jetbrains:annotations:23.0.0' + compileOnly 'net.luckperms:api:5.4' // Local implementation fileTree(dir: 'libs', include: '*.jar') diff --git a/src/main/java/com/cyr1en/commandprompter/CommandPrompter.java b/src/main/java/com/cyr1en/commandprompter/CommandPrompter.java index 72c4719..547969a 100644 --- a/src/main/java/com/cyr1en/commandprompter/CommandPrompter.java +++ b/src/main/java/com/cyr1en/commandprompter/CommandPrompter.java @@ -102,6 +102,8 @@ public void onEnable() { Bukkit.getScheduler().runTaskLater(this, () -> { hookContainer = new HookContainer(this); hookContainer.initHooks(); + headCache.registerFilters(); + promptManager.registerPrompts(); ChatPrompt.resolveListener(this); }, 1L); } diff --git a/src/main/java/com/cyr1en/commandprompter/api/prompt/InputValidator.java b/src/main/java/com/cyr1en/commandprompter/api/prompt/InputValidator.java new file mode 100644 index 0000000..c6381a8 --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/api/prompt/InputValidator.java @@ -0,0 +1,36 @@ +package com.cyr1en.commandprompter.api.prompt; + + +/** + * A functional interface for validating input. + * + *

Implement this interface to create a custom validator for your prompt.

+ */ +public interface InputValidator { + + /** + * Validate the input. + * + *

Return true if the input is valid, false otherwise.

+ * + * @param input the input to validate + * @return true if the input is valid, false otherwise + */ + boolean validate(String input); + + /** + * Get the alias for this validator. + * + *

The alias is used to identify the validator.

+ * + * @return the alias for this validator + */ + String alias(); + + /** + * Get the message to send when validation fails. + * + * @return the message to send when validation fails + */ + String messageOnFail(); +} diff --git a/src/main/java/com/cyr1en/commandprompter/api/prompt/Prompt.java b/src/main/java/com/cyr1en/commandprompter/api/prompt/Prompt.java index bdb01b7..962c6ca 100644 --- a/src/main/java/com/cyr1en/commandprompter/api/prompt/Prompt.java +++ b/src/main/java/com/cyr1en/commandprompter/api/prompt/Prompt.java @@ -30,7 +30,6 @@ import com.cyr1en.commandprompter.prompt.PromptParser; import java.util.List; -import java.util.regex.Pattern; public interface Prompt { @@ -70,35 +69,20 @@ public interface Prompt { PromptManager getPromptManager(); List getArgs(); - - /** - * set the regex check - * - * @param regexCheck regular expression to check prompt input - */ - void setRegexCheck(String regexCheck); - - /** - * set the regex check using {@link Pattern} - * - * @param regexPattern regular expression to check prompt input - */ - void setRegexCheck(Pattern regexPattern); - + /** - * Get the regex check + * Set the input validator * - * @return regular expression to check prompt input + * @param inputValidator input validator to check prompt input */ - Pattern getRegexCheck(); + void setInputValidator(InputValidator inputValidator); /** - * Check if the input is valid using {@link Prompt#getRegexCheck()} + * Get the input validator * - * @param input input to check - * @return true if input is valid + * @return input validator to check prompt input */ - boolean isValidInput(String input); + InputValidator getInputValidator(); /** * Returns a boolean value if inputs should be sanitized. diff --git a/src/main/java/com/cyr1en/commandprompter/config/CommandPrompterConfig.java b/src/main/java/com/cyr1en/commandprompter/config/CommandPrompterConfig.java index 7b24c46..05e3077 100644 --- a/src/main/java/com/cyr1en/commandprompter/config/CommandPrompterConfig.java +++ b/src/main/java/com/cyr1en/commandprompter/config/CommandPrompterConfig.java @@ -187,7 +187,17 @@ public record CommandPrompterConfig( "for CommandPrompter" }) @NodeDefault("true") - boolean commandTabComplete + boolean commandTabComplete, + + @ConfigNode + @NodeName("Ignore-MiniMessage") + @NodeComment({ + "If Prompt-Regex is left to default", + "this will ignore MiniMessage syntax." + }) + boolean ignoreMiniMessage + + ) implements AliasedSection { public String[] getPermissionAttachment(String key) { diff --git a/src/main/java/com/cyr1en/commandprompter/config/PromptConfig.java b/src/main/java/com/cyr1en/commandprompter/config/PromptConfig.java index 597789e..9fc66ac 100644 --- a/src/main/java/com/cyr1en/commandprompter/config/PromptConfig.java +++ b/src/main/java/com/cyr1en/commandprompter/config/PromptConfig.java @@ -1,12 +1,18 @@ package com.cyr1en.commandprompter.config; +import com.cyr1en.commandprompter.api.prompt.InputValidator; import com.cyr1en.commandprompter.config.annotations.field.*; import com.cyr1en.commandprompter.config.annotations.type.ConfigHeader; import com.cyr1en.commandprompter.config.annotations.type.ConfigPath; import com.cyr1en.commandprompter.config.annotations.type.Configuration; import com.cyr1en.commandprompter.prompt.ui.CacheFilter; +import com.cyr1en.commandprompter.prompt.validators.NoopValidator; +import com.cyr1en.commandprompter.prompt.validators.OnlinePlayerValidator; +import com.cyr1en.commandprompter.prompt.validators.RegexValidator; import com.cyr1en.kiso.mc.configuration.base.Config; +import java.util.regex.Pattern; + @Configuration @ConfigPath("prompt-config.yml") @ConfigHeader({"Prompts", "Configuration"}) @@ -21,6 +27,8 @@ public record PromptConfig( "PlayerUI formatting", "", "Skull-Name-Format - The display name format", " for the player heads", "", + "Skull-Custom-Model-Data - The custom model data for the", + " player heads", "", "Size - the size of the UI (multiple of 9, between 18-54)", "", "Cache-Size - Size for the head cache", "", "Cache-Delay - Delay in ticks after the player", "", @@ -33,6 +41,11 @@ public record PromptConfig( }) String skullNameFormat, + @ConfigNode + @NodeName("PlayerUI.Skull-Custom-Model-Data") + @NodeDefault("0") + int skullCustomModelData, + @ConfigNode @NodeName("PlayerUI.Size") @NodeDefault("54") @@ -287,20 +300,28 @@ public record PromptConfig( String strSampleErrMessage ) implements AliasedSection { - public String findIVRegexCheckInConfig(String alias) { + private String findIVRegexCheckInConfig(String alias) { return getIVValue("Alias", alias, "Regex"); } - public String getIVErrMessage(String alias) { + private String getIVErrMessage(String alias) { return getIVValue("Alias", alias, "Err-Message"); } - public String getIVErrMessageWithRegex(String regex) { - return getIVValue("Regex", regex, "Err-Message"); + private String getIVValue(String key, String keyVal, String query) { + return getInputValidationValue("Input-Validation", key, keyVal, query); } - public String getIVValue(String key, String keyVal, String query) { - return getInputValidationValue("Input-Validation", key, keyVal, query); + public InputValidator getInputValidator(String alias) { + if (alias == null || alias.isBlank()) + return new NoopValidator(); + var isPlayer = Boolean.parseBoolean(getIVValue("Alias", alias, "Online-Player")); + if (isPlayer) + return new OnlinePlayerValidator(alias, getIVErrMessage(alias)); + var regex = findIVRegexCheckInConfig(alias); + if (regex != null && !regex.isBlank()) + return new RegexValidator(alias, Pattern.compile(regex), getIVErrMessage(alias)); + return new NoopValidator(); } /** diff --git a/src/main/java/com/cyr1en/commandprompter/hook/HookContainer.java b/src/main/java/com/cyr1en/commandprompter/hook/HookContainer.java index a278d12..c359969 100644 --- a/src/main/java/com/cyr1en/commandprompter/hook/HookContainer.java +++ b/src/main/java/com/cyr1en/commandprompter/hook/HookContainer.java @@ -26,6 +26,8 @@ public void initHooks() { hook(CarbonChatHook.class); hook(VanishNoPacketHook.class); hook(PapiHook.class); + hook(TownyHook.class); + hook(LuckPermsHook.class); } @Override diff --git a/src/main/java/com/cyr1en/commandprompter/hook/hooks/FilterHook.java b/src/main/java/com/cyr1en/commandprompter/hook/hooks/FilterHook.java new file mode 100644 index 0000000..ec60c4d --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/hook/hooks/FilterHook.java @@ -0,0 +1,16 @@ +package com.cyr1en.commandprompter.hook.hooks; + +import com.cyr1en.commandprompter.prompt.ui.HeadCache; + +/** + * A hook that registers filters to the head cache. + */ +public interface FilterHook { + + /** + * Register filters to the head cache. + * + * @param headCache the head cache + */ + void registerFilters(HeadCache headCache); +} diff --git a/src/main/java/com/cyr1en/commandprompter/hook/hooks/LuckPermsHook.java b/src/main/java/com/cyr1en/commandprompter/hook/hooks/LuckPermsHook.java new file mode 100644 index 0000000..575165c --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/hook/hooks/LuckPermsHook.java @@ -0,0 +1,175 @@ +package com.cyr1en.commandprompter.hook.hooks; + +import com.cyr1en.commandprompter.CommandPrompter; +import com.cyr1en.commandprompter.hook.Hook; +import com.cyr1en.commandprompter.hook.annotations.TargetPlugin; +import com.cyr1en.commandprompter.prompt.ui.CacheFilter; +import com.cyr1en.commandprompter.prompt.ui.HeadCache; +import net.luckperms.api.LuckPerms; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.RegisteredServiceProvider; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Hook for LuckPerms plugin. + * + *

+ * Main component of this hook are the cache filters for LuckPerms groups. + */ +@TargetPlugin(pluginName = "LuckPerms") +public class LuckPermsHook extends BaseHook implements FilterHook { + + private LuckPerms api; + + /** + * Construct a new LuckPermsHook. + * + * @param plugin the plugin + */ + public LuckPermsHook(CommandPrompter plugin) { + super(plugin); + RegisteredServiceProvider provider = plugin.getServer().getServicesManager().getRegistration(LuckPerms.class); + if (provider != null) { + this.api = provider.getProvider(); + } else { + // If LuckPerms was loaded but the service provider was not found then we make sure that the container + // is empty. + plugin.getHookContainer().replace(LuckPermsHook.class, Hook.empty()); + } + } + + /** + * Register the cache filters for LuckPerms groups. + * + * @param cache the head cache. + */ + public void registerFilters(HeadCache cache) { + cache.registerFilter(new OwnGroupFilter(this)); + cache.registerFilter(new GroupFilter(this)); + } + + /** + * Get all players that are in the same group as the relative player. + * + * @param groupName the group name + * @return a list of players with the same group + */ + private List getPlayersWithGroup(String groupName) { + if (api == null || groupName.isBlank()) return List.of(); + return (List) Bukkit.getOnlinePlayers().stream() + .filter(p -> { + var user = api.getUserManager().getUser(p.getName()); + if (user == null) return false; + var group = user.getPrimaryGroup(); + return group.equals(groupName); + }).toList(); + } + + /** + * Get the LuckPerms API. + * + * @return the LuckPerms API + */ + private LuckPerms getApi() { + return api; + } + + + /** + * A PlayerUI filter that would filter all players that are in the same group as the relative player. + * + *

+ * This is a special case of {@link CacheFilter} where the regex key is 'og'. + * This would filter all players that are in the same group as the relative player. + */ + private static class OwnGroupFilter extends CacheFilter { + + private final LuckPermsHook hook; + + /** + * Construct a new own group filter. + * + * @param hook the LuckPerms hook + */ + public OwnGroupFilter(LuckPermsHook hook) { + super(Pattern.compile("lpo"), "PlayerUI.Filter-Format.LuckPermsOwnGroup"); + this.hook = hook; + } + + @Override + public CacheFilter reConstruct(String promptKey) { + // No need to re-construct + return this; + } + + /** + * Filter all players that are in the same group as the relative player. + * + * @param relativePlayer the players to filter + * @return a list of players with the same group + */ + @Override + public List filter(Player relativePlayer) { + var user = hook.getApi().getUserManager().getUser(relativePlayer.getUniqueId()); + if (user == null) return List.of(); + var group = user.getPrimaryGroup(); + return hook.getPlayersWithGroup(group); + } + } + + /** + * A PlayerUI filter that would filter all players based on the group name. + * + *

+ * The regex for this filter is 'g(\S+)'. In regex capturing group 1 is the group + * name that would be used to filter. + */ + private static class GroupFilter extends CacheFilter { + + private final String groupName; + private final LuckPermsHook hook; + + /** + * Default construct a new group filter. + * + * @param hook the LuckPerms hook + */ + public GroupFilter(LuckPermsHook hook) { + this("", hook); + } + + /** + * Constructor to construct a new group filter with a group name. + * + *

+ * This constructor will be used by the {@link #reConstruct(String)} function. + * + * @param groupName the group name that would be used to filter + * @param hook the LuckPerms hook + */ + public GroupFilter(String groupName, LuckPermsHook hook) { + super(Pattern.compile("lpg(\\S+);"), "PlayerUI.Filter-Format.LuckPermsGroup", 1); + this.groupName = groupName; + this.hook = hook; + } + + @Override + public CacheFilter reConstruct(String promptKey) { + var matcher = this.getRegexKey().matcher(promptKey); + var found = matcher.find(); + var groupName = found ? matcher.group(1) : ""; + return new GroupFilter(groupName, hook); + } + + @Override + public List filter(Player relativePlayer) { + var players = hook.getPlayersWithGroup(groupName); + hook.getPlugin().getPluginLogger().debug("Players: %s", players); + return players; + } + } + +} diff --git a/src/main/java/com/cyr1en/commandprompter/hook/hooks/TownyHook.java b/src/main/java/com/cyr1en/commandprompter/hook/hooks/TownyHook.java new file mode 100644 index 0000000..3270669 --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/hook/hooks/TownyHook.java @@ -0,0 +1,76 @@ +package com.cyr1en.commandprompter.hook.hooks; + +import com.cyr1en.commandprompter.CommandPrompter; +import com.cyr1en.commandprompter.hook.annotations.TargetPlugin; +import com.cyr1en.commandprompter.prompt.ui.CacheFilter; +import com.cyr1en.commandprompter.prompt.ui.HeadCache; +import com.palmergames.bukkit.towny.TownyAPI; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +@TargetPlugin(pluginName = "Towny") +public class TownyHook extends BaseHook implements FilterHook { + public TownyHook(CommandPrompter plugin) { + super(plugin); + } + + public void registerFilters(HeadCache cache) { + cache.registerFilter(new TownFilter()); + cache.registerFilter(new NationFilter()); + } + + /** + * A PlayerUI filter that would filter all players that are in the same town as the relative player. + */ + private static class TownFilter extends CacheFilter { + + + public TownFilter() { + super(Pattern.compile("tt"), "PlayerUI.Filter-Format.TownyTown"); + } + + @Override + public CacheFilter reConstruct(String promptKey) { + return this; + } + + @Override + public List filter(Player relativePlayer) { + var town = TownyAPI.getInstance().getTown(relativePlayer); + if (town == null) return List.of(); + return town.getResidents().stream() + .map(r -> Bukkit.getPlayer(r.getName())) + .filter(Objects::nonNull) + .toList(); + } + } + + /** + * A PlayerUI filter that would filter all players that are in the same nation as the relative player. + */ + private static class NationFilter extends CacheFilter { + + public NationFilter() { + super(Pattern.compile("tn"), "PlayerUI.Filter-Format.TownyNation"); + } + + @Override + public CacheFilter reConstruct(String promptKey) { + return this; + } + + @Override + public List filter(Player relativePlayer) { + var nation = TownyAPI.getInstance().getNation(relativePlayer); + if (nation == null) return List.of(); + return nation.getResidents().stream() + .map(r -> Bukkit.getPlayer(r.getName())) + .filter(Objects::nonNull) + .toList(); + } + } +} diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/PromptManager.java b/src/main/java/com/cyr1en/commandprompter/prompt/PromptManager.java index 4bfa53a..6b883b8 100644 --- a/src/main/java/com/cyr1en/commandprompter/prompt/PromptManager.java +++ b/src/main/java/com/cyr1en/commandprompter/prompt/PromptManager.java @@ -68,10 +68,9 @@ public PromptManager(CommandPrompter commandPrompter) { this.promptRegistry = new PromptRegistry(plugin); this.promptParser = new PromptParser(this); this.scheduler = Bukkit.getScheduler(); - registerPrompts(); } - private void registerPrompts() { + public void registerPrompts() { this.put("", ChatPrompt.class); this.put("a", AnvilPrompt.class); this.put(plugin.getHeadCache().makeFilteredPattern(), PlayerUIPrompt.class); @@ -119,8 +118,10 @@ public void processPrompt(PromptContext context) { return; var queue = promptRegistry.get(sender); - if (queue.isEmpty() && !queue.containsPCM()) + if (queue.isEmpty() && !queue.containsPCM()) { + dispatchQueue(sender, queue); return; + } if (!checkInput(queue, context)) return; @@ -189,11 +190,11 @@ private boolean checkInput(PromptQueue promptQueue, PromptContext context) { return true; var prompt = promptQueue.peek(); - if (prompt.isValidInput(context.getContent())) + var validator = prompt.getInputValidator(); + if (validator.validate(context.getContent())) return true; - var errMsg = plugin.getPromptConfig().getIVErrMessageWithRegex(prompt.getRegexCheck().pattern()); - plugin.getMessenger().sendMessage(context.getSender(), errMsg); + plugin.getMessenger().sendMessage(context.getSender(), validator.messageOnFail()); sendPrompt(context.getSender()); return false; } diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/PromptParser.java b/src/main/java/com/cyr1en/commandprompter/prompt/PromptParser.java index 2e902c2..605fd9a 100644 --- a/src/main/java/com/cyr1en/commandprompter/prompt/PromptParser.java +++ b/src/main/java/com/cyr1en/commandprompter/prompt/PromptParser.java @@ -25,8 +25,11 @@ package com.cyr1en.commandprompter.prompt; import com.cyr1en.commandprompter.CommandPrompter; +import com.cyr1en.commandprompter.api.prompt.InputValidator; import com.cyr1en.commandprompter.api.prompt.Prompt; import com.cyr1en.commandprompter.hook.hooks.PapiHook; +import com.cyr1en.commandprompter.prompt.validators.NoopValidator; +import com.cyr1en.commandprompter.util.MMUtil; import com.cyr1en.kiso.utils.SRegex; import org.bukkit.entity.Player; @@ -73,17 +76,30 @@ public String getEscapedRegex() { public boolean isParsable(PromptContext promptContext) { var prompts = getPrompts(promptContext); + if (plugin.getConfiguration().ignoreMiniMessage()) + prompts = MMUtil.filterOutMiniMessageTags(prompts); return !prompts.isEmpty(); } /** * Parses a contents of {@link PromptContext} * + *

+ * This method will parse the contents of the {@link PromptContext} and + * create a {@link PromptQueue} for the sender. It will also parse the + * {@link Prompt} and add it to the {@link PromptQueue}. + * + *

+ * This function returns the hashCode of the {@link PromptQueue} that was + * created. This is used to retroactively cancel the prompt within a certain time. + * * @param promptContext Context to parse * @return hashCode of the {@link PromptQueue} that was created. */ public int parsePrompts(PromptContext promptContext) { var prompts = getPrompts(promptContext); + prompts = MMUtil.filterOutMiniMessageTags(prompts); + plugin.getPluginLogger().debug("Prompts: " + prompts); var command = promptContext.getContent().trim(); @@ -120,7 +136,7 @@ public int parsePrompts(PromptContext promptContext) { var sender = promptContext.getSender(); var promptArgs = ArgumentUtil.findPattern(PromptArgument.class, cleanPrompt); plugin.getPluginLogger().debug("Prompt args: " + promptArgs); - var inputValidation = extractInputValidation(cleanPrompt); + var inputValidator = extractInputValidation(cleanPrompt); // Set papi placeholders if exists var promptTxt = ArgumentUtil.stripArgs(cleanPrompt); @@ -131,7 +147,7 @@ public int parsePrompts(PromptContext promptContext) { String.class, List.class) .newInstance(plugin, promptContext, promptTxt, promptArgs); - p.setRegexCheck(plugin.getPromptConfig().findIVRegexCheckInConfig(inputValidation)); + p.setInputValidator(inputValidator); if (promptArgs.contains(PromptArgument.DISABLE_SANITATION)) p.setInputSanitization(false); @@ -143,16 +159,16 @@ public int parsePrompts(PromptContext promptContext) { } } return manager.getPromptRegistry().get(promptContext.getSender()).hashCode(); - } - private String extractInputValidation(String prompt) { + private InputValidator extractInputValidation(String prompt) { // iv is with pattern -iv: var pattern = Pattern.compile(PromptArgument.INPUT_VALIDATION.getKey()); var matcher = pattern.matcher(prompt); - if (!matcher.find()) return ""; + if (!matcher.find()) return new NoopValidator(); var found = matcher.group(); - return found.split(":")[1]; + var alias = found.split(":")[1]; + return plugin.getPromptConfig().getInputValidator(alias); } private String resolvePapiPlaceholders(Player sender, String prompt) { diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/PromptQueue.java b/src/main/java/com/cyr1en/commandprompter/prompt/PromptQueue.java index 8fd6bfe..e889aee 100644 --- a/src/main/java/com/cyr1en/commandprompter/prompt/PromptQueue.java +++ b/src/main/java/com/cyr1en/commandprompter/prompt/PromptQueue.java @@ -4,12 +4,15 @@ import com.cyr1en.commandprompter.PluginLogger; import com.cyr1en.commandprompter.api.Dispatcher; import com.cyr1en.commandprompter.api.prompt.Prompt; +import com.cyr1en.commandprompter.util.MMUtil; +import com.cyr1en.kiso.utils.SRegex; import org.bukkit.entity.Player; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.function.Consumer; +import java.util.regex.Pattern; public class PromptQueue extends LinkedList { @@ -61,8 +64,17 @@ public boolean isConsoleDelegate() { public String getCompleteCommand() { command = command.formatted(completed); LinkedList completedClone = new LinkedList<>(this.completed); - while (!completedClone.isEmpty()) - command = command.replaceFirst(escapedRegex, completedClone.pollFirst()); + + // get all prompts that we have to replace in the command + var sRegex = new SRegex(); + var prompts = sRegex.find(Pattern.compile(escapedRegex), command).getResultsList(); + prompts = MMUtil.filterOutMiniMessageTags(prompts); + + for (String prompt : prompts) { + if (completedClone.isEmpty()) + break; + command = command.replaceFirst(prompt, completedClone.pollFirst()); + } return "/" + command; } diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/prompts/AbstractPrompt.java b/src/main/java/com/cyr1en/commandprompter/prompt/prompts/AbstractPrompt.java index c3108c9..6423536 100644 --- a/src/main/java/com/cyr1en/commandprompter/prompt/prompts/AbstractPrompt.java +++ b/src/main/java/com/cyr1en/commandprompter/prompt/prompts/AbstractPrompt.java @@ -25,14 +25,15 @@ package com.cyr1en.commandprompter.prompt.prompts; import com.cyr1en.commandprompter.CommandPrompter; +import com.cyr1en.commandprompter.api.prompt.InputValidator; import com.cyr1en.commandprompter.api.prompt.Prompt; import com.cyr1en.commandprompter.prompt.PromptContext; import com.cyr1en.commandprompter.prompt.PromptManager; import com.cyr1en.commandprompter.prompt.PromptParser; +import com.cyr1en.commandprompter.prompt.validators.NoopValidator; import com.cyr1en.commandprompter.util.Util; import java.util.List; -import java.util.regex.Pattern; public abstract class AbstractPrompt implements Prompt { @@ -43,7 +44,7 @@ public abstract class AbstractPrompt implements Prompt { private final List args; - private Pattern regexCheck; + private InputValidator validator; private boolean inputSanitation; @@ -55,7 +56,7 @@ public AbstractPrompt(CommandPrompter plugin, PromptContext context, this.promptManager = plugin.getPromptManager(); this.args = args; this.inputSanitation = true; - this.regexCheck = Pattern.compile(""); + this.validator = new NoopValidator(); } protected String stripColor(String msg) { @@ -95,18 +96,13 @@ public List getArgs() { } @Override - public void setRegexCheck(String regexCheck) { - this.regexCheck = Pattern.compile(regexCheck); + public void setInputValidator(InputValidator inputValidator) { + this.validator = inputValidator; } @Override - public void setRegexCheck(Pattern regexCheck) { - this.regexCheck = regexCheck; - } - - @Override - public Pattern getRegexCheck() { - return regexCheck; + public InputValidator getInputValidator() { + return this.validator; } @Override @@ -119,10 +115,4 @@ public boolean sanitizeInput() { return this.inputSanitation; } - @Override - public boolean isValidInput(String input) { - plugin.getPluginLogger().debug("Checking input with regex: " + regexCheck); - if (regexCheck.pattern().isBlank() || regexCheck.pattern().isEmpty()) return true; - return regexCheck.matcher(input).matches(); - } } diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/prompts/PlayerUIPrompt.java b/src/main/java/com/cyr1en/commandprompter/prompt/prompts/PlayerUIPrompt.java index b7fba87..91ee412 100644 --- a/src/main/java/com/cyr1en/commandprompter/prompt/prompts/PlayerUIPrompt.java +++ b/src/main/java/com/cyr1en/commandprompter/prompt/prompts/PlayerUIPrompt.java @@ -108,6 +108,7 @@ private List extractFilters() { var extractedFilters = new ArrayList(); for (var filter : headCache.getFilters()) { var capGroup = getCapturingGroup(filter); + getPlugin().getPluginLogger().debug("Capturing group: " + capGroup); var filterKey = matcher.group(capGroup); if (Objects.isNull(filterKey)) continue; extractedFilters.add(filter.reConstruct(promptKey)); @@ -141,8 +142,13 @@ private CacheFilter getFirstFilter(List filters) { } private int getCapturingGroup(CacheFilter cacheFilter) { - var index = headCache.getFilters().indexOf(cacheFilter); - return index + 2; + getPlugin().getPluginLogger().debug("Getting capturing group for filter: " + cacheFilter.getRegexKey()); + var idx = 2; // index starts at 2 because 0 is the whole match and 1 is just a blank. + for (var filter : headCache.getFilters()) { + if (filter.equals(cacheFilter)) return idx; + idx = idx + filter.getCapGroupOffset() + 1; + } + return -1; } private void send(Player p) { diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/ui/CacheFilter.java b/src/main/java/com/cyr1en/commandprompter/prompt/ui/CacheFilter.java index 83cf0a2..a320276 100644 --- a/src/main/java/com/cyr1en/commandprompter/prompt/ui/CacheFilter.java +++ b/src/main/java/com/cyr1en/commandprompter/prompt/ui/CacheFilter.java @@ -2,6 +2,7 @@ import com.cyr1en.commandprompter.CommandPrompter; import com.cyr1en.commandprompter.config.PromptConfig; +import com.cyr1en.commandprompter.prompt.PromptParser; import org.bukkit.entity.Player; import java.util.List; @@ -26,14 +27,21 @@ public abstract class CacheFilter { */ private final String configKey; + private final int capGroupOffset; + /** * Construct a new cache filter. * * @param regexKey the regex key */ public CacheFilter(Pattern regexKey, String configKey) { + this(regexKey, configKey, 0); + } + + public CacheFilter(Pattern regexKey, String configKey, int capGroupOffset) { this.regexKey = regexKey; this.configKey = configKey; + this.capGroupOffset = capGroupOffset; } /** @@ -65,13 +73,32 @@ public String getFormat(PromptConfig config) { } + /** + * Get the capture group offset. + * + * @return the capture group offset + */ + public int getCapGroupOffset() { + return capGroupOffset; + } + + @Override public String toString() { return this.getClass().getSimpleName(); } /** - * Clone this cache filter with a new prompt key. + * A method that allows you to reconstruct a subclass of {@link CacheFilter} + * based on the prompt key. + * + *

+ * In cases where the prompt key contains additional information, this method + * could be used to reconstruct a certain subclass of {@link CacheFilter}. + * + *

+ * Additional filter information can be parsed from the {@link PromptParser} + * but for better readability, it is recommended to use this method instead. * * @param promptKey the prompt key * @return the cloned cache filter @@ -135,7 +162,7 @@ public static class RadialFilter extends CacheFilter { * @param radius the radius */ public RadialFilter(int radius) { - super(Pattern.compile("r(\\d+)"), "PlayerUI.Filter-Format.Radial"); + super(Pattern.compile("r(\\d+)"), "PlayerUI.Filter-Format.Radial", 1); this.radius = radius; } diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/ui/HeadCache.java b/src/main/java/com/cyr1en/commandprompter/prompt/ui/HeadCache.java index 38e8826..204ac9d 100644 --- a/src/main/java/com/cyr1en/commandprompter/prompt/ui/HeadCache.java +++ b/src/main/java/com/cyr1en/commandprompter/prompt/ui/HeadCache.java @@ -2,7 +2,10 @@ import com.cyr1en.commandprompter.CommandPrompter; import com.cyr1en.commandprompter.PluginLogger; +import com.cyr1en.commandprompter.hook.hooks.FilterHook; +import com.cyr1en.commandprompter.hook.hooks.LuckPermsHook; import com.cyr1en.commandprompter.hook.hooks.PapiHook; +import com.cyr1en.commandprompter.hook.hooks.TownyHook; import com.cyr1en.commandprompter.util.Util; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; @@ -36,7 +39,6 @@ public HeadCache(CommandPrompter plugin) { this.plugin = plugin; this.logger = plugin.getPluginLogger(); this.filters = new ArrayList<>(); - registerFilters(); HEAD_CACHE = CacheBuilder.newBuilder().maximumSize(plugin.getPromptConfig().cacheSize()) .build(new CacheLoader<>() { @Override @@ -55,15 +57,28 @@ public HeadCache(CommandPrompter plugin) { }); } - private void registerFilters() { + private static final Class[] fHooks = new Class[]{ + TownyHook.class, + LuckPermsHook.class + }; + + public void registerFilters() { registerFilter(new CacheFilter.WorldFilter()); registerFilter(new CacheFilter.RadialFilter()); + for (var fHook : fHooks) { + plugin.getHookContainer() + .getHook(fHook) + .ifHooked(hook -> hook.registerFilters(this)) + .complete(); + } } public void registerFilter(CacheFilter filter) { if (Objects.isNull(filter)) return; - if (!filters.contains(filter)) + if (!filters.contains(filter)) { filters.add(filter); + logger.debug("Registered filter: " + filter.getClass().getSimpleName()); + } } public List getFilters() { @@ -146,6 +161,11 @@ private SkullMeta makeSkullMeta(Player owningPlayer, PluginLogger logger) { Objects.requireNonNull(skullMeta).setOwningPlayer(owningPlayer); var skullFormat = plugin.getPromptConfig().skullNameFormat(); + var customModelData = plugin.getPromptConfig().skullCustomModelData(); + if (customModelData != 0) { + logger.debug("Setting custom model data: %s", customModelData); + skullMeta.setCustomModelData(customModelData); + } var skullName = skullFormat.replaceAll("%s", owningPlayer.getName()); setDisplayName(skullMeta, skullName); logger.debug("Skull Meta: {%s. %s}", skullMeta.getDisplayName(), skullMeta.getOwningPlayer()); diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/validators/NoopValidator.java b/src/main/java/com/cyr1en/commandprompter/prompt/validators/NoopValidator.java new file mode 100644 index 0000000..38d02c8 --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/prompt/validators/NoopValidator.java @@ -0,0 +1,25 @@ +package com.cyr1en.commandprompter.prompt.validators; + +import com.cyr1en.commandprompter.api.prompt.InputValidator; + +/** + * A validator that does nothing. + *

+ * This will always return true. + */ +public class NoopValidator implements InputValidator { + @Override + public boolean validate(String input) { + return true; + } + + @Override + public String alias() { + return "noop"; + } + + @Override + public String messageOnFail() { + return ""; + } +} diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/validators/OnlinePlayerValidator.java b/src/main/java/com/cyr1en/commandprompter/prompt/validators/OnlinePlayerValidator.java new file mode 100644 index 0000000..360cf8f --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/prompt/validators/OnlinePlayerValidator.java @@ -0,0 +1,22 @@ +package com.cyr1en.commandprompter.prompt.validators; + +import com.cyr1en.commandprompter.api.prompt.InputValidator; +import org.bukkit.Bukkit; + +/** + * A validator that checks if the input is an online player. + * + * @param alias The alias for this validator. + * @param messageOnFail The message to send when validation fails. + */ +public record OnlinePlayerValidator(String alias, String messageOnFail) implements InputValidator { + + @Override + public boolean validate(String input) { + if (input == null || input.isBlank()) + return false; + var player = Bukkit.getPlayer(input); + return player != null && player.isOnline(); + } + +} diff --git a/src/main/java/com/cyr1en/commandprompter/prompt/validators/RegexValidator.java b/src/main/java/com/cyr1en/commandprompter/prompt/validators/RegexValidator.java new file mode 100644 index 0000000..f17179b --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/prompt/validators/RegexValidator.java @@ -0,0 +1,34 @@ +package com.cyr1en.commandprompter.prompt.validators; + +import com.cyr1en.commandprompter.api.prompt.InputValidator; + +import java.util.regex.Pattern; + +/** + * A validator that uses regex to validate input. + *

+ * This validator is used to validate input based on a regex pattern. + * + * @param alias The alias for this validator. + * @param regex The regex pattern to use for validation. + * @param messageOnFail The message to send when validation fails. + */ +public record RegexValidator(String alias, Pattern regex, String messageOnFail) implements InputValidator { + + @Override + public boolean validate(String input) { + if (input == null || regex == null) + return false; + return regex.matcher(input).matches(); + } + + /** + * Get the regex pattern. + * + * @return the regex pattern. + */ + @Override + public Pattern regex() { + return regex; + } +} diff --git a/src/main/java/com/cyr1en/commandprompter/util/MMUtil.java b/src/main/java/com/cyr1en/commandprompter/util/MMUtil.java new file mode 100644 index 0000000..1174286 --- /dev/null +++ b/src/main/java/com/cyr1en/commandprompter/util/MMUtil.java @@ -0,0 +1,28 @@ +package com.cyr1en.commandprompter.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.standard.StandardTags; + +import java.util.List; + +public class MMUtil { + + public static List filterOutMiniMessageTags(List strs) { + return strs.stream().filter(str -> !isMiniMessageTag(str)).toList(); + } + + /** + * Function that checks if the prompt is a mini message tag. + * + * @param str Prompt to check + * @return true if the prompt is a mini message tag, false otherwise. + */ + public static boolean isMiniMessageTag(String str) { + var serializer = MiniMessage.builder() + .tags(StandardTags.defaults()).build(); + var parsed = serializer.deserialize(str); + return !Component.text(str).equals(parsed); + } + +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index be9228f..72eda8a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,7 +2,7 @@ name: ${projectName} version: ${projectVersion} author: ${projectAuthor} description: ${projectDescription} -softdepend: [VentureChat, SuperVanish, PuerkasChat, PlaceholderAPI, CarbonChat] +softdepend: [VentureChat, SuperVanish, PuerkasChat, PlaceholderAPI, CarbonChat, Towny, PlaceholderAPI] main: ${projectEntry} api-version: 1.13 diff --git a/src/main/resources/runtime-deps.json b/src/main/resources/runtime-deps.json index b6437f9..99fa9d1 100644 --- a/src/main/resources/runtime-deps.json +++ b/src/main/resources/runtime-deps.json @@ -25,5 +25,14 @@ "com.cyr1en.commandapi" ], "sha1": "145ee0e27a97733cc4cc40641910aabfb70d18c2" + }, + "MiniMessage": { + "filename": "adventure-text-minimessage-4.15.0.jar", + "url": "https://repo1.maven.org/maven2/net/kyori/adventure-text-minimessage/4.15.0/adventure-text-minimessage-4.15.0.jar", + "relocation": [ + "net.kyori.adventure.text.minimessage", + "com.cyr1en.minimessage" + ], + "sha1": "16299c7bf78b2cd51d893046cfb69357355b276b" } } \ No newline at end of file