Skip to content

Commit 0da7130

Browse files
committed
Add clickable text and hover text
1 parent 9cb8d1c commit 0da7130

File tree

3 files changed

+112
-45
lines changed

3 files changed

+112
-45
lines changed

src/main/java/world/bentobox/bentobox/api/user/User.java

+101-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.Optional;
1111
import java.util.Set;
1212
import java.util.UUID;
13+
import java.util.regex.Matcher;
14+
import java.util.regex.Pattern;
1315

1416
import org.apache.commons.lang.math.NumberUtils;
1517
import org.bukkit.Bukkit;
@@ -36,6 +38,10 @@
3638

3739
import com.google.common.base.Enums;
3840

41+
import net.md_5.bungee.api.chat.ClickEvent;
42+
import net.md_5.bungee.api.chat.HoverEvent;
43+
import net.md_5.bungee.api.chat.TextComponent;
44+
import net.md_5.bungee.api.chat.hover.content.Text;
3945
import world.bentobox.bentobox.BentoBox;
4046
import world.bentobox.bentobox.api.addons.Addon;
4147
import world.bentobox.bentobox.api.events.OfflineMessageEvent;
@@ -584,16 +590,105 @@ public void sendMessage(String reference, String... variables) {
584590
}
585591

586592
/**
587-
* Sends a message to sender without any modification (colors, multi-lines,
588-
* placeholders).
589-
*
590-
* @param message - the message to send
593+
* Sends a raw message to the sender, parsing inline commands embedded within square brackets.
594+
* <p>
595+
* The method supports embedding clickable and hoverable actions into the message text using inline commands.
596+
* Recognized commands are:
597+
* <ul>
598+
* <li><code>[run_command: &lt;command&gt;]</code> - Runs the specified command when the message is clicked.</li>
599+
* <li><code>[suggest_command: &lt;command&gt;]</code> - Suggests the specified command in the chat input.</li>
600+
* <li><code>[copy_to_clipboard: &lt;text&gt;]</code> - Copies the specified text to the player's clipboard.</li>
601+
* <li><code>[open_url: &lt;url&gt;]</code> - Opens the specified URL when the message is clicked.</li>
602+
* <li><code>[hover: &lt;text&gt;]</code> - Shows the specified text when the message is hovered over.</li>
603+
* </ul>
604+
* <p>
605+
* The commands can be placed anywhere in the message and will apply to the entire message component.
606+
* If multiple commands of the same type are provided, only the first one encountered will be applied.
607+
* Unrecognized or invalid commands enclosed in square brackets will be preserved in the output text.
608+
* <p>
609+
* Example usage:
610+
* <pre>
611+
* sendRawMessage("Hello [not-a-command: hello][run_command: /help] World [hover: This is a hover text]");
612+
* </pre>
613+
* The above message will display "Hello [not-a-command: hello] World" where clicking the message runs the "/help" command,
614+
* and hovering over the message shows "This is a hover text".
615+
*
616+
* @param message The message to send, containing inline commands in square brackets.
591617
*/
592618
public void sendRawMessage(String message) {
619+
// Create a base TextComponent for the message
620+
TextComponent baseComponent = new TextComponent();
621+
622+
// Regex to find inline commands like [run_command: /help] and [hover: click for help!], or unrecognized commands
623+
Pattern pattern = Pattern.compile("\\[(\\w+): ([^\\]]+)]|\\[\\[(.*?)\\]]");
624+
Matcher matcher = pattern.matcher(message);
625+
626+
// Keep track of the current position in the message
627+
int lastMatchEnd = 0;
628+
ClickEvent clickEvent = null;
629+
HoverEvent hoverEvent = null;
630+
631+
while (matcher.find()) {
632+
// Add any text before the current match
633+
if (matcher.start() > lastMatchEnd) {
634+
String beforeMatch = message.substring(lastMatchEnd, matcher.start());
635+
baseComponent.addExtra(new TextComponent(beforeMatch));
636+
}
637+
638+
// Check if it's a recognized command or an unknown bracketed text
639+
if (matcher.group(1) != null && matcher.group(2) != null) {
640+
// Parse the inline command (action) and value
641+
String actionType = matcher.group(1).toUpperCase(Locale.ENGLISH); // e.g., RUN_COMMAND, HOVER
642+
String actionValue = matcher.group(2); // The command or text to display
643+
644+
// Apply the first valid click event or hover event encountered
645+
switch (actionType) {
646+
case "RUN_COMMAND":
647+
case "SUGGEST_COMMAND":
648+
case "COPY_TO_CLIPBOARD":
649+
case "OPEN_URL":
650+
if (clickEvent == null) {
651+
clickEvent = new ClickEvent(ClickEvent.Action.valueOf(actionType), actionValue);
652+
}
653+
break;
654+
case "HOVER":
655+
if (hoverEvent == null) {
656+
hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(actionValue));
657+
}
658+
break;
659+
default:
660+
// Unrecognized command; preserve it in the output text
661+
baseComponent.addExtra(new TextComponent(matcher.group(0)));
662+
}
663+
664+
} else if (matcher.group(3) != null) {
665+
// Unrecognized bracketed text; preserve it in the output
666+
baseComponent.addExtra(new TextComponent("[[" + matcher.group(3) + "]]"));
667+
}
668+
669+
// Update the last match end position
670+
lastMatchEnd = matcher.end();
671+
}
672+
673+
// Add any remaining text after the last match
674+
if (lastMatchEnd < message.length()) {
675+
String remainingText = message.substring(lastMatchEnd);
676+
baseComponent.addExtra(new TextComponent(remainingText));
677+
}
678+
679+
// Apply the first encountered ClickEvent and HoverEvent to the entire message
680+
if (clickEvent != null) {
681+
baseComponent.setClickEvent(clickEvent);
682+
}
683+
if (hoverEvent != null) {
684+
baseComponent.setHoverEvent(hoverEvent);
685+
}
686+
687+
// Send the final component to the sender
593688
if (sender != null) {
594-
sender.sendMessage(message);
689+
sender.spigot().sendMessage(baseComponent);
595690
} else {
596-
// Offline player fire event
691+
// Handle offline player messaging or alternative actions
597692
Bukkit.getPluginManager().callEvent(new OfflineMessageEvent(this.playerUUID, message));
598693
}
599694
}

src/main/java/world/bentobox/bentobox/managers/IslandsManager.java

-26
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,6 @@ public Optional<Island> getProtectedIslandAt(@NonNull Location location) {
650650
*/
651651
private CompletableFuture<Location> getAsyncSafeHomeLocation(@NonNull World world, @NonNull User user,
652652
String homeName) {
653-
BentoBox.getInstance().logDebug("Getting safe home location for " + user.getName());
654653
CompletableFuture<Location> result = new CompletableFuture<>();
655654
// Check if the world is a gamemode world and the player has an island
656655
Location islandLoc = getIslandLocation(world, user.getUniqueId());
@@ -670,16 +669,10 @@ private CompletableFuture<Location> getAsyncSafeHomeLocation(@NonNull World worl
670669
Location namedHome = homeName.isBlank() ? null : getHomeLocation(world, user, name);
671670
Location l = namedHome != null ? namedHome : defaultHome;
672671
if (l != null) {
673-
BentoBox.getInstance().logDebug("Loading the destination chunk asyc for " + user.getName());
674-
long time = System.currentTimeMillis();
675672
Util.getChunkAtAsync(l).thenRun(() -> {
676-
long duration = System.currentTimeMillis() - time;
677-
BentoBox.getInstance().logDebug("Chunk loaded asyc for " + user.getName() + " in " + duration + "ms");
678-
BentoBox.getInstance().logDebug("Checking if the location is safe for " + user.getName());
679673
// Check if it is safe
680674
if (isSafeLocation(l)) {
681675
result.complete(l);
682-
BentoBox.getInstance().logDebug("Location is safe for " + user.getName());
683676
return;
684677
}
685678
// To cover slabs, stairs and other half blocks, try one block above
@@ -688,64 +681,51 @@ private CompletableFuture<Location> getAsyncSafeHomeLocation(@NonNull World worl
688681
// Adjust the home location accordingly
689682
setHomeLocation(user, lPlusOne, name);
690683
result.complete(lPlusOne);
691-
BentoBox.getInstance().logDebug("Location is safe for " + user.getName());
692684
return;
693685
}
694686
// Try island
695687
tryIsland(result, islandLoc, user, name);
696688
});
697689
return result;
698690
}
699-
BentoBox.getInstance().logDebug("No home locations found for " + user.getName());
700691
// Try island
701692
tryIsland(result, islandLoc, user, name);
702693
return result;
703694
}
704695

705696
private void tryIsland(CompletableFuture<Location> result, Location islandLoc, @NonNull User user, String name) {
706-
BentoBox.getInstance().logDebug(user.getName() + ": we need to try other locations on the island. Load the island center chunk async...");
707-
long time = System.currentTimeMillis();
708697
Util.getChunkAtAsync(islandLoc).thenRun(() -> {
709-
long duration = System.currentTimeMillis() - time;
710-
BentoBox.getInstance().logDebug("Island center chunk loaded for " + user.getName() + " in " + duration + "ms");
711698
World w = islandLoc.getWorld();
712699
if (isSafeLocation(islandLoc)) {
713-
BentoBox.getInstance().logDebug("Location is safe for " + user.getName());
714700
setHomeLocation(user, islandLoc, name);
715701
result.complete(islandLoc.clone().add(new Vector(0.5D, 0, 0.5D)));
716702
return;
717703
} else {
718-
BentoBox.getInstance().logDebug("Location is not safe for " + user.getName());
719704
// If these island locations are not safe, then we need to get creative
720705
// Try the default location
721706
Location dl = islandLoc.clone().add(new Vector(0.5D, 5D, 2.5D));
722707
if (isSafeLocation(dl)) {
723708
setHomeLocation(user, dl, name);
724709
result.complete(dl);
725-
BentoBox.getInstance().logDebug("Found that the default spot is safe " + user.getName());
726710
return;
727711
}
728712
// Try just above the bedrock
729713
dl = islandLoc.clone().add(new Vector(0.5D, 5D, 0.5D));
730714
if (isSafeLocation(dl)) {
731715
setHomeLocation(user, dl, name);
732716
result.complete(dl);
733-
BentoBox.getInstance().logDebug("Location above bedrock is safe for " + user.getName());
734717
return;
735718
}
736-
BentoBox.getInstance().logDebug("Trying all locations up to max height above bedrock for " + user.getName());
737719
// Try all the way up to the sky
738720
for (int y = islandLoc.getBlockY(); y < w.getMaxHeight(); y++) {
739721
dl = new Location(w, islandLoc.getX() + 0.5D, y, islandLoc.getZ() + 0.5D);
740722
if (isSafeLocation(dl)) {
741723
setHomeLocation(user, dl, name);
742724
result.complete(dl);
743-
BentoBox.getInstance().logDebug("Location is safe for " + user.getName());
744725
return;
745726
}
746727
}
747728
}
748-
BentoBox.getInstance().logDebug("Nowhere is safe for " + user.getName());
749729
result.complete(null);
750730
});
751731

@@ -1071,27 +1051,21 @@ private CompletableFuture<Boolean> homeTeleportAsync(@NonNull World world, @NonN
10711051
user.sendMessage("commands.island.go.teleport");
10721052
goingHome.add(user.getUniqueId());
10731053
readyPlayer(player);
1074-
BentoBox.getInstance().logDebug(user.getName() + " is going home");
10751054
this.getAsyncSafeHomeLocation(world, user, name).thenAccept(home -> {
10761055
Island island = getIsland(world, user);
10771056
if (home == null) {
1078-
BentoBox.getInstance().logDebug("Try to fix this teleport location and teleport the player if possible " + user.getName());
10791057
// Try to fix this teleport location and teleport the player if possible
10801058
new SafeSpotTeleport.Builder(plugin).entity(player).island(island).homeName(name)
10811059
.thenRun(() -> teleported(world, user, name, newIsland, island))
10821060
.ifFail(() -> goingHome.remove(user.getUniqueId())).buildFuture().thenAccept(result::complete);
10831061
return;
10841062
}
1085-
BentoBox.getInstance().logDebug("Teleporting " + player.getName() + " async");
1086-
long time = System.currentTimeMillis();
10871063
PaperLib.teleportAsync(Objects.requireNonNull(player), home).thenAccept(b -> {
10881064
// Only run the commands if the player is successfully teleported
10891065
if (Boolean.TRUE.equals(b)) {
1090-
BentoBox.getInstance().logDebug("Teleported " + player.getName() + " async - took " + (System.currentTimeMillis() - time) + "ms");
10911066
teleported(world, user, name, newIsland, island);
10921067
result.complete(true);
10931068
} else {
1094-
BentoBox.getInstance().logDebug("Failed to teleport " + player.getName() + " async! - took " + (System.currentTimeMillis() - time) + "ms");
10951069
// Remove from mid-teleport set
10961070
goingHome.remove(user.getUniqueId());
10971071
result.complete(false);

src/main/java/world/bentobox/bentobox/util/teleport/SafeSpotTeleport.java

+11-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.bukkit.entity.Player;
2020
import org.bukkit.scheduler.BukkitTask;
2121
import org.bukkit.util.Vector;
22+
import org.eclipse.jdt.annotation.NonNull;
2223
import org.eclipse.jdt.annotation.Nullable;
2324

2425
import world.bentobox.bentobox.BentoBox;
@@ -38,8 +39,8 @@ public class SafeSpotTeleport {
3839
private static final long SPEED = 1;
3940
private static final int MAX_RADIUS = 50;
4041
// Parameters
41-
private final Entity entity;
42-
private final Location location;
42+
private final @NonNull Entity entity;
43+
private final @NonNull Location location;
4344
private final int homeNumber;
4445
private final BentoBox plugin;
4546
private final Runnable runnable;
@@ -64,8 +65,8 @@ public class SafeSpotTeleport {
6465
*/
6566
SafeSpotTeleport(Builder builder) {
6667
this.plugin = builder.getPlugin();
67-
this.entity = builder.getEntity();
68-
this.location = builder.getLocation();
68+
this.entity = Objects.requireNonNull(builder.getEntity());
69+
this.location = Objects.requireNonNull(builder.getLocation());
6970
this.portal = builder.isPortal();
7071
this.homeNumber = builder.getHomeNumber();
7172
this.homeName = builder.getHomeName();
@@ -86,7 +87,7 @@ void tryToGo(String failureMessage) {
8687
bestSpot = location;
8788
} else {
8889
// If this is not a portal teleport, then go to the safe location immediately
89-
Util.teleportAsync(entity, location).thenRun(() -> {
90+
Util.teleportAsync(Objects.requireNonNull(entity), Objects.requireNonNull(location)).thenRun(() -> {
9091
if (runnable != null) Bukkit.getScheduler().runTask(plugin, runnable);
9192
result.complete(true);
9293
});
@@ -122,7 +123,7 @@ boolean gatherChunks(String failureMessage) {
122123
}
123124

124125
// Get the chunk snapshot and scan it
125-
Util.getChunkAtAsync(world, chunkPair.x, chunkPair.z)
126+
Util.getChunkAtAsync(Objects.requireNonNull(world), chunkPair.x, chunkPair.z)
126127
.thenApply(Chunk::getChunkSnapshot)
127128
.whenCompleteAsync((snapshot, e) -> {
128129
if (snapshot != null && scanChunk(snapshot)) {
@@ -180,7 +181,8 @@ void makeAndTeleport(Material m) {
180181
location.getBlock().setType(Material.AIR, false);
181182
location.getBlock().getRelative(BlockFace.UP).setType(Material.AIR, false);
182183
location.getBlock().getRelative(BlockFace.UP).getRelative(BlockFace.UP).setType(m, false);
183-
Util.teleportAsync(entity, location.clone().add(new Vector(0.5D, 0D, 0.5D))).thenRun(() -> {
184+
Util.teleportAsync(Objects.requireNonNull(entity),
185+
Objects.requireNonNull(location.clone().add(new Vector(0.5D, 0D, 0.5D)))).thenRun(() -> {
184186
if (runnable != null) Bukkit.getScheduler().runTask(plugin, runnable);
185187
result.complete(true);
186188
});
@@ -275,19 +277,15 @@ boolean scanChunk(ChunkSnapshot chunk) {
275277
/**
276278
* Teleports entity to the safe spot
277279
*/
278-
void teleportEntity(final Location loc) {
280+
void teleportEntity(@NonNull final Location loc) {
279281
task.cancel();
280282
// Return to main thread and teleport the player
281283
Bukkit.getScheduler().runTask(plugin, () -> {
282-
BentoBox.getInstance().logDebug("Home number = " + homeNumber + " Home name = '" + homeName + "'");
283-
plugin.getIslands().getIslandAt(loc).ifPresent(is ->
284-
plugin.getIslands().getHomeLocations(is).forEach((k,v) -> BentoBox.getInstance().logDebug("'" + k + "' => " + v)));
285284
if (!portal && entity instanceof Player && (homeNumber > 0 || !homeName.isEmpty())) {
286-
BentoBox.getInstance().logDebug("Setting home");
287285
// Set home if so marked
288286
plugin.getIslands().setHomeLocation(User.getInstance(entity), loc, homeName);
289287
}
290-
Util.teleportAsync(entity, loc).thenRun(() -> {
288+
Util.teleportAsync(Objects.requireNonNull(entity), Objects.requireNonNull(loc)).thenRun(() -> {
291289
if (runnable != null) Bukkit.getScheduler().runTask(plugin, runnable);
292290
result.complete(true);
293291
});

0 commit comments

Comments
 (0)