diff --git a/.gitignore b/.gitignore index 278d1f64..f29a74b9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Compilation files *.class target +builds # Log files *.log @@ -31,4 +32,3 @@ hs_err_pid* # Maven dependency-reduced-pom.xml - diff --git a/CHANGELOG/kelp-v0.0.5.md b/CHANGELOG/kelp-v0.0.5.md new file mode 100644 index 00000000..bad482e0 --- /dev/null +++ b/CHANGELOG/kelp-v0.0.5.md @@ -0,0 +1,19 @@ +# v0.0.5 +> Release date: 30.12.2020 + +**The player interaction update**: +* Add `KelpServer` class, which serves as a kind of kelp wrapper for the `Bukkit` class: + * Broadcast messages to `KelpPlayers` (instead of bukkit players) + * Fetch all online `KelpPlayers` of the server +* You can now send centered chat messages using the `KelpPlayer` as well as the `KelpServer` class. +* Add player prompts accessible via the `KelpPlayer` class: + * `AnvilPrompt`: Use an anvil GUI to query text input from a player (default texts possible) + * `SignPrompt`: Use the sign editor to query multiple lines of text from a player (default lines are possible) + * `ChatPrompt`: Simple utility to use the chat as a method to query text input (default input not possible yet). +* Add methods to display a `boss bar` to a player (including process modification and boss bar removal) +* Add new custom event `KelpPlayerChangeSettingsEvent` that is triggered when a player changes their settings +* Fix bug that custom kelp inventory events crashed the plugin when they were actually used in an event handler +* Add `KelpPlayerLoginEvent` which is called on login of a player but after it has been initialized by kelp, so that calls to the `KelpPlayerRepository` are safe. If you want to handle logins now, it is recommended to use this event instead of the normal bukkit event. +* You can now send interactive messages to KelpPlayers. You can assign messages that are displayable when you hover over them and define events which are triggered when a player clicks on a message. You can combine unlimited components per message unlike the normal `TextComponent#addExtra` method which is wide spread across the spigot community. +* Documentation improvements in the code as well as in the GitHub wiki +* Create empty kelp sql module \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index e6ea35fa..5d4767cb 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -5,7 +5,7 @@ com.github.pxav.kelp parent - 0.0.4 + 0.0.5 4.0.0 @@ -139,7 +139,7 @@ org.spigotmc spigot-api - 1.14.4-R0.1-SNAPSHOT + 1.16.1-R0.1-SNAPSHOT provided diff --git a/core/src/main/java/de/pxav/kelp/core/KelpServer.java b/core/src/main/java/de/pxav/kelp/core/KelpServer.java new file mode 100644 index 00000000..395db25f --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/KelpServer.java @@ -0,0 +1,82 @@ +package de.pxav.kelp.core; + +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import de.pxav.kelp.core.player.KelpPlayer; +import de.pxav.kelp.core.player.KelpPlayerRepository; +import de.pxav.kelp.core.version.KelpVersion; +import org.bukkit.Bukkit; + +import java.util.Collection; + +/** + * A class description goes here. + * + * @author pxav + */ +@Singleton +public class KelpServer { + + private KelpPlayerRepository kelpPlayerRepository; + + @Inject + public KelpServer(KelpPlayerRepository kelpPlayerRepository) { + this.kelpPlayerRepository = kelpPlayerRepository; + } + + public KelpVersion getVersion() { + return KelpVersion.withBukkitVersion(Bukkit.getBukkitVersion()); + } + + public Collection getOnlinePlayers() { + Collection output = Lists.newArrayList(); + Bukkit.getOnlinePlayers().forEach(current -> output.add(kelpPlayerRepository.getKelpPlayer(current))); + return output; + } + + public int getMaxPlayers() { + return Bukkit.getMaxPlayers(); + } + + public void broadcastMessage(String message) { + Bukkit.getOnlinePlayers().forEach(current -> current.sendMessage(message)); + } + + public void broadcastCenteredMessage(String message) { + this.getOnlinePlayers().forEach(current -> current.sendCenteredMessage(message)); + } + + public void broadcastMessages(String... message) { + Bukkit.getOnlinePlayers().forEach(current -> { + for (String s : message) { + current.sendMessage(s); + } + }); + } + + public void broadcastCenteredMessages(String... message) { + this.getOnlinePlayers().forEach(current -> { + for (String s : message) { + current.sendCenteredMessage(s); + } + }); + } + + public void broadcastCenteredMessages(Collection messages) { + this.getOnlinePlayers().forEach(current -> { + for (String s : messages) { + current.sendCenteredMessage(s); + } + }); + } + + public void broadcastMessages(Collection messages) { + Bukkit.getOnlinePlayers().forEach(current -> { + for (String s : messages) { + current.sendMessage(s); + } + }); + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/common/StringUtils.java b/core/src/main/java/de/pxav/kelp/core/common/StringUtils.java index 727990c6..aaeb728d 100644 --- a/core/src/main/java/de/pxav/kelp/core/common/StringUtils.java +++ b/core/src/main/java/de/pxav/kelp/core/common/StringUtils.java @@ -2,9 +2,11 @@ import com.google.common.collect.Lists; import com.google.inject.Singleton; +import net.md_5.bungee.api.ChatColor; import java.util.Collections; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; /** * This class contains useful methods to work with strings @@ -241,6 +243,66 @@ public boolean isStyleCode(char indicator) { return false; } + public ChatColor getChatColor(char formattingCode) { + return isFormattingCode(formattingCode) + ? ChatColor.getByChar(formattingCode) + : ChatColor.WHITE; + } + + public org.bukkit.ChatColor getBukkitChatColor(char formattingCode) { + return isFormattingCode(formattingCode) + ? org.bukkit.ChatColor.getByChar(formattingCode) + : org.bukkit.ChatColor.WHITE; + } + + public ChatColor getChatColor(String formattingCode) { + char code = formattingCode.charAt(1); + return isFormattingCode(code) && formattingCode.charAt(0) == '§' + ? ChatColor.getByChar(code) + : ChatColor.WHITE; + } + + public org.bukkit.ChatColor getBukkitChatColor(String formattingCode) { + char code = formattingCode.charAt(1); + return isFormattingCode(code) && formattingCode.charAt(0) == '§' + ? org.bukkit.ChatColor.getByChar(code) + : org.bukkit.ChatColor.WHITE; + } + + public String endsWithColorCode(String text) { + if (text.length() < 2) { + return null; + } + + String code = text.substring(text.length() - 2); + if (code.charAt(0) == '§' && isColorCode(code.charAt(1))) { + return code; + } + + return null; + } + + public String endsWithFormattingCode(String text) { + if (text.length() < 2) { + return null; + } + + String code = text.substring(text.length() - 2); + if (code.charAt(0) == '§' && isFormattingCode(code.charAt(1))) { + return code; + } + + return null; + } + + public char randomColorCode() { + return colorCodes[ThreadLocalRandom.current().nextInt(colorCodes.length - 1)]; + } + + public char randomStyleCode() { + return styleCodes[ThreadLocalRandom.current().nextInt(colorCodes.length - 1)]; + } + /** * @param indicator The indicator of the code you want to check. * @return {@code true} if the either a color code or a style code. diff --git a/core/src/main/java/de/pxav/kelp/core/configuration/internal/KelpDefaultConfiguration.java b/core/src/main/java/de/pxav/kelp/core/configuration/internal/KelpDefaultConfiguration.java index d0595c70..413fdcca 100644 --- a/core/src/main/java/de/pxav/kelp/core/configuration/internal/KelpDefaultConfiguration.java +++ b/core/src/main/java/de/pxav/kelp/core/configuration/internal/KelpDefaultConfiguration.java @@ -20,7 +20,7 @@ public final class KelpDefaultConfiguration extends KelpConfiguration { @Override public void defineDefaults() { - defaultValues.add(new ConfigurationAttribute(developmentMode(), false)); + add(developmentMode(), false); } public String developmentMode() { diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryCloseEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryCloseEvent.java index 0e96d043..04c3895a 100644 --- a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryCloseEvent.java +++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryCloseEvent.java @@ -34,4 +34,9 @@ public boolean isAnimated() { public HandlerList getHandlers() { return handlers; } + + public static HandlerList getHandlerList() { + return handlers; + } + } diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryOpenEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryOpenEvent.java index ad4fb632..bdf23053 100644 --- a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryOpenEvent.java +++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryOpenEvent.java @@ -35,4 +35,8 @@ public HandlerList getHandlers() { return handlers; } + public static HandlerList getHandlerList() { + return handlers; + } + } diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryUpdateEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryUpdateEvent.java index 9d48e3a4..1f7eb7ba 100644 --- a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryUpdateEvent.java +++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpInventoryUpdateEvent.java @@ -28,4 +28,9 @@ public KelpInventory getInventory() { public HandlerList getHandlers() { return handlers; } + + public static HandlerList getHandlerList() { + return handlers; + } + } diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerLoginEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerLoginEvent.java new file mode 100644 index 00000000..36df3e8d --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerLoginEvent.java @@ -0,0 +1,71 @@ +package de.pxav.kelp.core.event.kelpevent; + +import de.pxav.kelp.core.player.KelpPlayer; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.event.player.PlayerLoginEvent; + +/** + * This event is triggered on any player login. It is recommended to use this + * event instead of the normal {@link PlayerLoginEvent} as this event is used + * to handle the handshake between the client and server. + * + * If a kelp application would try to convert the {@link Player} instance into a + * {@link KelpPlayer} instance, there would be a {@link NullPointerException} as + * the player has not been registered yet. So the login event handler that creates + * the {@link KelpPlayer} instances calls this event after it has finished so that + * it is safe to use the kelp player object. + * + * All methods (except {@link #getKelpPlayer()}) are the same as those from the normal + * bukkit event, so you can look for more detailed information there. + * + * @author pxav + */ +public class KelpPlayerLoginEvent extends PlayerEvent { + + // list of all event handlers listening for this event + private static final HandlerList handlers = new HandlerList(); + + private String hostname; + private PlayerLoginEvent.Result result; + private String message; + private KelpPlayer kelpPlayer; + + public KelpPlayerLoginEvent(Player who, KelpPlayer kelpPlayer, String hostname, PlayerLoginEvent.Result result, String message) { + super(who); + this.hostname = hostname; + this.result = result; + this.message = message; + this.kelpPlayer = kelpPlayer; + } + + /** + * @return The {@link KelpPlayer} instance of the player who logged in. + */ + public KelpPlayer getKelpPlayer() { + return kelpPlayer; + } + + public String getHostname() { + return hostname; + } + + public PlayerLoginEvent.Result getResult() { + return result; + } + + public String getMessage() { + return message; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerUpdateSettingsEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerUpdateSettingsEvent.java new file mode 100644 index 00000000..3e00baea --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerUpdateSettingsEvent.java @@ -0,0 +1,123 @@ +package de.pxav.kelp.core.event.kelpevent; + +import de.pxav.kelp.core.player.KelpPlayer; +import de.pxav.kelp.core.player.PlayerChatVisibility; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; + +/** + * This event is triggered when the client settings of a player change. + * It is triggered before the settings are actually updated in the + * {@link KelpPlayer} class, so you can do comparisons between the old + * and new values if you wish. + * + * @author pxav + */ +public class KelpPlayerUpdateSettingsEvent extends PlayerEvent { + + // list of all event handlers listening for this event + private static final HandlerList handlers = new HandlerList(); + + // the player whose settings changed. + private KelpPlayer who; + + // the stage when the settings changed + private SettingsUpdateStage updateStage; + + // the new client language. + private String language; + + // the new client view distance. + private int viewDistance; + + // the new chat visibility + private PlayerChatVisibility playerChatVisibility; + + // whether the player has chat colors enabled. + private boolean chatColorEnabled; + + public KelpPlayerUpdateSettingsEvent(KelpPlayer who, + SettingsUpdateStage updateStage, + String language, + int viewDistance, + PlayerChatVisibility playerChatVisibility, + boolean chatColorEnabled) { + super(who.getBukkitPlayer()); + this.who = who; + this.updateStage = updateStage; + this.language = language; + this.viewDistance = viewDistance; + this.playerChatVisibility = playerChatVisibility; + this.chatColorEnabled = chatColorEnabled; + } + + /** + * @return How the server received the settings update. Look into the documentation + * of {@link SettingsUpdateStage} for more detailed information. + */ + public SettingsUpdateStage getUpdateStage() { + return updateStage; + } + + public String getLanguage() { + return this.language; + } + + public boolean hasLanguageChanged() { + return !this.language.equalsIgnoreCase(who.getClientLanguage()); + } + + public int getViewDistance() { + return viewDistance; + } + + public boolean hasViewDistanceChanged() { + return viewDistance != who.getClientViewDistance(); + } + + public boolean getChatColorEnabled() { + return chatColorEnabled; + } + + /** + * Returns {@code true} if the chat color setting has changed. + * @return if the chat color setting has changed. + */ + public boolean hasChatColorChanged() { + return chatColorEnabled != who.isPlayerChatColorEnabled(); + } + + /** + * Get the updated chat visibility. + * @return the updated chat visibility. + */ + public PlayerChatVisibility getChatVisibility() { + return playerChatVisibility; + } + + /** + * Returns {@code true} if the chat visibility settings have changed. + * @return if the chat visibility settings have changed. + */ + public boolean hasChatVisibilityChanged() { + return playerChatVisibility != who.getPlayerChatVisibility(); + } + + /** + * Returns the kelp player instance of the player whose settings have changed. + * @return the kelp player instance of the player whose settings have changed. + */ + public KelpPlayer getKelpPlayer() { + return who; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/SettingsUpdateStage.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/SettingsUpdateStage.java new file mode 100644 index 00000000..afcc239d --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/SettingsUpdateStage.java @@ -0,0 +1,31 @@ +package de.pxav.kelp.core.event.kelpevent; + +/** + * Describes when an update of client settings has been received. + * There are two ways the server can receive information about + * the player's client settings: + * - By loading default settings on each plugin startup + * - By catching the packet sent by the client during the server runtime. + * + * This enum is used by {@link KelpPlayerUpdateSettingsEvent} to provide information + * about when the settings have changed. + * + * @author pxav + */ +public enum SettingsUpdateStage { + + /** + * When the player has updated its settings during the server runtime + * and the server received an incoming settings packet. This packet is + * sent when the player presses the {@code Done} button in the video settings + * tab for example. + */ + PACKET_PLAY_IN, + + /** + * When the server automatically updates the settings values for the player + * on every plugin startup (relevant for server reloads for example). + */ + PLUGIN_STARTUP + +} diff --git a/core/src/main/java/de/pxav/kelp/core/inventory/KelpInventoryRepository.java b/core/src/main/java/de/pxav/kelp/core/inventory/KelpInventoryRepository.java index 2ff469d5..5b2e5faf 100644 --- a/core/src/main/java/de/pxav/kelp/core/inventory/KelpInventoryRepository.java +++ b/core/src/main/java/de/pxav/kelp/core/inventory/KelpInventoryRepository.java @@ -16,7 +16,6 @@ import org.bukkit.Bukkit; import org.bukkit.inventory.Inventory; -import java.lang.reflect.Method; import java.util.Map; import java.util.UUID; @@ -36,8 +35,6 @@ public class KelpInventoryRepository { private MethodFinder methodFinder; private KelpListenerRepository kelpListenerRepository; - private Map methods = Maps.newHashMap(); - private Map playerInventories = Maps.newHashMap(); private Map> playerPages = Maps.newHashMap(); private Map playerAnimations = Maps.newHashMap(); @@ -58,16 +55,6 @@ public void loadMaterials() { this.materialVersionTemplate.defineDefaults(); } -/* public void detectInventories(String... packages) { - methodFinder.filter(packages, MethodCriterion.annotatedWith(CreateInventory.class)).forEach(method -> { - CreateInventory annotation = method.getAnnotation(CreateInventory.class); - String identifier = annotation.identifier(); - - methods.put(identifier, method); - - }); - }*/ - /** * Opens a kelp inventory to the given player. This also adds the * player to all required lists. diff --git a/core/src/main/java/de/pxav/kelp/core/inventory/material/MaterialContainer.java b/core/src/main/java/de/pxav/kelp/core/inventory/material/MaterialContainer.java new file mode 100644 index 00000000..f09dbcc9 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/inventory/material/MaterialContainer.java @@ -0,0 +1,32 @@ +package de.pxav.kelp.core.inventory.material; + +import org.bukkit.Material; + +/** + * A class description goes here. + * + * @author pxav + */ +public class MaterialContainer { + + private String materialName; + private short subId; + + public MaterialContainer(String materialName, short subId) { + this.materialName = materialName; + this.subId = subId; + } + + public String getMaterialName() { + return materialName; + } + + public Material getBukkitMaterial() { + return Material.valueOf(materialName); + } + + public short getSubId() { + return subId; + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/inventory/material/MaterialRepository.java b/core/src/main/java/de/pxav/kelp/core/inventory/material/MaterialRepository.java index 85a6400d..c6ef58f7 100644 --- a/core/src/main/java/de/pxav/kelp/core/inventory/material/MaterialRepository.java +++ b/core/src/main/java/de/pxav/kelp/core/inventory/material/MaterialRepository.java @@ -6,6 +6,7 @@ import com.google.inject.Singleton; import java.util.Map; +import java.util.function.BiConsumer; /** * This repository class is used to save all material names of the @@ -52,6 +53,14 @@ public String getMaterial(String kelpMaterialName) { return this.getMaterial(kelpMaterial); } + public MaterialContainer getBukkitMaterial(KelpMaterial kelpMaterial) { + String[] format = materials.get(kelpMaterial).split(":"); + if (format.length == 1) { + return new MaterialContainer(format[0], (short) 0); + } + return new MaterialContainer(format[0], Short.parseShort(format[1])); + } + /** * Gets the kelp material matching the given bukkit material name. * This method does not support sub IDs. @@ -74,6 +83,9 @@ public KelpMaterial getKelpMaterial(String bukkitMaterial) { * @return The final kelp material. */ public KelpMaterial getKelpMaterial(String bukkitMaterial, short subId) { + if (subId == 0) { + return this.getKelpMaterial(bukkitMaterial); + } return materials.inverse().get(bukkitMaterial + ":" + subId); } @@ -88,6 +100,9 @@ public KelpMaterial getKelpMaterial(String bukkitMaterial, short subId) { * @return The kelp material matching the given bukkit material. */ public KelpMaterial getKelpMaterial(String bukkitMaterial, int subId) { + if (subId == 0) { + return this.getKelpMaterial(bukkitMaterial); + } return materials.inverse().get(bukkitMaterial + ":" + subId); } diff --git a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java index 79c0a056..34ee9b2c 100644 --- a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java +++ b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java @@ -1,5 +1,6 @@ package de.pxav.kelp.core.player; +import com.google.common.base.Preconditions; import de.pxav.kelp.core.entity.KelpEntityType; import de.pxav.kelp.core.entity.LivingKelpEntity; import de.pxav.kelp.core.entity.version.EntityVersionTemplate; @@ -8,6 +9,16 @@ import de.pxav.kelp.core.inventory.type.KelpInventory; import de.pxav.kelp.core.particle.type.ParticleType; import de.pxav.kelp.core.particle.version.ParticleVersionTemplate; +import de.pxav.kelp.core.player.bossbar.BossBarColor; +import de.pxav.kelp.core.player.bossbar.BossBarStyle; +import de.pxav.kelp.core.player.message.InteractiveMessage; +import de.pxav.kelp.core.player.prompt.anvil.AnvilPrompt; +import de.pxav.kelp.core.player.prompt.anvil.AnvilPromptVersionTemplate; +import de.pxav.kelp.core.player.prompt.chat.ChatPromptVersionTemplate; +import de.pxav.kelp.core.player.prompt.chat.DefaultFontSize; +import de.pxav.kelp.core.player.prompt.chat.SimpleChatPrompt; +import de.pxav.kelp.core.player.prompt.sign.SignPrompt; +import de.pxav.kelp.core.player.prompt.sign.SignPromptVersionTemplate; import de.pxav.kelp.core.sidebar.SidebarRepository; import de.pxav.kelp.core.sound.KelpSound; import org.bukkit.Location; @@ -47,6 +58,9 @@ public class KelpPlayer extends LivingKelpEntity { private SidebarRepository sidebarRepository; private KelpInventoryRepository inventoryRepository; private ParticleVersionTemplate particleVersionTemplate; + private SignPromptVersionTemplate signPromptVersionTemplate; + private AnvilPromptVersionTemplate anvilPromptVersionTemplate; + private ChatPromptVersionTemplate chatPromptVersionTemplate; private Player bukkitPlayer; @@ -70,6 +84,9 @@ public KelpPlayer(Player bukkitPlayer, KelpInventoryRepository inventoryRepository, KelpPlayerRepository kelpPlayerRepository, ParticleVersionTemplate particleVersionTemplate, + SignPromptVersionTemplate signPromptVersionTemplate, + AnvilPromptVersionTemplate anvilPromptVersionTemplate, + ChatPromptVersionTemplate chatPromptVersionTemplate, EntityVersionTemplate entityVersionTemplate, LivingEntityVersionTemplate livingEntityVersionTemplate, UUID uuid, @@ -87,6 +104,21 @@ public KelpPlayer(Player bukkitPlayer, this.sidebarRepository = sidebarRepository; this.inventoryRepository = inventoryRepository; this.particleVersionTemplate = particleVersionTemplate; + this.signPromptVersionTemplate = signPromptVersionTemplate; + this.chatPromptVersionTemplate = chatPromptVersionTemplate; + this.anvilPromptVersionTemplate = anvilPromptVersionTemplate; + } + + public SignPrompt openSignPrompt() { + return new SignPrompt(this.getBukkitPlayer(), this.signPromptVersionTemplate); + } + + public AnvilPrompt openAnvilPrompt() { + return new AnvilPrompt(this.getBukkitPlayer(), this.anvilPromptVersionTemplate); + } + + public SimpleChatPrompt openSimpleChatPrompt() { + return new SimpleChatPrompt(this.getBukkitPlayer(), this.chatPromptVersionTemplate); } /** @@ -773,6 +805,210 @@ public KelpPlayer sendMessages(Collection messages) { return this; } + public KelpPlayer sendPrefixedMessages(String prefix, String... messages) { + for (String message : messages) { + sendMessage(prefix + message); + } + return this; + } + + public KelpPlayer sendPrefixedMessages(String prefix, Collection messages) { + for (String message : messages) { + sendMessage(prefix + message); + } + return this; + } + + public void sendCenteredMessage(String message) { + Preconditions.checkNotNull(message); + if(message.equals("")) { + this.sendMessage(""); + } + + final int CENTER_PX = 154; + int messagePxSize = 0; + boolean previousCode = false; + boolean isBold = false; + + for(char c : message.toCharArray()){ + if(c == '§') { + previousCode = true; + continue; + } + + if(previousCode) { + previousCode = false; + isBold = c == 'l' || c == 'L'; + } else { + DefaultFontSize dFI = DefaultFontSize.getDefaultFontInfo(c); + messagePxSize += isBold ? dFI.getBoldLength() : dFI.getLength(); + messagePxSize++; + } + } + + int halvedMessageSize = messagePxSize / 2; + int toCompensate = CENTER_PX - halvedMessageSize; + int spaceLength = DefaultFontSize.SPACE.getLength() + 1; + int compensated = 0; + StringBuilder sb = new StringBuilder(); + while(compensated < toCompensate){ + sb.append(" "); + compensated += spaceLength; + } + this.sendMessage(sb.toString() + message); + } + + public void sendCenteredMessages(String... messages) { + for (String s : messages) { + this.sendCenteredMessage(s); + } + } + + public void sendCenteredMessages(Collection messages) { + for (String s : messages) { + this.sendCenteredMessage(s); + } + } + + public void sendCenteredMessages(String header, String footer, String... messages) { + if (header != null) { + this.sendMessage(header); + } + for (String s : messages) { + this.sendCenteredMessage(s); + } + if (footer != null) { + this.sendMessage(footer); + } + } + + public void sendCenteredMessages(String header, Collection messages, String footer) { + if (header != null) { + this.sendMessage(header); + } + for (String s : messages) { + this.sendCenteredMessage(s); + } + if (footer != null) { + this.sendMessage(footer); + } + } + + /** + * Sends a boss bar to the player by spawning a boss entity near it. If you use this + * method in 1.8, please keep in mind that bar colors other than {@code PURPLE} and bar styles + * other than {@code SOLID} are not supported. + * + * @param message The message you want to be displayed above the boss bar. + * @param health How much the boss bar should be loaded (equivalent to how much + * health the boss entity has. 300f is a full boss bar and 0f an empty one). + * @param barColor The color of the boss bar. Please note that in 1.8 only + * {@code PURPLE} is allowed. If you use any color, no exception + * is thrown but purple will be chosen automatically. + * @param barStyle The style of the boss bar (how many segments?, ...). Note that + * in 1.8 only {@code SOLID} is supported. If you use any different + * style, no exception will be thrown, but {@code SOLID} is chosen + * automatically. + */ + public void sendBossBar(String message, float health, BossBarColor barColor, BossBarStyle barStyle) { + playerVersionTemplate.sendBossBar(bukkitPlayer, message, health, barColor, barStyle); + } + + /** + * Sends a boss bar to the player by spawning a boss entity near it. + * This method uses {@link BossBarColor#PURPLE} and {@link BossBarStyle#SOLID} + * in order to be compatible with all versions. If you want to use another style or color + * you can use {@link #sendBossBar(String, float, BossBarColor, BossBarStyle)} instead. + * + * @param message The message you want to be displayed above the boss bar. + */ + public void sendBossBar(String message) { + playerVersionTemplate.sendBossBar(bukkitPlayer, message, 300f, BossBarColor.PURPLE, BossBarStyle.SOLID); + } + + /** + * Sets the progress of the player's boss bar by modifying the + * health of the boss bar entity. As withers are used for that + * purpose, the maximum value {@code 300f} represents full boss + * bar and {@code 0f} would be an empty boss bar (equivalent to + * the wither dieing.) + * + * @param health The health of the boss bar entity. + */ + public void setBossBarProgressHealth(float health) { + playerVersionTemplate.setBossBarProgress(bukkitPlayer, health); + } + + /** + * Sets the progress of the player's boss bar, where 1 means + * that the bar is fully loaded and 0 means the bar is completely + * unloaded (equivalent to the boss entity being killed). + * + * @param percentage The percentage value of the progress between 0 and 1. + */ + public void setBossBarProgress(double percentage) { + float health = 300f * (float) percentage; + setBossBarProgressHealth(health); + } + + /** + * Sets the progress of the player's boss bar. This method + * automatically calculates the percentage value based on the + * value representing your maximum and the current state you want + * to display. Example: If you have a 60 seconds timer and there + * are still 20 seconds to wait, you pass 60 seconds as your maximum + * and 40 seconds as your current value. + * + * @param current The current state of reaching the maximum in absolute numbers. + * @param max The maximum value that is reachable for parameter {@code current}. + */ + public void setBossBarProgress(int current, int max) { + double percentage = (double) current / (double) max; + setBossBarProgress(percentage); + } + + /** + * Sets the progress of the player's boss bar. This method + * automatically calculates the percentage value based on the + * value representing your maximum and the current state you want + * to display. Example: If you have a 60 seconds timer and there + * are still 20 seconds to wait, you pass 60 seconds as your maximum + * and 40 seconds as your current value. + * + * @param current The current state of reaching the maximum in absolute numbers. + * @param max The maximum value that is reachable for parameter {@code current}. + */ + public void setBossBarProgress(double current, double max) { + double percentage = current / max; + setBossBarProgress(percentage); + } + + /** + * Makes the boss bar disappear for the player. + */ + public void removeBossBar() { + playerVersionTemplate.removeBossBar(bukkitPlayer); + } + + /** + * Sends an interactive message to the player. An interactive message is a message + * the player can click on and events (execute a command, open a url, ...) are triggered. + * You can also add hover events to it. You can add as many components as you want. + * More detailed information about how to build an interactive message can be found out + * in {@link InteractiveMessage}. + * + * @param interactiveMessage The interactive message you want to send to the player. + */ + public void sendInteractiveMessage(InteractiveMessage interactiveMessage) { + playerVersionTemplate.sendInteractiveMessage(bukkitPlayer, interactiveMessage); + } + + /** + * Gets the bukkit instance of the current {@link Player}. Be aware that + * by using this, you might lose version independence. + * + * @return The current bukkit player instance. + */ public Player getBukkitPlayer() { return bukkitPlayer; } diff --git a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java index 7d26fcd2..d93c061d 100644 --- a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java +++ b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java @@ -9,6 +9,9 @@ import de.pxav.kelp.core.logger.KelpLogger; import de.pxav.kelp.core.logger.LogLevel; import de.pxav.kelp.core.particle.version.ParticleVersionTemplate; +import de.pxav.kelp.core.player.prompt.anvil.AnvilPromptVersionTemplate; +import de.pxav.kelp.core.player.prompt.chat.ChatPromptVersionTemplate; +import de.pxav.kelp.core.player.prompt.sign.SignPromptVersionTemplate; import de.pxav.kelp.core.sidebar.SidebarRepository; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -48,22 +51,22 @@ public class KelpPlayerRepository { private EntityVersionTemplate entityVersionTemplate; private LivingEntityVersionTemplate livingEntityVersionTemplate; private ParticleVersionTemplate particleVersionTemplate; + private SignPromptVersionTemplate signPromptVersionTemplate; + private AnvilPromptVersionTemplate anvilPromptVersionTemplate; + private ChatPromptVersionTemplate chatPromptVersionTemplate; @Inject - public KelpPlayerRepository(PlayerVersionTemplate playerVersionTemplate, - KelpLogger logger, - SidebarRepository sidebarRepository, - KelpInventoryRepository inventoryRepository, - EntityVersionTemplate entityVersionTemplate, - LivingEntityVersionTemplate livingEntityVersionTemplate, - ParticleVersionTemplate particleVersionTemplate) { + public KelpPlayerRepository(PlayerVersionTemplate playerVersionTemplate, SidebarRepository sidebarRepository, KelpInventoryRepository inventoryRepository, KelpLogger logger, EntityVersionTemplate entityVersionTemplate, LivingEntityVersionTemplate livingEntityVersionTemplate, ParticleVersionTemplate particleVersionTemplate, SignPromptVersionTemplate signPromptVersionTemplate, AnvilPromptVersionTemplate anvilPromptVersionTemplate, ChatPromptVersionTemplate chatPromptVersionTemplate) { this.playerVersionTemplate = playerVersionTemplate; - this.logger = logger; this.sidebarRepository = sidebarRepository; this.inventoryRepository = inventoryRepository; + this.logger = logger; this.entityVersionTemplate = entityVersionTemplate; this.livingEntityVersionTemplate = livingEntityVersionTemplate; this.particleVersionTemplate = particleVersionTemplate; + this.signPromptVersionTemplate = signPromptVersionTemplate; + this.anvilPromptVersionTemplate = anvilPromptVersionTemplate; + this.chatPromptVersionTemplate = chatPromptVersionTemplate; } /** @@ -218,6 +221,9 @@ private KelpPlayer newKelpPlayerFrom(Player bukkitPlayer) { inventoryRepository, this, particleVersionTemplate, + signPromptVersionTemplate, + anvilPromptVersionTemplate, + chatPromptVersionTemplate, entityVersionTemplate, livingEntityVersionTemplate, playerVersionTemplate.getUniqueId(bukkitPlayer), diff --git a/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java b/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java index 4e3e8407..fe98e01b 100644 --- a/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java +++ b/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java @@ -1,18 +1,15 @@ package de.pxav.kelp.core.player; import de.pxav.kelp.core.application.KelpVersionTemplate; +import de.pxav.kelp.core.player.bossbar.BossBarColor; +import de.pxav.kelp.core.player.bossbar.BossBarStyle; +import de.pxav.kelp.core.player.message.InteractiveMessage; import de.pxav.kelp.core.sound.KelpSound; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.bukkit.permissions.Permissible; -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionAttachment; -import org.bukkit.permissions.PermissionAttachmentInfo; -import org.bukkit.plugin.Plugin; import java.net.InetSocketAddress; -import java.util.Set; import java.util.UUID; /** @@ -888,4 +885,54 @@ public abstract class PlayerVersionTemplate { */ public abstract void sendMessage(Player player, String message); + /** + * Sends a boss bar to the player by spawning a boss entity near it. If you use this + * method in 1.8, please keep in mind that bar colors other than {@code PURPLE} and bar styles + * other than {@code SOLID} are not supported. + * + * @param player The player you want to send the message to. + * @param message The message you want to be displayed above the boss bar. + * @param health How much the boss bar should be loaded (equivalent to how much + * health the boss entity has. 300f is a full boss bar and 0f an empty one). + * @param barColor The color of the boss bar. Please note that in 1.8 only + * {@code PURPLE} is allowed. If you use any color, no exception + * is thrown but purple will be chosen automatically. + * @param barStyle The style of the boss bar (how many segments?, ...). Note that + * in 1.8 only {@code SOLID} is supported. If you use any different + * style, no exception will be thrown, but {@code SOLID} is chosen + * automatically. + */ + public abstract void sendBossBar(Player player, String message, float health, BossBarColor barColor, BossBarStyle barStyle); + + /** + * Sets the progress of the player's boss bar by modifying the + * health of the boss bar entity. As withers are used for that + * purpose, the maximum value {@code 300f} represents full boss + * bar and {@code 0f} would be an empty boss bar (equivalent to + * the wither dieing.) + * + * @param health The health of the boss bar entity. + */ + public abstract void setBossBarProgress(Player player, float health); + + /** + * Makes the boss bar disappear for the given player. + * + * @param player The player whose boss bar you want to remove. + */ + public abstract void removeBossBar(Player player); + + /** + * Sends an interactive message to the player. An interactive message is a message + * the player can click on and events (execute a command, open a url, ...) are triggered. + * You can also add hover events to it. You can add as many components as you want. + * More detailed information about how to build an interactive message can be found out + * in {@link InteractiveMessage}. + * + * @param player The player who should receive this message and be able + * to interact with it. + * @param interactiveMessage The actual message you want to send to the player. + */ + public abstract void sendInteractiveMessage(Player player, InteractiveMessage interactiveMessage); + } diff --git a/core/src/main/java/de/pxav/kelp/core/player/bossbar/BossBarColor.java b/core/src/main/java/de/pxav/kelp/core/player/bossbar/BossBarColor.java new file mode 100644 index 00000000..c8dfd2a1 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/bossbar/BossBarColor.java @@ -0,0 +1,19 @@ +package de.pxav.kelp.core.player.bossbar; + +import de.pxav.kelp.core.version.KelpVersion; +import de.pxav.kelp.core.version.SinceKelpVersion; + +/** + * A class description goes here. + * + * @author pxav + */ +public enum BossBarColor { + + @SinceKelpVersion(KelpVersion.MC_1_8_0) + PURPLE, + + @SinceKelpVersion(KelpVersion.MC_1_9_0) + YELLOW; + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/bossbar/BossBarStyle.java b/core/src/main/java/de/pxav/kelp/core/player/bossbar/BossBarStyle.java new file mode 100644 index 00000000..3dcafd98 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/bossbar/BossBarStyle.java @@ -0,0 +1,21 @@ +package de.pxav.kelp.core.player.bossbar; + +import de.pxav.kelp.core.version.KelpVersion; +import de.pxav.kelp.core.version.SinceKelpVersion; + +/** + * A class description goes here. + * + * @author pxav + */ +public enum BossBarStyle { + + @SinceKelpVersion(KelpVersion.MC_1_8_0) + SOLID, + + SEGMENTS_6, + SEGMENTS_10, + SEGMENTS_12, + SEGMENTS_20; + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/message/InteractiveMessage.java b/core/src/main/java/de/pxav/kelp/core/player/message/InteractiveMessage.java new file mode 100644 index 00000000..2baf7a17 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/message/InteractiveMessage.java @@ -0,0 +1,59 @@ +package de.pxav.kelp.core.player.message; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * An interactive message is a chat message on which + * players can click and trigger a specific event. + * + * @author pxav + */ +public class InteractiveMessage { + + // all components (different interactions) of the message. + private Collection components; + + private InteractiveMessage() { + // initialize the component list + this.components = new ArrayList<>(); + } + + /** + * Factory method for the interactive message. Use this method + * if you want to create a new message as the normal constructor + * is private. + * + * @return A new instance of an interactive message. + */ + public static InteractiveMessage create() { + return new InteractiveMessage(); + } + + /** + * Add a new component to the message. This does not mean that you create + * a new line, but a new segment of events. + * + * Example: If you have a message to accept a party, you create a component + * for the prefix and the normal text ([PARTY] Click here to...) and a component, + * which is clickable and accepts the party (...accept the party.). Unlike in the + * normal spigot api, you can create unlimited components with unlimited color codes. + * + * @param messageComponent The component to add. + * @return The current message object (used for fluent builder structure). + */ + public InteractiveMessage addComponent(MessageComponent messageComponent) { + this.components.add(messageComponent); + return this; + } + + /** + * Returns a full {@link Collection} of all components of the interactive message. + * + * @return All components of the message. + */ + public Collection getComponents() { + return components; + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/message/MessageClickAction.java b/core/src/main/java/de/pxav/kelp/core/player/message/MessageClickAction.java new file mode 100644 index 00000000..d4a098ec --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/message/MessageClickAction.java @@ -0,0 +1,57 @@ +package de.pxav.kelp.core.player.message; + +import de.pxav.kelp.core.version.KelpVersion; +import de.pxav.kelp.core.version.SinceKelpVersion; + +/** + * Describes a click action that should be performed when + * an {@link InteractiveMessage} is clicked. + * + * You can apply those actions via {@link MessageComponent} + * + * @author pxav + */ +public enum MessageClickAction { + + /** + * A given command is executed when the player clicks the message. + * Prefixing the message with a {@code /} is not necessary. + */ + EXECUTE_COMMAND, + + /** + * Suggests a given command to the player. This means that the command + * is written to the player's chat text box but not executed yet. + */ + SUGGEST_COMMAND, + + /** + * Opens a given URL to the player, which can be a website. Be sure to + * prefix your message with {@code https://} in this case. + */ + OPEN_URL, + + /** + * Opens a file at the given file path. Please note that only server files + * can be accessed. Local files of the client cannot be queried. + */ + OPEN_FILE, + + /** + * Changes the page of a book. + */ + CHANGE_PAGE, + + /** + * Makes the player send a chat message under his name. + */ + SEND_CHAT_MESSAGE, + + /** + * Copies a given text into the player's clipboard. Please note that + * this is not available in all versions. + */ + @SinceKelpVersion(KelpVersion.MC_1_14_0) + COPY_TO_CLIPBOARD, + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/message/MessageComponent.java b/core/src/main/java/de/pxav/kelp/core/player/message/MessageComponent.java new file mode 100644 index 00000000..faf6a65b --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/message/MessageComponent.java @@ -0,0 +1,324 @@ +package de.pxav.kelp.core.player.message; + +import java.io.File; +import java.net.URL; + +/** + * A message component is a part/segment of an {@link InteractiveMessage}. + * Unlike normal spigot {@link net.md_5.bungee.api.chat.TextComponent} you can add + * unlimited components to a clickable/hoverable message, so you can have + * different click events in one chat line/chat message. + * + * In order to keep control over those different events, you divide your message + * into different components: If you have a message for editing a player report + * for example, you have a message showing different options: + * {@code [SERVER] Select an option: BAN PLAYER | KICK PLAYER | CONTINUE} + * + * For the prefix and the message, you would create a component with no events, + * but then you would append a new component for each of the options (ban, kick, continue). + * + * For each component you can select what should happen when a player hovers over a message or + * clicks it. + * + * You can apply chat colors by using {@code § + code id (e. g. §a, §4, ...)}. If you want to use + * a style code (such as underlining, obfuscating, etc.) only use those codes in combination + * with a color code ({@code §a§o} not {@code §o}) + * + * @author pxav + */ +public class MessageComponent { + + // the text of the message displayed in the chat. + private String text; + + // the type of action to be performed when the message is clicked. + private MessageClickAction clickAction; + + // the type of action to be performed when the message is hovered. + private MessageHoverAction hoverAction; + + // Values for the individual event types. If you have EXECUTE_COMMAND + // for example, you pass a string with the command here. + private Object clickValue; + private Object hoverValue; + + private MessageComponent() {} + + /** + * Static factory method. If you want to create new instances of message + * components, use this method instead of the normal constructor. + * + * @return A new instance of a message component. + */ + public static MessageComponent create() { + return new MessageComponent(); + } + + /** + * The text which should appear in the chat when this component is rendered. + * + * @param text The text to be shown in the chat line. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent text(String text) { + this.text = text; + return this; + } + + /** + * Sets the click event action. Please remember to set the corresponding + * value as well. + * + * @param clickAction The action to be performed when the component is clicked. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent clickAction(MessageClickAction clickAction) { + this.clickAction = clickAction; + return this; + } + + /** + * Sets the hover event action. Please remember to set the corresponding + * hover value as well. + * + * @param hoverAction The action to be performed when the player hovers + * over the component. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent hoverAction(MessageHoverAction hoverAction) { + this.hoverAction = hoverAction; + return this; + } + + /** + * Sets the value of the currently set click event. If you selected + * EXECUTE_COMMAND as click event, pass the command here. + * + * @param clickValue The value of the click event. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent clickValue(Object clickValue) { + this.clickValue = clickValue; + return this; + } + + /** + * Sets the value of the currently set hover event. If you selected + * SHOW_MESSAGE as hover event, pass the message here. + * + * @param hoverValue The value of the hover event. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent hoverValue(Object hoverValue) { + this.hoverValue = hoverValue; + return this; + } + + /** + * Executes a command when the message is clicked. If the player who clicks + * the message does not have permissions to execute it, the command won't be executed. + * + * @param command The command to be executed by the player. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent executeCommandOnClick(String command) { + this.clickAction = MessageClickAction.EXECUTE_COMMAND; + this.clickValue = command; + return this; + } + + /** + * Copies the given command or message to the player's chat text box. + * + * @param command The command/message to be pasted to the chat box. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent suggestCommandOnClick(String command) { + this.clickAction = MessageClickAction.SUGGEST_COMMAND; + this.clickValue = command; + return this; + } + + /** + * Opens the given url to the player. This can be a website for example. + * + * @param url The {@code url} to be opened. If you want to open a website + * please remember to prefix your message with {@code https://}. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent openURLOnClick(String url) { + this.clickAction = MessageClickAction.OPEN_URL; + this.clickValue = url; + return this; + } + + /** + * Opens the given url to the player. This can be a website for example. + * + * @param url The {@code url} to be opened. If you want to open a website + * please remember to prefix your message with {@code https://}. + * This method only takes the path of the given {@link URL} object + * with {@link URL#getPath()}. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent openURLOnClick(URL url) { + this.clickAction = MessageClickAction.OPEN_URL; + this.clickValue = url.getPath(); + return this; + } + + /** + * Opens the file at the given location when the player clicks on the + * message. Please ensure that the requested file is on the file system + * of the host machine and not on the client side as the server does not + * have access to those files. + * + * @param url The URL where the file is located. This method only takes the + * URL path with {@link URL#getPath()}. + * @return The current component instance for fluent builder design. + */ + public MessageComponent openFileOnClick(URL url) { + this.clickAction = MessageClickAction.OPEN_FILE; + this.clickValue = url.getPath(); + return this; + } + + /** + * Opens the file at the given location when the player clicks on the + * message. Please ensure that the requested file is on the file system + * of the host machine and not on the client side as the server does not + * have access to those files. + * + * @param file The file object of the file to be accessed. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent openFileOnClick(File file) { + this.clickAction = MessageClickAction.OPEN_FILE; + this.clickValue = file.getPath(); + return this; + } + + /** + * Opens the file at the given location when the player clicks on the + * message. Please ensure that the requested file is on the file system + * of the host machine and not on the client side as the server does not + * have access to those files. + * + * @param filePath The path of the file to be accessed. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent openFileOnClick(String filePath) { + this.clickAction = MessageClickAction.OPEN_FILE; + this.clickValue = filePath; + return this; + } + + /** + * Makes the player sends a chat message under his name when they click + * the message. + * + * @param message The message to be sent by the player. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent sendChatMessageOnClick(String message) { + this.clickAction = MessageClickAction.SEND_CHAT_MESSAGE; + this.clickValue = message; + return this; + } + + /** + * When the player clicks on the message a given text will + * be copied to its clipboard. + * + * @param text The text to be copied. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent copyToClipboardOnClick(String text) { + this.clickAction = MessageClickAction.COPY_TO_CLIPBOARD; + this.clickValue = text; + return this; + } + + /** + * Changes the page of the current player book. + * + * @param page The page to navigate to. + * + * @return The current component instance for fluent builder design. + */ + public MessageComponent changeBookPageOnClick(int page) { + this.clickAction = MessageClickAction.CHANGE_PAGE; + this.clickValue = page; + return this; + } + + /** + * Sets the hover event to showing a message. + * + * @param message The message to be shown when the player hovers over + * the chat message. + * @return The current component instance for fluent builder design. + */ + public MessageComponent showMessageOnHover(String message) { + this.hoverAction = MessageHoverAction.SHOW_MESSAGE; + this.hoverValue = message; + return this; + } + + /** + * Returns the type of action that should be performed + * when a player clicks on the message. + * @return The click action type. + */ + public MessageClickAction getClickAction() { + return clickAction; + } + + /** + * Returns the type of action that should be performed + * when a player hovers over the message. + * @return The hover action type. + */ + public MessageHoverAction getHoverAction() { + return hoverAction; + } + + /** + * Returns the event value which is set for the click event. + * If you selected SUGGEST_COMMAND, the command is returned here. + * @return The click event value object. + */ + public Object getClickValue() { + return clickValue; + } + + /** + * Returns the event value which is set for the hover event. + * If you selected SHOW_MESSAGE, the message is returned here. + * @return The hover event value object. + */ + public Object getHoverValue() { + return hoverValue; + } + + /** + * Returns the text displayed in the chat line. + * @return The text of the component. + */ + public String getText() { + return text; + } +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/message/MessageHoverAction.java b/core/src/main/java/de/pxav/kelp/core/player/message/MessageHoverAction.java new file mode 100644 index 00000000..af41ec38 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/message/MessageHoverAction.java @@ -0,0 +1,16 @@ +package de.pxav.kelp.core.player.message; + +/** + * Describes the action to be performed when a {@link MessageComponent} + * is hovered by a player with the mouse courser. + * + * @author pxav + */ +public enum MessageHoverAction { + + /** + * Displays a given message to the player. + */ + SHOW_MESSAGE + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/PromptResponseType.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/PromptResponseType.java new file mode 100644 index 00000000..7f5fd5b4 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/PromptResponseType.java @@ -0,0 +1,29 @@ +package de.pxav.kelp.core.player.prompt; + +/** + * This constant describes if an input from any player prompt + * has been accepted by the handler. If the prompt asked for a + * number but the input also consists of alphabetical chars for + * example, the handler would return {@link PromptResponseType#TRY_AGAIN}, + * which means that the prompt will ask the player again. + * + * This simplifies input validation by a lot. + * + * @author pxav + */ +public enum PromptResponseType { + + /** + * The result is valid and has been accepted by the result handler. + * The prompt will close. + */ + ACCEPTED, + + /** + * The result is invalid and has not been accepted by the result handler. + * The prompt will open again asking for another input by the player until + * the handler accepts it. + */ + TRY_AGAIN + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/PromptTimeout.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/PromptTimeout.java new file mode 100644 index 00000000..a049de4b --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/PromptTimeout.java @@ -0,0 +1,105 @@ +package de.pxav.kelp.core.player.prompt; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * This class is used to create possible timeouts for player + * prompts. You can configure that your plugin waits for 10 + * seconds until it will automatically close the prompt again + * if the player has given no valid input during that time. + * + * In addition, you can define what should happen on a timeout + * by configuring a {@link Runnable}, which can be used to + * send messages or play sounds. + * + * @author pxav + */ +public class PromptTimeout { + + // how long the server should wait for an input + private int timeout = 0; + private TimeUnit timeUnit = TimeUnit.MILLISECONDS; + + // the code to be executed when the prompt times out + // (can be used to send messages, play sounds, ...) + private Runnable onTimeout; + + // whether the runnable should be executed on an async thread + private boolean async = true; + + // whether the prompt should be closed on timeout. + private boolean closeOnTimeout = false; + + // the task id of the scheduler handling the timeout. + private UUID taskId; + + public PromptTimeout() {} + + public PromptTimeout(int timeout, TimeUnit timeUnit) { + this.timeout = timeout; + this.timeUnit = timeUnit; + } + + public PromptTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout, boolean async, boolean closeOnTimeout) { + this.timeout = timeout; + this.timeUnit = timeUnit; + this.onTimeout = onTimeout; + this.async = async; + this.closeOnTimeout = closeOnTimeout; + } + + /** + * Sets the kelp-internal task id of the scheduler handling the timeout. + * This scheduler also executes the runnable {@link #getOnTimeout()} later. + * + * @param taskId The id of the scheduler that handles the timeout. + */ + public void setTaskId(UUID taskId) { + this.taskId = taskId; + } + + /** + * @return the time to wait until the prompt times out in the unit provided in {@link #getTimeUnit()} + */ + public int getTimeout() { + return timeout; + } + + /** + * @return The code that should be executed when the prompt times out. + */ + public Runnable getOnTimeout() { + return onTimeout; + } + + /** + * @return the {@link TimeUnit} of the provided time out time in {@link #getTimeout()} + */ + public TimeUnit getTimeUnit() { + return timeUnit; + } + + /** + * @return whether the prompt should be closed on timeout. + */ + public boolean shouldCloseOnTimeout() { + return this.closeOnTimeout; + } + + /** + * @return whether the given runnable ({@link #getOnTimeout()}) should be + * executed asynchronously. + */ + public boolean isAsync() { + return async; + } + + /** + * @return The kelp-internal task id of the scheduler handling the timeout. + */ + public UUID getTaskId() { + return taskId; + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/SimplePromptResponseHandler.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/SimplePromptResponseHandler.java new file mode 100644 index 00000000..2fb7d996 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/SimplePromptResponseHandler.java @@ -0,0 +1,24 @@ +package de.pxav.kelp.core.player.prompt; + +/** + * This interface should be implemented by every class handling the input + * of a simple prompt (Chat or anvil). If you want to handle a sign prompt, + * please use {@link de.pxav.kelp.core.player.prompt.sign.SignPromptResponseHandler}. + * + * @author pxav + */ +public interface SimplePromptResponseHandler { + + /** + * This method is executed every time a player submits their prompt + * input to the server. Then you can check this input and decide whether + * it is valid and the prompt can be closed or whether the player + * has to try it again. + * + * @param input The input sent by the player. + * @return Whether the player's input was valid and the prompt may close + * or whether the player has to try again. + */ + PromptResponseType accept(String input); + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/anvil/AnvilPrompt.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/anvil/AnvilPrompt.java new file mode 100644 index 00000000..7ff5744b --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/anvil/AnvilPrompt.java @@ -0,0 +1,187 @@ +package de.pxav.kelp.core.player.prompt.anvil; + +import com.google.common.collect.Lists; +import de.pxav.kelp.core.inventory.material.KelpMaterial; +import de.pxav.kelp.core.player.prompt.PromptTimeout; +import de.pxav.kelp.core.player.prompt.SimplePromptResponseHandler; +import de.pxav.kelp.core.player.prompt.sign.SignPrompt; +import de.pxav.kelp.core.player.prompt.sign.SignPromptResponseHandler; +import de.pxav.kelp.core.player.prompt.sign.SignPromptVersionTemplate; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * An anvil prompt is a simple and intuitive way to query user input. + * The player is presented an anvil gui and can use the renaming line + * as text prompt. The result item is used to confirm the input, which + * can be handled with a {@link SimplePromptResponseHandler}. + * + * @author pxav + */ +public class AnvilPrompt { + + private final AnvilPromptVersionTemplate anvilPromptVersionTemplate; + + private final Player player; + private String initialText = ""; + private KelpMaterial sourceMaterial = KelpMaterial.NAME_TAG; + private Runnable onClose; + private PromptTimeout timeout = new PromptTimeout(); + private SimplePromptResponseHandler responseHandler; + + public AnvilPrompt(Player player, AnvilPromptVersionTemplate anvilPromptVersionTemplate) { + this.player = player; + this.anvilPromptVersionTemplate = anvilPromptVersionTemplate; + } + + /** + * Give the prompt a default text that is displayed when the gui opens. + * + * @param initialText The text you want to display on inventory open. + * @return The current prompt instance for fluent builder design. + */ + public AnvilPrompt initialText(String initialText) { + this.initialText = initialText; + return this; + } + + /** + * Sets the source material used in the anvil inventory. The source item is + * on the very left slot and basically is the item the player is renaming. + * This material will be used for the result item later as well on which the + * player has to click in order to confirm their input. + * + * @param kelpMaterial The material you want to use for source and result. + * @return The current prompt instance for fluent builder design. + */ + public AnvilPrompt sourceMaterial(KelpMaterial kelpMaterial) { + this.sourceMaterial = kelpMaterial; + return this; + } + + /** + * Define what should happen when the player closes their inventory manually. + * + * @param onClose The code that should be executed. + * @return The current prompt instance for fluent builder design. + */ + public AnvilPrompt onClose(Runnable onClose) { + this.onClose = onClose; + return this; + } + + /** + * Configures a synchronous timeout. That means that the code which is ran on timeout will + * be executed on the server's main thread. You should prefer this option if you know your + * actions are not thread-safe. + * + * @param timeout How long it should take until the prompt times out. + * @param timeUnit Provide a time unit for the {@code timeout} value. + * @param onTimeout Define what should happen when the player closes its inventory. + * Please check if your actions are thread safe. If so, use {@link #withAsyncTimeout(int, TimeUnit, Runnable, boolean)} + * instead. + * @param closeOnTimeout Should the inventory be closed when the player prompt times out? + * @return The current prompt instance for fluent builder design. + */ + public AnvilPrompt withSyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout, boolean closeOnTimeout) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, false, closeOnTimeout); + return this; + } + + /** + * Configures an asynchronous timeout. That means that the code which is ran on timeout will + * be executed on another thread than the server's main thread. You should prefer this option + * if you know your actions are thread-safe or you can jump to the main thread easily. + * + * @param timeout How long it should take until the prompt times out. + * @param timeUnit Provide a time unit for the {@code timeout} value. + * @param onTimeout Define what should happen when the player closes its inventory. + * Please check if your actions are thread safe. If not, use {@link #withSyncTimeout(int, TimeUnit, Runnable)} + * instead. + * @param closeOnTimeOut Should the inventory be closed when the prompt times out? + * @return The current prompt instance for fluent builder design. + */ + public AnvilPrompt withAsyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout, boolean closeOnTimeOut) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, true, closeOnTimeOut); + return this; + } + + /** + * Configures a synchronous timeout. That means that the code which is ran on timeout will + * be executed on the server's main thread. You should prefer this option if you know your + * actions are not thread-safe. + * + * This method closes the inventory by default. If you want to change that, use + * {@link #withSyncTimeout(int, TimeUnit, Runnable, boolean)}. + * + * @param timeout How long it should take until the prompt times out. + * @param timeUnit Provide a time unit for the {@code timeout} value. + * @param onTimeout Define what should happen when the player closes its inventory. + * Please check if your actions are thread safe. If so, use {@link #withAsyncTimeout(int, TimeUnit, Runnable)} + * instead. + * @return The current prompt instance for fluent builder design. + */ + public AnvilPrompt withSyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, false, true); + return this; + } + + /** + * Configures an asynchronous timeout. That means that the code which is ran on timeout will + * be executed on another thread than the server's main thread. You should prefer this option + * if you know your actions are thread-safe or you can jump to the main thread easily. + * + * This method closes the inventory by default. If you want to change that, use + * {@link #withAsyncTimeout(int, TimeUnit, Runnable, boolean)}. + * + * @param timeout How long it should take until the prompt times out. + * @param timeUnit Provide a time unit for the {@code timeout} value. + * @param onTimeout Define what should happen when the player closes its inventory. + * Please check if your actions are thread safe. If not, use {@link #withSyncTimeout(int, TimeUnit, Runnable)} + * instead. + * @return The current prompt instance for fluent builder design. + */ + public AnvilPrompt withAsyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, true, true); + return this; + } + + /** + * Provide the handler class handling the input of the player. + * + * @param responseHandler The handler class implementing {@link SimplePromptResponseHandler} + */ + public void handle(SimplePromptResponseHandler responseHandler) { + this.responseHandler = responseHandler; + this.anvilPromptVersionTemplate.openPrompt(this.player, this.initialText, this.sourceMaterial, this.onClose, this.timeout, this.responseHandler); + } + + /** + * Gets the instance of the response handler for the current prompt. + * If no handler has been defined, it will return {@code null}. + * @return The current prompt response handler. + */ + public SimplePromptResponseHandler getResponseHandler() { + return responseHandler; + } + + /** + * Gets the default text that is displayed when the gui opens. + * @return The initial text of the current anvil prompt. + */ + public String getInitialText() { + return this.initialText; + } + + /** + * Gets all information about the timeout configuration. More information about that + * can be found in {@link PromptTimeout} + * @return The current prompt timeout configuration. + */ + public PromptTimeout getTimeout() { + return timeout; + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/anvil/AnvilPromptVersionTemplate.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/anvil/AnvilPromptVersionTemplate.java new file mode 100644 index 00000000..5709af28 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/anvil/AnvilPromptVersionTemplate.java @@ -0,0 +1,44 @@ +package de.pxav.kelp.core.player.prompt.anvil; + +import de.pxav.kelp.core.application.KelpVersionTemplate; +import de.pxav.kelp.core.inventory.material.KelpMaterial; +import de.pxav.kelp.core.player.KelpPlayer; +import de.pxav.kelp.core.player.prompt.PromptTimeout; +import de.pxav.kelp.core.player.prompt.SimplePromptResponseHandler; +import org.bukkit.entity.Player; + +/** + * This version template is used to handle version specific stuff + * for anvil prompts. + * + * @author pxav + */ +@KelpVersionTemplate +public abstract class AnvilPromptVersionTemplate { + + /** + * Opens a new anvil prompt to the given player. This means that the player + * will see an anvil GUI and has to enter a text. After that he can confirm this + * text by clicking on the result item at the very right of the inventory. + * Then the input can be handled and the plugin can decide whether it accepts + * the input or the player has to try again. + * + * @param player The player you want the prompt to open to. + * @param initialText The initial text which should already be in the text line as default input. + * @param sourceMaterial The material to use in the inventory on the left slot (the item that is actually + * renamed and the item that has the default name) as well as in the right result + * slot (the item used to confirm the input). + * @param onClose If the player closes their inventory manually while being in the prompt, what should happen? + * @param timeout If you want to enable a timeout, you can configure it here. If you don't want + * a timeout, pass {@code null}. + * @param handler The class handling the input by the player. More detailed information can be found + * at {@link SimplePromptResponseHandler} + */ + public abstract void openPrompt(Player player, + String initialText, + KelpMaterial sourceMaterial, + Runnable onClose, + PromptTimeout timeout, + SimplePromptResponseHandler handler); + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/ChatPromptVersionTemplate.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/ChatPromptVersionTemplate.java new file mode 100644 index 00000000..0089deb2 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/ChatPromptVersionTemplate.java @@ -0,0 +1,30 @@ +package de.pxav.kelp.core.player.prompt.chat; + +import de.pxav.kelp.core.application.KelpVersionTemplate; +import de.pxav.kelp.core.player.KelpPlayer; +import de.pxav.kelp.core.player.prompt.PromptTimeout; +import de.pxav.kelp.core.player.prompt.SimplePromptResponseHandler; +import org.bukkit.entity.Player; + +import java.util.Collection; + +/** + * This version template is used for handling the simple chat prompt for players. + * + * @author pxav + */ +@KelpVersionTemplate +public abstract class ChatPromptVersionTemplate { + + /** + * Sets the player's chat into chat prompt mode. This means that the normal + * chat is disabled and messages are not visible for other players anymore. + * If the player's input is handled successfully, the chat is turned into + * normal mode again. + * + * @param simpleChatPrompt The {@link SimpleChatPrompt} to open. It can be built using + * an instance of {@link KelpPlayer}. + */ + public abstract void simpleChatPrompt(SimpleChatPrompt simpleChatPrompt); + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/DefaultFontSize.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/DefaultFontSize.java new file mode 100644 index 00000000..67bf5859 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/DefaultFontSize.java @@ -0,0 +1,145 @@ +package de.pxav.kelp.core.player.prompt.chat; + +/** + * This class is used to decide how big different chars appear in the chat, + * which is important for deciding where to put a message if it should be + * centered. + * + * This class is written by SirSpoodles from SpigotMC who provided a method for + * sending centered chat messages. Link to the corresponding spigot page: + * https://www.spigotmc.org/threads/free-code-sending-perfectly-centered-chat-message.95872/ + * + * The file has been modified and some further chars have been implemented to + * be more universal and applicable for more use cases. + * + * @author SirSpoodles + * @author pxav + */ +public enum DefaultFontSize { + + A('A', 5), + a('a', 5), + B('B', 5), + b('b', 5), + C('C', 5), + c('c', 5), + D('D', 5), + d('d', 5), + E('E', 5), + e('e', 5), + F('F', 5), + f('f', 4), + G('G', 5), + g('g', 5), + H('H', 5), + h('h', 5), + I('I', 3), + i('i', 1), + J('J', 5), + j('j', 5), + K('K', 5), + k('k', 4), + L('L', 5), + l('l', 1), + M('M', 5), + m('m', 5), + N('N', 5), + n('n', 5), + O('O', 5), + o('o', 5), + P('P', 5), + p('p', 5), + Q('Q', 5), + q('q', 5), + R('R', 5), + r('r', 5), + S('S', 5), + s('s', 5), + T('T', 5), + t('t', 4), + U('U', 5), + u('u', 5), + V('V', 5), + v('v', 5), + W('W', 5), + w('w', 5), + X('X', 5), + x('x', 5), + Y('Y', 5), + y('y', 5), + Z('Z', 5), + z('z', 5), + NUM_1('1', 5), + NUM_2('2', 5), + NUM_3('3', 5), + NUM_4('4', 5), + NUM_5('5', 5), + NUM_6('6', 5), + NUM_7('7', 5), + NUM_8('8', 5), + NUM_9('9', 5), + NUM_0('0', 5), + EXCLAMATION_POINT('!', 1), + AT_SYMBOL('@', 6), + NUM_SIGN('#', 5), + DOLLAR_SIGN('$', 5), + PERCENT('%', 5), + UP_ARROW('^', 5), + AMPERSAND('&', 5), + ASTERISK('*', 5), + LEFT_PARENTHESIS('(', 4), + RIGHT_PERENTHESIS(')', 4), + MINUS('-', 5), + UNDERSCORE('_', 5), + PLUS_SIGN('+', 5), + EQUALS_SIGN('=', 5), + LEFT_CURL_BRACE('{', 4), + RIGHT_CURL_BRACE('}', 4), + LEFT_BRACKET('[', 3), + RIGHT_BRACKET(']', 3), + COLON(':', 1), + SEMI_COLON(';', 1), + DOUBLE_QUOTE('"', 3), + SINGLE_QUOTE('\'', 1), + LEFT_ARROW('<', 4), + RIGHT_ARROW('>', 4), + QUESTION_MARK('?', 5), + SLASH('/', 5), + BACK_SLASH('\\', 5), + LINE('|', 1), + TILDE('~', 5), + TICK('`', 2), + PERIOD('.', 1), + COMMA(',', 1), + SPACE(' ', 3), + DEFAULT('a', 4); + + private char character; + private int length; + + DefaultFontSize(char character, int length) { + this.character = character; + this.length = length; + } + + public char getCharacter(){ + return this.character; + } + + public int getLength(){ + return this.length; + } + + public int getBoldLength(){ + if(this == DefaultFontSize.SPACE) return this.getLength(); + return this.length + 1; + } + + public static DefaultFontSize getDefaultFontInfo(char c){ + for(DefaultFontSize dFI : DefaultFontSize.values()){ + if(dFI.getCharacter() == c) return dFI; + } + return DefaultFontSize.DEFAULT; + } + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/SimpleChatPrompt.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/SimpleChatPrompt.java new file mode 100644 index 00000000..ae557825 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/chat/SimpleChatPrompt.java @@ -0,0 +1,199 @@ +package de.pxav.kelp.core.player.prompt.chat; + +import de.pxav.kelp.core.player.prompt.PromptTimeout; +import de.pxav.kelp.core.player.prompt.SimplePromptResponseHandler; +import org.bukkit.entity.Player; + +import java.util.concurrent.TimeUnit; + +/** + * The simple chat prompt is a way for developers to simply query + * small text data from the player. An example would be setting a nickname, etc. + * + * For querying the input, the normal chat text box is used and the normal chat is + * disabled for the player while they type in their input. The user can exit at + * every time by using an exit flag and an optional echo can be configured. + * + * If the input has been accepted or the player has left the input mode, the player + * is allowed to write into the normal public chat again. + * + * @author pxav + */ +public class SimpleChatPrompt { + + private final ChatPromptVersionTemplate chatPromptVersionTemplate; + + private final Player player; + private String cancelFlag; + private boolean enableEcho = false; + private String echoPrefix = ""; + private String echoSuffix = ""; + private Runnable onCancel; + private PromptTimeout timeout = new PromptTimeout(); + private SimplePromptResponseHandler responseHandler; + + public SimpleChatPrompt(Player player, ChatPromptVersionTemplate chatPromptVersionTemplate) { + this.player = player; + this.chatPromptVersionTemplate = chatPromptVersionTemplate; + } + + /** + * Defines the message a player has to type in order to exit the + * prompt and use the chat normally again. + * + * @param cancelFlag The string a player has to type in order to exit again. + * @return The current instance of the prompt for fluent builder design. + */ + public SimpleChatPrompt cancelFlag(String cancelFlag) { + this.cancelFlag = cancelFlag; + return this; + } + + /** + * Enables an echo for the input the player types. An echo is a + * message printing the input the player typed into the chat again + * to make the input processing more transparent and allow the player + * to find typos, etc. + * + * @param prefix The text that should be displayed in front of the actual input. + * Can also be used to provide color codes for the message. + * Choose {@code null} if you do not want a prefix. + * @param suffix The text that should be displayed after the actual input. + * Choose {@code null} if you want no suffix. + * @return The current instance of the prompt for fluent builder design. + */ + public SimpleChatPrompt enableEcho(String prefix, String suffix) { + this.enableEcho = true; + this.echoPrefix = prefix; + this.echoSuffix = suffix; + return this; + } + + /** + * Disables the echo message. An echo message is a message printing + * the input from the player into the chat again so that the input + * processing becomes more transparent for the player. + * + * @return The current instance of the prompt for fluent builder design. + */ + public SimpleChatPrompt disableEcho() { + this.enableEcho = false; + return this; + } + + /** + * Enables a timeout for the current chat prompt. More detailed information + * about timeouts can be found in {@link PromptTimeout}. + * + * The code provided in the timeout is executed synchronously. Use + * {@link #withAsyncTimeout(int, TimeUnit, Runnable)} to invert that. + * + * @param timeout The amount of time that has to pass until the prompt times out. + * @param timeUnit The {@link TimeUnit} of the given {@code timeout}. + * @param onTimeout The code that should be executed when the prompt times out. + * In this case, this code is executed synchronously. + * @return The current instance of the prompt for fluent builder design. + */ + public SimpleChatPrompt withSyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, false, false); + return this; + } + + /** + * Enables a timeout for the current chat prompt. More detailed information + * about timeouts can be found in {@link PromptTimeout}. + * + * The code provided in the timeout is executed asynchronously. Use + * {@link #withSyncTimeout(int, TimeUnit, Runnable)} to invert that. + * + * @param timeout The amount of time that has to pass until the prompt times out. + * @param timeUnit The {@link TimeUnit} of the given {@code timeout}. + * @param onTimeout The code that should be executed when the prompt times out. + * In this case, this code is executed asynchronously. + * @return The current instance of the prompt for fluent builder design. + */ + public SimpleChatPrompt withAsyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, true, false); + return this; + } + + /** + * Defines the code that should be executed when the player enters + * the exit flag defined in {@link #cancelFlag(String)} to exit the + * prompt. + * + * @param onCancel The code to be executed when the player exits the prompt. + * @return The current instance of the prompt for fluent builder design. + */ + public SimpleChatPrompt onCancel(Runnable onCancel) { + this.onCancel = onCancel; + return this; + } + + /** + * Defines the handler that should handle the input of the player. + * More detailed information can be found in {@link SimplePromptResponseHandler}. + * + * @param responseHandler The class to handle the prompt's input. + */ + public void handle(SimplePromptResponseHandler responseHandler) { + this.responseHandler = responseHandler; + this.chatPromptVersionTemplate.simpleChatPrompt(this); + } + + /** + * @return The code handling the prompt's input string. + */ + public SimplePromptResponseHandler getResponseHandler() { + return responseHandler; + } + + /** + * @return The text a player has to type in order to exit the prompt and use the normal chat again. + */ + public String getCancelFlag() { + return this.cancelFlag; + } + + /** + * @return Detailed information about the prompt timeout if a timeout has been enabled. + */ + public PromptTimeout getTimeout() { + return timeout; + } + + /** + * @return whether echo messages are enabled. + */ + public boolean isEchoEnabled() { + return enableEcho; + } + + /** + * @return The prefix of the echo message. (might be {@code null}!) + */ + public String getEchoPrefix() { + return echoPrefix; + } + + /** + * @return The suffix of the echo message. (might be {@code null}!) + */ + public String getEchoSuffix() { + return echoSuffix; + } + + /** + * @return the code that should be executed when the player enters the exit flag defined in {@link #cancelFlag(String)} + */ + public Runnable getOnCancel() { + return onCancel; + } + + /** + * @return the player who owns the current prompt. + */ + public Player getPlayer() { + return player; + } +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPrompt.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPrompt.java new file mode 100644 index 00000000..dcb3d3c3 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPrompt.java @@ -0,0 +1,113 @@ +package de.pxav.kelp.core.player.prompt.sign; + +import com.google.common.collect.Lists; +import de.pxav.kelp.core.player.prompt.PromptTimeout; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * This class represents a sign prompt. + * + * @author pxav + */ +public class SignPrompt { + + private final SignPromptVersionTemplate signPromptVersionTemplate; + + private final Player player; + private List initialLines = Lists.newArrayList(); + private PromptTimeout timeout = new PromptTimeout(); + private SignPromptResponseHandler responseHandler; + + public SignPrompt(Player player, SignPromptVersionTemplate signPromptVersionTemplate) { + this.player = player; + this.signPromptVersionTemplate = signPromptVersionTemplate; + } + + /** + * Define the default lines of the sign the player edits. If you + * only want to change the first line of the sign, you only have to + * set the first list element and leave the other 3 slots empty ({@code null}). + * + * @param initialLines The default text lines of the sign in chronological order. + * @return The current prompt instance for fluent builder design. + */ + public SignPrompt initialLines(List initialLines) { + this.initialLines = initialLines; + return this; + } + + /** + * Adds a synchronous timeout to the prompt, which means that the code provided in {@code onTimeout} + * is executed on the server's main thread. + * + * @param timeout How long it should take until the prompt times out. + * @param timeUnit Provide a unit for the above {@code timeout} + * @param onTimeout The code that should be executed when the prompt times out. + * If your code is thread-safe you might want to choose + * {@link #withAsyncTimeout(int, TimeUnit, Runnable)} instead. + * @return The current prompt instance for fluent builder design. + */ + public SignPrompt withSyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, false, false); + return this; + } + + /** + * Adds an asynchronous timeout to the prompt, which means that the code provided in {@code onTimeout} + * is executed on a thread other than the main thread. + * + * @param timeout How long it should take until the prompt times out. + * @param timeUnit Provide a unit for the above {@code timeout} + * @param onTimeout The code that should be executed when the prompt times out. + * Make sure the code is thread-safe. If this is not possible, + * choose {@link #withSyncTimeout(int, TimeUnit, Runnable)} instead. + * @return The current prompt instance for fluent builder design. + */ + public SignPrompt withAsyncTimeout(int timeout, TimeUnit timeUnit, Runnable onTimeout) { + this.timeout = new PromptTimeout(timeout, timeUnit, onTimeout, true, false); + return this; + } + + /** + * Provide the handler class implementing {@link SignPromptResponseHandler} interface and + * handling the input provided by the player in the sign editor. + * + * @param responseHandler The handler class for the player's input. + */ + public void handle(SignPromptResponseHandler responseHandler) { + this.responseHandler = responseHandler; + this.signPromptVersionTemplate.openSignPrompt(this.player, this.initialLines, this.timeout, this.responseHandler); + } + + /** + * Gets the current response handler for the sign prompt. + * + * @return The current reponse handler for the sign prompt. + */ + public SignPromptResponseHandler getResponseHandler() { + return responseHandler; + } + + /** + * Gets a list of the default lines displayed when the sign + * editor opens for the first time. + * + * @return The default text written on the sign. + */ + public List getInitialLines() { + return initialLines; + } + + /** + * Gets all information about the timeout configuration. More information about that + * can be found in {@link PromptTimeout} + * + * @return The current prompt timeout configuration. + */ + public PromptTimeout getTimeout() { + return timeout; + } +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPromptResponseHandler.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPromptResponseHandler.java new file mode 100644 index 00000000..885d1dba --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPromptResponseHandler.java @@ -0,0 +1,37 @@ +package de.pxav.kelp.core.player.prompt.sign; + +import de.pxav.kelp.core.player.prompt.PromptResponseType; + +import java.util.List; + +/** + * This interface is used to handle the input provided by a + * {@link SignPrompt}, similar to the {@link de.pxav.kelp.core.player.prompt.SimplePromptResponseHandler} + * with the only exception that this handles takes a list of strings instead of + * a single string, as a sign can hold up to 4 lines of text. + * Each item in the list represents one line of the sign, so the maximum length of the + * list is {@code 4} and the maximum index {@code 3}. + * + * This class should be implemented by every class handling sign input. + * + * @author pxav + */ +public interface SignPromptResponseHandler { + + /** + * This method is executed when the player of the current sign + * prompt submits their input by pressing {@code 'Done'} in the sign + * editor GUI. + * + * @param input The lines the player has written on the sign. + * Each list item represents one line on the sign + * in chronological order. So the 0th item is the + * 1st line on the sign, the 1st list item is the + * 2nd line on the sign, ... + * @return Whether the input was valid ({@link PromptResponseType#ACCEPTED}) and the + * prompt may be closed now, or if the input was invalid ({@link PromptResponseType#TRY_AGAIN}) + * and the player has to try another input. + */ + PromptResponseType accept(List input); + +} diff --git a/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPromptVersionTemplate.java b/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPromptVersionTemplate.java new file mode 100644 index 00000000..dc690941 --- /dev/null +++ b/core/src/main/java/de/pxav/kelp/core/player/prompt/sign/SignPromptVersionTemplate.java @@ -0,0 +1,38 @@ +package de.pxav.kelp.core.player.prompt.sign; + +import de.pxav.kelp.core.application.KelpVersionTemplate; +import de.pxav.kelp.core.player.KelpPlayer; +import de.pxav.kelp.core.player.prompt.PromptTimeout; +import org.bukkit.entity.Player; + +import java.util.List; + +/** + * This version template is used for handling - especially opening - the sign prompt + * to a specific player. This process requires packets. + * + * @author pxav + */ +@KelpVersionTemplate +public abstract class SignPromptVersionTemplate { + + /** + * Opens the sign editor to a specific player. If the player clicks {@code Done} in this editor, + * the response handler is called automatically. + * + * @param player The player you want the prompt to show to. + * @param initialLines Pre-defined input. When the editor opens, there won't be an empty sign, but + * already some lines written to it providing help to the player on which input + * format is expected for example. The list follows a chronological order, so the + * 0th list item is the 1st line and so on. If you do not want a default input to + * be displayed, simply pass an empty list here. + * @param timeout If you want to enable a timeout, you can configure it here. Detailed information on + * how to do that can be found in {@link PromptTimeout}. If you want to disable timeout, + * pass {@code null} here. + * @param responseHandler The code handling the input by the player. If the player submits their input by pressing + * {@code Done}, the {@link SignPromptResponseHandler#accept(List)} method is executed. + * More information about handling the response can be found in {@link SignPromptResponseHandler}. + */ + public abstract void openSignPrompt(Player player, List initialLines, PromptTimeout timeout, SignPromptResponseHandler responseHandler); + +} diff --git a/core/src/main/java/de/pxav/kelp/core/scheduler/synchronize/ServerMainThread.java b/core/src/main/java/de/pxav/kelp/core/scheduler/synchronize/ServerMainThread.java index f853fc79..bb800430 100644 --- a/core/src/main/java/de/pxav/kelp/core/scheduler/synchronize/ServerMainThread.java +++ b/core/src/main/java/de/pxav/kelp/core/scheduler/synchronize/ServerMainThread.java @@ -117,6 +117,13 @@ public static class RunParallel { * @param runnable The code to execute on the main thread */ public static void run(Runnable runnable) { + + // if we are already on the main thread, directly execute the operation instead of creating a new scheduler. + if (Bukkit.isPrimaryThread()) { + runnable.run(); + return; + } + Bukkit.getScheduler().scheduleSyncDelayedTask(KelpPlugin.getPlugin(KelpPlugin.class), runnable); } diff --git a/core/src/test/java/de/pxav/kelp/core/test/common/StringUtilsTest.java b/core/src/test/java/de/pxav/kelp/core/test/common/StringUtilsTest.java new file mode 100644 index 00000000..ca0ff71a --- /dev/null +++ b/core/src/test/java/de/pxav/kelp/core/test/common/StringUtilsTest.java @@ -0,0 +1,65 @@ +package de.pxav.kelp.core.test.common; + +import de.pxav.kelp.core.common.StringUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.UUID; + +/** + * This class contains a bunch of unit tests testing the + * functionality of {@link StringUtils}. + * + * @author pxav + */ +public class StringUtilsTest { + + private StringUtils stringUtils; + + /** + * Before each test, a fresh instance of {@link StringUtils} + * is created for neutral, unaffected test results. + */ + @Before + public void newInstance() { + this.stringUtils = new StringUtils(); + } + + @Test + public void testColorCodeEndings() { + char colorCode = stringUtils.randomColorCode(); + String text1 = UUID.randomUUID() + "§" + colorCode; + + Assert.assertEquals("§" + colorCode, stringUtils.endsWithColorCode(text1)); + } + + @Test + public void testLastFormattingCodesOf() { + String lastCodes1 = stringUtils.lastFormattingCodesOf("§b§lYOUR TITLE STRING HERE"); + Assert.assertEquals(lastCodes1, "§b§l"); + + String lastCodes2 = stringUtils.lastFormattingCodesOf("§b§lYOUR TITLE STRING §oHERE"); + Assert.assertEquals(lastCodes2, "§b§l§o"); + } + + @Test + public void testStyleCodeExtraction() { + List styleCodes1 = stringUtils.extractStyleCodes("§a§lWelcome §6to the §oserver"); + Assert.assertEquals(styleCodes1.size(), 2); + } + + @Test + public void testColorCodeExtraction() { + List styleCodes1 = stringUtils.extractColorCodes("§a§lWelcome §6to the §oserver"); + Assert.assertEquals(styleCodes1.size(), 2); + } + + @Test + public void testFormattingCodeExtraction() { + List styleCodes1 = stringUtils.extractFormattingCodes("§a§lWelcome §6to the §oserver"); + Assert.assertEquals(styleCodes1.size(), 4); + } + +} diff --git a/core/src/test/java/de/pxav/kelp/core/test/scheduler/TimeConverterTest.java b/core/src/test/java/de/pxav/kelp/core/test/scheduler/TimeConverterTest.java new file mode 100644 index 00000000..ef7f16eb --- /dev/null +++ b/core/src/test/java/de/pxav/kelp/core/test/scheduler/TimeConverterTest.java @@ -0,0 +1,30 @@ +package de.pxav.kelp.core.test.scheduler; + +import de.pxav.kelp.core.scheduler.TimeConverter; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +/** + * This class tests if the {@link TimeConverter} converts + * ticks and real time correctly. + * + * @author pxav + */ +public class TimeConverterTest { + + @Test + public void testSecondsToTicks() { + Assert.assertEquals(TimeConverter.secondsToTicks(1), 20); + Assert.assertEquals(TimeConverter.secondsToTicks(3), 60); + Assert.assertEquals(TimeConverter.secondsToTicks(10), 200); + } + + @Test + public void testRealTimeToTicks() { + Assert.assertEquals(TimeConverter.getTicks(20, TimeUnit.MINUTES), 24_000); + Assert.assertEquals(TimeConverter.getTicks(1, TimeUnit.HOURS), 72_000); + } + +} diff --git a/kelp-sql/pom.xml b/kelp-sql/pom.xml new file mode 100644 index 00000000..2dac1ee2 --- /dev/null +++ b/kelp-sql/pom.xml @@ -0,0 +1,72 @@ + + + + parent + com.github.pxav.kelp + 0.0.5 + + 4.0.0 + + kelp-sql + + + + com.google.inject + guice + + + + com.github.pxav.kelp + core + 0.0.5 + provided + + + org.spigotmc + plugin-annotations + + + + + + org.spigotmc + spigot-api + 1.14-R0.1-SNAPSHOT + provided + + + + io.netty + netty-buffer + 5.0.0.Alpha2 + provided + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 8 + 8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + + + \ No newline at end of file diff --git a/kelp-sql/src/main/java/de/pxav/kelp/sql/KelpSqlModule.java b/kelp-sql/src/main/java/de/pxav/kelp/sql/KelpSqlModule.java new file mode 100644 index 00000000..9138049f --- /dev/null +++ b/kelp-sql/src/main/java/de/pxav/kelp/sql/KelpSqlModule.java @@ -0,0 +1,27 @@ +package de.pxav.kelp.sql; + +import de.pxav.kelp.core.application.KelpApplication; +import de.pxav.kelp.core.application.NewKelpApplication; + +/** + * A class description goes here. + * + * @author pxav + */ +@NewKelpApplication(applicationName = "KelpSQL", + version = "0.0.5", + authors = "pxav", + description = "Adds basic support for sql databases and hibernate integration") +public class KelpSqlModule extends KelpApplication { + + @Override + public void onEnable() { + super.onEnable(); + } + + @Override + public void onDisable() { + super.onDisable(); + } + +} diff --git a/pom.xml b/pom.xml index aa37e9ef..409ceb8f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.github.pxav.kelp parent pom - 0.0.4 + 0.0.5 Kelp A cross-version spigot framework to avoid boilerplate code and make your plugin compatible with multiple spigot versions easily @@ -54,6 +54,7 @@ v1_8_implementation testing-module v_1_14_implementation + kelp-sql @@ -66,6 +67,12 @@ com.google.inject guice 4.0 + + + com.google.guava + guava + + @@ -196,6 +203,8 @@ false true + ${project.artifactId}-${project.version}-shaded + ../builds @@ -236,6 +245,11 @@ + + org.apache.maven.plugins + maven-jar-plugin + 2.3.2 + diff --git a/testing-module/pom.xml b/testing-module/pom.xml index 07eef8aa..155ed166 100644 --- a/testing-module/pom.xml +++ b/testing-module/pom.xml @@ -5,7 +5,7 @@ parent com.github.pxav.kelp - 0.0.4 + 0.0.5 4.0.0 @@ -45,7 +45,7 @@ com.github.pxav.kelp core - 0.0.4 + 0.0.5 provided diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/command/kbossbar/TestBossBarCommand.java b/testing-module/src/main/java/de/pxav/kelp/testing/command/kbossbar/TestBossBarCommand.java new file mode 100644 index 00000000..de064102 --- /dev/null +++ b/testing-module/src/main/java/de/pxav/kelp/testing/command/kbossbar/TestBossBarCommand.java @@ -0,0 +1,155 @@ +package de.pxav.kelp.testing.command.kbossbar; + +import de.pxav.kelp.core.command.CreateCommand; +import de.pxav.kelp.core.command.ExecutorType; +import de.pxav.kelp.core.command.KelpCommand; +import de.pxav.kelp.core.player.KelpPlayer; +import de.pxav.kelp.core.player.bossbar.BossBarColor; +import de.pxav.kelp.core.player.bossbar.BossBarStyle; +import org.bukkit.ChatColor; + +/** + * A class description goes here. + * + * @author pxav + */ +@CreateCommand(name = "kbossbar", executorType = ExecutorType.PLAYER_ONLY) +public class TestBossBarCommand extends KelpCommand { + + @Override + public void onCommandRegister() { + noPlayerMessage("§cYou have to be a player to execute this command"); + permission("kelp.test.bossbar"); + allowCustomParameters(true); + } + + @Override + public void onCommand(KelpPlayer player, String[] args) { + + if (args.length >= 2 && args[0].equalsIgnoreCase("create")) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 1; i < args.length; i++) { + stringBuilder.append(args[i]).append(" "); + } + String message = ChatColor.translateAlternateColorCodes('&', stringBuilder.toString()); + player.sendBossBar(message); + player.sendMessage("§8[§2Kelp§8] §7A boss bar with the message §a" + message + " §7has been created"); + return; + } + + if (args.length >= 2 && args[0].equalsIgnoreCase("createprop")) { + BossBarColor color = getColor(args[1]); + BossBarStyle style = getStyle(args[2]); + + if (color == null) { + player.sendMessage("§8[§2Kelp§8] §cUnknown color (§7'" + args[1] + "'§c) §cuse §7/kbossbar colors §cto see all colors available."); + return; + } + + if (style == null) { + player.sendMessage("§8[§2Kelp§8] §cUnknown style (§7'" + args[2] + "'§c) §cuse §7/kbossbar styles §cto see all styles available."); + return; + } + + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 3; i < args.length; i++) { + stringBuilder.append(args[i]).append(" "); + } + String message = ChatColor.translateAlternateColorCodes('&', stringBuilder.toString()); + player.sendBossBar(message, 300f, color, style); + player.sendMessage("§8[§2Kelp§8] §7A boss bar with the message §a" + message + " §7has been created. "); + return; + } + + if (args.length == 2 && args[0].equalsIgnoreCase("progress")) { + double percentage; + try { + percentage = Double.parseDouble(args[1]); + } catch (NumberFormatException e) { + player.sendMessage("§8[§2Kelp§8] §cInvalid percentage values. "); + return; + } + + if (percentage < 0 || percentage > 1) { + player.sendMessage("§8[§2Kelp§8] §cValue out of range. Please choose a value between 0 and 1."); + } + + player.setBossBarProgress(percentage); + player.sendMessage("§8[§2Kelp§8] §7Progress of bossbar set. Current value §a" + (percentage * 100) + "%"); + + return; + } + + if (args.length == 3 && args[0].equalsIgnoreCase("progress")) { + int current, max; + try { + max = Integer.parseInt(args[2]); + current = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage("§8[§2Kelp§8] §cInvalid numerical values."); + return; + } + + player.setBossBarProgress(current, max); + player.sendMessage("§8[§2Kelp§8] §7Progress of bossbar set. Current value §a" + current + " §7of §a" + max); + return; + } + + if (args.length == 1 && args[0].equalsIgnoreCase("colors")) { + player.sendMessage("§8[§2Kelp§8] §7The following colors are available§8:"); + for (BossBarColor value : BossBarColor.values()) { + player.sendMessage("§8[§2Kelp§8] §8- §a" + value.name()); + } + return; + } + + if (args.length == 1 && args[0].equalsIgnoreCase("styles")) { + player.sendMessage("§8[§2Kelp§8] §7The following styles are available§8:"); + for (BossBarStyle value : BossBarStyle.values()) { + player.sendMessage("§8[§2Kelp§8] §8- §a" + value.name()); + } + return; + } + + if (args.length == 1 && args[0].equalsIgnoreCase("remove")) { + player.removeBossBar(); + player.sendMessage("§8[§2Kelp§8] §7The boss bar has been §aremoved"); + return; + } + + this.sendHelp(player); + + } + + private BossBarColor getColor(String color) { + for (BossBarColor value : BossBarColor.values()) { + if (value.name().equalsIgnoreCase(color)) { + return value; + } + } + return null; + } + + private BossBarStyle getStyle(String style) { + for (BossBarStyle value : BossBarStyle.values()) { + if (value.name().equalsIgnoreCase(style)) { + return value; + } + } + return null; + } + + private void sendHelp(KelpPlayer player) { + player.sendPrefixedMessages("§8[§2Kelp§8] ", + "§8§m-------------------------------", + "§7/kbossar create ", + "§7/kbossar createprop