Skip to content
Closed
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.release>21</maven.compiler.release>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public RegioneratorExecutor(@NotNull Regionerator plugin,
@NotNull Map<String, DeletionRunnable> deletionRunnables) {
this.plugin = plugin;
this.deletionRunnables = deletionRunnables;
flagHandler = new FlagHandler(plugin);
this.flagHandler = new FlagHandler(plugin);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -43,13 +50,38 @@ 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();
NamespacedKey key = getLogoutKey(event.getPlayer().getUniqueId());
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();
Expand All @@ -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> player,
@NotNull Location spawnLoc,
@NotNull java.util.function.Consumer<Location> 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.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't delete existing comments.

if (chunkPdc.has(logoutKey, PersistentDataType.BYTE)) {
chunkPdc.remove(logoutKey);
Expand All @@ -75,41 +156,43 @@ 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;
}
}
}

// 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down