diff --git a/bukkit/src/main/java/net/william278/huskhomes/command/BukkitCommand.java b/bukkit/src/main/java/net/william278/huskhomes/command/BukkitCommand.java index 4590c970..94d0798e 100644 --- a/bukkit/src/main/java/net/william278/huskhomes/command/BukkitCommand.java +++ b/bukkit/src/main/java/net/william278/huskhomes/command/BukkitCommand.java @@ -146,7 +146,7 @@ public enum Type { TPA_HERE_COMMAND((plugin) -> new TeleportRequestCommand(plugin, TeleportRequest.Type.TPA_HERE)), TPACCEPT_COMMAND((plugin) -> new TpRespondCommand(plugin, true)), TPDECLINE_COMMAND((plugin) -> new TpRespondCommand(plugin, false)), - RTP_COMMAND(RtpCommand::new), + RTP_COMMAND(RTPCommand::new), TP_IGNORE_COMMAND(TpIgnoreCommand::new), TP_OFFLINE_COMMAND(TpOfflineCommand::new), TP_ALL_COMMAND(TpAllCommand::new), diff --git a/common/src/main/java/net/william278/huskhomes/api/BaseHuskHomesAPI.java b/common/src/main/java/net/william278/huskhomes/api/BaseHuskHomesAPI.java index 843016b6..e2ee29fc 100644 --- a/common/src/main/java/net/william278/huskhomes/api/BaseHuskHomesAPI.java +++ b/common/src/main/java/net/william278/huskhomes/api/BaseHuskHomesAPI.java @@ -23,6 +23,9 @@ import net.william278.huskhomes.HuskHomes; import net.william278.huskhomes.config.Locales; import net.william278.huskhomes.config.Settings; +import net.william278.huskhomes.network.Broker; +import net.william278.huskhomes.network.Message; +import net.william278.huskhomes.network.Payload; import net.william278.huskhomes.position.Home; import net.william278.huskhomes.position.Position; import net.william278.huskhomes.position.Warp; @@ -38,10 +41,7 @@ import java.time.Duration; import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -63,6 +63,11 @@ public class BaseHuskHomesAPI { */ protected final HuskHomes plugin; + /** + * (Internal use only - Random integer for RTP. + */ + private final Random random = new Random(); + /** * (Internal use only) - Constructor, instantiating the base API class. */ @@ -766,16 +771,67 @@ public final TeleportBuilder teleportBuilder() { /** * Attempt to teleport an {@link OnlineUser} to a randomly generated {@link Position}. The {@link Position} will be - * generated by the current {@link RandomTeleportEngine}. + * generated by the current {@link RandomTeleportEngine} on a randomly chosen server from + * {@link Settings.RtpSettings Allowed Servers} (If {@link Settings.CrossServerSettings Broker Type} + * is {@link Broker.Type#REDIS}. * * @param user The {@link OnlineUser} to teleport * @param timedTeleport Whether the teleport should be timed or not (requiring a warmup where they must stand still * for a period of time) * @param rtpArgs Arguments that will be passed to the implementing {@link RandomTeleportEngine} * @since 3.0 + * @apiNote This method was updated in version 4.6.3 to support the new Cross-Server RTP function, + * for the original function use {@link #randomlyTeleportPlayerLocally(OnlineUser, boolean, String...)} */ public final void randomlyTeleportPlayer(@NotNull OnlineUser user, boolean timedTeleport, @NotNull String... rtpArgs) { + if (plugin.getSettings().getRtp().isCrossServer() && (plugin.getSettings().getCrossServer().isEnabled() + && plugin.getSettings().getCrossServer().getBrokerType() == Broker.Type.REDIS)) { + List allowedServers = plugin.getSettings().getRtp().getRandomTargetServers(); + String randomServer = allowedServers.get(random.nextInt(allowedServers.size())); + if (randomServer.equals(plugin.getServerName())) { + randomlyTeleportPlayerLocally(user, timedTeleport, rtpArgs); + return; + } + Message.builder() + .scope(Message.Scope.SERVER) + .target(randomServer) + .type(Message.Type.REQUEST_RTP_LOCATION) + .payload(Payload.withStringList( + List.of(user.getPosition().getWorld().getName(), user.getUsername()))) + .build().send(plugin.getMessenger(), user); + return; + } + randomlyTeleportPlayerLocally(user, timedTeleport, rtpArgs); + } + + /** + * Attempt to teleport an {@link OnlineUser} to a randomly generated {@link Position}. The {@link Position} will be + * generated by the current {@link RandomTeleportEngine}. + * + * @param user The {@link OnlineUser} to teleport + * @since 3.0 + * @apiNote This method was updated in version 4.6.3 to support the new Cross-Server RTP function, + * for the original function use {@link #randomlyTeleportPlayerLocally(OnlineUser)} + */ + public final void randomlyTeleportPlayer(@NotNull OnlineUser user) { + this.randomlyTeleportPlayer(user, false); + } + + + /** + * Attempt to teleport an {@link OnlineUser} to a randomly generated {@link Position}. The {@link Position} will be + * generated by the current {@link RandomTeleportEngine}. (Does not RTP cross-server, use + * {@link #randomlyTeleportPlayer(OnlineUser, boolean, String...)}) + * + * @param user The {@link OnlineUser} to teleport + * @param timedTeleport Whether the teleport should be timed or not (requiring a warmup where they must stand still + * for a period of time) + * @param rtpArgs Arguments that will be passed to the implementing {@link RandomTeleportEngine} + * @since 4.6.3 + */ + public final void randomlyTeleportPlayerLocally(@NotNull OnlineUser user, boolean timedTeleport, + @NotNull String... rtpArgs) { plugin.getRandomTeleportEngine() .getRandomPosition(user.getPosition().getWorld(), rtpArgs) .thenAccept(position -> { @@ -794,13 +850,14 @@ public final void randomlyTeleportPlayer(@NotNull OnlineUser user, boolean timed /** * Attempt to teleport an {@link OnlineUser} to a randomly generated {@link Position}. The {@link Position} will be - * generated by the current {@link RandomTeleportEngine}. + * generated by the current {@link RandomTeleportEngine}. (Does not RTP cross-server, use + * {@link #randomlyTeleportPlayer(OnlineUser)}) * * @param user The {@link OnlineUser} to teleport - * @since 3.0 + * @since 4.6.3 */ - public final void randomlyTeleportPlayer(@NotNull OnlineUser user) { - this.randomlyTeleportPlayer(user, false); + public final void randomlyTeleportPlayerLocally(@NotNull OnlineUser user) { + this.randomlyTeleportPlayerLocally(user, false); } /** diff --git a/common/src/main/java/net/william278/huskhomes/command/RtpCommand.java b/common/src/main/java/net/william278/huskhomes/command/RTPCommand.java similarity index 79% rename from common/src/main/java/net/william278/huskhomes/command/RtpCommand.java rename to common/src/main/java/net/william278/huskhomes/command/RTPCommand.java index 1b6f63cb..cea3ed74 100644 --- a/common/src/main/java/net/william278/huskhomes/command/RtpCommand.java +++ b/common/src/main/java/net/william278/huskhomes/command/RTPCommand.java @@ -20,6 +20,9 @@ package net.william278.huskhomes.command; import net.william278.huskhomes.HuskHomes; +import net.william278.huskhomes.network.Broker; +import net.william278.huskhomes.network.Message; +import net.william278.huskhomes.network.Payload; import net.william278.huskhomes.position.World; import net.william278.huskhomes.teleport.Teleport; import net.william278.huskhomes.teleport.TeleportBuilder; @@ -32,10 +35,13 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Random; -public class RtpCommand extends Command implements UserListTabProvider { +public class RTPCommand extends Command implements UserListTabProvider { - protected RtpCommand(@NotNull HuskHomes plugin) { + private final Random random = new Random(); + + protected RTPCommand(@NotNull HuskHomes plugin) { super("rtp", List.of(), "[player] [world]", plugin); addAdditionalPermissions(Map.of( @@ -140,6 +146,37 @@ private void executeRtp(@NotNull OnlineUser teleporter, @NotNull CommandUser exe // Generate a random position plugin.getLocales().getLocale("teleporting_random_generation") .ifPresent(teleporter::sendMessage); + + if (plugin.getSettings().getRtp().isCrossServer() && plugin.getSettings().getCrossServer().isEnabled() + && plugin.getSettings().getCrossServer().getBrokerType() == Broker.Type.REDIS) { + List allowedServers = plugin.getSettings().getRtp().getRandomTargetServers(); + String randomServer = allowedServers.get(random.nextInt(allowedServers.size())); + if (randomServer.equals(plugin.getServerName())) { + performLocalRTP(teleporter, executor, world, args); + return; + } + Message.builder() + .type(Message.Type.REQUEST_RTP_LOCATION) + .scope(Message.Scope.SERVER) + .target(randomServer) + .payload(Payload.withRTPRequest(Payload.RTPRequest.of(teleporter.getUsername(), world.getName()))) + .build().send(plugin.getMessenger(), teleporter); + return; + } + + performLocalRTP(teleporter, executor, world, args); + } + + /** + * Performs the RTP locally. + * + * @param teleporter person to teleport + * @param executor the person executing the teleport + * @param world the world to teleport to + * @param args rtp engine args + */ + private void performLocalRTP(@NotNull OnlineUser teleporter, @NotNull CommandUser executor, @NotNull World world, + @NotNull String[] args) { plugin.getRandomTeleportEngine() .getRandomPosition(world, args.length > 1 ? removeFirstArg(args) : args) .thenAccept(position -> { @@ -157,5 +194,4 @@ private void executeRtp(@NotNull OnlineUser teleporter, @NotNull CommandUser exe builder.buildAndComplete(executor.equals(teleporter), args); }); } - } diff --git a/common/src/main/java/net/william278/huskhomes/config/Settings.java b/common/src/main/java/net/william278/huskhomes/config/Settings.java index b16b8b9a..fa4f2637 100644 --- a/common/src/main/java/net/william278/huskhomes/config/Settings.java +++ b/common/src/main/java/net/william278/huskhomes/config/Settings.java @@ -340,6 +340,13 @@ public boolean isWorldRtpRestricted(@NotNull World world) { .map(n -> n.startsWith("minecraft:") ? n.substring(10) : n) .anyMatch(n -> n.equalsIgnoreCase(filteredName)); } + + @Comment("Whether or not RTP should perform cross-server.") + private boolean crossServer = false; + + @Comment({"List of server in which /rtp is allowed. (Only relevant when using cross server mode WITH REDIS)", + "If a server is not defined here the RTP logic has no way of knowing its existence."}) + private List randomTargetServers = List.of("server-01", "server-02"); } @Comment("Action cooldown settings. Docs: https://william278.net/docs/huskhomes/cooldowns") diff --git a/common/src/main/java/net/william278/huskhomes/network/Broker.java b/common/src/main/java/net/william278/huskhomes/network/Broker.java index bd2f7320..18233e9a 100644 --- a/common/src/main/java/net/william278/huskhomes/network/Broker.java +++ b/common/src/main/java/net/william278/huskhomes/network/Broker.java @@ -23,8 +23,11 @@ import net.william278.huskhomes.HuskHomes; import net.william278.huskhomes.position.Home; import net.william278.huskhomes.position.Warp; +import net.william278.huskhomes.position.World; import net.william278.huskhomes.teleport.Teleport; +import net.william278.huskhomes.teleport.TeleportBuilder; import net.william278.huskhomes.user.OnlineUser; +import net.william278.huskhomes.util.TransactionResolver; import org.jetbrains.annotations.NotNull; import java.util.Locale; @@ -113,10 +116,55 @@ protected void handle(@NotNull OnlineUser receiver, @NotNull Message message) { plugin.getManager().homes().updatePublicHomeCache(); plugin.getManager().warps().updateWarpCache(); } + case RTP_LOCATION -> message.getPayload() + .getRTPResponse() + .ifPresentOrElse(response -> { + final TeleportBuilder builder = Teleport.builder(plugin) + .teleporter(receiver) + .actions(TransactionResolver.Action.RANDOM_TELEPORT) + .target(response.getPosition()); + builder.buildAndComplete(true); + }, () -> plugin.getLocales().getLocale("error_rtp_randomization_timeout") + .ifPresent(receiver::sendMessage)); default -> throw new IllegalStateException("Unexpected value: " + message.getType()); } } + /** + * Separate handler for RTP Request because it doesn't need a receiver to handle it. + * + * @param message the message to handle + */ + protected void handleRTPRequest(@NotNull Message message) { + if (message.getSourceServer().equals(getServer())) { + return; + } + + message.getPayload() + .getRTPRequest() + .ifPresent((request) -> { + Optional world = plugin.getWorlds().stream() + .filter(w -> w.getName().equals(request.getWorldName())).findFirst(); + if (world.isEmpty()) { + throw new RuntimeException("%s requested a position in a world we don't have! World: %s" + .formatted(message.getSourceServer(), request.getWorldName())); + } + plugin.getRandomTeleportEngine().getRandomPosition(world.get(), null) + .thenAccept((position) -> { + final Message.Builder builder = Message.builder() + .type(Message.Type.RTP_LOCATION) + .target(request.getUsername()); + if (position.isEmpty()) { + builder.payload(Payload.empty()); + } else { + builder.payload(Payload.withRTPResponse( + Payload.RTPResponse.of(request.getUsername(), position.get()))); + } + builder.build().send(plugin.getMessenger(), request.getUsername()); + }); + }); + } + /** * Initialize the message broker. * @@ -132,6 +180,13 @@ protected void handle(@NotNull OnlineUser receiver, @NotNull Message message) { */ protected abstract void send(@NotNull Message message, @NotNull OnlineUser sender); + /** + * Send a message to the broker. (For Redis Only) + * + * @param message the message to send + */ + protected abstract void send(@NotNull Message message); + /** * Move an {@link OnlineUser} to a new server on the proxy network. * diff --git a/common/src/main/java/net/william278/huskhomes/network/Message.java b/common/src/main/java/net/william278/huskhomes/network/Message.java index 285599c9..ecfb4768 100644 --- a/common/src/main/java/net/william278/huskhomes/network/Message.java +++ b/common/src/main/java/net/william278/huskhomes/network/Message.java @@ -75,6 +75,13 @@ public void send(@NotNull Broker broker, @NotNull OnlineUser sender) { broker.send(this, sender); } + public void send(@NotNull Broker broker, @NotNull String sender) { + this.sender = sender; + this.sourceServer = broker.getServer(); + broker.send(this); + } + + @NotNull public Type getType() { return type; @@ -173,6 +180,8 @@ public enum Type { UPDATE_HOME, UPDATE_WARP, UPDATE_CACHES, + REQUEST_RTP_LOCATION, + RTP_LOCATION, } public enum Scope { diff --git a/common/src/main/java/net/william278/huskhomes/network/Payload.java b/common/src/main/java/net/william278/huskhomes/network/Payload.java index 21807785..d92e22d4 100644 --- a/common/src/main/java/net/william278/huskhomes/network/Payload.java +++ b/common/src/main/java/net/william278/huskhomes/network/Payload.java @@ -21,7 +21,9 @@ import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; +import lombok.Value; import net.william278.huskhomes.position.Position; +import net.william278.huskhomes.position.World; import net.william278.huskhomes.teleport.TeleportRequest; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,11 +40,25 @@ public class Payload { @Expose private Position position; + @Nullable + @Expose + private World world; + @Nullable @Expose @SerializedName("teleport_request") private TeleportRequest teleportRequest; + @Nullable + @Expose + @SerializedName("rtp_response") + private RTPResponse rtpResponse; + + @Nullable + @Expose + @SerializedName("rtp_request") + private RTPRequest rtpRequest; + @Nullable @Expose private String string; @@ -75,6 +91,19 @@ public static Payload withPosition(@NotNull Position position) { return payload; } + /** + * Returns a payload containing a {@link World}. + * + * @param world the world to send + * @return a payload containing the world + */ + @NotNull + public static Payload withWorld(@NotNull World world) { + final Payload payload = new Payload(); + payload.world = world; + return payload; + } + /** * Returns a payload containing a {@link TeleportRequest}. * @@ -108,6 +137,26 @@ public static Payload withStringList(@NotNull List target) { return payload; } + /** + * An RTP Response field. + */ + @NotNull + public static Payload withRTPResponse(@NotNull RTPResponse rtpResponse) { + final Payload payload = new Payload(); + payload.rtpResponse = rtpResponse; + return payload; + } + + /** + * An RTP Request field. + */ + @NotNull + public static Payload withRTPRequest(@NotNull RTPRequest rtpRequest) { + final Payload payload = new Payload(); + payload.rtpRequest = rtpRequest; + return payload; + } + private Payload() { } @@ -118,6 +167,12 @@ public Optional getPosition() { return Optional.ofNullable(position); } + /** + * A world field. + */ + public Optional getWorld() { + return Optional.ofNullable(world); + } /** * A teleport request field. @@ -140,4 +195,29 @@ public Optional> getStringList() { return Optional.ofNullable(stringList); } + /** + * An RTP response. + */ + public Optional getRTPResponse() { + return Optional.ofNullable(rtpResponse); + } + + /** + * An RTP request. + */ + public Optional getRTPRequest() { + return Optional.ofNullable(rtpRequest); + } + + @Value(staticConstructor = "of") + public static class RTPResponse { + @Expose String username; + @Expose Position position; + } + + @Value(staticConstructor = "of") + public static class RTPRequest { + @Expose String username; + @Expose String worldName; + } } diff --git a/common/src/main/java/net/william278/huskhomes/network/PluginMessageBroker.java b/common/src/main/java/net/william278/huskhomes/network/PluginMessageBroker.java index 5c4eb8b1..45c95852 100644 --- a/common/src/main/java/net/william278/huskhomes/network/PluginMessageBroker.java +++ b/common/src/main/java/net/william278/huskhomes/network/PluginMessageBroker.java @@ -96,6 +96,16 @@ protected void send(@NotNull Message message, @NotNull OnlineUser sender) { sender.sendPluginMessage(messageWriter.toByteArray()); } + /** + * Send a message to the broker. (For Redis Only) + * + * @param message the message to send + */ + @Override + protected void send(@NotNull Message message) { + throw new IllegalStateException("Tried to send a plugin message without a sender!"); + } + @Override public void changeServer(@NotNull OnlineUser user, @NotNull String server) { user.dismount().thenRun(() -> { diff --git a/common/src/main/java/net/william278/huskhomes/network/RedisBroker.java b/common/src/main/java/net/william278/huskhomes/network/RedisBroker.java index d1fe3611..4b5b4c4e 100644 --- a/common/src/main/java/net/william278/huskhomes/network/RedisBroker.java +++ b/common/src/main/java/net/william278/huskhomes/network/RedisBroker.java @@ -100,6 +100,11 @@ protected void send(@NotNull Message message, @NotNull OnlineUser sender) { plugin.runAsync(() -> subscriber.send(message)); } + @Override + protected void send(@NotNull Message message) { + plugin.runAsync(() -> subscriber.send(message)); + } + @Override @Blocking public void close() { @@ -199,6 +204,11 @@ public void onMessage(@NotNull String channel, @NotNull String encoded) { return; } + if (message.getType() == Message.Type.REQUEST_RTP_LOCATION) { + broker.handleRTPRequest(message); + return; + } + if (message.getScope() == Message.Scope.PLAYER) { broker.plugin.getOnlineUsers().stream() .filter(online -> message.getTarget().equals(Message.TARGET_ALL) diff --git a/common/src/main/java/net/william278/huskhomes/random/NormalDistributionEngine.java b/common/src/main/java/net/william278/huskhomes/random/NormalDistributionEngine.java index 7611d313..8e0920b3 100644 --- a/common/src/main/java/net/william278/huskhomes/random/NormalDistributionEngine.java +++ b/common/src/main/java/net/william278/huskhomes/random/NormalDistributionEngine.java @@ -21,6 +21,7 @@ import net.william278.huskhomes.HuskHomes; import net.william278.huskhomes.config.Settings; +import net.william278.huskhomes.network.Broker; import net.william278.huskhomes.position.Location; import net.william278.huskhomes.position.Position; import net.william278.huskhomes.position.World; @@ -45,6 +46,13 @@ public NormalDistributionEngine(@NotNull HuskHomes plugin) { this.radius = plugin.getSettings().getRtp().getRegion(); this.mean = plugin.getSettings().getRtp().getDistributionMean(); this.standardDeviation = plugin.getSettings().getRtp().getDistributionStandardDeviation(); + + if (plugin.getSettings().getRtp().isCrossServer() + && (plugin.getSettings().getCrossServer().isEnabled() + && plugin.getSettings().getCrossServer().getBrokerType() != Broker.Type.REDIS)) { + plugin.log(Level.WARNING, "Cross-server /rtp support has been disabled as " + + "a REDIS message broker is required for this feature."); + } } // Utility for determining a valid spawn radius diff --git a/docs/Setup.md b/docs/Setup.md index 3ed7504e..9a1102db 100644 --- a/docs/Setup.md +++ b/docs/Setup.md @@ -49,6 +49,15 @@ These instructions are for installing HuskHomes on multiple Spigot, Fabric or Sp - Provided your MySQL database credentials were correct, your network should now be setup to use HuskHomes! - You can delete the `HuskHomesData.db` SQLite flat file that was generated, if you would like. +
+Cross-Server RTP + +When using Cross-Server RTP 3 things must be true: +1. You must set `rtp.cross-server` to `true` +2. You must be using Redis as your message broker +3. The server names in `rtp.allowed-servers` must match the `server.yml` & Proxy values! +
+ ## Next steps * [Commands & Permissions](Commands) * [[Config Files]] diff --git a/fabric/src/main/java/net/william278/huskhomes/command/FabricCommand.java b/fabric/src/main/java/net/william278/huskhomes/command/FabricCommand.java index 9628d369..7a881282 100644 --- a/fabric/src/main/java/net/william278/huskhomes/command/FabricCommand.java +++ b/fabric/src/main/java/net/william278/huskhomes/command/FabricCommand.java @@ -146,7 +146,7 @@ public enum Type { TPA_HERE_COMMAND((plugin) -> new TeleportRequestCommand(plugin, TeleportRequest.Type.TPA_HERE)), TPACCEPT_COMMAND((plugin) -> new TpRespondCommand(plugin, true)), TPDECLINE_COMMAND((plugin) -> new TpRespondCommand(plugin, false)), - RTP_COMMAND(RtpCommand::new), + RTP_COMMAND(RTPCommand::new), TP_IGNORE_COMMAND(TpIgnoreCommand::new), TP_OFFLINE_COMMAND(TpOfflineCommand::new), TP_ALL_COMMAND(TpAllCommand::new), diff --git a/sponge/src/main/java/net/william278/huskhomes/command/SpongeCommand.java b/sponge/src/main/java/net/william278/huskhomes/command/SpongeCommand.java index a0177369..8356613b 100644 --- a/sponge/src/main/java/net/william278/huskhomes/command/SpongeCommand.java +++ b/sponge/src/main/java/net/william278/huskhomes/command/SpongeCommand.java @@ -173,7 +173,7 @@ public enum Type { TPA_HERE_COMMAND((plugin) -> new TeleportRequestCommand(plugin, TeleportRequest.Type.TPA_HERE)), TPACCEPT_COMMAND((plugin) -> new TpRespondCommand(plugin, true)), TPDECLINE_COMMAND((plugin) -> new TpRespondCommand(plugin, false)), - RTP_COMMAND(RtpCommand::new), + RTP_COMMAND(RTPCommand::new), TP_IGNORE_COMMAND(TpIgnoreCommand::new), TP_OFFLINE_COMMAND(TpOfflineCommand::new), TP_ALL_COMMAND(TpAllCommand::new),