Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clickable text and hover text #2529

Merged
merged 1 commit into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 101 additions & 6 deletions src/main/java/world/bentobox/bentobox/api/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
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;
Expand All @@ -36,6 +38,10 @@

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;
Expand Down Expand Up @@ -584,16 +590,105 @@ public void sendMessage(String reference, String... variables) {
}

/**
* 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.
* <p>
* The method supports embedding clickable and hoverable actions into the message text using inline commands.
* Recognized commands are:
* <ul>
* <li><code>[run_command: &lt;command&gt;]</code> - Runs the specified command when the message is clicked.</li>
* <li><code>[suggest_command: &lt;command&gt;]</code> - Suggests the specified command in the chat input.</li>
* <li><code>[copy_to_clipboard: &lt;text&gt;]</code> - Copies the specified text to the player's clipboard.</li>
* <li><code>[open_url: &lt;url&gt;]</code> - Opens the specified URL when the message is clicked.</li>
* <li><code>[hover: &lt;text&gt;]</code> - Shows the specified text when the message is hovered over.</li>
* </ul>
* <p>
* 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.
* <p>
* Example usage:
* <pre>
* sendRawMessage("Hello [not-a-command: hello][run_command: /help] World [hover: This is a hover text]");
* </pre>
* 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));
}
}
Expand Down
26 changes: 0 additions & 26 deletions src/main/java/world/bentobox/bentobox/managers/IslandsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,6 @@ public Optional<Island> getProtectedIslandAt(@NonNull Location location) {
*/
private CompletableFuture<Location> getAsyncSafeHomeLocation(@NonNull World world, @NonNull User user,
String homeName) {
BentoBox.getInstance().logDebug("Getting safe home location for " + user.getName());
CompletableFuture<Location> result = new CompletableFuture<>();
// Check if the world is a gamemode world and the player has an island
Location islandLoc = getIslandLocation(world, user.getUniqueId());
Expand All @@ -670,16 +669,10 @@ private CompletableFuture<Location> getAsyncSafeHomeLocation(@NonNull World worl
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
Expand All @@ -688,64 +681,51 @@ private CompletableFuture<Location> getAsyncSafeHomeLocation(@NonNull World worl
// Adjust the home location accordingly
setHomeLocation(user, lPlusOne, name);
result.complete(lPlusOne);
BentoBox.getInstance().logDebug("Location is safe for " + user.getName());
return;
}
// Try island
tryIsland(result, islandLoc, user, name);
});
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<Location> 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
dl = islandLoc.clone().add(new Vector(0.5D, 5D, 0.5D));
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);
});

Expand Down Expand Up @@ -1071,27 +1051,21 @@ private CompletableFuture<Boolean> homeTeleportAsync(@NonNull World world, @NonN
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -86,7 +87,7 @@ void tryToGo(String failureMessage) {
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);
});
Expand Down Expand Up @@ -122,7 +123,7 @@ boolean gatherChunks(String failureMessage) {
}

// 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)) {
Expand Down Expand Up @@ -180,7 +181,8 @@ void makeAndTeleport(Material m) {
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);
});
Expand Down Expand Up @@ -275,19 +277,15 @@ boolean scanChunk(ChunkSnapshot chunk) {
/**
* 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);
});
Expand Down
Loading