From a509dea797da58e30b1344fba292c4f076403315 Mon Sep 17 00:00:00 2001 From: Maddy Miller Date: Tue, 4 Aug 2026 22:15:23 +1000 Subject: [PATCH] Add an async teleport API --- .../sk89q/worldedit/bukkit/BukkitEntity.java | 17 +++++++- .../sk89q/worldedit/bukkit/BukkitPlayer.java | 24 ++++++++++- .../coremc/internal/CoreMcPlayer.java | 42 +++++++++++++++++-- .../com/sk89q/worldedit/entity/Player.java | 20 +++++++++ .../platform/AbstractPlayerActor.java | 7 ++++ .../extension/platform/Locatable.java | 33 +++++++++++++++ .../extension/platform/PlayerProxy.java | 11 +++++ .../worldedit/extent/ChangeSetExtent.java | 7 ++++ 8 files changed, 155 insertions(+), 6 deletions(-) diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java index a4968810ff..7980e3cfb6 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java @@ -32,6 +32,7 @@ import io.papermc.lib.PaperLib; import java.lang.ref.WeakReference; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; @@ -78,7 +79,7 @@ public boolean setLocation(Location location) { org.bukkit.entity.Entity entity = entityRef.get(); if (entity != null) { if (WorldEditPlugin.getInstance().isFolia()) { - var _ = PaperLib.teleportAsync(entity, BukkitAdapter.adapt(location)); + var _ = setLocationAsync(location); return true; } else { return entity.teleport(BukkitAdapter.adapt(location)); @@ -88,6 +89,20 @@ public boolean setLocation(Location location) { } } + @Override + public CompletableFuture setLocationAsync(Location location) { + org.bukkit.entity.Entity entity = entityRef.get(); + if (entity != null) { + if (PaperLib.isPaper()) { + return entity.teleportAsync(BukkitAdapter.adapt(location)); + } else { + return CompletableFuture.completedFuture(entity.teleport(BukkitAdapter.adapt(location))); + } + } else { + return CompletableFuture.completedFuture(false); + } + } + @Override public BaseEntity getState() { org.bukkit.entity.Entity entity = entityRef.get(); diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java index bc366ebd4e..e3dc53af05 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java @@ -55,6 +55,7 @@ import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; public class BukkitPlayer extends AbstractPlayerActor { @@ -150,13 +151,23 @@ public boolean trySetPosition(Vector3 pos, float pitch, float yaw) { Location location = new Location(player.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch); if (WorldEditPlugin.getInstance().isFolia()) { - var _ = PaperLib.teleportAsync(player, location); + var _ = trySetPositionAsync(pos, pitch, yaw); return true; } else { return player.teleport(location); } } + @Override + public CompletableFuture trySetPositionAsync(Vector3 pos, float pitch, float yaw) { + Location location = new Location(player.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch); + if (PaperLib.isPaper()) { + return player.teleportAsync(location); + } else { + return CompletableFuture.completedFuture(player.teleport(location)); + } + } + @Override public String[] getGroups() { return plugin.getPermissionsResolver().getGroups(player); @@ -232,13 +243,22 @@ public com.sk89q.worldedit.util.Location getLocation() { @Override public boolean setLocation(com.sk89q.worldedit.util.Location location) { if (WorldEditPlugin.getInstance().isFolia()) { - var _ = PaperLib.teleportAsync(player, BukkitAdapter.adapt(location)); + var _ = setLocationAsync(location); return true; } else { return player.teleport(BukkitAdapter.adapt(location)); } } + @Override + public CompletableFuture setLocationAsync(com.sk89q.worldedit.util.Location location) { + if (PaperLib.isPaper()) { + return player.teleportAsync(BukkitAdapter.adapt(location)); + } else { + return CompletableFuture.completedFuture(player.teleport(BukkitAdapter.adapt(location))); + } + } + @SuppressWarnings("deprecation") // Paper's deprecation, we need to support Spigot still @Override public Locale getLocale() { diff --git a/worldedit-core-mc/src/main/java/com/sk89q/worldedit/coremc/internal/CoreMcPlayer.java b/worldedit-core-mc/src/main/java/com/sk89q/worldedit/coremc/internal/CoreMcPlayer.java index 12c2cfa658..9071b8d32d 100644 --- a/worldedit-core-mc/src/main/java/com/sk89q/worldedit/coremc/internal/CoreMcPlayer.java +++ b/worldedit-core-mc/src/main/java/com/sk89q/worldedit/coremc/internal/CoreMcPlayer.java @@ -43,8 +43,10 @@ import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; import net.minecraft.world.InteractionHand; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.block.entity.BlockEntityTypes; import org.enginehub.linbus.tree.LinCompoundTag; import org.enginehub.worldeditcui.protocol.CUIPacket; @@ -52,6 +54,7 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; /** @@ -108,15 +111,48 @@ public Location getLocation() { @Override public boolean setLocation(Location location) { ServerLevel level = platform.getAdapter().toNativeWorld((World) location.getExtent()); - this.player.teleportTo( + return this.player.teleportTo( level, location.getX(), location.getY(), location.getZ(), Set.of(), location.getYaw(), location.getPitch(), true ); - // This may be false if the teleport was cancelled by a mod - return this.player.level() == level; + } + + @Override + public CompletableFuture setLocationAsync(Location location) { + ServerLevel level = platform.getAdapter().toNativeWorld((World) location.getExtent()); + ChunkPos chunkPosition = new ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4); + CompletableFuture chunkLoaded = new CompletableFuture<>(); + + return level.getServer() + .submit(() -> { + var _ = level.getChunkSource().addTicketAndLoadWithRadius( + TicketType.PORTAL, + chunkPosition, + 0 + ).whenComplete((ignored, throwable) -> { + if (throwable == null) { + chunkLoaded.complete(null); + } else { + chunkLoaded.completeExceptionally(throwable); + } + }); + }) + .thenCompose(ignored -> chunkLoaded) + .thenCompose(ignored -> level.getServer().submit(() -> this.player.teleportTo( + level, + location.getX(), location.getY(), location.getZ(), + Set.of(), + location.getYaw(), location.getPitch(), + true + ))); + } + + @Override + public CompletableFuture trySetPositionAsync(Vector3 pos, float pitch, float yaw) { + return setLocationAsync(new Location(getWorld(), pos, yaw, pitch)); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java b/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java index da435da12b..1be5df86d2 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java @@ -36,6 +36,7 @@ import com.sk89q.worldedit.world.block.BlockStateHolder; import com.sk89q.worldedit.world.gamemode.GameMode; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; /** @@ -318,6 +319,25 @@ default boolean trySetPosition(Vector3 pos, float pitch, float yaw) { return true; } + /** + * Attempt to move the player, asynchronously when supported. + * + *

+ * This action may fail, due to other mods cancelling the move. + * If so, the returned future will complete with {@code false}. + * Platforms without asynchronous teleport support may perform the move + * synchronously and return an already-completed future. + *

+ * + * @param pos where to move them + * @param pitch the pitch (up/down) of the player's view in degrees + * @param yaw the yaw (left/right) of the player's view in degrees + * @return a future that completes with whether the move was able to occur + */ + default CompletableFuture trySetPositionAsync(Vector3 pos, float pitch, float yaw) { + return CompletableFuture.completedFuture(trySetPosition(pos, pitch, yaw)); + } + /** * Sends a fake block to the client. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlayerActor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlayerActor.java index ef4ba0ae02..c8d220364f 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlayerActor.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlayerActor.java @@ -49,6 +49,7 @@ import com.sk89q.worldedit.world.item.ItemTypes; import java.io.File; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; /** @@ -497,6 +498,12 @@ public boolean trySetPosition(Vector3 pos) { return trySetPosition(pos, location.getPitch(), location.getYaw()); } + @Override + public CompletableFuture trySetPositionAsync(Vector3 pos) { + final Location location = getLocation(); + return trySetPositionAsync(pos, location.getPitch(), location.getYaw()); + } + @Override public File openFileOpenDialog(String[] extensions) { printError(TranslatableComponent.of("worldedit.platform.no-file-dialog")); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Locatable.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Locatable.java index 2acb333020..c22a1ae9bb 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Locatable.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Locatable.java @@ -23,6 +23,8 @@ import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.util.Location; +import java.util.concurrent.CompletableFuture; + public interface Locatable { /** @@ -50,6 +52,22 @@ default Location getBlockLocation() { */ boolean setLocation(Location location); + /** + * Sets the location of this actor, asynchronously when supported. + * + *

+ * The returned future completes with whether the teleport succeeded. + * Platforms without asynchronous teleport support may perform the + * teleport synchronously and return an already-completed future. + *

+ * + * @param location the new location of the actor + * @return a future that completes with whether the teleport succeeded + */ + default CompletableFuture setLocationAsync(Location location) { + return CompletableFuture.completedFuture(setLocation(location)); + } + /** * Sets the position of this actor. * @@ -77,6 +95,21 @@ default boolean trySetPosition(Vector3 pos) { return setLocation(new Location(getExtent(), pos)); } + /** + * Attempts to set the position of this actor, asynchronously when supported. + * + *

+ * This action may fail, due to other mods cancelling the move. + * If so, the returned future will complete with {@code false}. + *

+ * + * @param pos the position to set + * @return a future that completes with whether the position was able to be set + */ + default CompletableFuture trySetPositionAsync(Vector3 pos) { + return setLocationAsync(new Location(getExtent(), pos)); + } + /** * Get the extent that this actor is in. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlayerProxy.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlayerProxy.java index 7811d2fd79..33cf68178b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlayerProxy.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlayerProxy.java @@ -37,6 +37,7 @@ import java.util.Locale; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; @@ -104,11 +105,21 @@ public boolean setLocation(Location location) { return basePlayer.setLocation(location); } + @Override + public CompletableFuture setLocationAsync(Location location) { + return basePlayer.setLocationAsync(location); + } + @Override public boolean trySetPosition(Vector3 pos, float pitch, float yaw) { return basePlayer.trySetPosition(pos, pitch, yaw); } + @Override + public CompletableFuture trySetPositionAsync(Vector3 pos, float pitch, float yaw) { + return basePlayer.trySetPositionAsync(pos, pitch, yaw); + } + @Override public World getWorld() { return world; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/ChangeSetExtent.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/ChangeSetExtent.java index 53f5cd706e..a5e56cce27 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/ChangeSetExtent.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/ChangeSetExtent.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; @@ -163,6 +164,12 @@ public boolean setLocation(Location location) { return entity.setLocation(location); } + @Override + public CompletableFuture setLocationAsync(Location location) { + // TODO Add a changeset for this. + return entity.setLocationAsync(location); + } + @Override public Extent getExtent() { return entity.getExtent();