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