diff --git a/pom.xml b/pom.xml
index bc8a65db..5e2b5669 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
UTF-8
- 17
+ 21
diff --git a/src/main/java/com/github/jikoo/regionerator/Regionerator.java b/src/main/java/com/github/jikoo/regionerator/Regionerator.java
index c9f97a44..0f207137 100644
--- a/src/main/java/com/github/jikoo/regionerator/Regionerator.java
+++ b/src/main/java/com/github/jikoo/regionerator/Regionerator.java
@@ -163,7 +163,9 @@ public void reloadFeatures() {
// Enable world case correction listener.
getServer().getPluginManager().registerEvents(new WorldListener(this), this);
// Enable rescue tagging listener.
- getServer().getPluginManager().registerEvents(new RescueListener(this), this);
+ RescueListener rescueListener = new RescueListener(this);
+ getServer().getPluginManager().registerEvents(rescueListener, this);
+ rescueListener.registerPaperAsyncSpawnIfPresent();
// Always enable hook listener in case someone else adds hooks.
getServer().getPluginManager().registerEvents(new HookListener(this), this);
diff --git a/src/main/java/com/github/jikoo/regionerator/commands/RegioneratorExecutor.java b/src/main/java/com/github/jikoo/regionerator/commands/RegioneratorExecutor.java
index 1d044eec..ddd5643a 100644
--- a/src/main/java/com/github/jikoo/regionerator/commands/RegioneratorExecutor.java
+++ b/src/main/java/com/github/jikoo/regionerator/commands/RegioneratorExecutor.java
@@ -46,7 +46,7 @@ public RegioneratorExecutor(@NotNull Regionerator plugin,
@NotNull Map deletionRunnables) {
this.plugin = plugin;
this.deletionRunnables = deletionRunnables;
- flagHandler = new FlagHandler(plugin);
+ this.flagHandler = new FlagHandler(plugin);
}
@Override
diff --git a/src/main/java/com/github/jikoo/regionerator/listeners/RescueListener.java b/src/main/java/com/github/jikoo/regionerator/listeners/RescueListener.java
index 9f3821dc..5399a46b 100644
--- a/src/main/java/com/github/jikoo/regionerator/listeners/RescueListener.java
+++ b/src/main/java/com/github/jikoo/regionerator/listeners/RescueListener.java
@@ -11,6 +11,14 @@
package com.github.jikoo.regionerator.listeners;
import com.github.jikoo.regionerator.Regionerator;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -20,8 +28,10 @@
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
+import org.bukkit.event.EventException;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.persistence.PersistentDataContainer;
@@ -30,9 +40,6 @@
import org.jetbrains.annotations.NotNull;
import org.spigotmc.event.player.PlayerSpawnLocationEvent;
-import java.util.Collection;
-import java.util.UUID;
-
public class RescueListener implements Listener {
private final @NotNull Regionerator plugin;
@@ -43,6 +50,30 @@ public RescueListener(@NotNull Regionerator plugin) {
this.loggedInSinceFeature = new NamespacedKey(plugin, "safe-logout");
}
+ /**
+ * Paper fires {@code AsyncPlayerSpawnLocationEvent} instead of {@link PlayerSpawnLocationEvent}. We keep Spigot
+ * compatibility by registering a reflective handler only if the Paper event class is present at runtime.
+ */
+ public void registerPaperAsyncSpawnIfPresent() {
+ final Class> asyncEventClass;
+ try {
+ asyncEventClass = Class.forName("io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent");
+ } catch (ClassNotFoundException ignored) {
+ return; // Not running on Paper with this API.
+ }
+
+ Bukkit.getPluginManager().registerEvent(
+ asyncEventClass.asSubclass(Event.class),
+ this,
+ EventPriority.LOWEST,
+ (listener, event) -> handlePaperAsyncSpawn(event),
+ plugin,
+ true
+ );
+
+ plugin.getLogger().fine("Registered Paper AsyncPlayerSpawnLocationEvent handler (reflective).");
+ }
+
@EventHandler(priority = EventPriority.MONITOR) // Run late so we get final post-plugin-modification location
private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
Chunk chunk = event.getPlayer().getLocation().getChunk();
@@ -50,6 +81,7 @@ private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
chunk.getPersistentDataContainer().set(key, PersistentDataType.BYTE, (byte) 1);
}
+ @SuppressWarnings("deprecation") // Paper warns about this; Paper handler is registered reflectively when available.
@EventHandler(priority = EventPriority.LOWEST) // Run early so everyone overrides us
private void onPlayerSpawn(@NotNull PlayerSpawnLocationEvent event) {
Player player = event.getPlayer();
@@ -60,9 +92,58 @@ private void onPlayerSpawn(@NotNull PlayerSpawnLocationEvent event) {
return;
}
- Chunk chunk = event.getSpawnLocation().getChunk();
+ applySpawnRescue(player.getUniqueId(), Optional.of(player), event.getSpawnLocation(), event::setSpawnLocation);
+ }
+
+ private void handlePaperAsyncSpawn(@NotNull Event event) throws EventException {
+ // Runs on an async thread; we must compute the result on the main thread and block.
+ final Object connection;
+ final Object profile;
+ final UUID uuid;
+ final Location spawnLoc;
+ try {
+ connection = event.getClass().getMethod("getConnection").invoke(event);
+ profile = connection.getClass().getMethod("getProfile").invoke(connection);
+ uuid = (UUID) profile.getClass().getMethod("getId").invoke(profile);
+ spawnLoc = (Location) event.getClass().getMethod("getSpawnLocation").invoke(event);
+ } catch (ReflectiveOperationException ex) {
+ throw new EventException(ex);
+ }
+ if (uuid == null || spawnLoc == null) {
+ return;
+ }
+
+ try {
+ Location result = Bukkit.getScheduler().callSyncMethod(plugin, () ->
+ computeRescueSpawn(uuid, spawnLoc)
+ ).get(5, TimeUnit.SECONDS);
+ if (result != null) {
+ event.getClass().getMethod("setSpawnLocation", Location.class).invoke(event, result);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException | TimeoutException e) {
+ plugin.getLogger().log(Level.WARNING, "Failed to compute rescue spawn for async spawn event", e);
+ } catch (ReflectiveOperationException e) {
+ throw new EventException(e);
+ }
+ }
+
+ private @NotNull Location computeRescueSpawn(@NotNull UUID uuid, @NotNull Location spawnLoc) {
+ final Location[] out = new Location[] { spawnLoc };
+ applySpawnRescue(uuid, Optional.empty(), spawnLoc, loc -> out[0] = loc);
+ return out[0];
+ }
+
+ private void applySpawnRescue(
+ @NotNull UUID uuid,
+ @NotNull Optional player,
+ @NotNull Location spawnLoc,
+ @NotNull java.util.function.Consumer setSpawnLocation
+ ) {
+ Chunk chunk = spawnLoc.getChunk();
PersistentDataContainer chunkPdc = chunk.getPersistentDataContainer();
- NamespacedKey logoutKey = getLogoutKey(player.getUniqueId());
+ NamespacedKey logoutKey = getLogoutKey(uuid);
// If the key is set, the chunk has not been deleted since the player last logged out.
if (chunkPdc.has(logoutKey, PersistentDataType.BYTE)) {
chunkPdc.remove(logoutKey);
@@ -75,17 +156,17 @@ private void onPlayerSpawn(@NotNull PlayerSpawnLocationEvent event) {
}
// Only rescue safe players if configured to do so.
- if (!plugin.config().rescueIfSafe() && !isUnsafe(event.getSpawnLocation())) {
+ if (!plugin.config().rescueIfSafe() && !isUnsafe(spawnLoc)) {
return;
}
// If rescuing up, check if top block can be stood on safely.
if (plugin.config().rescueToTopBlock()) {
- World world = event.getSpawnLocation().getWorld();
+ World world = spawnLoc.getWorld();
if (world != null) {
- Block topBlock = world.getHighestBlockAt(event.getSpawnLocation());
+ Block topBlock = world.getHighestBlockAt(spawnLoc);
if (!isUnsafe(topBlock.getType()) && !isNotStandable(topBlock)) {
- event.setSpawnLocation(topBlock.getLocation().add(0.5, 1, 0.5));
+ setSpawnLocation.accept(topBlock.getLocation().add(0.5, 1, 0.5));
return;
}
}
@@ -93,23 +174,25 @@ private void onPlayerSpawn(@NotNull PlayerSpawnLocationEvent event) {
// If rescuing to personal respawn location, do so if available.
if (plugin.config().rescueToRespawn()) {
- Location spawnLoc = player.getBedSpawnLocation();
- if (spawnLoc != null) {
- event.setSpawnLocation(spawnLoc);
+ Location respawn = player.map(Player::getBedSpawnLocation)
+ .orElseGet(() -> Bukkit.getOfflinePlayer(uuid).getBedSpawnLocation());
+ if (respawn != null) {
+ setSpawnLocation.accept(respawn);
return;
}
}
// Otherwise, use respawn location of rescue world.
- World defaultWorld = event.getSpawnLocation().getWorld();
+ World defaultWorld = spawnLoc.getWorld();
if (defaultWorld == null) {
- // Prefer event world, but fall through to player world.
- // Theoretically player world may be default here, so we should avoid it.
- defaultWorld = player.getWorld();
+ defaultWorld = player.map(Player::getWorld).orElse(null);
+ }
+ if (defaultWorld == null) {
+ return;
}
World spawnWorld = plugin.config().getRescueWorld(defaultWorld);
- event.setSpawnLocation(spawnWorld.getSpawnLocation());
+ setSpawnLocation.accept(spawnWorld.getSpawnLocation());
}
private boolean isUnsafe(@NotNull Location location) {
diff --git a/src/main/java/com/github/jikoo/regionerator/world/DummyChunk.java b/src/main/java/com/github/jikoo/regionerator/world/DummyChunk.java
index af28e139..d7b7e002 100644
--- a/src/main/java/com/github/jikoo/regionerator/world/DummyChunk.java
+++ b/src/main/java/com/github/jikoo/regionerator/world/DummyChunk.java
@@ -86,7 +86,7 @@ public int getZ() {
@Override
public boolean isLoaded() {
- return this.world.isChunkLoaded(chunkZ, chunkZ);
+ return this.world.isChunkLoaded(this.chunkX, this.chunkZ);
}
@Override