From 9e873f50d3b1d7f81596b1ec01fa9248276e77c2 Mon Sep 17 00:00:00 2001
From: Lulu13022002 <41980282+Lulu13022002@users.noreply.github.com>
Date: Tue, 29 Apr 2025 14:57:36 +0200
Subject: [PATCH] Fix inconsistencies between offline/online spawn position
 getter (#11960)

---
 .../main/java/org/bukkit/OfflinePlayer.java   | 23 ++++++++++++++----
 .../java/org/bukkit/entity/HumanEntity.java   |  6 +++--
 .../main/java/org/bukkit/entity/Player.java   | 14 +++++++++++
 .../craftbukkit/CraftOfflinePlayer.java       | 24 ++++++++++---------
 .../craftbukkit/entity/CraftPlayer.java       | 19 ++++++++-------
 5 files changed, 60 insertions(+), 26 deletions(-)

diff --git a/paper-api/src/main/java/org/bukkit/OfflinePlayer.java b/paper-api/src/main/java/org/bukkit/OfflinePlayer.java
index c749a898e9..6f83097b10 100644
--- a/paper-api/src/main/java/org/bukkit/OfflinePlayer.java
+++ b/paper-api/src/main/java/org/bukkit/OfflinePlayer.java
@@ -287,13 +287,28 @@ public interface OfflinePlayer extends ServerOperator, AnimalTamer, Configuratio
     // Paper end
 
     /**
-     * Gets the Location where the player will spawn at, null if they
+     * Gets the Location where the player will spawn at, {@code null} if they
+     * don't have a valid respawn point.
+     * <br>
+     * Unlike online players, the location if found will not be loaded by default.
+     *
+     * @return respawn location if exists, otherwise {@code null}.
+     * @see #getRespawnLocation(boolean) for more fine-grained control over chunk loading and validation behaviour.
+     */
+    default @Nullable Location getRespawnLocation() {
+        return this.getRespawnLocation(false); // keep old behavior for offline players
+    }
+
+    /**
+     * Gets the Location where the player will spawn at, {@code null} if they
      * don't have a valid respawn point.
      *
-     * @return respawn location if exists, otherwise null.
+     * @param loadLocationAndValidate load the expected respawn location to retrieve the exact position of the spawn
+     *                                block and check if this position is still valid or not. Loading the location
+     *                                will induce a sync chunk load and must hence be used with caution.
+     * @return respawn location if exists, otherwise {@code null}.
      */
-    @Nullable
-    public Location getRespawnLocation();
+    @Nullable Location getRespawnLocation(boolean loadLocationAndValidate);
 
     /**
      * Increments the given statistic for this player.
diff --git a/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java b/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java
index a4484ad3cb..e8d7c31576 100644
--- a/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java
+++ b/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java
@@ -485,9 +485,11 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder
      * to validate if the current respawn location is still valid.
      *
      * @return respawn location if exists, otherwise null.
+     * @deprecated this method doesn't take the respawn angle into account, use
+     * {@link Player#getRespawnLocation(boolean)} with loadLocationAndValidate = false instead
      */
-    @Nullable
-    Location getPotentialRespawnLocation();
+    @Deprecated(since = "1.21.5")
+    @Nullable Location getPotentialRespawnLocation();
 
     /**
      * @return the player's fishing hook if they are fishing
diff --git a/paper-api/src/main/java/org/bukkit/entity/Player.java b/paper-api/src/main/java/org/bukkit/entity/Player.java
index 9ccb7e9016..ac341838b6 100644
--- a/paper-api/src/main/java/org/bukkit/entity/Player.java
+++ b/paper-api/src/main/java/org/bukkit/entity/Player.java
@@ -545,6 +545,20 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
      */
     public boolean isSleepingIgnored();
 
+    /**
+     * Gets the Location where the player will spawn at, {@code null} if they
+     * don't have a valid respawn point.
+     * <br>
+     * Unlike offline players, the location if found will be loaded to validate by default.
+     *
+     * @return respawn location if exists, otherwise {@code null}.
+     * @see #getRespawnLocation(boolean) for more fine-grained control over chunk loading and validation behaviour.
+     */
+    @Override
+    default @Nullable Location getRespawnLocation() {
+        return this.getRespawnLocation(true);
+    }
+
     /**
      * Sets the Location where the player will spawn at their bed.
      *
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
index 79e226da35..43ae147ae1 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
@@ -6,7 +6,6 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.Date;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import net.minecraft.core.GlobalPos;
@@ -33,8 +32,6 @@ import org.bukkit.configuration.serialization.SerializableAs;
 import org.bukkit.craftbukkit.util.CraftLocation;
 import org.bukkit.entity.EntityType;
 import org.bukkit.entity.Player;
-import org.bukkit.metadata.MetadataValue;
-import org.bukkit.plugin.Plugin;
 
 @SerializableAs("Player")
 public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializable {
@@ -362,18 +359,23 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa
     }
 
     @Override
-    public Location getRespawnLocation() {
-        CompoundTag data = this.getData();
+    public Location getRespawnLocation(final boolean loadLocationAndValidate) {
+        final CompoundTag data = this.getData();
         if (data == null) return null;
 
         final ServerPlayer.RespawnConfig respawnConfig = data.read("respawn", ServerPlayer.RespawnConfig.CODEC).orElse(null);
-        if (respawnConfig != null) {
-            final ServerLevel level = this.server.console.getLevel(respawnConfig.dimension());
-            if (level != null) {
-                return CraftLocation.toBukkit(respawnConfig.pos(), level.getWorld(), respawnConfig.angle(), 0);
-            }
+        if (respawnConfig == null) return null;
+
+        final ServerLevel level = this.server.console.getLevel(respawnConfig.dimension());
+        if (level == null) return null;
+
+        if (!loadLocationAndValidate) {
+            return CraftLocation.toBukkit(respawnConfig.pos(), level.getWorld(), respawnConfig.angle(), 0);
         }
-        return null;
+
+        return ServerPlayer.findRespawnAndUseSpawnBlock(level, respawnConfig, false)
+            .map(resolvedPos -> CraftLocation.toBukkit(resolvedPos.position(), level.getWorld(), resolvedPos.yaw(), 0))
+            .orElse(null);
     }
 
     private ServerStatsCounter getStatisticManager() {
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index df6612a86f..0d319b7c39 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -1541,19 +1541,20 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
     }
 
     @Override
-    public Location getRespawnLocation() {
+    public Location getRespawnLocation(final boolean loadLocationAndValidate) {
         final ServerPlayer.RespawnConfig respawnConfig = this.getHandle().getRespawnConfig();
         if (respawnConfig == null) return null;
 
-        ServerLevel world = this.getHandle().server.getLevel(respawnConfig.dimension());
-        if (world != null) {
-            Optional<ServerPlayer.RespawnPosAngle> spawnLoc = ServerPlayer.findRespawnAndUseSpawnBlock(world, respawnConfig, true);
-            if (spawnLoc.isPresent()) {
-                ServerPlayer.RespawnPosAngle vec = spawnLoc.get();
-                return CraftLocation.toBukkit(vec.position(), world.getWorld(), vec.yaw(), 0);
-            }
+        final ServerLevel world = this.getHandle().server.getLevel(respawnConfig.dimension());
+        if (world == null) return null;
+
+        if (!loadLocationAndValidate) {
+            return CraftLocation.toBukkit(respawnConfig.pos(), world.getWorld(), respawnConfig.angle(), 0);
         }
-        return null;
+
+        return ServerPlayer.findRespawnAndUseSpawnBlock(world, respawnConfig, false)
+            .map(pos -> CraftLocation.toBukkit(pos.position(), world.getWorld(), pos.yaw(), 0))
+            .orElse(null);
     }
 
     @Override