From 0da7130165be1afbbb763cfad3e937c26b5062e2 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 4 Oct 2024 18:35:09 -0700 Subject: [PATCH] Add clickable text and hover text --- .../bentobox/bentobox/api/user/User.java | 107 +++++++++++++++++- .../bentobox/managers/IslandsManager.java | 26 ----- .../util/teleport/SafeSpotTeleport.java | 24 ++-- 3 files changed, 112 insertions(+), 45 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/api/user/User.java b/src/main/java/world/bentobox/bentobox/api/user/User.java index 8e7c32260..64278ed29 100644 --- a/src/main/java/world/bentobox/bentobox/api/user/User.java +++ b/src/main/java/world/bentobox/bentobox/api/user/User.java @@ -10,6 +10,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.lang.math.NumberUtils; import org.bukkit.Bukkit; @@ -36,6 +38,10 @@ import org.eclipse.jdt.annotation.Nullable; import com.google.common.base.Enums; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.Addon; import world.bentobox.bentobox.api.events.OfflineMessageEvent; @@ -584,16 +590,105 @@ public class User implements MetaDataAble { } /** - * Sends a message to sender without any modification (colors, multi-lines, - * placeholders). - * - * @param message - the message to send + * Sends a raw message to the sender, parsing inline commands embedded within square brackets. + *

+ * The method supports embedding clickable and hoverable actions into the message text using inline commands. + * Recognized commands are: + *

+ *

+ * The commands can be placed anywhere in the message and will apply to the entire message component. + * If multiple commands of the same type are provided, only the first one encountered will be applied. + * Unrecognized or invalid commands enclosed in square brackets will be preserved in the output text. + *

+ * Example usage: + *

+     * sendRawMessage("Hello [not-a-command: hello][run_command: /help] World [hover: This is a hover text]");
+     * 
+ * The above message will display "Hello [not-a-command: hello] World" where clicking the message runs the "/help" command, + * and hovering over the message shows "This is a hover text". + * + * @param message The message to send, containing inline commands in square brackets. */ public void sendRawMessage(String message) { + // Create a base TextComponent for the message + TextComponent baseComponent = new TextComponent(); + + // Regex to find inline commands like [run_command: /help] and [hover: click for help!], or unrecognized commands + Pattern pattern = Pattern.compile("\\[(\\w+): ([^\\]]+)]|\\[\\[(.*?)\\]]"); + Matcher matcher = pattern.matcher(message); + + // Keep track of the current position in the message + int lastMatchEnd = 0; + ClickEvent clickEvent = null; + HoverEvent hoverEvent = null; + + while (matcher.find()) { + // Add any text before the current match + if (matcher.start() > lastMatchEnd) { + String beforeMatch = message.substring(lastMatchEnd, matcher.start()); + baseComponent.addExtra(new TextComponent(beforeMatch)); + } + + // Check if it's a recognized command or an unknown bracketed text + if (matcher.group(1) != null && matcher.group(2) != null) { + // Parse the inline command (action) and value + String actionType = matcher.group(1).toUpperCase(Locale.ENGLISH); // e.g., RUN_COMMAND, HOVER + String actionValue = matcher.group(2); // The command or text to display + + // Apply the first valid click event or hover event encountered + switch (actionType) { + case "RUN_COMMAND": + case "SUGGEST_COMMAND": + case "COPY_TO_CLIPBOARD": + case "OPEN_URL": + if (clickEvent == null) { + clickEvent = new ClickEvent(ClickEvent.Action.valueOf(actionType), actionValue); + } + break; + case "HOVER": + if (hoverEvent == null) { + hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(actionValue)); + } + break; + default: + // Unrecognized command; preserve it in the output text + baseComponent.addExtra(new TextComponent(matcher.group(0))); + } + + } else if (matcher.group(3) != null) { + // Unrecognized bracketed text; preserve it in the output + baseComponent.addExtra(new TextComponent("[[" + matcher.group(3) + "]]")); + } + + // Update the last match end position + lastMatchEnd = matcher.end(); + } + + // Add any remaining text after the last match + if (lastMatchEnd < message.length()) { + String remainingText = message.substring(lastMatchEnd); + baseComponent.addExtra(new TextComponent(remainingText)); + } + + // Apply the first encountered ClickEvent and HoverEvent to the entire message + if (clickEvent != null) { + baseComponent.setClickEvent(clickEvent); + } + if (hoverEvent != null) { + baseComponent.setHoverEvent(hoverEvent); + } + + // Send the final component to the sender if (sender != null) { - sender.sendMessage(message); + sender.spigot().sendMessage(baseComponent); } else { - // Offline player fire event + // Handle offline player messaging or alternative actions Bukkit.getPluginManager().callEvent(new OfflineMessageEvent(this.playerUUID, message)); } } diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java index 6f35ecf20..f3cfaf0fc 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java @@ -650,7 +650,6 @@ public class IslandsManager { */ private CompletableFuture getAsyncSafeHomeLocation(@NonNull World world, @NonNull User user, String homeName) { - BentoBox.getInstance().logDebug("Getting safe home location for " + user.getName()); CompletableFuture result = new CompletableFuture<>(); // Check if the world is a gamemode world and the player has an island Location islandLoc = getIslandLocation(world, user.getUniqueId()); @@ -670,16 +669,10 @@ public class IslandsManager { Location namedHome = homeName.isBlank() ? null : getHomeLocation(world, user, name); Location l = namedHome != null ? namedHome : defaultHome; if (l != null) { - BentoBox.getInstance().logDebug("Loading the destination chunk asyc for " + user.getName()); - long time = System.currentTimeMillis(); Util.getChunkAtAsync(l).thenRun(() -> { - long duration = System.currentTimeMillis() - time; - BentoBox.getInstance().logDebug("Chunk loaded asyc for " + user.getName() + " in " + duration + "ms"); - BentoBox.getInstance().logDebug("Checking if the location is safe for " + user.getName()); // Check if it is safe if (isSafeLocation(l)) { result.complete(l); - BentoBox.getInstance().logDebug("Location is safe for " + user.getName()); return; } // To cover slabs, stairs and other half blocks, try one block above @@ -688,7 +681,6 @@ public class IslandsManager { // Adjust the home location accordingly setHomeLocation(user, lPlusOne, name); result.complete(lPlusOne); - BentoBox.getInstance().logDebug("Location is safe for " + user.getName()); return; } // Try island @@ -696,33 +688,25 @@ public class IslandsManager { }); return result; } - BentoBox.getInstance().logDebug("No home locations found for " + user.getName()); // Try island tryIsland(result, islandLoc, user, name); return result; } private void tryIsland(CompletableFuture result, Location islandLoc, @NonNull User user, String name) { - BentoBox.getInstance().logDebug(user.getName() + ": we need to try other locations on the island. Load the island center chunk async..."); - long time = System.currentTimeMillis(); Util.getChunkAtAsync(islandLoc).thenRun(() -> { - long duration = System.currentTimeMillis() - time; - BentoBox.getInstance().logDebug("Island center chunk loaded for " + user.getName() + " in " + duration + "ms"); World w = islandLoc.getWorld(); if (isSafeLocation(islandLoc)) { - BentoBox.getInstance().logDebug("Location is safe for " + user.getName()); setHomeLocation(user, islandLoc, name); result.complete(islandLoc.clone().add(new Vector(0.5D, 0, 0.5D))); return; } else { - BentoBox.getInstance().logDebug("Location is not safe for " + user.getName()); // If these island locations are not safe, then we need to get creative // Try the default location Location dl = islandLoc.clone().add(new Vector(0.5D, 5D, 2.5D)); if (isSafeLocation(dl)) { setHomeLocation(user, dl, name); result.complete(dl); - BentoBox.getInstance().logDebug("Found that the default spot is safe " + user.getName()); return; } // Try just above the bedrock @@ -730,22 +714,18 @@ public class IslandsManager { if (isSafeLocation(dl)) { setHomeLocation(user, dl, name); result.complete(dl); - BentoBox.getInstance().logDebug("Location above bedrock is safe for " + user.getName()); return; } - BentoBox.getInstance().logDebug("Trying all locations up to max height above bedrock for " + user.getName()); // Try all the way up to the sky for (int y = islandLoc.getBlockY(); y < w.getMaxHeight(); y++) { dl = new Location(w, islandLoc.getX() + 0.5D, y, islandLoc.getZ() + 0.5D); if (isSafeLocation(dl)) { setHomeLocation(user, dl, name); result.complete(dl); - BentoBox.getInstance().logDebug("Location is safe for " + user.getName()); return; } } } - BentoBox.getInstance().logDebug("Nowhere is safe for " + user.getName()); result.complete(null); }); @@ -1071,27 +1051,21 @@ public class IslandsManager { user.sendMessage("commands.island.go.teleport"); goingHome.add(user.getUniqueId()); readyPlayer(player); - BentoBox.getInstance().logDebug(user.getName() + " is going home"); this.getAsyncSafeHomeLocation(world, user, name).thenAccept(home -> { Island island = getIsland(world, user); if (home == null) { - BentoBox.getInstance().logDebug("Try to fix this teleport location and teleport the player if possible " + user.getName()); // Try to fix this teleport location and teleport the player if possible new SafeSpotTeleport.Builder(plugin).entity(player).island(island).homeName(name) .thenRun(() -> teleported(world, user, name, newIsland, island)) .ifFail(() -> goingHome.remove(user.getUniqueId())).buildFuture().thenAccept(result::complete); return; } - BentoBox.getInstance().logDebug("Teleporting " + player.getName() + " async"); - long time = System.currentTimeMillis(); PaperLib.teleportAsync(Objects.requireNonNull(player), home).thenAccept(b -> { // Only run the commands if the player is successfully teleported if (Boolean.TRUE.equals(b)) { - BentoBox.getInstance().logDebug("Teleported " + player.getName() + " async - took " + (System.currentTimeMillis() - time) + "ms"); teleported(world, user, name, newIsland, island); result.complete(true); } else { - BentoBox.getInstance().logDebug("Failed to teleport " + player.getName() + " async! - took " + (System.currentTimeMillis() - time) + "ms"); // Remove from mid-teleport set goingHome.remove(user.getUniqueId()); result.complete(false); diff --git a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java index 24899f3b4..402e6ea1d 100644 --- a/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java +++ b/src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java @@ -19,6 +19,7 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; @@ -38,8 +39,8 @@ public class SafeSpotTeleport { private static final long SPEED = 1; private static final int MAX_RADIUS = 50; // Parameters - private final Entity entity; - private final Location location; + private final @NonNull Entity entity; + private final @NonNull Location location; private final int homeNumber; private final BentoBox plugin; private final Runnable runnable; @@ -64,8 +65,8 @@ public class SafeSpotTeleport { */ SafeSpotTeleport(Builder builder) { this.plugin = builder.getPlugin(); - this.entity = builder.getEntity(); - this.location = builder.getLocation(); + this.entity = Objects.requireNonNull(builder.getEntity()); + this.location = Objects.requireNonNull(builder.getLocation()); this.portal = builder.isPortal(); this.homeNumber = builder.getHomeNumber(); this.homeName = builder.getHomeName(); @@ -86,7 +87,7 @@ public class SafeSpotTeleport { bestSpot = location; } else { // If this is not a portal teleport, then go to the safe location immediately - Util.teleportAsync(entity, location).thenRun(() -> { + Util.teleportAsync(Objects.requireNonNull(entity), Objects.requireNonNull(location)).thenRun(() -> { if (runnable != null) Bukkit.getScheduler().runTask(plugin, runnable); result.complete(true); }); @@ -122,7 +123,7 @@ public class SafeSpotTeleport { } // Get the chunk snapshot and scan it - Util.getChunkAtAsync(world, chunkPair.x, chunkPair.z) + Util.getChunkAtAsync(Objects.requireNonNull(world), chunkPair.x, chunkPair.z) .thenApply(Chunk::getChunkSnapshot) .whenCompleteAsync((snapshot, e) -> { if (snapshot != null && scanChunk(snapshot)) { @@ -180,7 +181,8 @@ public class SafeSpotTeleport { location.getBlock().setType(Material.AIR, false); location.getBlock().getRelative(BlockFace.UP).setType(Material.AIR, false); location.getBlock().getRelative(BlockFace.UP).getRelative(BlockFace.UP).setType(m, false); - Util.teleportAsync(entity, location.clone().add(new Vector(0.5D, 0D, 0.5D))).thenRun(() -> { + Util.teleportAsync(Objects.requireNonNull(entity), + Objects.requireNonNull(location.clone().add(new Vector(0.5D, 0D, 0.5D)))).thenRun(() -> { if (runnable != null) Bukkit.getScheduler().runTask(plugin, runnable); result.complete(true); }); @@ -275,19 +277,15 @@ public class SafeSpotTeleport { /** * Teleports entity to the safe spot */ - void teleportEntity(final Location loc) { + void teleportEntity(@NonNull final Location loc) { task.cancel(); // Return to main thread and teleport the player Bukkit.getScheduler().runTask(plugin, () -> { - BentoBox.getInstance().logDebug("Home number = " + homeNumber + " Home name = '" + homeName + "'"); - plugin.getIslands().getIslandAt(loc).ifPresent(is -> - plugin.getIslands().getHomeLocations(is).forEach((k,v) -> BentoBox.getInstance().logDebug("'" + k + "' => " + v))); if (!portal && entity instanceof Player && (homeNumber > 0 || !homeName.isEmpty())) { - BentoBox.getInstance().logDebug("Setting home"); // Set home if so marked plugin.getIslands().setHomeLocation(User.getInstance(entity), loc, homeName); } - Util.teleportAsync(entity, loc).thenRun(() -> { + Util.teleportAsync(Objects.requireNonNull(entity), Objects.requireNonNull(loc)).thenRun(() -> { if (runnable != null) Bukkit.getScheduler().runTask(plugin, runnable); result.complete(true); });