--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -4,10 +4,14 @@
 import com.mojang.authlib.GameProfile;
 import com.mojang.datafixers.util.Either;
 import com.mojang.logging.LogUtils;
+import com.mojang.serialization.DataResult;
 import com.mojang.serialization.Dynamic;
 import java.net.InetSocketAddress;
+import java.net.SocketAddress;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.Set;
@@ -36,6 +40,7 @@
 import net.minecraft.network.chat.CommonComponents;
 import net.minecraft.network.chat.Component;
 import net.minecraft.network.chat.HoverEvent;
+import net.minecraft.network.chat.MutableComponent;
 import net.minecraft.network.chat.OutgoingChatMessage;
 import net.minecraft.network.chat.RemoteChatSession;
 import net.minecraft.network.protocol.Packet;
@@ -89,10 +94,8 @@
 import net.minecraft.util.Mth;
 import net.minecraft.util.RandomSource;
 import net.minecraft.util.Unit;
-import net.minecraft.world.Container;
-import net.minecraft.world.InteractionHand;
-import net.minecraft.world.MenuProvider;
 import net.minecraft.world.damagesource.DamageSource;
+import net.minecraft.world.damagesource.DamageSources;
 import net.minecraft.world.effect.MobEffectInstance;
 import net.minecraft.world.effect.MobEffects;
 import net.minecraft.world.entity.Entity;
@@ -116,14 +119,15 @@
 import net.minecraft.world.entity.monster.warden.WardenSpawnTracker;
 import net.minecraft.world.entity.player.ChatVisiblity;
 import net.minecraft.world.entity.player.Inventory;
-import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.entity.projectile.AbstractArrow;
 import net.minecraft.world.entity.vehicle.AbstractMinecart;
 import net.minecraft.world.entity.vehicle.Boat;
+import net.minecraft.world.food.FoodData;
 import net.minecraft.world.inventory.AbstractContainerMenu;
 import net.minecraft.world.inventory.ContainerListener;
 import net.minecraft.world.inventory.ContainerSynchronizer;
 import net.minecraft.world.inventory.HorseInventoryMenu;
+import net.minecraft.world.inventory.InventoryMenu;
 import net.minecraft.world.inventory.ResultSlot;
 import net.minecraft.world.inventory.Slot;
 import net.minecraft.world.item.ComplexItem;
@@ -134,8 +138,6 @@
 import net.minecraft.world.item.ServerItemCooldowns;
 import net.minecraft.world.item.WrittenBookItem;
 import net.minecraft.world.item.crafting.RecipeHolder;
-import net.minecraft.world.item.enchantment.EnchantmentHelper;
-import net.minecraft.world.item.trading.MerchantOffers;
 import net.minecraft.world.level.ChunkPos;
 import net.minecraft.world.level.GameRules;
 import net.minecraft.world.level.GameType;
@@ -143,12 +145,14 @@
 import net.minecraft.world.level.biome.BiomeManager;
 import net.minecraft.world.level.block.BedBlock;
 import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.ChestBlock;
 import net.minecraft.world.level.block.HorizontalDirectionalBlock;
 import net.minecraft.world.level.block.RespawnAnchorBlock;
 import net.minecraft.world.level.block.entity.BlockEntity;
 import net.minecraft.world.level.block.entity.CommandBlockEntity;
 import net.minecraft.world.level.block.entity.SignBlockEntity;
 import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.dimension.LevelStem;
 import net.minecraft.world.level.gameevent.GameEvent;
 import net.minecraft.world.level.portal.DimensionTransition;
 import net.minecraft.world.level.storage.LevelData;
@@ -157,22 +161,55 @@
 import net.minecraft.world.scores.PlayerTeam;
 import net.minecraft.world.scores.ScoreAccess;
 import net.minecraft.world.scores.ScoreHolder;
+import org.slf4j.Logger;
+import net.minecraft.world.Container;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.MenuProvider;
+// CraftBukkit start
+import net.minecraft.world.damagesource.CombatTracker;
+import net.minecraft.world.item.enchantment.EnchantmentEffectComponents;
+import net.minecraft.world.item.enchantment.EnchantmentHelper;
+import net.minecraft.world.item.trading.MerchantOffers;
+import net.minecraft.world.scores.Scoreboard;
 import net.minecraft.world.scores.Team;
 import net.minecraft.world.scores.criteria.ObjectiveCriteria;
-import org.slf4j.Logger;
+import io.papermc.paper.adventure.PaperAdventure; // Paper
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.WeatherType;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.CraftWorldBorder;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.event.CraftEventFactory;
+import org.bukkit.craftbukkit.event.CraftPortalEvent;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.CraftDimensionUtil;
+import org.bukkit.craftbukkit.util.CraftLocation;
+import org.bukkit.entity.Player;
+import org.bukkit.event.entity.EntityExhaustionEvent;
+import org.bukkit.event.player.PlayerBedLeaveEvent;
+import org.bukkit.event.player.PlayerChangedMainHandEvent;
+import org.bukkit.event.player.PlayerChangedWorldEvent;
+import org.bukkit.event.player.PlayerLocaleChangeEvent;
+import org.bukkit.event.player.PlayerPortalEvent;
+import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.event.player.PlayerSpawnChangeEvent;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
+import org.bukkit.event.player.PlayerToggleSneakEvent;
+import org.bukkit.inventory.MainHand;
+// CraftBukkit end
+
+public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
 
-public class ServerPlayer extends Player {
     private static final Logger LOGGER = LogUtils.getLogger();
+    public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving
     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
     private static final int FLY_STAT_RECORDING_SPEED = 25;
-    public static final double INTERACTION_DISTANCE_VERIFICATION_BUFFER = 1.0;
-    private static final AttributeModifier CREATIVE_BLOCK_INTERACTION_RANGE_MODIFIER = new AttributeModifier(
-        ResourceLocation.withDefaultNamespace("creative_mode_block_range"), 0.5, AttributeModifier.Operation.ADD_VALUE
-    );
-    private static final AttributeModifier CREATIVE_ENTITY_INTERACTION_RANGE_MODIFIER = new AttributeModifier(
-        ResourceLocation.withDefaultNamespace("creative_mode_entity_range"), 2.0, AttributeModifier.Operation.ADD_VALUE
-    );
+    public static final double INTERACTION_DISTANCE_VERIFICATION_BUFFER = 1.0D;
+    private static final AttributeModifier CREATIVE_BLOCK_INTERACTION_RANGE_MODIFIER = new AttributeModifier(ResourceLocation.withDefaultNamespace("creative_mode_block_range"), 0.5D, AttributeModifier.Operation.ADD_VALUE);
+    private static final AttributeModifier CREATIVE_ENTITY_INTERACTION_RANGE_MODIFIER = new AttributeModifier(ResourceLocation.withDefaultNamespace("creative_mode_entity_range"), 2.0D, AttributeModifier.Operation.ADD_VALUE);
     public ServerGamePacketListenerImpl connection;
     public final MinecraftServer server;
     public final ServerPlayerGameMode gameMode;
@@ -189,29 +226,30 @@
     private boolean lastFoodSaturationZero = true;
     public int lastSentExp = -99999999;
     public int spawnInvulnerableTime = 60;
-    private ChatVisiblity chatVisibility = ChatVisiblity.FULL;
-    private boolean canChatColor = true;
-    private long lastActionTime = Util.getMillis();
+    private ChatVisiblity chatVisibility;
+    private boolean canChatColor;
+    private long lastActionTime;
     @Nullable
     private Entity camera;
     public boolean isChangingDimension;
     public boolean seenCredits;
-    private final ServerRecipeBook recipeBook = new ServerRecipeBook();
+    private final ServerRecipeBook recipeBook;
     @Nullable
     private Vec3 levitationStartPos;
     private int levitationStartTime;
     private boolean disconnected;
-    private int requestedViewDistance = 2;
-    public String language = "en_us";
+    private int requestedViewDistance;
+    public String language = null; // CraftBukkit - default  // Paper - default to null
+    public java.util.Locale adventure$locale = java.util.Locale.US; // Paper
     @Nullable
     private Vec3 startingToFallPosition;
     @Nullable
     private Vec3 enteredNetherPosition;
     @Nullable
     private Vec3 enteredLavaOnVehiclePosition;
-    private SectionPos lastSectionPos = SectionPos.of(0, 0, 0);
-    private ChunkTrackingView chunkTrackingView = ChunkTrackingView.EMPTY;
-    private ResourceKey<Level> respawnDimension = Level.OVERWORLD;
+    private SectionPos lastSectionPos;
+    private ChunkTrackingView chunkTrackingView;
+    private ResourceKey<Level> respawnDimension;
     @Nullable
     private BlockPos respawnPosition;
     private boolean respawnForced;
@@ -220,80 +258,240 @@
     private boolean textFilteringEnabled;
     private boolean allowsListing;
     private boolean spawnExtraParticlesOnFall;
-    public WardenSpawnTracker wardenSpawnTracker = new WardenSpawnTracker(0, 0, 0);
+    public WardenSpawnTracker wardenSpawnTracker;
     @Nullable
     private BlockPos raidOmenPosition;
-    private Vec3 lastKnownClientMovement = Vec3.ZERO;
-    public final ContainerSynchronizer containerSynchronizer = new ContainerSynchronizer() {
-        @Override
-        public void sendInitialData(AbstractContainerMenu handler, NonNullList<ItemStack> stacks, ItemStack cursorStack, int[] properties) {
-            ServerPlayer.this.connection.send(new ClientboundContainerSetContentPacket(handler.containerId, handler.incrementStateId(), stacks, cursorStack));
-
-            for (int i = 0; i < properties.length; i++) {
-                this.broadcastDataValue(handler, i, properties[i]);
-            }
-        }
-
-        @Override
-        public void sendSlotChange(AbstractContainerMenu handler, int slot, ItemStack stack) {
-            ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(handler.containerId, handler.incrementStateId(), slot, stack));
-        }
-
-        @Override
-        public void sendCarriedChange(AbstractContainerMenu handler, ItemStack stack) {
-            ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(-1, handler.incrementStateId(), -1, stack));
-        }
-
-        @Override
-        public void sendDataChange(AbstractContainerMenu handler, int property, int value) {
-            this.broadcastDataValue(handler, property, value);
-        }
-
-        private void broadcastDataValue(AbstractContainerMenu handler, int property, int value) {
-            ServerPlayer.this.connection.send(new ClientboundContainerSetDataPacket(handler.containerId, property, value));
-        }
-    };
-    private final ContainerListener containerListener = new ContainerListener() {
-        @Override
-        public void slotChanged(AbstractContainerMenu handler, int slotId, ItemStack stack) {
-            Slot slot = handler.getSlot(slotId);
-            if (!(slot instanceof ResultSlot)) {
-                if (slot.container == ServerPlayer.this.getInventory()) {
-                    CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
-                }
-            }
-        }
-
-        @Override
-        public void dataChanged(AbstractContainerMenu handler, int property, int value) {
-        }
-    };
+    private Vec3 lastKnownClientMovement;
+    public final ContainerSynchronizer containerSynchronizer;
+    private final ContainerListener containerListener;
     @Nullable
     private RemoteChatSession chatSession;
     @Nullable
     public final Object object;
     private int containerCounter;
     public boolean wonGame;
+    private int containerUpdateDelay; // Paper - Configurable container update tick rate
+    public long loginTime; // Paper - Replace OfflinePlayer#getLastPlayed
+    public int patrolSpawnDelay; // Paper - Pillager patrol spawn settings and per player options
+    // Paper start - cancellable death event
+    public boolean queueHealthUpdatePacket;
+    public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
+    // Paper end - cancellable death event
+    // Paper start - Optional per player mob spawns
+    public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length;
+    public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper
+    // Paper end - Optional per player mob spawns
+    public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff
+
+    // CraftBukkit start
+    public CraftPlayer.TransferCookieConnection transferCookieConnection;
+    public String displayName;
+    public net.kyori.adventure.text.Component adventure$displayName; // Paper
+    public Component listName;
+    public org.bukkit.Location compassTarget;
+    public int newExp = 0;
+    public int newLevel = 0;
+    public int newTotalExp = 0;
+    public boolean keepLevel = false;
+    public double maxHealthCache;
+    public boolean joining = true;
+    public boolean sentListPacket = false;
+    public boolean supressTrackerForLogin = false; // Paper - Fire PlayerJoinEvent when Player is actually ready
+    // CraftBukkit end
+    public boolean isRealPlayer; // Paper
+    public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
+    public @Nullable String clientBrandName = null; // Paper - Brand support
+    public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
+
+    // Paper start - rewrite chunk system
+    private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
+    private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
+
+    @Override
+    public final boolean moonrise$isRealPlayer() {
+        return this.isRealPlayer;
+    }
+
+    @Override
+    public final void moonrise$setRealPlayer(final boolean real) {
+        this.isRealPlayer = real;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader() {
+        return this.chunkLoader;
+    }
+
+    @Override
+    public final void moonrise$setChunkLoader(final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader) {
+        this.chunkLoader = loader;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
+        return this.viewDistanceHolder;
+    }
+    // Paper end - rewrite chunk system
 
     public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
         super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
+        this.chatVisibility = ChatVisiblity.FULL;
+        this.canChatColor = true;
+        this.lastActionTime = Util.getMillis();
+        this.recipeBook = new ServerRecipeBook();
+        this.requestedViewDistance = 2;
+        this.language =  null; // Paper - default to null
+        this.lastSectionPos = SectionPos.of(0, 0, 0);
+        this.chunkTrackingView = ChunkTrackingView.EMPTY;
+        this.respawnDimension = Level.OVERWORLD;
+        this.wardenSpawnTracker = new WardenSpawnTracker(0, 0, 0);
+        this.lastKnownClientMovement = Vec3.ZERO;
+        this.containerSynchronizer = new ContainerSynchronizer() {
+            @Override
+            public void sendInitialData(AbstractContainerMenu handler, NonNullList<ItemStack> stacks, ItemStack cursorStack, int[] properties) {
+                ServerPlayer.this.connection.send(new ClientboundContainerSetContentPacket(handler.containerId, handler.incrementStateId(), stacks, cursorStack));
+
+                for (int i = 0; i < properties.length; ++i) {
+                    this.broadcastDataValue(handler, i, properties[i]);
+                }
+
+            }
+
+            // Paper start - Sync offhand slot in menus
+            @Override
+            public void sendOffHandSlotChange() {
+                ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(ServerPlayer.this.inventoryMenu.containerId, ServerPlayer.this.inventoryMenu.incrementStateId(), net.minecraft.world.inventory.InventoryMenu.SHIELD_SLOT, ServerPlayer.this.inventoryMenu.getSlot(net.minecraft.world.inventory.InventoryMenu.SHIELD_SLOT).getItem().copy()));
+            }
+            // Paper end - Sync offhand slot in menus
+
+            @Override
+            public void sendSlotChange(AbstractContainerMenu handler, int slot, ItemStack stack) {
+                ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(handler.containerId, handler.incrementStateId(), slot, stack));
+            }
+
+            @Override
+            public void sendCarriedChange(AbstractContainerMenu handler, ItemStack stack) {
+                ServerPlayer.this.connection.send(new ClientboundContainerSetSlotPacket(-1, handler.incrementStateId(), -1, stack));
+            }
+
+            @Override
+            public void sendDataChange(AbstractContainerMenu handler, int property, int value) {
+                this.broadcastDataValue(handler, property, value);
+            }
+
+            private void broadcastDataValue(AbstractContainerMenu handler, int property, int value) {
+                ServerPlayer.this.connection.send(new ClientboundContainerSetDataPacket(handler.containerId, property, value));
+            }
+        };
+        this.containerListener = new ContainerListener() {
+            @Override
+            public void slotChanged(AbstractContainerMenu handler, int slotId, ItemStack stack) {
+                Slot slot = handler.getSlot(slotId);
+
+                if (!(slot instanceof ResultSlot)) {
+                    if (slot.container == ServerPlayer.this.getInventory()) {
+                        CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
+                    }
+
+                }
+            }
+            // Paper start - Add PlayerInventorySlotChangeEvent
+            @Override
+            public void slotChanged(AbstractContainerMenu handler, int slotId, ItemStack oldStack, ItemStack stack) {
+                Slot slot = handler.getSlot(slotId);
+                if (!(slot instanceof ResultSlot)) {
+                    if (slot.container == ServerPlayer.this.getInventory()) {
+                        if (io.papermc.paper.event.player.PlayerInventorySlotChangeEvent.getHandlerList().getRegisteredListeners().length == 0) {
+                            CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
+                            return;
+                        }
+                        io.papermc.paper.event.player.PlayerInventorySlotChangeEvent event = new io.papermc.paper.event.player.PlayerInventorySlotChangeEvent(ServerPlayer.this.getBukkitEntity(), slotId, CraftItemStack.asBukkitCopy(oldStack), CraftItemStack.asBukkitCopy(stack));
+                        event.callEvent();
+                        if (event.shouldTriggerAdvancements()) {
+                            CriteriaTriggers.INVENTORY_CHANGED.trigger(ServerPlayer.this, ServerPlayer.this.getInventory(), stack);
+                        }
+                    }
+                }
+            }
+            // Paper end - Add PlayerInventorySlotChangeEvent
+
+            @Override
+            public void dataChanged(AbstractContainerMenu handler, int property, int value) {}
+        };
         this.textFilter = server.createTextFilterForPlayer(this);
         this.gameMode = server.createGameModeForPlayer(this);
         this.server = server;
         this.stats = server.getPlayerList().getPlayerStats(this);
         this.advancements = server.getPlayerList().getPlayerAdvancements(this);
-        this.moveTo(this.adjustSpawnLocation(world, world.getSharedSpawnPos()).getBottomCenter(), 0.0F, 0.0F);
-        this.updateOptions(clientOptions);
+        // this.moveTo(this.adjustSpawnLocation(world, world.getSharedSpawnPos()).getBottomCenter(), 0.0F, 0.0F); // Paper - Don't move existing players to world spawn
+        this.updateOptionsNoEvents(clientOptions); // Paper - don't call options events on login
         this.object = null;
+
+        // CraftBukkit start
+        this.displayName = this.getScoreboardName();
+        this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper
+        this.bukkitPickUpLoot = true;
+        this.maxHealthCache = this.getMaxHealth();
+    }
+
+    // Use method to resend items in hands in case of client desync, because the item use got cancelled.
+    // For example, when cancelling the leash event
+    @Deprecated // Paper - this shouldn't be used, use the regular sendAllDataToRemote call to resync all
+    public void resendItemInHands() {
+        this.containerMenu.findSlot(this.getInventory(), this.getInventory().selected).ifPresent(s -> {
+            this.containerSynchronizer.sendSlotChange(this.containerMenu, s, this.getMainHandItem());
+        });
+        this.containerSynchronizer.sendSlotChange(this.inventoryMenu, InventoryMenu.SHIELD_SLOT, this.getOffhandItem());
     }
 
+    // Yes, this doesn't match Vanilla, but it's the best we can do for now.
+    // If this is an issue, PRs are welcome
+    public final BlockPos getSpawnPoint(ServerLevel worldserver) {
+        BlockPos blockposition = worldserver.getSharedSpawnPos();
+
+        if (worldserver.dimensionType().hasSkyLight() && worldserver.serverLevelData.getGameType() != GameType.ADVENTURE) {
+            int i = Math.max(0, this.server.getSpawnRadius(worldserver));
+            int j = Mth.floor(worldserver.getWorldBorder().getDistanceToBorder((double) blockposition.getX(), (double) blockposition.getZ()));
+
+            if (j < i) {
+                i = j;
+            }
+
+            if (j <= 1) {
+                i = 1;
+            }
+
+            long k = (long) (i * 2 + 1);
+            long l = k * k;
+            int i1 = l > 2147483647L ? Integer.MAX_VALUE : (int) l;
+            int j1 = this.getCoprime(i1);
+            int k1 = RandomSource.create().nextInt(i1);
+
+            for (int l1 = 0; l1 < i1; ++l1) {
+                int i2 = (k1 + j1 * l1) % i1;
+                int j2 = i2 % (i * 2 + 1);
+                int k2 = i2 / (i * 2 + 1);
+                BlockPos blockposition1 = PlayerRespawnLogic.getOverworldRespawnPos(worldserver, blockposition.getX() + j2 - i, blockposition.getZ() + k2 - i);
+
+                if (blockposition1 != null) {
+                    return blockposition1;
+                }
+            }
+        }
+
+        return blockposition;
+    }
+    // CraftBukkit end
+
     @Override
     public BlockPos adjustSpawnLocation(ServerLevel world, BlockPos basePos) {
-        AABB aABB = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO);
-        BlockPos blockPos = basePos;
-        if (world.dimensionType().hasSkyLight() && world.getServer().getWorldData().getGameType() != GameType.ADVENTURE) {
+        AABB axisalignedbb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO);
+        BlockPos blockposition1 = basePos;
+
+        if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit
             int i = Math.max(0, this.server.getSpawnRadius(world));
-            int j = Mth.floor(world.getWorldBorder().getDistanceToBorder((double)basePos.getX(), (double)basePos.getZ()));
+            int j = Mth.floor(world.getWorldBorder().getDistanceToBorder((double) basePos.getX(), (double) basePos.getZ()));
+
             if (j < i) {
                 i = j;
             }
@@ -302,34 +500,35 @@
                 i = 1;
             }
 
-            long l = (long)(i * 2 + 1);
-            long m = l * l;
-            int k = m > 2147483647L ? Integer.MAX_VALUE : (int)m;
-            int n = this.getCoprime(k);
-            int o = RandomSource.create().nextInt(k);
+            long k = (long) (i * 2 + 1);
+            long l = k * k;
+            int i1 = l > 2147483647L ? Integer.MAX_VALUE : (int) l;
+            int j1 = this.getCoprime(i1);
+            int k1 = RandomSource.create().nextInt(i1);
 
-            for (int p = 0; p < k; p++) {
-                int q = (o + n * p) % k;
-                int r = q % (i * 2 + 1);
-                int s = q / (i * 2 + 1);
-                blockPos = PlayerRespawnLogic.getOverworldRespawnPos(world, basePos.getX() + r - i, basePos.getZ() + s - i);
-                if (blockPos != null && world.noCollision(this, aABB.move(blockPos.getBottomCenter()))) {
-                    return blockPos;
+            for (int l1 = 0; l1 < i1; ++l1) {
+                int i2 = (k1 + j1 * l1) % i1;
+                int j2 = i2 % (i * 2 + 1);
+                int k2 = i2 / (i * 2 + 1);
+
+                blockposition1 = PlayerRespawnLogic.getOverworldRespawnPos(world, basePos.getX() + j2 - i, basePos.getZ() + k2 - i);
+                if (blockposition1 != null && world.noCollision(this, axisalignedbb.move(blockposition1.getBottomCenter()))) {
+                    return blockposition1;
                 }
             }
 
-            blockPos = basePos;
+            blockposition1 = basePos;
         }
 
-        while (!world.noCollision(this, aABB.move(blockPos.getBottomCenter())) && blockPos.getY() < world.getMaxBuildHeight() - 1) {
-            blockPos = blockPos.above();
+        while (!world.noCollision(this, axisalignedbb.move(blockposition1.getBottomCenter())) && blockposition1.getY() < world.getMaxBuildHeight() - 1) {
+            blockposition1 = blockposition1.above();
         }
 
-        while (world.noCollision(this, aABB.move(blockPos.below().getBottomCenter())) && blockPos.getY() > world.getMinBuildHeight() + 1) {
-            blockPos = blockPos.below();
+        while (world.noCollision(this, axisalignedbb.move(blockposition1.below().getBottomCenter())) && blockposition1.getY() > world.getMinBuildHeight() + 1) {
+            blockposition1 = blockposition1.below();
         }
 
-        return blockPos;
+        return blockposition1;
     }
 
     private int getCoprime(int horizontalSpawnArea) {
@@ -339,72 +538,114 @@
     @Override
     public void readAdditionalSaveData(CompoundTag nbt) {
         super.readAdditionalSaveData(nbt);
+        DataResult dataresult;
+        Logger logger;
+
         if (nbt.contains("warden_spawn_tracker", 10)) {
-            WardenSpawnTracker.CODEC
-                .parse(new Dynamic<>(NbtOps.INSTANCE, nbt.get("warden_spawn_tracker")))
-                .resultOrPartial(LOGGER::error)
-                .ifPresent(sculkShriekerWarningManager -> this.wardenSpawnTracker = sculkShriekerWarningManager);
+            dataresult = WardenSpawnTracker.CODEC.parse(new Dynamic(NbtOps.INSTANCE, nbt.get("warden_spawn_tracker")));
+            logger = ServerPlayer.LOGGER;
+            Objects.requireNonNull(logger);
+            ((DataResult<WardenSpawnTracker>) dataresult).resultOrPartial(logger::error).ifPresent((wardenspawntracker) -> {
+                this.wardenSpawnTracker = wardenspawntracker;
+            });
         }
 
         if (nbt.contains("enteredNetherPosition", 10)) {
-            CompoundTag compoundTag = nbt.getCompound("enteredNetherPosition");
-            this.enteredNetherPosition = new Vec3(compoundTag.getDouble("x"), compoundTag.getDouble("y"), compoundTag.getDouble("z"));
+            CompoundTag nbttagcompound1 = nbt.getCompound("enteredNetherPosition");
+
+            this.enteredNetherPosition = new Vec3(nbttagcompound1.getDouble("x"), nbttagcompound1.getDouble("y"), nbttagcompound1.getDouble("z"));
         }
 
         this.seenCredits = nbt.getBoolean("seenCredits");
         if (nbt.contains("recipeBook", 10)) {
             this.recipeBook.fromNbt(nbt.getCompound("recipeBook"), this.server.getRecipeManager());
         }
+        this.getBukkitEntity().readExtraData(nbt); // CraftBukkit
 
         if (this.isSleeping()) {
             this.stopSleeping();
         }
 
+        // CraftBukkit start
+        String spawnWorld = nbt.getString("SpawnWorld");
+        CraftWorld oldWorld = (CraftWorld) Bukkit.getWorld(spawnWorld);
+        if (oldWorld != null) {
+            this.respawnDimension = oldWorld.getHandle().dimension();
+        }
+        // CraftBukkit end
+
         if (nbt.contains("SpawnX", 99) && nbt.contains("SpawnY", 99) && nbt.contains("SpawnZ", 99)) {
             this.respawnPosition = new BlockPos(nbt.getInt("SpawnX"), nbt.getInt("SpawnY"), nbt.getInt("SpawnZ"));
             this.respawnForced = nbt.getBoolean("SpawnForced");
             this.respawnAngle = nbt.getFloat("SpawnAngle");
             if (nbt.contains("SpawnDimension")) {
-                this.respawnDimension = Level.RESOURCE_KEY_CODEC
-                    .parse(NbtOps.INSTANCE, nbt.get("SpawnDimension"))
-                    .resultOrPartial(LOGGER::error)
-                    .orElse(Level.OVERWORLD);
+                DataResult<ResourceKey<Level>> dataresult1 = Level.RESOURCE_KEY_CODEC.parse(NbtOps.INSTANCE, nbt.get("SpawnDimension")); // CraftBukkit - decompile error
+                Logger logger1 = ServerPlayer.LOGGER;
+
+                Objects.requireNonNull(logger1);
+                this.respawnDimension = (ResourceKey) dataresult1.resultOrPartial(logger1::error).orElse(Level.OVERWORLD);
             }
         }
 
         this.spawnExtraParticlesOnFall = nbt.getBoolean("spawn_extra_particles_on_fall");
-        Tag tag = nbt.get("raid_omen_position");
-        if (tag != null) {
-            BlockPos.CODEC.parse(NbtOps.INSTANCE, tag).resultOrPartial(LOGGER::error).ifPresent(startRaidPos -> this.raidOmenPosition = startRaidPos);
+        Tag nbtbase = nbt.get("raid_omen_position");
+
+        if (nbtbase != null) {
+            dataresult = BlockPos.CODEC.parse(NbtOps.INSTANCE, nbtbase);
+            logger = ServerPlayer.LOGGER;
+            Objects.requireNonNull(logger);
+            ((DataResult<BlockPos>) dataresult).resultOrPartial(logger::error).ifPresent((blockposition) -> { // CraftBukkit - decompile error
+                this.raidOmenPosition = blockposition;
+            });
         }
+
     }
 
     @Override
     public void addAdditionalSaveData(CompoundTag nbt) {
         super.addAdditionalSaveData(nbt);
-        WardenSpawnTracker.CODEC
-            .encodeStart(NbtOps.INSTANCE, this.wardenSpawnTracker)
-            .resultOrPartial(LOGGER::error)
-            .ifPresent(encoded -> nbt.put("warden_spawn_tracker", encoded));
+        DataResult<Tag> dataresult = WardenSpawnTracker.CODEC.encodeStart(NbtOps.INSTANCE, this.wardenSpawnTracker); // CraftBukkit - decompile error
+        Logger logger = ServerPlayer.LOGGER;
+
+        Objects.requireNonNull(logger);
+        dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> {
+            nbt.put("warden_spawn_tracker", nbtbase);
+        });
         this.storeGameTypes(nbt);
         nbt.putBoolean("seenCredits", this.seenCredits);
         if (this.enteredNetherPosition != null) {
-            CompoundTag compoundTag = new CompoundTag();
-            compoundTag.putDouble("x", this.enteredNetherPosition.x);
-            compoundTag.putDouble("y", this.enteredNetherPosition.y);
-            compoundTag.putDouble("z", this.enteredNetherPosition.z);
-            nbt.put("enteredNetherPosition", compoundTag);
+            CompoundTag nbttagcompound1 = new CompoundTag();
+
+            nbttagcompound1.putDouble("x", this.enteredNetherPosition.x);
+            nbttagcompound1.putDouble("y", this.enteredNetherPosition.y);
+            nbttagcompound1.putDouble("z", this.enteredNetherPosition.z);
+            nbt.put("enteredNetherPosition", nbttagcompound1);
         }
 
         Entity entity = this.getRootVehicle();
-        Entity entity2 = this.getVehicle();
-        if (entity2 != null && entity != this && entity.hasExactlyOnePlayerPassenger()) {
-            CompoundTag compoundTag2 = new CompoundTag();
-            CompoundTag compoundTag3 = new CompoundTag();
-            entity.save(compoundTag3);
-            compoundTag2.putUUID("Attach", entity2.getUUID());
-            compoundTag2.put("Entity", compoundTag3);
-            nbt.put("RootVehicle", compoundTag2);
+        Entity entity1 = this.getVehicle();
+
+        // CraftBukkit start - handle non-persistent vehicles
+        boolean persistVehicle = true;
+        if (entity1 != null) {
+            Entity vehicle;
+            for (vehicle = entity1; vehicle != null; vehicle = vehicle.getVehicle()) {
+                if (!vehicle.persist) {
+                    persistVehicle = false;
+                    break;
+                }
+            }
+        }
+
+        if (persistVehicle && entity1 != null && entity != this && entity.hasExactlyOnePlayerPassenger() && !entity.isRemoved()) { // Paper - Ensure valid vehicle status
+            // CraftBukkit end
+            CompoundTag nbttagcompound2 = new CompoundTag();
+            CompoundTag nbttagcompound3 = new CompoundTag();
+
+            entity.save(nbttagcompound3);
+            nbttagcompound2.putUUID("Attach", entity1.getUUID());
+            nbttagcompound2.put("Entity", nbttagcompound3);
+            nbt.put("RootVehicle", nbttagcompound2);
         }
 
         nbt.put("recipeBook", this.recipeBook.toNbt());
@@ -415,25 +656,55 @@
             nbt.putInt("SpawnZ", this.respawnPosition.getZ());
             nbt.putBoolean("SpawnForced", this.respawnForced);
             nbt.putFloat("SpawnAngle", this.respawnAngle);
-            ResourceLocation.CODEC
-                .encodeStart(NbtOps.INSTANCE, this.respawnDimension.location())
-                .resultOrPartial(LOGGER::error)
-                .ifPresent(encoded -> nbt.put("SpawnDimension", encoded));
+            dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.respawnDimension.location());
+            logger = ServerPlayer.LOGGER;
+            Objects.requireNonNull(logger);
+            dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> {
+                nbt.put("SpawnDimension", nbtbase);
+            });
         }
+        this.getBukkitEntity().setExtraData(nbt); // CraftBukkit
 
         nbt.putBoolean("spawn_extra_particles_on_fall", this.spawnExtraParticlesOnFall);
         if (this.raidOmenPosition != null) {
-            BlockPos.CODEC
-                .encodeStart(NbtOps.INSTANCE, this.raidOmenPosition)
-                .resultOrPartial(LOGGER::error)
-                .ifPresent(encoded -> nbt.put("raid_omen_position", encoded));
+            dataresult = BlockPos.CODEC.encodeStart(NbtOps.INSTANCE, this.raidOmenPosition);
+            logger = ServerPlayer.LOGGER;
+            Objects.requireNonNull(logger);
+            dataresult.resultOrPartial(logger::error).ifPresent((nbtbase) -> {
+                nbt.put("raid_omen_position", nbtbase);
+            });
+        }
+
+    }
+
+    // CraftBukkit start - World fallback code, either respawn location or global spawn
+    public void spawnIn(Level world) {
+        this.setLevel(world);
+        if (world == null) {
+            this.unsetRemoved();
+            Vec3 position = null;
+            if (this.respawnDimension != null) {
+                world = this.server.getLevel(this.respawnDimension);
+                if (world != null && this.getRespawnPosition() != null) {
+                    position = ServerPlayer.findRespawnAndUseSpawnBlock((ServerLevel) world, this.getRespawnPosition(), this.getRespawnAngle(), false, false).map(ServerPlayer.RespawnPosAngle::position).orElse(null);
+                }
+            }
+            if (world == null || position == null) {
+                world = ((CraftWorld) Bukkit.getServer().getWorlds().get(0)).getHandle();
+                position = Vec3.atCenterOf(world.getSharedSpawnPos());
+            }
+            this.setLevel(world);
+            this.setPosRaw(position.x(), position.y(), position.z()); // Paper - don't register to chunks yet
         }
+        this.gameMode.setLevel((ServerLevel) world);
     }
+    // CraftBukkit end
 
     public void setExperiencePoints(int points) {
-        float f = (float)this.getXpNeededForNextLevel();
-        float g = (f - 1.0F) / f;
-        this.experienceProgress = Mth.clamp((float)points / f, 0.0F, g);
+        float f = (float) this.getXpNeededForNextLevel();
+        float f1 = (f - 1.0F) / f;
+
+        this.experienceProgress = Mth.clamp((float) points / f, 0.0F, f1);
         this.lastSentExp = -1;
     }
 
@@ -487,20 +758,31 @@
 
     @Override
     public void tick() {
+        // CraftBukkit start
+        if (this.joining) {
+            this.joining = false;
+        }
+        // CraftBukkit end
         this.gameMode.tick();
         this.wardenSpawnTracker.tick();
-        this.spawnInvulnerableTime--;
+        --this.spawnInvulnerableTime;
         if (this.invulnerableTime > 0) {
-            this.invulnerableTime--;
+            --this.invulnerableTime;
         }
 
-        this.containerMenu.broadcastChanges();
-        if (!this.level().isClientSide && !this.containerMenu.stillValid(this)) {
-            this.closeContainer();
+        // Paper start - Configurable container update tick rate
+        if (--containerUpdateDelay <= 0) {
+            this.containerMenu.broadcastChanges();
+            containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate;
+        }
+        // Paper end - Configurable container update tick rate
+        if (!this.level().isClientSide && this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen
+            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason
             this.containerMenu = this.inventoryMenu;
         }
 
         Entity entity = this.getCamera();
+
         if (entity != this) {
             if (entity.isAlive()) {
                 this.absMoveTo(entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot());
@@ -525,45 +807,48 @@
     }
 
     private void updatePlayerAttributes() {
-        AttributeInstance attributeInstance = this.getAttribute(Attributes.BLOCK_INTERACTION_RANGE);
-        if (attributeInstance != null) {
+        AttributeInstance attributemodifiable = this.getAttribute(Attributes.BLOCK_INTERACTION_RANGE);
+
+        if (attributemodifiable != null) {
             if (this.isCreative()) {
-                attributeInstance.addOrUpdateTransientModifier(CREATIVE_BLOCK_INTERACTION_RANGE_MODIFIER);
+                attributemodifiable.addOrUpdateTransientModifier(ServerPlayer.CREATIVE_BLOCK_INTERACTION_RANGE_MODIFIER);
             } else {
-                attributeInstance.removeModifier(CREATIVE_BLOCK_INTERACTION_RANGE_MODIFIER);
+                attributemodifiable.removeModifier(ServerPlayer.CREATIVE_BLOCK_INTERACTION_RANGE_MODIFIER);
             }
         }
 
-        AttributeInstance attributeInstance2 = this.getAttribute(Attributes.ENTITY_INTERACTION_RANGE);
-        if (attributeInstance2 != null) {
+        AttributeInstance attributemodifiable1 = this.getAttribute(Attributes.ENTITY_INTERACTION_RANGE);
+
+        if (attributemodifiable1 != null) {
             if (this.isCreative()) {
-                attributeInstance2.addOrUpdateTransientModifier(CREATIVE_ENTITY_INTERACTION_RANGE_MODIFIER);
+                attributemodifiable1.addOrUpdateTransientModifier(ServerPlayer.CREATIVE_ENTITY_INTERACTION_RANGE_MODIFIER);
             } else {
-                attributeInstance2.removeModifier(CREATIVE_ENTITY_INTERACTION_RANGE_MODIFIER);
+                attributemodifiable1.removeModifier(ServerPlayer.CREATIVE_ENTITY_INTERACTION_RANGE_MODIFIER);
             }
         }
+
     }
 
     public void doTick() {
         try {
-            if (!this.isSpectator() || !this.touchingUnloadedChunk()) {
+            if (valid && !this.isSpectator() || !this.touchingUnloadedChunk()) { // Paper - don't tick dead players that are not in the world currently (pending respawn)
                 super.tick();
             }
 
-            for (int i = 0; i < this.getInventory().getContainerSize(); i++) {
-                ItemStack itemStack = this.getInventory().getItem(i);
-                if (itemStack.getItem().isComplex()) {
-                    Packet<?> packet = ((ComplexItem)itemStack.getItem()).getUpdatePacket(itemStack, this.level(), this);
+            for (int i = 0; i < this.getInventory().getContainerSize(); ++i) {
+                ItemStack itemstack = this.getInventory().getItem(i);
+
+                if (itemstack.getItem().isComplex()) {
+                    Packet<?> packet = ((ComplexItem) itemstack.getItem()).getUpdatePacket(itemstack, this.level(), this);
+
                     if (packet != null) {
                         this.connection.send(packet);
                     }
                 }
             }
 
-            if (this.getHealth() != this.lastSentHealth
-                || this.lastSentFood != this.foodData.getFoodLevel()
-                || this.foodData.getSaturationLevel() == 0.0F != this.lastFoodSaturationZero) {
-                this.connection.send(new ClientboundSetHealthPacket(this.getHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel()));
+            if (this.getHealth() != this.lastSentHealth || this.lastSentFood != this.foodData.getFoodLevel() || this.foodData.getSaturationLevel() == 0.0F != this.lastFoodSaturationZero) {
+                this.connection.send(new ClientboundSetHealthPacket(this.getBukkitEntity().getScaledHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel())); // CraftBukkit
                 this.lastSentHealth = this.getHealth();
                 this.lastSentFood = this.foodData.getFoodLevel();
                 this.lastFoodSaturationZero = this.foodData.getSaturationLevel() == 0.0F;
@@ -576,27 +861,33 @@
 
             if (this.foodData.getFoodLevel() != this.lastRecordedFoodLevel) {
                 this.lastRecordedFoodLevel = this.foodData.getFoodLevel();
-                this.updateScoreForCriteria(ObjectiveCriteria.FOOD, Mth.ceil((float)this.lastRecordedFoodLevel));
+                this.updateScoreForCriteria(ObjectiveCriteria.FOOD, Mth.ceil((float) this.lastRecordedFoodLevel));
             }
 
             if (this.getAirSupply() != this.lastRecordedAirLevel) {
                 this.lastRecordedAirLevel = this.getAirSupply();
-                this.updateScoreForCriteria(ObjectiveCriteria.AIR, Mth.ceil((float)this.lastRecordedAirLevel));
+                this.updateScoreForCriteria(ObjectiveCriteria.AIR, Mth.ceil((float) this.lastRecordedAirLevel));
             }
 
             if (this.getArmorValue() != this.lastRecordedArmor) {
                 this.lastRecordedArmor = this.getArmorValue();
-                this.updateScoreForCriteria(ObjectiveCriteria.ARMOR, Mth.ceil((float)this.lastRecordedArmor));
+                this.updateScoreForCriteria(ObjectiveCriteria.ARMOR, Mth.ceil((float) this.lastRecordedArmor));
             }
 
             if (this.totalExperience != this.lastRecordedExperience) {
                 this.lastRecordedExperience = this.totalExperience;
-                this.updateScoreForCriteria(ObjectiveCriteria.EXPERIENCE, Mth.ceil((float)this.lastRecordedExperience));
+                this.updateScoreForCriteria(ObjectiveCriteria.EXPERIENCE, Mth.ceil((float) this.lastRecordedExperience));
+            }
+
+            // CraftBukkit start - Force max health updates
+            if (this.maxHealthCache != this.getMaxHealth()) {
+                this.getBukkitEntity().updateScaledHealth();
             }
+            // CraftBukkit end
 
             if (this.experienceLevel != this.lastRecordedLevel) {
                 this.lastRecordedLevel = this.experienceLevel;
-                this.updateScoreForCriteria(ObjectiveCriteria.LEVEL, Mth.ceil((float)this.lastRecordedLevel));
+                this.updateScoreForCriteria(ObjectiveCriteria.LEVEL, Mth.ceil((float) this.lastRecordedLevel));
             }
 
             if (this.totalExperience != this.lastSentExp) {
@@ -607,11 +898,27 @@
             if (this.tickCount % 20 == 0) {
                 CriteriaTriggers.LOCATION.trigger(this);
             }
-        } catch (Throwable var4) {
-            CrashReport crashReport = CrashReport.forThrowable(var4, "Ticking player");
-            CrashReportCategory crashReportCategory = crashReport.addCategory("Player being ticked");
-            this.fillCrashReportCategory(crashReportCategory);
-            throw new ReportedException(crashReport);
+
+            // CraftBukkit start - initialize oldLevel, fire PlayerLevelChangeEvent, and tick client-sided world border
+            if (this.oldLevel == -1) {
+                this.oldLevel = this.experienceLevel;
+            }
+
+            if (this.oldLevel != this.experienceLevel) {
+                CraftEventFactory.callPlayerLevelChangeEvent(this.getBukkitEntity(), this.oldLevel, this.experienceLevel);
+                this.oldLevel = this.experienceLevel;
+            }
+
+            if (this.getBukkitEntity().hasClientWorldBorder()) {
+                ((CraftWorldBorder) this.getBukkitEntity().getWorldBorder()).getHandle().tick();
+            }
+            // CraftBukkit end
+        } catch (Throwable throwable) {
+            CrashReport crashreport = CrashReport.forThrowable(throwable, "Ticking player");
+            CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Player being ticked");
+
+            this.fillCrashReportCategory(crashreportsystemdetails);
+            throw new ReportedException(crashreport);
         }
     }
 
@@ -632,6 +939,7 @@
                 CriteriaTriggers.FALL_AFTER_EXPLOSION.trigger(this, this.currentImpulseImpactPos, this.currentExplosionCause);
             }
         }
+
     }
 
     public void trackEnteredOrExitedLavaOnVehicle() {
@@ -646,41 +954,129 @@
         if (this.enteredLavaOnVehiclePosition != null && (this.getVehicle() == null || !this.getVehicle().isInLava())) {
             this.enteredLavaOnVehiclePosition = null;
         }
+
     }
 
     private void updateScoreForCriteria(ObjectiveCriteria criterion, int score) {
-        this.getScoreboard().forAllObjectives(criterion, this, innerScore -> innerScore.set(score));
+        // CraftBukkit - Use our scores instead
+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(criterion, this, (scoreaccess) -> {
+            scoreaccess.set(score);
+        });
     }
 
+    // Paper start - PlayerDeathEvent#getItemsToKeep
+    private static void processKeep(org.bukkit.event.entity.PlayerDeathEvent event, NonNullList<ItemStack> inv) {
+        List<org.bukkit.inventory.ItemStack> itemsToKeep = event.getItemsToKeep();
+        if (inv == null) {
+            // remainder of items left in toKeep - plugin added stuff on death that wasn't in the initial loot?
+            if (!itemsToKeep.isEmpty()) {
+                for (org.bukkit.inventory.ItemStack itemStack : itemsToKeep) {
+                    event.getEntity().getInventory().addItem(itemStack);
+                }
+            }
+
+            return;
+        }
+
+        for (int i = 0; i < inv.size(); ++i) {
+            ItemStack item = inv.get(i);
+            if (EnchantmentHelper.has(item, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP) || itemsToKeep.isEmpty() || item.isEmpty()) {
+                inv.set(i, ItemStack.EMPTY);
+                continue;
+            }
+
+            final org.bukkit.inventory.ItemStack bukkitStack = item.getBukkitStack();
+            boolean keep = false;
+            final Iterator<org.bukkit.inventory.ItemStack> iterator = itemsToKeep.iterator();
+            while (iterator.hasNext()) {
+                final org.bukkit.inventory.ItemStack itemStack = iterator.next();
+                if (bukkitStack.equals(itemStack)) {
+                    iterator.remove();
+                    keep = true;
+                    break;
+                }
+            }
+
+            if (!keep) {
+                inv.set(i, ItemStack.EMPTY);
+            }
+        }
+    }
+    // Paper end - PlayerDeathEvent#getItemsToKeep
+
     @Override
     public void die(DamageSource damageSource) {
-        this.gameEvent(GameEvent.ENTITY_DIE);
-        boolean bl = this.level().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES);
-        if (bl) {
-            Component component = this.getCombatTracker().getDeathMessage();
-            this.connection
-                .send(
-                    new ClientboundPlayerCombatKillPacket(this.getId(), component),
-                    PacketSendListener.exceptionallySend(
-                        () -> {
-                            int i = 256;
-                            String string = component.getString(256);
-                            Component component2 = Component.translatable(
-                                "death.attack.message_too_long", Component.literal(string).withStyle(ChatFormatting.YELLOW)
-                            );
-                            Component component3 = Component.translatable("death.attack.even_more_magic", this.getDisplayName())
-                                .withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, component2)));
-                            return new ClientboundPlayerCombatKillPacket(this.getId(), component3);
-                        }
-                    )
-                );
-            Team team = this.getTeam();
-            if (team == null || team.getDeathMessageVisibility() == Team.Visibility.ALWAYS) {
-                this.server.getPlayerList().broadcastSystemMessage(component, false);
-            } else if (team.getDeathMessageVisibility() == Team.Visibility.HIDE_FOR_OTHER_TEAMS) {
-                this.server.getPlayerList().broadcastSystemToTeam(this, component);
-            } else if (team.getDeathMessageVisibility() == Team.Visibility.HIDE_FOR_OWN_TEAM) {
-                this.server.getPlayerList().broadcastSystemToAllExceptTeam(this, component);
+        // this.gameEvent(GameEvent.ENTITY_DIE); // Paper - move below event cancellation check
+        boolean flag = this.level().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES);
+        // CraftBukkit start - fire PlayerDeathEvent
+        if (this.isRemoved()) {
+            return;
+        }
+        List<DefaultDrop> loot = new java.util.ArrayList<>(this.getInventory().getContainerSize()); // Paper - Restore vanilla drops behavior
+        boolean keepInventory = this.level().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || this.isSpectator();
+
+        if (!keepInventory) {
+            for (ItemStack item : this.getInventory().getContents()) {
+                if (!item.isEmpty() && !EnchantmentHelper.has(item, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) {
+                    loot.add(new DefaultDrop(item, stack -> this.drop(stack, true, false, false))); // Paper - Restore vanilla drops behavior; drop function taken from Inventory#dropAll (don't fire drop event)
+                }
+            }
+        }
+        if (this.shouldDropLoot() && this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Paper - fix player loottables running when mob loot gamerule is false
+        // SPIGOT-5071: manually add player loot tables (SPIGOT-5195 - ignores keepInventory rule)
+        this.dropFromLootTable(damageSource, this.lastHurtByPlayerTime > 0);
+        // Paper - Restore vanilla drops behaviour; custom death loot is a noop on server player, remove.
+
+        loot.addAll(this.drops);
+        this.drops.clear(); // SPIGOT-5188: make sure to clear
+        } // Paper - fix player loottables running when mob loot gamerule is false
+
+        Component defaultMessage = this.getCombatTracker().getDeathMessage();
+
+        String deathmessage = defaultMessage.getString();
+        this.keepLevel = keepInventory; // SPIGOT-2222: pre-set keepLevel
+        org.bukkit.event.entity.PlayerDeathEvent event = CraftEventFactory.callPlayerDeathEvent(this, damageSource, loot, PaperAdventure.asAdventure(defaultMessage), keepInventory); // Paper - Adventure
+        // Paper start - cancellable death event
+        if (event.isCancelled()) {
+            // make compatible with plugins that might have already set the health in an event listener
+            if (this.getHealth() <= 0) {
+                this.setHealth((float) event.getReviveHealth());
+            }
+            return;
+        }
+        this.gameEvent(GameEvent.ENTITY_DIE); // moved from the top of this method
+        // Paper end
+
+        // SPIGOT-943 - only call if they have an inventory open
+        if (this.containerMenu != this.inventoryMenu) {
+            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DEATH); // Paper - Inventory close reason
+        }
+
+        net.kyori.adventure.text.Component deathMessage = event.deathMessage() != null ? event.deathMessage() : net.kyori.adventure.text.Component.empty(); // Paper - Adventure
+
+        if (deathMessage != null && deathMessage != net.kyori.adventure.text.Component.empty() && flag) { // Paper - Adventure // TODO: allow plugins to override?
+            Component ichatbasecomponent = PaperAdventure.asVanilla(deathMessage); // Paper - Adventure
+
+            this.connection.send(new ClientboundPlayerCombatKillPacket(this.getId(), ichatbasecomponent), PacketSendListener.exceptionallySend(() -> {
+                boolean flag1 = true;
+                String s = ichatbasecomponent.getString(256);
+                MutableComponent ichatmutablecomponent = Component.translatable("death.attack.message_too_long", Component.literal(s).withStyle(ChatFormatting.YELLOW));
+                MutableComponent ichatmutablecomponent1 = Component.translatable("death.attack.even_more_magic", this.getDisplayName()).withStyle((chatmodifier) -> {
+                    return chatmodifier.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, ichatmutablecomponent));
+                });
+
+                return new ClientboundPlayerCombatKillPacket(this.getId(), ichatmutablecomponent1);
+            }));
+            PlayerTeam scoreboardteam = this.getTeam();
+
+            if (scoreboardteam != null && scoreboardteam.getDeathMessageVisibility() != Team.Visibility.ALWAYS) {
+                if (scoreboardteam.getDeathMessageVisibility() == Team.Visibility.HIDE_FOR_OTHER_TEAMS) {
+                    this.server.getPlayerList().broadcastSystemToTeam(this, ichatbasecomponent);
+                } else if (scoreboardteam.getDeathMessageVisibility() == Team.Visibility.HIDE_FOR_OWN_TEAM) {
+                    this.server.getPlayerList().broadcastSystemToAllExceptTeam(this, ichatbasecomponent);
+                }
+            } else {
+                this.server.getPlayerList().broadcastSystemMessage(ichatbasecomponent, false);
             }
         } else {
             this.connection.send(new ClientboundPlayerCombatKillPacket(this.getId(), CommonComponents.EMPTY));
@@ -690,20 +1086,32 @@
         if (this.level().getGameRules().getBoolean(GameRules.RULE_FORGIVE_DEAD_PLAYERS)) {
             this.tellNeutralMobsThatIDied();
         }
-
-        if (!this.isSpectator()) {
-            this.dropAllDeathLoot(this.serverLevel(), damageSource);
+        // SPIGOT-5478 must be called manually now
+        if (event.shouldDropExperience()) this.dropExperience(damageSource.getEntity()); // Paper - tie to event
+        // we clean the player's inventory after the EntityDeathEvent is called so plugins can get the exact state of the inventory.
+        if (!event.getKeepInventory()) {
+            // Paper start - PlayerDeathEvent#getItemsToKeep
+            for (NonNullList<ItemStack> inv : this.getInventory().compartments) {
+                processKeep(event, inv);
+            }
+            processKeep(event, null);
+            // Paper end - PlayerDeathEvent#getItemsToKeep
         }
 
-        this.getScoreboard().forAllObjectives(ObjectiveCriteria.DEATH_COUNT, this, ScoreAccess::increment);
-        LivingEntity livingEntity = this.getKillCredit();
-        if (livingEntity != null) {
-            this.awardStat(Stats.ENTITY_KILLED_BY.get(livingEntity.getType()));
-            livingEntity.awardKillScore(this, this.deathScore, damageSource);
-            this.createWitherRose(livingEntity);
+        this.setCamera(this); // Remove spectated target
+        // CraftBukkit end
+
+        // CraftBukkit - Get our scores instead
+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.DEATH_COUNT, this, ScoreAccess::increment);
+        LivingEntity entityliving = this.getKillCredit();
+
+        if (entityliving != null) {
+            this.awardStat(Stats.ENTITY_KILLED_BY.get(entityliving.getType()));
+            entityliving.awardKillScore(this, this.deathScore, damageSource);
+            this.createWitherRose(entityliving);
         }
 
-        this.level().broadcastEntityEvent(this, (byte)3);
+        this.level().broadcastEntityEvent(this, (byte) 3);
         this.awardStat(Stats.DEATHS);
         this.resetStat(Stats.CUSTOM.get(Stats.TIME_SINCE_DEATH));
         this.resetStat(Stats.CUSTOM.get(Stats.TIME_SINCE_REST));
@@ -715,12 +1123,13 @@
     }
 
     private void tellNeutralMobsThatIDied() {
-        AABB aABB = new AABB(this.blockPosition()).inflate(32.0, 10.0, 32.0);
-        this.level()
-            .getEntitiesOfClass(Mob.class, aABB, EntitySelector.NO_SPECTATORS)
-            .stream()
-            .filter(entity -> entity instanceof NeutralMob)
-            .forEach(entity -> ((NeutralMob)entity).playerDied(this));
+        AABB axisalignedbb = (new AABB(this.blockPosition())).inflate(32.0D, 10.0D, 32.0D);
+
+        this.level().getEntitiesOfClass(Mob.class, axisalignedbb, EntitySelector.NO_SPECTATORS).stream().filter((entityinsentient) -> {
+            return entityinsentient instanceof NeutralMob;
+        }).forEach((entityinsentient) -> {
+            ((NeutralMob) entityinsentient).playerDied(this);
+        });
     }
 
     @Override
@@ -728,10 +1137,12 @@
         if (entityKilled != this) {
             super.awardKillScore(entityKilled, score, damageSource);
             this.increaseScore(score);
-            this.getScoreboard().forAllObjectives(ObjectiveCriteria.KILL_COUNT_ALL, this, ScoreAccess::increment);
-            if (entityKilled instanceof Player) {
+            // CraftBukkit - Get our scores instead
+            this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.KILL_COUNT_ALL, this, ScoreAccess::increment);
+            if (entityKilled instanceof net.minecraft.world.entity.player.Player) {
                 this.awardStat(Stats.PLAYER_KILLS);
-                this.getScoreboard().forAllObjectives(ObjectiveCriteria.KILL_COUNT_PLAYERS, this, ScoreAccess::increment);
+                // CraftBukkit - Get our scores instead
+                this.level().getCraftServer().getScoreboardManager().forAllObjectives(ObjectiveCriteria.KILL_COUNT_PLAYERS, this, ScoreAccess::increment);
             } else {
                 this.awardStat(Stats.MOB_KILLS);
             }
@@ -743,13 +1154,17 @@
     }
 
     private void handleTeamKill(ScoreHolder targetScoreHolder, ScoreHolder aboutScoreHolder, ObjectiveCriteria[] criterions) {
-        PlayerTeam playerTeam = this.getScoreboard().getPlayersTeam(aboutScoreHolder.getScoreboardName());
-        if (playerTeam != null) {
-            int i = playerTeam.getColor().getId();
+        PlayerTeam scoreboardteam = this.getScoreboard().getPlayersTeam(aboutScoreHolder.getScoreboardName());
+
+        if (scoreboardteam != null) {
+            int i = scoreboardteam.getColor().getId();
+
             if (i >= 0 && i < criterions.length) {
-                this.getScoreboard().forAllObjectives(criterions[i], targetScoreHolder, ScoreAccess::increment);
+                // CraftBukkit - Get our scores instead
+                this.level().getCraftServer().getScoreboardManager().forAllObjectives(criterions[i], targetScoreHolder, ScoreAccess::increment);
             }
         }
+
     }
 
     @Override
@@ -757,79 +1172,140 @@
         if (this.isInvulnerableTo(source)) {
             return false;
         } else {
-            boolean bl = this.server.isDedicatedServer() && this.isPvpAllowed() && source.is(DamageTypeTags.IS_FALL);
-            if (!bl && this.spawnInvulnerableTime > 0 && !source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) {
+            boolean flag = this.server.isDedicatedServer() && this.isPvpAllowed() && source.is(DamageTypeTags.IS_FALL);
+
+            if (!flag && this.spawnInvulnerableTime > 0 && !source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) {
                 return false;
             } else {
                 Entity entity = source.getEntity();
-                if (entity instanceof Player player && !this.canHarmPlayer(player)) {
-                    return false;
-                }
 
-                if (entity instanceof AbstractArrow abstractArrow && abstractArrow.getOwner() instanceof Player player2 && !this.canHarmPlayer(player2)) {
-                    return false;
+                if (entity instanceof net.minecraft.world.entity.player.Player) {
+                    net.minecraft.world.entity.player.Player entityhuman = (net.minecraft.world.entity.player.Player) entity;
+
+                    if (!this.canHarmPlayer(entityhuman)) {
+                        return false;
+                    }
                 }
 
-                return super.hurt(source, amount);
+                if (entity instanceof AbstractArrow) {
+                    AbstractArrow entityarrow = (AbstractArrow) entity;
+                    Entity entity1 = entityarrow.getOwner();
+
+                    if (entity1 instanceof net.minecraft.world.entity.player.Player) {
+                        net.minecraft.world.entity.player.Player entityhuman1 = (net.minecraft.world.entity.player.Player) entity1;
+
+                        if (!this.canHarmPlayer(entityhuman1)) {
+                            return false;
+                        }
+                    }
+                }
+                // Paper start - cancellable death events
+                //return super.hurt(source, amount);
+                this.queueHealthUpdatePacket = true;
+                boolean damaged = super.hurt(source, amount);
+                this.queueHealthUpdatePacket = false;
+                if (this.queuedHealthUpdatePacket != null) {
+                    this.connection.send(this.queuedHealthUpdatePacket);
+                    this.queuedHealthUpdatePacket = null;
+                }
+                return damaged;
+                // Paper end
             }
         }
     }
 
     @Override
-    public boolean canHarmPlayer(Player player) {
-        return this.isPvpAllowed() && super.canHarmPlayer(player);
+    public boolean canHarmPlayer(net.minecraft.world.entity.player.Player player) {
+        return !this.isPvpAllowed() ? false : super.canHarmPlayer(player);
     }
 
     private boolean isPvpAllowed() {
-        return this.server.isPvpAllowed();
+        // CraftBukkit - this.server.isPvpAllowed() -> this.world.pvpMode
+        return this.level().pvpMode;
     }
 
-    public DimensionTransition findRespawnPositionAndUseSpawnBlock(boolean alive, DimensionTransition.PostDimensionTransition postDimensionTransition) {
-        BlockPos blockPos = this.getRespawnPosition();
+    // CraftBukkit start
+    public DimensionTransition findRespawnPositionAndUseSpawnBlock(boolean flag, DimensionTransition.PostDimensionTransition dimensiontransition_a, PlayerRespawnEvent.RespawnReason reason) {
+        DimensionTransition dimensionTransition;
+        boolean isBedSpawn = false;
+        boolean isAnchorSpawn = false;
+        // CraftBukkit end
+        BlockPos blockposition = this.getRespawnPosition();
         float f = this.getRespawnAngle();
-        boolean bl = this.isRespawnForced();
-        ServerLevel serverLevel = this.server.getLevel(this.getRespawnDimension());
-        if (serverLevel != null && blockPos != null) {
-            Optional<ServerPlayer.RespawnPosAngle> optional = findRespawnAndUseSpawnBlock(serverLevel, blockPos, f, bl, alive);
+        boolean flag1 = this.isRespawnForced();
+        ServerLevel worldserver = this.server.getLevel(this.getRespawnDimension());
+
+        if (worldserver != null && blockposition != null) {
+            Optional<ServerPlayer.RespawnPosAngle> optional = ServerPlayer.findRespawnAndUseSpawnBlock(worldserver, blockposition, f, flag1, flag);
+
             if (optional.isPresent()) {
-                ServerPlayer.RespawnPosAngle respawnPosAngle = optional.get();
-                return new DimensionTransition(serverLevel, respawnPosAngle.position(), Vec3.ZERO, respawnPosAngle.yaw(), 0.0F, postDimensionTransition);
+                ServerPlayer.RespawnPosAngle entityplayer_respawnposangle = (ServerPlayer.RespawnPosAngle) optional.get();
+
+                // CraftBukkit start
+                isBedSpawn = entityplayer_respawnposangle.isBedSpawn();
+                isAnchorSpawn = entityplayer_respawnposangle.isAnchorSpawn();
+                dimensionTransition = new DimensionTransition(worldserver, entityplayer_respawnposangle.position(), Vec3.ZERO, entityplayer_respawnposangle.yaw(), 0.0F, dimensiontransition_a);
+                // CraftBukkit end
             } else {
-                return DimensionTransition.missingRespawnBlock(this.server.overworld(), this, postDimensionTransition);
+                dimensionTransition = DimensionTransition.missingRespawnBlock(this.server.overworld(), this, dimensiontransition_a); // CraftBukkit
             }
         } else {
-            return new DimensionTransition(this.server.overworld(), this, postDimensionTransition);
+            dimensionTransition = new DimensionTransition(this.server.overworld(), this, dimensiontransition_a); // CraftBukkit
+        }
+        // CraftBukkit start
+        if (reason == null) {
+            return dimensionTransition;
+        }
+
+        Player respawnPlayer = this.getBukkitEntity();
+        Location location = CraftLocation.toBukkit(dimensionTransition.pos(), dimensionTransition.newLevel().getWorld(), dimensionTransition.yRot(), dimensionTransition.xRot());
+
+        // Paper start - respawn flags
+        com.google.common.collect.ImmutableSet.Builder<org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag> builder = com.google.common.collect.ImmutableSet.builder();
+        if (reason == org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL) {
+            builder.add(org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag.END_PORTAL);
+        }
+        PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(respawnPlayer, location, isBedSpawn, isAnchorSpawn, reason, builder);
+        // Paper end - respawn flags
+        this.level().getCraftServer().getPluginManager().callEvent(respawnEvent);
+        // Spigot Start
+        if (this.connection.isDisconnected()) {
+            return null;
         }
+        // Spigot End
+
+        location = respawnEvent.getRespawnLocation();
+
+        return new DimensionTransition(((CraftWorld) location.getWorld()).getHandle(), CraftLocation.toVec3D(location), dimensionTransition.speed(), location.getYaw(), location.getPitch(), dimensionTransition.missingRespawnBlock(), dimensionTransition.postDimensionTransition(), dimensionTransition.cause());
+        // CraftBukkit end
     }
 
-    public static Optional<ServerPlayer.RespawnPosAngle> findRespawnAndUseSpawnBlock(
-        ServerLevel world, BlockPos pos, float spawnAngle, boolean spawnForced, boolean alive
-    ) {
-        BlockState blockState = world.getBlockState(pos);
-        Block block = blockState.getBlock();
-        if (block instanceof RespawnAnchorBlock && (spawnForced || blockState.getValue(RespawnAnchorBlock.CHARGE) > 0) && RespawnAnchorBlock.canSetSpawn(world)
-            )
-         {
+    public static Optional<ServerPlayer.RespawnPosAngle> findRespawnAndUseSpawnBlock(ServerLevel world, BlockPos pos, float spawnAngle, boolean spawnForced, boolean alive) {
+        BlockState iblockdata = world.getBlockState(pos);
+        Block block = iblockdata.getBlock();
+
+        if (block instanceof RespawnAnchorBlock && (spawnForced || (Integer) iblockdata.getValue(RespawnAnchorBlock.CHARGE) > 0) && RespawnAnchorBlock.canSetSpawn(world)) {
             Optional<Vec3> optional = RespawnAnchorBlock.findStandUpPosition(EntityType.PLAYER, world, pos);
+
             if (!spawnForced && !alive && optional.isPresent()) {
-                world.setBlock(pos, blockState.setValue(RespawnAnchorBlock.CHARGE, Integer.valueOf(blockState.getValue(RespawnAnchorBlock.CHARGE) - 1)), 3);
+                world.setBlock(pos, (BlockState) iblockdata.setValue(RespawnAnchorBlock.CHARGE, (Integer) iblockdata.getValue(RespawnAnchorBlock.CHARGE) - 1), 3);
             }
 
-            return optional.map(respawnPos -> ServerPlayer.RespawnPosAngle.of(respawnPos, pos));
+            return optional.map((vec3d) -> {
+                return ServerPlayer.RespawnPosAngle.of(vec3d, pos, false, true); // CraftBukkit
+            });
         } else if (block instanceof BedBlock && BedBlock.canSetSpawn(world)) {
-            return BedBlock.findStandUpPosition(EntityType.PLAYER, world, pos, blockState.getValue(BedBlock.FACING), spawnAngle)
-                .map(respawnPos -> ServerPlayer.RespawnPosAngle.of(respawnPos, pos));
+            return BedBlock.findStandUpPosition(EntityType.PLAYER, world, pos, (Direction) iblockdata.getValue(BedBlock.FACING), spawnAngle).map((vec3d) -> {
+                return ServerPlayer.RespawnPosAngle.of(vec3d, pos, true, false); // CraftBukkit
+            });
         } else if (!spawnForced) {
             return Optional.empty();
         } else {
-            boolean bl = block.isPossibleToRespawnInThis(blockState);
-            BlockState blockState2 = world.getBlockState(pos.above());
-            boolean bl2 = blockState2.getBlock().isPossibleToRespawnInThis(blockState2);
-            return bl && bl2
-                ? Optional.of(
-                    new ServerPlayer.RespawnPosAngle(new Vec3((double)pos.getX() + 0.5, (double)pos.getY() + 0.1, (double)pos.getZ() + 0.5), spawnAngle)
-                )
-                : Optional.empty();
+            boolean flag2 = block.isPossibleToRespawnInThis(iblockdata);
+            BlockState iblockdata1 = world.getBlockState(pos.above());
+            boolean flag3 = iblockdata1.getBlock().isPossibleToRespawnInThis(iblockdata1);
+
+            return flag2 && flag3 ? Optional.of(new ServerPlayer.RespawnPosAngle(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY() + 0.1D, (double) pos.getZ() + 0.5D), spawnAngle, false, false)) : Optional.empty(); // CraftBukkit
         }
     }
 
@@ -841,11 +1317,13 @@
             this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 0.0F));
             this.seenCredits = true;
         }
+
     }
 
     @Nullable
     @Override
     public Entity changeDimension(DimensionTransition teleportTarget) {
+        if (this.isSleeping()) return null; // CraftBukkit - SPIGOT-3154
         if (this.isRemoved()) {
             return null;
         } else {
@@ -853,65 +1331,150 @@
                 this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F));
             }
 
-            ServerLevel serverLevel = teleportTarget.newLevel();
-            ServerLevel serverLevel2 = this.serverLevel();
-            ResourceKey<Level> resourceKey = serverLevel2.dimension();
-            if (serverLevel.dimension() == resourceKey) {
-                this.connection.teleport(teleportTarget.pos().x, teleportTarget.pos().y, teleportTarget.pos().z, teleportTarget.yRot(), teleportTarget.xRot());
+            ServerLevel worldserver = teleportTarget.newLevel();
+            ServerLevel worldserver1 = this.serverLevel();
+            // CraftBukkit start
+            ResourceKey<LevelStem> resourcekey = worldserver1.getTypeKey();
+
+            if (worldserver != null && worldserver.dimension() == worldserver1.dimension()) { // CraftBukkit
+                // Paper start - gateway-specific teleport event
+                if (this.portalProcess != null && this.portalProcess.isSamePortal(((net.minecraft.world.level.block.EndGatewayBlock) net.minecraft.world.level.block.Blocks.END_GATEWAY)) && this.serverLevel().getBlockEntity(this.portalProcess.getEntryPosition()) instanceof net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity theEndGatewayBlockEntity) {
+                    Location to = CraftLocation.toBukkit(teleportTarget.pos(), this.serverLevel().getWorld(), teleportTarget.yRot(), teleportTarget.xRot());
+                    final com.destroystokyo.paper.event.player.PlayerTeleportEndGatewayEvent event = new com.destroystokyo.paper.event.player.PlayerTeleportEndGatewayEvent(this.getBukkitEntity(), this.getBukkitEntity().getLocation(), to, new org.bukkit.craftbukkit.block.CraftEndGateway(to.getWorld(), theEndGatewayBlockEntity));
+                    if (!event.callEvent() || event.getTo() == null) {
+                        return null;
+                    }
+                    this.connection.teleport(event.getTo());
+                } else {
+                    // Paper end - gateway-specific teleport event
+                boolean result = this.connection.teleport(teleportTarget.pos().x, teleportTarget.pos().y, teleportTarget.pos().z, teleportTarget.yRot(), teleportTarget.xRot(), teleportTarget.cause());
+                if (!result) {
+                    return null;
+                }
+                // CraftBukkit end
+                } // Paper
                 this.connection.resetPosition();
                 teleportTarget.postDimensionTransition().onTransition(this);
                 return this;
             } else {
+                // CraftBukkit start
+                /*
                 this.isChangingDimension = true;
-                LevelData levelData = serverLevel.getLevelData();
-                this.connection.send(new ClientboundRespawnPacket(this.createCommonSpawnInfo(serverLevel), (byte)3));
-                this.connection.send(new ClientboundChangeDifficultyPacket(levelData.getDifficulty(), levelData.isDifficultyLocked()));
-                PlayerList playerList = this.server.getPlayerList();
-                playerList.sendPlayerPermissionLevel(this);
-                serverLevel2.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
+                WorldData worlddata = worldserver.getLevelData();
+
+                this.connection.send(new PacketPlayOutRespawn(this.createCommonSpawnInfo(worldserver), (byte) 3));
+                this.connection.send(new PacketPlayOutServerDifficulty(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
+                PlayerList playerlist = this.server.getPlayerList();
+
+                playerlist.sendPlayerPermissionLevel(this);
+                worldserver1.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
                 this.unsetRemoved();
-                serverLevel2.getProfiler().push("moving");
-                if (resourceKey == Level.OVERWORLD && serverLevel.dimension() == Level.NETHER) {
+                */
+                // CraftBukkit end
+                worldserver1.getProfiler().push("moving");
+                if (worldserver != null && resourcekey == LevelStem.OVERWORLD && worldserver.getTypeKey() == LevelStem.NETHER) { // CraftBukkit - empty to fall through to null to event
                     this.enteredNetherPosition = this.position();
                 }
+                // CraftBukkit start
+                Location enter = this.getBukkitEntity().getLocation();
+                Location exit =/* (worldserver == null) ? null : // Paper - always non-null */CraftLocation.toBukkit(teleportTarget.pos(), worldserver.getWorld(), teleportTarget.yRot(), teleportTarget.xRot());
+                PlayerTeleportEvent tpEvent = new PlayerTeleportEvent(this.getBukkitEntity(), enter, exit, teleportTarget.cause());
+                Bukkit.getServer().getPluginManager().callEvent(tpEvent);
+                if (tpEvent.isCancelled() || tpEvent.getTo() == null) {
+                    return null;
+                }
+                exit = tpEvent.getTo();
+                worldserver = ((CraftWorld) exit.getWorld()).getHandle();
+                // CraftBukkit end
 
-                serverLevel2.getProfiler().pop();
-                serverLevel2.getProfiler().push("placing");
-                this.setServerLevel(serverLevel);
-                this.connection.teleport(teleportTarget.pos().x, teleportTarget.pos().y, teleportTarget.pos().z, teleportTarget.yRot(), teleportTarget.xRot());
+                worldserver1.getProfiler().pop();
+                worldserver1.getProfiler().push("placing");
+                // CraftBukkit start
+                this.isChangingDimension = true; // CraftBukkit - Set teleport invulnerability only if player changing worlds
+                LevelData worlddata = worldserver.getLevelData();
+
+                this.connection.send(new ClientboundRespawnPacket(this.createCommonSpawnInfo(worldserver), (byte) 3));
+                this.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
+                PlayerList playerlist = this.server.getPlayerList();
+
+                playerlist.sendPlayerPermissionLevel(this);
+                worldserver1.removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
+                this.unsetRemoved();
+                // CraftBukkit end
+                this.setServerLevel(worldserver);
+                this.connection.teleport(exit); // CraftBukkit - use internal teleport without event
                 this.connection.resetPosition();
-                serverLevel.addDuringTeleport(this);
-                serverLevel2.getProfiler().pop();
-                this.triggerDimensionChangeTriggers(serverLevel2);
+                worldserver.addDuringTeleport(this);
+                worldserver1.getProfiler().pop();
+                this.triggerDimensionChangeTriggers(worldserver1);
                 this.connection.send(new ClientboundPlayerAbilitiesPacket(this.getAbilities()));
-                playerList.sendLevelInfo(this, serverLevel);
-                playerList.sendAllPlayerInfo(this);
-                playerList.sendActivePlayerEffects(this);
+                playerlist.sendLevelInfo(this, worldserver);
+                playerlist.sendAllPlayerInfo(this);
+                playerlist.sendActivePlayerEffects(this);
                 teleportTarget.postDimensionTransition().onTransition(this);
                 this.lastSentExp = -1;
                 this.lastSentHealth = -1.0F;
                 this.lastSentFood = -1;
+
+                // CraftBukkit start
+                PlayerChangedWorldEvent changeEvent = new PlayerChangedWorldEvent(this.getBukkitEntity(), worldserver1.getWorld());
+                this.level().getCraftServer().getPluginManager().callEvent(changeEvent);
+                // CraftBukkit end
+                // Paper start - Reset shield blocking on dimension change
+                if (this.isBlocking()) {
+                    this.stopUsingItem();
+                }
+                // Paper end - Reset shield blocking on dimension change
                 return this;
             }
         }
     }
 
+    // CraftBukkit start
+    @Override
+    public CraftPortalEvent callPortalEvent(Entity entity, Location exit, TeleportCause cause, int searchRadius, int creationRadius) {
+        Location enter = this.getBukkitEntity().getLocation();
+        PlayerPortalEvent event = new PlayerPortalEvent(this.getBukkitEntity(), enter, exit, cause, searchRadius, true, creationRadius);
+        Bukkit.getServer().getPluginManager().callEvent(event);
+        if (event.isCancelled() || event.getTo() == null || event.getTo().getWorld() == null) {
+            return null;
+        }
+        return new CraftPortalEvent(event);
+    }
+    // CraftBukkit end
+
     public void triggerDimensionChangeTriggers(ServerLevel origin) {
-        ResourceKey<Level> resourceKey = origin.dimension();
-        ResourceKey<Level> resourceKey2 = this.level().dimension();
-        CriteriaTriggers.CHANGED_DIMENSION.trigger(this, resourceKey, resourceKey2);
-        if (resourceKey == Level.NETHER && resourceKey2 == Level.OVERWORLD && this.enteredNetherPosition != null) {
+        ResourceKey<Level> resourcekey = origin.dimension();
+        ResourceKey<Level> resourcekey1 = this.level().dimension();
+        // CraftBukkit start
+        ResourceKey<Level> maindimensionkey = CraftDimensionUtil.getMainDimensionKey(origin);
+        ResourceKey<Level> maindimensionkey1 = CraftDimensionUtil.getMainDimensionKey(this.level());
+
+        // Paper start - Add option for strict advancement dimension checks
+        if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.strictAdvancementDimensionCheck) {
+            maindimensionkey = resourcekey;
+            maindimensionkey1 = resourcekey1;
+        }
+        // Paper end - Add option for strict advancement dimension checks
+        CriteriaTriggers.CHANGED_DIMENSION.trigger(this, maindimensionkey, maindimensionkey1);
+        if (maindimensionkey != resourcekey || maindimensionkey1 != resourcekey1) {
+            CriteriaTriggers.CHANGED_DIMENSION.trigger(this, resourcekey, resourcekey1);
+        }
+
+        if (maindimensionkey == Level.NETHER && maindimensionkey1 == Level.OVERWORLD && this.enteredNetherPosition != null) {
+            // CraftBukkit end
             CriteriaTriggers.NETHER_TRAVEL.trigger(this, this.enteredNetherPosition);
         }
 
-        if (resourceKey2 != Level.NETHER) {
+        if (maindimensionkey1 != Level.NETHER) { // CraftBukkit
             this.enteredNetherPosition = null;
         }
+
     }
 
     @Override
     public boolean broadcastToPlayer(ServerPlayer spectator) {
-        return spectator.isSpectator() ? this.getCamera() == this : !this.isSpectator() && super.broadcastToPlayer(spectator);
+        return spectator.isSpectator() ? this.getCamera() == this : (this.isSpectator() ? false : super.broadcastToPlayer(spectator));
     }
 
     @Override
@@ -920,49 +1483,77 @@
         this.containerMenu.broadcastChanges();
     }
 
-    @Override
-    public Either<Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos pos) {
-        Direction direction = this.level().getBlockState(pos).getValue(HorizontalDirectionalBlock.FACING);
-        if (this.isSleeping() || !this.isAlive()) {
-            return Either.left(Player.BedSleepingProblem.OTHER_PROBLEM);
-        } else if (!this.level().dimensionType().natural()) {
-            return Either.left(Player.BedSleepingProblem.NOT_POSSIBLE_HERE);
-        } else if (!this.bedInRange(pos, direction)) {
-            return Either.left(Player.BedSleepingProblem.TOO_FAR_AWAY);
-        } else if (this.bedBlocked(pos, direction)) {
-            return Either.left(Player.BedSleepingProblem.OBSTRUCTED);
-        } else {
-            this.setRespawnPosition(this.level().dimension(), pos, this.getYRot(), false, true);
-            if (this.level().isDay()) {
-                return Either.left(Player.BedSleepingProblem.NOT_POSSIBLE_NOW);
+    // CraftBukkit start - moved bed result checks from below into separate method
+    private Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> getBedResult(BlockPos blockposition, Direction enumdirection) {
+        if (!this.isSleeping() && this.isAlive()) {
+            if (!this.level().dimensionType().natural() || !this.level().dimensionType().bedWorks()) {
+                return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.NOT_POSSIBLE_HERE);
+            } else if (!this.bedInRange(blockposition, enumdirection)) {
+                return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.TOO_FAR_AWAY);
+            } else if (this.bedBlocked(blockposition, enumdirection)) {
+                return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.OBSTRUCTED);
             } else {
-                if (!this.isCreative()) {
-                    double d = 8.0;
-                    double e = 5.0;
-                    Vec3 vec3 = Vec3.atBottomCenterOf(pos);
-                    List<Monster> list = this.level()
-                        .getEntitiesOfClass(
-                            Monster.class,
-                            new AABB(vec3.x() - 8.0, vec3.y() - 5.0, vec3.z() - 8.0, vec3.x() + 8.0, vec3.y() + 5.0, vec3.z() + 8.0),
-                            entity -> entity.isPreventingPlayerRest(this)
-                        );
-                    if (!list.isEmpty()) {
-                        return Either.left(Player.BedSleepingProblem.NOT_SAFE);
+                this.setRespawnPosition(this.level().dimension(), blockposition, this.getYRot(), false, true, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.BED); // Paper - Add PlayerSetSpawnEvent
+                if (this.level().isDay()) {
+                    return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.NOT_POSSIBLE_NOW);
+                } else {
+                    if (!this.isCreative()) {
+                        double d0 = 8.0D;
+                        double d1 = 5.0D;
+                        Vec3 vec3d = Vec3.atBottomCenterOf(blockposition);
+                        List<Monster> list = this.level().getEntitiesOfClass(Monster.class, new AABB(vec3d.x() - 8.0D, vec3d.y() - 5.0D, vec3d.z() - 8.0D, vec3d.x() + 8.0D, vec3d.y() + 5.0D, vec3d.z() + 8.0D), (entitymonster) -> {
+                            return entitymonster.isPreventingPlayerRest(this);
+                        });
+
+                        if (!list.isEmpty()) {
+                            return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.NOT_SAFE);
+                        }
                     }
-                }
 
-                Either<Player.BedSleepingProblem, Unit> either = super.startSleepInBed(pos).ifRight(unit -> {
-                    this.awardStat(Stats.SLEEP_IN_BED);
-                    CriteriaTriggers.SLEPT_IN_BED.trigger(this);
-                });
-                if (!this.serverLevel().canSleepThroughNights()) {
-                    this.displayClientMessage(Component.translatable("sleep.not_possible"), true);
+                    return Either.right(Unit.INSTANCE);
                 }
+            }
+        } else {
+            return Either.left(net.minecraft.world.entity.player.Player.BedSleepingProblem.OTHER_PROBLEM);
+        }
+    }
 
-                ((ServerLevel)this.level()).updateSleepingPlayerList();
-                return either;
+    @Override
+    public Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> startSleepInBed(BlockPos blockposition, boolean force) {
+        Direction enumdirection = (Direction) this.level().getBlockState(blockposition).getValue(HorizontalDirectionalBlock.FACING);
+        Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> bedResult = this.getBedResult(blockposition, enumdirection);
+
+        if (bedResult.left().orElse(null) == net.minecraft.world.entity.player.Player.BedSleepingProblem.OTHER_PROBLEM) {
+            return bedResult; // return immediately if the result is not bypassable by plugins
+        }
+
+        if (force) {
+            bedResult = Either.right(Unit.INSTANCE);
+        }
+
+        bedResult = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBedEnterEvent(this, blockposition, bedResult);
+        if (bedResult.left().isPresent()) {
+            return bedResult;
+        }
+
+        {
+            {
+                {
+                    Either<net.minecraft.world.entity.player.Player.BedSleepingProblem, Unit> either = super.startSleepInBed(blockposition, force).ifRight((unit) -> {
+                        this.awardStat(Stats.SLEEP_IN_BED);
+                        CriteriaTriggers.SLEPT_IN_BED.trigger(this);
+                    });
+
+                    if (!this.serverLevel().canSleepThroughNights()) {
+                        this.displayClientMessage(Component.translatable("sleep.not_possible"), true);
+                    }
+
+                    ((ServerLevel) this.level()).updateSleepingPlayerList();
+                    return either;
+                }
             }
         }
+        // CraftBukkit end
     }
 
     @Override
@@ -976,25 +1567,46 @@
     }
 
     private boolean isReachableBedBlock(BlockPos pos) {
-        Vec3 vec3 = Vec3.atBottomCenterOf(pos);
-        return Math.abs(this.getX() - vec3.x()) <= 3.0 && Math.abs(this.getY() - vec3.y()) <= 2.0 && Math.abs(this.getZ() - vec3.z()) <= 3.0;
+        Vec3 vec3d = Vec3.atBottomCenterOf(pos);
+
+        return Math.abs(this.getX() - vec3d.x()) <= 3.0D && Math.abs(this.getY() - vec3d.y()) <= 2.0D && Math.abs(this.getZ() - vec3d.z()) <= 3.0D;
     }
 
     private boolean bedBlocked(BlockPos pos, Direction direction) {
-        BlockPos blockPos = pos.above();
-        return !this.freeAt(blockPos) || !this.freeAt(blockPos.relative(direction.getOpposite()));
+        BlockPos blockposition1 = pos.above();
+
+        return !this.freeAt(blockposition1) || !this.freeAt(blockposition1.relative(direction.getOpposite()));
     }
 
     @Override
     public void stopSleepInBed(boolean skipSleepTimer, boolean updateSleepingPlayers) {
+        if (!this.isSleeping()) return; // CraftBukkit - Can't leave bed if not in one!
+        // CraftBukkit start - fire PlayerBedLeaveEvent
+        CraftPlayer player = this.getBukkitEntity();
+        BlockPos bedPosition = this.getSleepingPos().orElse(null);
+
+        org.bukkit.block.Block bed;
+        if (bedPosition != null) {
+            bed = this.level().getWorld().getBlockAt(bedPosition.getX(), bedPosition.getY(), bedPosition.getZ());
+        } else {
+            bed = this.level().getWorld().getBlockAt(player.getLocation());
+        }
+
+        PlayerBedLeaveEvent event = new PlayerBedLeaveEvent(player, bed, true);
+        this.level().getCraftServer().getPluginManager().callEvent(event);
+        if (event.isCancelled()) {
+            return;
+        }
+        // CraftBukkit end
         if (this.isSleeping()) {
             this.serverLevel().getChunkSource().broadcastAndSend(this, new ClientboundAnimatePacket(this, 2));
         }
 
         super.stopSleepInBed(skipSleepTimer, updateSleepingPlayers);
         if (this.connection != null) {
-            this.connection.teleport(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
+            this.connection.teleport(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), TeleportCause.EXIT_BED); // CraftBukkit
         }
+
     }
 
     @Override
@@ -1005,33 +1617,35 @@
 
     @Override
     public boolean isInvulnerableTo(DamageSource damageSource) {
-        return super.isInvulnerableTo(damageSource) || this.isChangingDimension();
+        return super.isInvulnerableTo(damageSource) || this.isChangingDimension()  || !this.level().paperConfig().collisions.allowPlayerCrammingDamage && damageSource == damageSources().cramming(); // Paper - disable player cramming
     }
 
     @Override
-    protected void checkFallDamage(double heightDifference, boolean onGround, BlockState state, BlockPos landedPosition) {
-    }
+    protected void checkFallDamage(double heightDifference, boolean onGround, BlockState state, BlockPos landedPosition) {}
 
     @Override
     protected void onChangedBlock(ServerLevel world, BlockPos pos) {
         if (!this.isSpectator()) {
             super.onChangedBlock(world, pos);
         }
+
     }
 
     public void doCheckFallDamage(double xDifference, double yDifference, double zDifference, boolean onGround) {
         if (!this.touchingUnloadedChunk()) {
             this.checkSupportingBlock(onGround, new Vec3(xDifference, yDifference, zDifference));
-            BlockPos blockPos = this.getOnPosLegacy();
-            BlockState blockState = this.level().getBlockState(blockPos);
+            BlockPos blockposition = this.getOnPosLegacy();
+            BlockState iblockdata = this.level().getBlockState(blockposition);
+
             if (this.spawnExtraParticlesOnFall && onGround && this.fallDistance > 0.0F) {
-                Vec3 vec3 = blockPos.getCenter().add(0.0, 0.5, 0.0);
-                int i = (int)Mth.clamp(50.0F * this.fallDistance, 0.0F, 200.0F);
-                this.serverLevel().sendParticles(new BlockParticleOption(ParticleTypes.BLOCK, blockState), vec3.x, vec3.y, vec3.z, i, 0.3F, 0.3F, 0.3F, 0.15F);
+                Vec3 vec3d = blockposition.getCenter().add(0.0D, 0.5D, 0.0D);
+                int i = (int) Mth.clamp(50.0F * this.fallDistance, 0.0F, 200.0F);
+
+                this.serverLevel().sendParticles(new BlockParticleOption(ParticleTypes.BLOCK, iblockdata), vec3d.x, vec3d.y, vec3d.z, i, 0.30000001192092896D, 0.30000001192092896D, 0.30000001192092896D, 0.15000000596046448D);
                 this.spawnExtraParticlesOnFall = false;
             }
 
-            super.checkFallDamage(yDifference, onGround, blockState, blockPos);
+            super.checkFallDamage(yDifference, onGround, iblockdata, blockposition);
         }
     }
 
@@ -1048,6 +1662,7 @@
         if (this.level().tickRateManager().runsNormally()) {
             super.pushEntities();
         }
+
     }
 
     @Override
@@ -1056,8 +1671,9 @@
         this.connection.send(new ClientboundOpenSignEditorPacket(sign.getBlockPos(), front));
     }
 
-    public void nextContainerCounter() {
+    public int nextContainerCounter() { // CraftBukkit - void -> int
         this.containerCounter = this.containerCounter % 100 + 1;
+        return this.containerCounter; // CraftBukkit
     }
 
     @Override
@@ -1065,23 +1681,56 @@
         if (factory == null) {
             return OptionalInt.empty();
         } else {
+            // CraftBukkit start - SPIGOT-6552: Handle inventory closing in CraftEventFactory#callInventoryOpenEvent(...)
+            /*
             if (this.containerMenu != this.inventoryMenu) {
                 this.closeContainer();
             }
+            */
+            // CraftBukkit end
 
             this.nextContainerCounter();
-            AbstractContainerMenu abstractContainerMenu = factory.createMenu(this.containerCounter, this.getInventory(), this);
-            if (abstractContainerMenu == null) {
+            AbstractContainerMenu container = factory.createMenu(this.containerCounter, this.getInventory(), this);
+
+            Component title = null; // Paper - Add titleOverride to InventoryOpenEvent
+            // CraftBukkit start - Inventory open hook
+            if (container != null) {
+                container.setTitle(factory.getDisplayName());
+
+                boolean cancelled = false;
+                // Paper start - Add titleOverride to InventoryOpenEvent
+                final com.mojang.datafixers.util.Pair<net.kyori.adventure.text.Component, AbstractContainerMenu> result = CraftEventFactory.callInventoryOpenEventWithTitle(this, container, cancelled);
+                container = result.getSecond();
+                title = PaperAdventure.asVanilla(result.getFirst());
+                // Paper end - Add titleOverride to InventoryOpenEvent
+                if (container == null && !cancelled) { // Let pre-cancelled events fall through
+                    // SPIGOT-5263 - close chest if cancelled
+                    if (factory instanceof Container) {
+                        ((Container) factory).stopOpen(this);
+                    } else if (factory instanceof ChestBlock.DoubleInventory) {
+                        // SPIGOT-5355 - double chests too :(
+                        ((ChestBlock.DoubleInventory) factory).inventorylargechest.stopOpen(this);
+                        // Paper start - Fix InventoryOpenEvent cancellation
+                    } else if (!this.enderChestInventory.isActiveChest(null)) {
+                        this.enderChestInventory.stopOpen(this);
+                        // Paper end - Fix InventoryOpenEvent cancellation
+                    }
+                    return OptionalInt.empty();
+                }
+            }
+            // CraftBukkit end
+            if (container == null) {
                 if (this.isSpectator()) {
                     this.displayClientMessage(Component.translatable("container.spectatorCantOpen").withStyle(ChatFormatting.RED), true);
                 }
 
                 return OptionalInt.empty();
             } else {
-                this.connection
-                    .send(new ClientboundOpenScreenPacket(abstractContainerMenu.containerId, abstractContainerMenu.getType(), factory.getDisplayName()));
-                this.initMenu(abstractContainerMenu);
-                this.containerMenu = abstractContainerMenu;
+                // CraftBukkit start
+                this.containerMenu = container;
+                if (!this.isImmobile()) this.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(), Objects.requireNonNullElseGet(title, container::getTitle))); // Paper - Add titleOverride to InventoryOpenEvent
+                // CraftBukkit end
+                this.initMenu(container);
                 return OptionalInt.of(this.containerCounter);
             }
         }
@@ -1094,14 +1743,26 @@
 
     @Override
     public void openHorseInventory(AbstractHorse horse, Container inventory) {
+        // CraftBukkit start - Inventory open hook
+        this.nextContainerCounter();
+        AbstractContainerMenu container = new HorseInventoryMenu(this.containerCounter, this.getInventory(), inventory, horse, horse.getInventoryColumns());
+        container.setTitle(horse.getDisplayName());
+        container = CraftEventFactory.callInventoryOpenEvent(this, container);
+
+        if (container == null) {
+            inventory.stopOpen(this);
+            return;
+        }
+        // CraftBukkit end
         if (this.containerMenu != this.inventoryMenu) {
-            this.closeContainer();
+            this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.OPEN_NEW); // Paper - Inventory close reason
         }
 
-        this.nextContainerCounter();
+        // this.nextContainerCounter(); // CraftBukkit - moved up
         int i = horse.getInventoryColumns();
+
         this.connection.send(new ClientboundHorseScreenOpenPacket(this.containerCounter, i, horse.getId()));
-        this.containerMenu = new HorseInventoryMenu(this.containerCounter, this.getInventory(), inventory, horse, i);
+        this.containerMenu = container; // CraftBukkit
         this.initMenu(this.containerMenu);
     }
 
@@ -1114,6 +1775,7 @@
 
             this.connection.send(new ClientboundOpenBookPacket(hand));
         }
+
     }
 
     @Override
@@ -1123,9 +1785,28 @@
 
     @Override
     public void closeContainer() {
+        // Paper start - Inventory close reason
+        this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNKNOWN);
+    }
+    @Override
+    public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
+        CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
+        // Paper end - Inventory close reason
         this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
         this.doCloseContainer();
     }
+    // Paper start - special close for unloaded inventory
+    @Override
+    public void closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
+        // copied from above
+        CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit
+        // Paper end
+        // copied from below
+        this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId));
+        this.containerMenu = this.inventoryMenu;
+        // do not run close logic
+    }
+    // Paper end - special close for unloaded inventory
 
     @Override
     public void doCloseContainer() {
@@ -1145,82 +1826,99 @@
             }
 
             this.jumping = jumping;
+            // CraftBukkit start
+            if (sneaking != this.isShiftKeyDown()) {
+                PlayerToggleSneakEvent event = new PlayerToggleSneakEvent(this.getBukkitEntity(), sneaking);
+                this.server.server.getPluginManager().callEvent(event);
+
+                if (event.isCancelled()) {
+                    return;
+                }
+            }
+            // CraftBukkit end
             this.setShiftKeyDown(sneaking);
         }
+
     }
 
     @Override
     public void travel(Vec3 movementInput) {
-        double d = this.getX();
-        double e = this.getY();
-        double f = this.getZ();
+        double d0 = this.getX();
+        double d1 = this.getY();
+        double d2 = this.getZ();
+
         super.travel(movementInput);
-        this.checkMovementStatistics(this.getX() - d, this.getY() - e, this.getZ() - f);
+        this.checkMovementStatistics(this.getX() - d0, this.getY() - d1, this.getZ() - d2);
     }
 
     @Override
     public void rideTick() {
-        double d = this.getX();
-        double e = this.getY();
-        double f = this.getZ();
+        double d0 = this.getX();
+        double d1 = this.getY();
+        double d2 = this.getZ();
+
         super.rideTick();
-        this.checkRidingStatistics(this.getX() - d, this.getY() - e, this.getZ() - f);
+        this.checkRidingStatistics(this.getX() - d0, this.getY() - d1, this.getZ() - d2);
     }
 
     public void checkMovementStatistics(double deltaX, double deltaY, double deltaZ) {
-        if (!this.isPassenger() && !didNotMove(deltaX, deltaY, deltaZ)) {
+        if (!this.isPassenger() && !ServerPlayer.didNotMove(deltaX, deltaY, deltaZ)) {
+            int i;
+
             if (this.isSwimming()) {
-                int i = Math.round((float)Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
+                i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
                 if (i > 0) {
                     this.awardStat(Stats.SWIM_ONE_CM, i);
-                    this.causeFoodExhaustion(0.01F * (float)i * 0.01F);
+                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.SWIM); // CraftBukkit - EntityExhaustionEvent // Spigot
                 }
             } else if (this.isEyeInFluid(FluidTags.WATER)) {
-                int j = Math.round((float)Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
-                if (j > 0) {
-                    this.awardStat(Stats.WALK_UNDER_WATER_ONE_CM, j);
-                    this.causeFoodExhaustion(0.01F * (float)j * 0.01F);
+                i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
+                if (i > 0) {
+                    this.awardStat(Stats.WALK_UNDER_WATER_ONE_CM, i);
+                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.WALK_UNDERWATER); // CraftBukkit - EntityExhaustionEvent // Spigot
                 }
             } else if (this.isInWater()) {
-                int k = Math.round((float)Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) * 100.0F);
-                if (k > 0) {
-                    this.awardStat(Stats.WALK_ON_WATER_ONE_CM, k);
-                    this.causeFoodExhaustion(0.01F * (float)k * 0.01F);
+                i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) * 100.0F);
+                if (i > 0) {
+                    this.awardStat(Stats.WALK_ON_WATER_ONE_CM, i);
+                    this.causeFoodExhaustion(this.level().spigotConfig.swimMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.WALK_ON_WATER); // CraftBukkit - EntityExhaustionEvent // Spigot
                 }
             } else if (this.onClimbable()) {
-                if (deltaY > 0.0) {
-                    this.awardStat(Stats.CLIMB_ONE_CM, (int)Math.round(deltaY * 100.0));
+                if (deltaY > 0.0D) {
+                    this.awardStat(Stats.CLIMB_ONE_CM, (int) Math.round(deltaY * 100.0D));
                 }
             } else if (this.onGround()) {
-                int l = Math.round((float)Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) * 100.0F);
-                if (l > 0) {
+                i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) * 100.0F);
+                if (i > 0) {
                     if (this.isSprinting()) {
-                        this.awardStat(Stats.SPRINT_ONE_CM, l);
-                        this.causeFoodExhaustion(0.1F * (float)l * 0.01F);
+                        this.awardStat(Stats.SPRINT_ONE_CM, i);
+                        this.causeFoodExhaustion(this.level().spigotConfig.sprintMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.SPRINT); // CraftBukkit - EntityExhaustionEvent // Spigot
                     } else if (this.isCrouching()) {
-                        this.awardStat(Stats.CROUCH_ONE_CM, l);
-                        this.causeFoodExhaustion(0.0F * (float)l * 0.01F);
+                        this.awardStat(Stats.CROUCH_ONE_CM, i);
+                        this.causeFoodExhaustion(this.level().spigotConfig.otherMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.CROUCH); // CraftBukkit - EntityExhaustionEvent // Spigot
                     } else {
-                        this.awardStat(Stats.WALK_ONE_CM, l);
-                        this.causeFoodExhaustion(0.0F * (float)l * 0.01F);
+                        this.awardStat(Stats.WALK_ONE_CM, i);
+                        this.causeFoodExhaustion(this.level().spigotConfig.otherMultiplier * (float) i * 0.01F, EntityExhaustionEvent.ExhaustionReason.WALK); // CraftBukkit - EntityExhaustionEvent // Spigot
                     }
                 }
             } else if (this.isFallFlying()) {
-                int m = Math.round((float)Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
-                this.awardStat(Stats.AVIATE_ONE_CM, m);
+                i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
+                this.awardStat(Stats.AVIATE_ONE_CM, i);
             } else {
-                int n = Math.round((float)Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) * 100.0F);
-                if (n > 25) {
-                    this.awardStat(Stats.FLY_ONE_CM, n);
+                i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) * 100.0F);
+                if (i > 25) {
+                    this.awardStat(Stats.FLY_ONE_CM, i);
                 }
             }
+
         }
     }
 
     private void checkRidingStatistics(double deltaX, double deltaY, double deltaZ) {
-        if (this.isPassenger() && !didNotMove(deltaX, deltaY, deltaZ)) {
-            int i = Math.round((float)Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
+        if (this.isPassenger() && !ServerPlayer.didNotMove(deltaX, deltaY, deltaZ)) {
+            int i = Math.round((float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 100.0F);
             Entity entity = this.getVehicle();
+
             if (entity instanceof AbstractMinecart) {
                 this.awardStat(Stats.MINECART_ONE_CM, i);
             } else if (entity instanceof Boat) {
@@ -1232,23 +1930,26 @@
             } else if (entity instanceof Strider) {
                 this.awardStat(Stats.STRIDER_ONE_CM, i);
             }
+
         }
     }
 
     private static boolean didNotMove(double deltaX, double deltaY, double deltaZ) {
-        return deltaX == 0.0 && deltaY == 0.0 && deltaZ == 0.0;
+        return deltaX == 0.0D && deltaY == 0.0D && deltaZ == 0.0D;
     }
 
     @Override
     public void awardStat(Stat<?> stat, int amount) {
         this.stats.increment(this, stat, amount);
-        this.getScoreboard().forAllObjectives(stat, this, score -> score.add(amount));
+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(stat, this, (scoreaccess) -> {
+            scoreaccess.add(amount);
+        });
     }
 
     @Override
     public void resetStat(Stat<?> stat) {
         this.stats.setValue(this, stat, 0);
-        this.getScoreboard().forAllObjectives(stat, this, ScoreAccess::reset);
+        this.level().getCraftServer().getScoreboardManager().forAllObjectives(stat, this, ScoreAccess::reset); // CraftBukkit - Get our scores instead
     }
 
     @Override
@@ -1263,8 +1964,11 @@
 
     @Override
     public void awardRecipesByKey(List<ResourceLocation> recipes) {
-        List<RecipeHolder<?>> list = recipes.stream().flatMap(recipe -> this.server.getRecipeManager().byKey(recipe).stream()).collect(Collectors.toList());
-        this.awardRecipes(list);
+        List<RecipeHolder<?>> list1 = (List) recipes.stream().flatMap((minecraftkey) -> {
+            return this.server.getRecipeManager().byKey(minecraftkey).stream();
+        }).collect(Collectors.toList());
+
+        this.awardRecipes(list1);
     }
 
     @Override
@@ -1281,9 +1985,17 @@
     public void disconnect() {
         this.disconnected = true;
         this.ejectPassengers();
+
+        // Paper start - Workaround vehicle not tracking the passenger disconnection dismount
+        if (this.isPassenger() && this.getVehicle() instanceof ServerPlayer) {
+            this.stopRiding();
+        }
+        // Paper end - Workaround vehicle not tracking the passenger disconnection dismount
+
         if (this.isSleeping()) {
             this.stopSleepInBed(true, false);
         }
+
     }
 
     public boolean hasDisconnected() {
@@ -1292,6 +2004,7 @@
 
     public void resetSentInfo() {
         this.lastSentHealth = -1.0E8F;
+        this.lastSentExp = -1; // CraftBukkit - Added to reset
     }
 
     @Override
@@ -1302,9 +2015,10 @@
     @Override
     public void completeUsingItem() {
         if (!this.useItem.isEmpty() && this.isUsingItem()) {
-            this.connection.send(new ClientboundEntityEventPacket(this, (byte)9));
+            this.connection.send(new ClientboundEntityEventPacket(this, (byte) 9));
             super.completeUsingItem();
         }
+
     }
 
     @Override
@@ -1314,8 +2028,9 @@
     }
 
     public void lookAt(EntityAnchorArgument.Anchor anchorPoint, Entity targetEntity, EntityAnchorArgument.Anchor targetAnchor) {
-        Vec3 vec3 = targetAnchor.apply(targetEntity);
-        super.lookAt(anchorPoint, vec3);
+        Vec3 vec3d = targetAnchor.apply(targetEntity);
+
+        super.lookAt(anchorPoint, vec3d);
         this.connection.send(new ClientboundPlayerLookAtPacket(anchorPoint, targetEntity, targetAnchor));
     }
 
@@ -1325,14 +2040,17 @@
         this.gameMode.setGameModeForPlayer(oldPlayer.gameMode.getGameModeForPlayer(), oldPlayer.gameMode.getPreviousGameModeForPlayer());
         this.onUpdateAbilities();
         this.getAttributes().assignBaseValues(oldPlayer.getAttributes());
-        this.setHealth(this.getMaxHealth());
+        // this.setHealth(this.getMaxHealth()); // CraftBukkit
         if (alive) {
             this.getInventory().replaceWith(oldPlayer.getInventory());
             this.setHealth(oldPlayer.getHealth());
             this.foodData = oldPlayer.foodData;
+            Iterator iterator = oldPlayer.getActiveEffects().iterator();
 
-            for (MobEffectInstance mobEffectInstance : oldPlayer.getActiveEffects()) {
-                this.addEffect(new MobEffectInstance(mobEffectInstance));
+            while (iterator.hasNext()) {
+                MobEffectInstance mobeffect = (MobEffectInstance) iterator.next();
+
+                // this.addEffect(new MobEffect(mobeffect)); // CraftBukkit
             }
 
             this.experienceLevel = oldPlayer.experienceLevel;
@@ -1350,11 +2068,11 @@
 
         this.enchantmentSeed = oldPlayer.enchantmentSeed;
         this.enderChestInventory = oldPlayer.enderChestInventory;
-        this.getEntityData().set(DATA_PLAYER_MODE_CUSTOMISATION, oldPlayer.getEntityData().get(DATA_PLAYER_MODE_CUSTOMISATION));
+        this.getEntityData().set(ServerPlayer.DATA_PLAYER_MODE_CUSTOMISATION, (Byte) oldPlayer.getEntityData().get(ServerPlayer.DATA_PLAYER_MODE_CUSTOMISATION));
         this.lastSentExp = -1;
         this.lastSentHealth = -1.0F;
         this.lastSentFood = -1;
-        this.recipeBook.copyOverData(oldPlayer.recipeBook);
+        // this.recipeBook.copyOverData(entityplayer.recipeBook); // CraftBukkit
         this.seenCredits = oldPlayer.seenCredits;
         this.enteredNetherPosition = oldPlayer.enteredNetherPosition;
         this.chunkTrackingView = oldPlayer.chunkTrackingView;
@@ -1390,7 +2108,7 @@
             this.levitationStartPos = null;
         }
 
-        CriteriaTriggers.EFFECTS_CHANGED.trigger(this, null);
+        CriteriaTriggers.EFFECTS_CHANGED.trigger(this, (Entity) null);
     }
 
     @Override
@@ -1405,20 +2123,27 @@
 
     @Override
     public boolean teleportTo(ServerLevel world, double destX, double destY, double destZ, Set<RelativeMovement> flags, float yaw, float pitch) {
-        ChunkPos chunkPos = new ChunkPos(BlockPos.containing(destX, destY, destZ));
-        world.getChunkSource().addRegionTicket(TicketType.POST_TELEPORT, chunkPos, 1, this.getId());
+        // CraftBukkit start
+        return this.teleportTo(world, destX, destY, destZ, flags, yaw, pitch, TeleportCause.UNKNOWN);
+    }
+
+    public boolean teleportTo(ServerLevel worldserver, double d0, double d1, double d2, Set<RelativeMovement> set, float f, float f1, TeleportCause cause) {
+        // CraftBukkit end
+        ChunkPos chunkcoordintpair = new ChunkPos(BlockPos.containing(d0, d1, d2));
+
+        worldserver.getChunkSource().addRegionTicket(TicketType.POST_TELEPORT, chunkcoordintpair, 1, this.getId());
         this.stopRiding();
         if (this.isSleeping()) {
             this.stopSleepInBed(true, true);
         }
 
-        if (world == this.level()) {
-            this.connection.teleport(destX, destY, destZ, yaw, pitch, flags);
+        if (worldserver == this.level()) {
+            this.connection.teleport(d0, d1, d2, f, f1, set, cause); // CraftBukkit
         } else {
-            this.teleportTo(world, destX, destY, destZ, yaw, pitch);
+            this.teleportTo(worldserver, d0, d1, d2, f, f1, cause); // CraftBukkit
         }
 
-        this.setYHeadRot(yaw);
+        this.setYHeadRot(f);
         return true;
     }
 
@@ -1447,29 +2172,38 @@
     }
 
     public ServerLevel serverLevel() {
-        return (ServerLevel)this.level();
+        return (ServerLevel) this.level();
     }
 
     public boolean setGameMode(GameType gameMode) {
-        boolean bl = this.isSpectator();
-        if (!this.gameMode.changeGameModeForPlayer(gameMode)) {
-            return false;
+        // Paper start - Expand PlayerGameModeChangeEvent
+        org.bukkit.event.player.PlayerGameModeChangeEvent event = this.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.UNKNOWN, null);
+        return event == null ? false : event.isCancelled();
+    }
+    @Nullable
+    public org.bukkit.event.player.PlayerGameModeChangeEvent setGameMode(GameType gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause cause, @Nullable net.kyori.adventure.text.Component message) {
+        boolean flag = this.isSpectator();
+
+        org.bukkit.event.player.PlayerGameModeChangeEvent event = this.gameMode.changeGameModeForPlayer(gameMode, cause, message);
+        if (event == null || event.isCancelled()) {
+            return null;
+            // Paper end - Expand PlayerGameModeChangeEvent
         } else {
-            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, (float)gameMode.getId()));
+            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, (float) gameMode.getId()));
             if (gameMode == GameType.SPECTATOR) {
                 this.removeEntitiesOnShoulder();
                 this.stopRiding();
                 EnchantmentHelper.stopLocationBasedEffects(this);
             } else {
                 this.setCamera(this);
-                if (bl) {
+                if (flag) {
                     EnchantmentHelper.runLocationChangedEffects(this.serverLevel(), this);
                 }
             }
 
             this.onUpdateAbilities();
             this.updateEffectVisibility();
-            return true;
+            return event; // Paper - Expand PlayerGameModeChangeEvent
         }
     }
 
@@ -1490,56 +2224,90 @@
 
     public void sendSystemMessage(Component message, boolean overlay) {
         if (this.acceptsSystemMessages(overlay)) {
-            this.connection
-                .send(
-                    new ClientboundSystemChatPacket(message, overlay),
-                    PacketSendListener.exceptionallySend(
-                        () -> {
-                            if (this.acceptsSystemMessages(false)) {
-                                int i = 256;
-                                String string = message.getString(256);
-                                Component component2 = Component.literal(string).withStyle(ChatFormatting.YELLOW);
-                                return new ClientboundSystemChatPacket(
-                                    Component.translatable("multiplayer.message_not_delivered", component2).withStyle(ChatFormatting.RED), false
-                                );
-                            } else {
-                                return null;
-                            }
-                        }
-                    )
-                );
+            this.connection.send(new ClientboundSystemChatPacket(message, overlay), PacketSendListener.exceptionallySend(() -> {
+                if (this.acceptsSystemMessages(false)) {
+                    boolean flag1 = true;
+                    String s = message.getString(256);
+                    MutableComponent ichatmutablecomponent = Component.literal(s).withStyle(ChatFormatting.YELLOW);
+
+                    return new ClientboundSystemChatPacket(Component.translatable("multiplayer.message_not_delivered", ichatmutablecomponent).withStyle(ChatFormatting.RED), false);
+                } else {
+                    return null;
+                }
+            }));
         }
     }
 
     public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params) {
+        // Paper start
+        this.sendChatMessage(message, filterMaskEnabled, params, null);
+    }
+    public void sendChatMessage(OutgoingChatMessage message, boolean filterMaskEnabled, ChatType.Bound params, @Nullable Component unsigned) {
+        // Paper end
         if (this.acceptsChatMessages()) {
-            message.sendToPlayer(this, filterMaskEnabled, params);
+            message.sendToPlayer(this, filterMaskEnabled, params, unsigned); // Paper
         }
+
     }
 
     public String getIpAddress() {
-        return this.connection.getRemoteAddress() instanceof InetSocketAddress inetSocketAddress
-            ? InetAddresses.toAddrString(inetSocketAddress.getAddress())
-            : "<unknown>";
+        SocketAddress socketaddress = this.connection.getRemoteAddress();
+
+        if (socketaddress instanceof InetSocketAddress inetsocketaddress) {
+            return InetAddresses.toAddrString(inetsocketaddress.getAddress());
+        } else {
+            return "<unknown>";
+        }
     }
 
+    // Paper start - Client option API
+    private java.util.Map<com.destroystokyo.paper.ClientOption<?>, ?> getClientOptionMap(String locale, int viewDistance, com.destroystokyo.paper.ClientOption.ChatVisibility chatVisibility, boolean chatColors, com.destroystokyo.paper.PaperSkinParts skinParts, org.bukkit.inventory.MainHand mainHand, boolean allowsServerListing, boolean textFilteringEnabled) {
+        java.util.Map<com.destroystokyo.paper.ClientOption<?>, Object> map = new java.util.HashMap<>();
+        map.put(com.destroystokyo.paper.ClientOption.LOCALE, locale);
+        map.put(com.destroystokyo.paper.ClientOption.VIEW_DISTANCE, viewDistance);
+        map.put(com.destroystokyo.paper.ClientOption.CHAT_VISIBILITY, chatVisibility);
+        map.put(com.destroystokyo.paper.ClientOption.CHAT_COLORS_ENABLED, chatColors);
+        map.put(com.destroystokyo.paper.ClientOption.SKIN_PARTS, skinParts);
+        map.put(com.destroystokyo.paper.ClientOption.MAIN_HAND, mainHand);
+        map.put(com.destroystokyo.paper.ClientOption.ALLOW_SERVER_LISTINGS, allowsServerListing);
+        map.put(com.destroystokyo.paper.ClientOption.TEXT_FILTERING_ENABLED, textFilteringEnabled);
+        return map;
+    }
+    // Paper end
+
     public void updateOptions(ClientInformation clientOptions) {
+        new com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent(getBukkitEntity(), getClientOptionMap(clientOptions.language(), clientOptions.viewDistance(), com.destroystokyo.paper.ClientOption.ChatVisibility.valueOf(clientOptions.chatVisibility().name()), clientOptions.chatColors(), new com.destroystokyo.paper.PaperSkinParts(clientOptions.modelCustomisation()), clientOptions.mainHand() == HumanoidArm.LEFT ? MainHand.LEFT : MainHand.RIGHT, clientOptions.allowsListing(), clientOptions.textFilteringEnabled())).callEvent(); // Paper - settings event
+        // CraftBukkit start
+        if (this.getMainArm() != clientOptions.mainHand()) {
+            PlayerChangedMainHandEvent event = new PlayerChangedMainHandEvent(this.getBukkitEntity(), this.getMainArm() == HumanoidArm.LEFT ? MainHand.LEFT : MainHand.RIGHT);
+            this.server.server.getPluginManager().callEvent(event);
+        }
+        if (this.language == null || !this.language.equals(clientOptions.language())) { // Paper
+            PlayerLocaleChangeEvent event = new PlayerLocaleChangeEvent(this.getBukkitEntity(), clientOptions.language());
+            this.server.server.getPluginManager().callEvent(event);
+        }
+        // CraftBukkit end
+        // Paper start - don't call options events on login
+        this.updateOptionsNoEvents(clientOptions);
+    }
+    public void updateOptionsNoEvents(ClientInformation clientOptions) {
+        // Paper end
         this.language = clientOptions.language();
+        this.adventure$locale = java.util.Objects.requireNonNullElse(net.kyori.adventure.translation.Translator.parseLocale(this.language), java.util.Locale.US); // Paper
         this.requestedViewDistance = clientOptions.viewDistance();
         this.chatVisibility = clientOptions.chatVisibility();
         this.canChatColor = clientOptions.chatColors();
         this.textFilteringEnabled = clientOptions.textFilteringEnabled();
         this.allowsListing = clientOptions.allowsListing();
-        this.getEntityData().set(DATA_PLAYER_MODE_CUSTOMISATION, (byte)clientOptions.modelCustomisation());
-        this.getEntityData().set(DATA_PLAYER_MAIN_HAND, (byte)clientOptions.mainHand().getId());
+        this.getEntityData().set(ServerPlayer.DATA_PLAYER_MODE_CUSTOMISATION, (byte) clientOptions.modelCustomisation());
+        this.getEntityData().set(ServerPlayer.DATA_PLAYER_MAIN_HAND, (byte) clientOptions.mainHand().getId());
     }
 
     public ClientInformation clientInformation() {
-        int i = this.getEntityData().get(DATA_PLAYER_MODE_CUSTOMISATION);
-        HumanoidArm humanoidArm = HumanoidArm.BY_ID.apply(this.getEntityData().get(DATA_PLAYER_MAIN_HAND));
-        return new ClientInformation(
-            this.language, this.requestedViewDistance, this.chatVisibility, this.canChatColor, i, humanoidArm, this.textFilteringEnabled, this.allowsListing
-        );
+        byte b0 = (Byte) this.getEntityData().get(ServerPlayer.DATA_PLAYER_MODE_CUSTOMISATION);
+        HumanoidArm enummainhand = (HumanoidArm) HumanoidArm.BY_ID.apply((Byte) this.getEntityData().get(ServerPlayer.DATA_PLAYER_MAIN_HAND));
+
+        return new ClientInformation(this.language, this.requestedViewDistance, this.chatVisibility, this.canChatColor, b0, enummainhand, this.textFilteringEnabled, this.allowsListing);
     }
 
     public boolean canChatInColor() {
@@ -1551,7 +2319,7 @@
     }
 
     private boolean acceptsSystemMessages(boolean overlay) {
-        return this.chatVisibility != ChatVisiblity.HIDDEN || overlay;
+        return this.chatVisibility == ChatVisiblity.HIDDEN ? overlay : true;
     }
 
     private boolean acceptsChatMessages() {
@@ -1591,18 +2359,39 @@
         } else {
             super.updateInvisibilityStatus();
         }
+
     }
 
     public Entity getCamera() {
-        return (Entity)(this.camera == null ? this : this.camera);
+        return (Entity) (this.camera == null ? this : this.camera);
     }
 
     public void setCamera(@Nullable Entity entity) {
-        Entity entity2 = this.getCamera();
-        this.camera = (Entity)(entity == null ? this : entity);
-        if (entity2 != this.camera) {
-            if (this.camera.level() instanceof ServerLevel serverLevel) {
-                this.teleportTo(serverLevel, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot());
+        Entity entity1 = this.getCamera();
+
+        this.camera = (Entity) (entity == null ? this : entity);
+        if (entity1 != this.camera) {
+            // Paper start - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
+            if (this.camera == this) {
+                com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent playerStopSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent(this.getBukkitEntity(), entity1.getBukkitEntity());
+                if (!playerStopSpectatingEntityEvent.callEvent()) {
+                    this.camera = entity1; // rollback camera entity again
+                    return;
+                }
+            } else {
+                com.destroystokyo.paper.event.player.PlayerStartSpectatingEntityEvent playerStartSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStartSpectatingEntityEvent(this.getBukkitEntity(), entity1.getBukkitEntity(), entity.getBukkitEntity());
+                if (!playerStartSpectatingEntityEvent.callEvent()) {
+                    this.camera = entity1; // rollback camera entity again
+                    return;
+                }
+            }
+            // Paper end - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
+            Level world = this.camera.level();
+
+            if (world instanceof ServerLevel) {
+                ServerLevel worldserver = (ServerLevel) world;
+
+                this.teleportTo(worldserver, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), TeleportCause.SPECTATE); // CraftBukkit
             }
 
             if (entity != null) {
@@ -1612,6 +2401,7 @@
             this.connection.send(new ClientboundSetCameraPacket(this.camera));
             this.connection.resetPosition();
         }
+
     }
 
     @Override
@@ -1619,6 +2409,7 @@
         if (!this.isChangingDimension) {
             super.processPortalCooldown();
         }
+
     }
 
     @Override
@@ -1628,6 +2419,7 @@
         } else {
             super.attack(target);
         }
+
     }
 
     public long getLastActionTime() {
@@ -1636,7 +2428,7 @@
 
     @Nullable
     public Component getTabListDisplayName() {
-        return null;
+        return this.listName; // CraftBukkit
     }
 
     @Override
@@ -1657,14 +2449,25 @@
         return this.advancements;
     }
 
+    // CraftBukkit start
     public void teleportTo(ServerLevel targetWorld, double x, double y, double z, float yaw, float pitch) {
+        this.teleportTo(targetWorld, x, y, z, yaw, pitch, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN);
+    }
+
+    public void teleportTo(ServerLevel worldserver, double d0, double d1, double d2, float f, float f1, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) {
+        // CraftBukkit end
         this.setCamera(this);
         this.stopRiding();
-        if (targetWorld == this.level()) {
-            this.connection.teleport(x, y, z, yaw, pitch);
+        /* CraftBukkit start - replace with bukkit handling for multi-world
+        if (worldserver == this.level()) {
+            this.connection.teleport(d0, d1, d2, f, f1);
         } else {
-            this.changeDimension(new DimensionTransition(targetWorld, new Vec3(x, y, z), Vec3.ZERO, yaw, pitch, DimensionTransition.DO_NOTHING));
+            this.changeDimension(new DimensionTransition(worldserver, new Vec3D(d0, d1, d2), Vec3D.ZERO, f, f1, DimensionTransition.DO_NOTHING));
         }
+        */
+        this.getBukkitEntity().teleport(new Location(worldserver.getWorld(), d0, d1, d2, f, f1), cause);
+        // CraftBukkit end
+
     }
 
     @Nullable
@@ -1688,11 +2491,44 @@
         this.setRespawnPosition(player.getRespawnDimension(), player.getRespawnPosition(), player.getRespawnAngle(), player.isRespawnForced(), false);
     }
 
+    @Deprecated // Paper - Add PlayerSetSpawnEvent
     public void setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos pos, float angle, boolean forced, boolean sendMessage) {
+        // Paper start - Add PlayerSetSpawnEvent
+        this.setRespawnPosition(dimension, pos, angle, forced, sendMessage, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.UNKNOWN);
+    }
+    @Deprecated
+    public boolean setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos pos, float angle, boolean forced, boolean sendMessage, PlayerSpawnChangeEvent.Cause cause) {
+        return this.setRespawnPosition(dimension, pos, angle, forced, sendMessage, cause == PlayerSpawnChangeEvent.Cause.RESET ?
+            com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN : com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.valueOf(cause.name()));
+    }
+    public boolean setRespawnPosition(ResourceKey<Level> dimension, @Nullable BlockPos pos, float angle, boolean forced, boolean sendMessage, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause cause) {
+        Location spawnLoc = null;
+        boolean willNotify = false;
         if (pos != null) {
-            boolean bl = pos.equals(this.respawnPosition) && dimension.equals(this.respawnDimension);
-            if (sendMessage && !bl) {
-                this.sendSystemMessage(Component.translatable("block.minecraft.set_spawn"));
+            boolean flag2 = pos.equals(this.respawnPosition) && dimension.equals(this.respawnDimension);
+            spawnLoc = io.papermc.paper.util.MCUtil.toLocation(this.getServer().getLevel(dimension), pos);
+            spawnLoc.setYaw(angle);
+            willNotify = sendMessage && !flag2;
+        }
+
+        PlayerSpawnChangeEvent dumbEvent = new PlayerSpawnChangeEvent(this.getBukkitEntity(), spawnLoc, forced,
+            cause == com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ? PlayerSpawnChangeEvent.Cause.RESET : PlayerSpawnChangeEvent.Cause.valueOf(cause.name()));
+        dumbEvent.callEvent();
+
+        com.destroystokyo.paper.event.player.PlayerSetSpawnEvent event = new com.destroystokyo.paper.event.player.PlayerSetSpawnEvent(this.getBukkitEntity(), cause, dumbEvent.getNewSpawn(), dumbEvent.isForced(), willNotify, willNotify ? net.kyori.adventure.text.Component.translatable("block.minecraft.set_spawn") : null);
+        event.setCancelled(dumbEvent.isCancelled());
+        if (!event.callEvent()) {
+            return false;
+        }
+        if (event.getLocation() != null) {
+            dimension = event.getLocation().getWorld() != null ? ((CraftWorld) event.getLocation().getWorld()).getHandle().dimension() : dimension;
+            pos = io.papermc.paper.util.MCUtil.toBlockPosition(event.getLocation());
+            angle = event.getLocation().getYaw();
+            forced = event.isForced();
+            // Paper end - Add PlayerSetSpawnEvent
+
+            if (event.willNotifyPlayer() && event.getNotification() != null) { // Paper - Add PlayerSetSpawnEvent
+                this.sendSystemMessage(PaperAdventure.asVanilla(event.getNotification())); // Paper - Add PlayerSetSpawnEvent
             }
 
             this.respawnPosition = pos;
@@ -1705,6 +2541,8 @@
             this.respawnAngle = 0.0F;
             this.respawnForced = false;
         }
+
+        return true; // Paper - Add PlayerSetSpawnEvent
     }
 
     public SectionPos getLastSectionPos() {
@@ -1725,31 +2563,35 @@
 
     @Override
     public void playNotifySound(SoundEvent sound, SoundSource category, float volume, float pitch) {
-        this.connection
-            .send(
-                new ClientboundSoundPacket(
-                    BuiltInRegistries.SOUND_EVENT.wrapAsHolder(sound), category, this.getX(), this.getY(), this.getZ(), volume, pitch, this.random.nextLong()
-                )
-            );
+        this.connection.send(new ClientboundSoundPacket(BuiltInRegistries.SOUND_EVENT.wrapAsHolder(sound), category, this.getX(), this.getY(), this.getZ(), volume, pitch, this.random.nextLong()));
     }
 
     @Override
     public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership) {
-        ItemEntity itemEntity = super.drop(stack, throwRandomly, retainOwnership);
-        if (itemEntity == null) {
+        // CraftBukkit start - SPIGOT-2942: Add boolean to call event
+        return this.drop(stack, throwRandomly, retainOwnership, true);
+    }
+
+    @Override
+    public ItemEntity drop(ItemStack itemstack, boolean flag, boolean flag1, boolean callEvent) {
+        ItemEntity entityitem = super.drop(itemstack, flag, flag1, callEvent);
+        // CraftBukkit end
+
+        if (entityitem == null) {
             return null;
         } else {
-            this.level().addFreshEntity(itemEntity);
-            ItemStack itemStack = itemEntity.getItem();
-            if (retainOwnership) {
-                if (!itemStack.isEmpty()) {
-                    this.awardStat(Stats.ITEM_DROPPED.get(itemStack.getItem()), stack.getCount());
+            this.level().addFreshEntity(entityitem);
+            ItemStack itemstack1 = entityitem.getItem();
+
+            if (flag1) {
+                if (!itemstack1.isEmpty()) {
+                    this.awardStat(Stats.ITEM_DROPPED.get(itemstack1.getItem()), itemstack1.getCount()); // Paper - Fix PlayerDropItemEvent using wrong item
                 }
 
                 this.awardStat(Stats.DROP);
             }
 
-            return itemEntity;
+            return entityitem;
         }
     }
 
@@ -1768,25 +2610,33 @@
     }
 
     private GameType calculateGameModeForNewPlayer(@Nullable GameType backupGameMode) {
-        GameType gameType = this.server.getForcedGameType();
-        if (gameType != null) {
-            return gameType;
-        } else {
-            return backupGameMode != null ? backupGameMode : this.server.getDefaultGameType();
-        }
+        GameType enumgamemode1 = this.server.getForcedGameType();
+
+        return enumgamemode1 != null ? enumgamemode1 : (backupGameMode != null ? backupGameMode : this.server.getDefaultGameType());
     }
 
     public void loadGameTypes(@Nullable CompoundTag nbt) {
-        this.gameMode
-            .setGameModeForPlayer(this.calculateGameModeForNewPlayer(readPlayerMode(nbt, "playerGameType")), readPlayerMode(nbt, "previousPlayerGameType"));
+        // Paper start - Expand PlayerGameModeChangeEvent
+        if (this.server.getForcedGameType() != null && this.server.getForcedGameType() != ServerPlayer.readPlayerMode(nbt, "playerGameType")) {
+            if (new org.bukkit.event.player.PlayerGameModeChangeEvent(this.getBukkitEntity(), org.bukkit.GameMode.getByValue(this.server.getDefaultGameType().getId()), org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, null).callEvent()) {
+                this.gameMode.setGameModeForPlayer(this.server.getForcedGameType(), GameType.DEFAULT_MODE);
+            } else {
+                this.gameMode.setGameModeForPlayer(ServerPlayer.readPlayerMode(nbt,"playerGameType"), ServerPlayer.readPlayerMode(nbt, "previousPlayerGameType"));
+            }
+            return;
+        }
+        // Paper end - Expand PlayerGameModeChangeEvent
+        this.gameMode.setGameModeForPlayer(this.calculateGameModeForNewPlayer(ServerPlayer.readPlayerMode(nbt, "playerGameType")), ServerPlayer.readPlayerMode(nbt, "previousPlayerGameType"));
     }
 
     private void storeGameTypes(CompoundTag nbt) {
         nbt.putInt("playerGameType", this.gameMode.getGameModeForPlayer().getId());
-        GameType gameType = this.gameMode.getPreviousGameModeForPlayer();
-        if (gameType != null) {
-            nbt.putInt("previousPlayerGameType", gameType.getId());
+        GameType enumgamemode = this.gameMode.getPreviousGameModeForPlayer();
+
+        if (enumgamemode != null) {
+            nbt.putInt("previousPlayerGameType", enumgamemode.getId());
         }
+
     }
 
     @Override
@@ -1795,7 +2645,7 @@
     }
 
     public boolean shouldFilterMessageTo(ServerPlayer player) {
-        return player != this && (this.textFilteringEnabled || player.textFilteringEnabled);
+        return player == this ? false : this.textFilteringEnabled || player.textFilteringEnabled;
     }
 
     @Override
@@ -1810,10 +2660,13 @@
     }
 
     public boolean drop(boolean entireStack) {
-        Inventory inventory = this.getInventory();
-        ItemStack itemStack = inventory.removeFromSelected(entireStack);
-        this.containerMenu.findSlot(inventory, inventory.selected).ifPresent(index -> this.containerMenu.setRemoteSlot(index, inventory.getSelected()));
-        return this.drop(itemStack, false, true) != null;
+        Inventory playerinventory = this.getInventory();
+        ItemStack itemstack = playerinventory.removeFromSelected(entireStack);
+
+        this.containerMenu.findSlot(playerinventory, playerinventory.selected).ifPresent((i) -> {
+            this.containerMenu.setRemoteSlot(i, playerinventory.getSelected());
+        });
+        return this.drop(itemstack, false, true) != null;
     }
 
     public boolean allowsListing() {
@@ -1833,9 +2686,11 @@
     public void onItemPickup(ItemEntity item) {
         super.onItemPickup(item);
         Entity entity = item.getOwner();
+
         if (entity != null) {
             CriteriaTriggers.THROWN_ITEM_PICKED_UP_BY_PLAYER.trigger(this, item.getItem(), entity);
         }
+
     }
 
     public void setChatSession(RemoteChatSession session) {
@@ -1849,7 +2704,7 @@
 
     @Override
     public void indicateDamage(double deltaX, double deltaZ) {
-        this.hurtDir = (float)(Mth.atan2(deltaZ, deltaX) * 180.0F / (float)Math.PI - (double)this.getYRot());
+        this.hurtDir = (float) (Mth.atan2(deltaZ, deltaX) * 57.2957763671875D - (double) this.getYRot());
         this.connection.send(new ClientboundHurtAnimationPacket(this));
     }
 
@@ -1859,8 +2714,10 @@
             this.setKnownMovement(Vec3.ZERO);
             entity.positionRider(this);
             this.connection.teleport(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
-            if (entity instanceof LivingEntity livingEntity) {
-                this.server.getPlayerList().sendActiveEffects(livingEntity, this.connection);
+            if (entity instanceof LivingEntity) {
+                LivingEntity entityliving = (LivingEntity) entity;
+
+                this.server.getPlayerList().sendActiveEffects(entityliving, this.connection);
             }
 
             return true;
@@ -1871,27 +2728,29 @@
 
     @Override
     public void stopRiding() {
+        // Paper start - Force entity dismount during teleportation
+        this.stopRiding(false);
+    }
+    @Override
+    public void stopRiding(boolean suppressCancellation) {
+        // Paper end - Force entity dismount during teleportation
         Entity entity = this.getVehicle();
-        super.stopRiding();
-        if (entity instanceof LivingEntity livingEntity) {
-            for (MobEffectInstance mobEffectInstance : livingEntity.getActiveEffects()) {
-                this.connection.send(new ClientboundRemoveMobEffectPacket(entity.getId(), mobEffectInstance.getEffect()));
+
+        super.stopRiding(suppressCancellation); // Paper - Force entity dismount during teleportation
+        if (entity instanceof LivingEntity entityliving) {
+            Iterator iterator = entityliving.getActiveEffects().iterator();
+
+            while (iterator.hasNext()) {
+                MobEffectInstance mobeffect = (MobEffectInstance) iterator.next();
+
+                this.connection.send(new ClientboundRemoveMobEffectPacket(entity.getId(), mobeffect.getEffect()));
             }
         }
+
     }
 
     public CommonPlayerSpawnInfo createCommonSpawnInfo(ServerLevel world) {
-        return new CommonPlayerSpawnInfo(
-            world.dimensionTypeRegistration(),
-            world.dimension(),
-            BiomeManager.obfuscateSeed(world.getSeed()),
-            this.gameMode.getGameModeForPlayer(),
-            this.gameMode.getPreviousGameModeForPlayer(),
-            world.isDebug(),
-            world.isFlat(),
-            this.getLastDeathLocation(),
-            this.getPortalCooldown()
-        );
+        return new CommonPlayerSpawnInfo(world.dimensionTypeRegistration(), world.dimension(), BiomeManager.obfuscateSeed(world.getSeed()), this.gameMode.getGameModeForPlayer(), this.gameMode.getPreviousGameModeForPlayer(), world.isDebug(), world.isFlat(), this.getLastDeathLocation(), this.getPortalCooldown());
     }
 
     public void setRaidOmenPosition(BlockPos startRaidPos) {
@@ -1910,6 +2769,7 @@
     @Override
     public Vec3 getKnownMovement() {
         Entity entity = this.getVehicle();
+
         return entity != null && entity.getControllingPassenger() != this ? entity.getKnownMovement() : this.lastKnownClientMovement;
     }
 
@@ -1928,14 +2788,162 @@
         this.awardStat(Stats.ITEM_BROKEN.get(item));
     }
 
-    public static record RespawnPosAngle(Vec3 position, float yaw) {
-        public static ServerPlayer.RespawnPosAngle of(Vec3 respawnPos, BlockPos currentPos) {
-            return new ServerPlayer.RespawnPosAngle(respawnPos, calculateLookAtYaw(respawnPos, currentPos));
+    // CraftBukkit start
+    public static record RespawnPosAngle(Vec3 position, float yaw, boolean isBedSpawn, boolean isAnchorSpawn) {
+
+        public static ServerPlayer.RespawnPosAngle of(Vec3 vec3d, BlockPos blockposition, boolean isBedSpawn, boolean isAnchorSpawn) {
+            return new ServerPlayer.RespawnPosAngle(vec3d, calculateLookAtYaw(vec3d, blockposition), isBedSpawn, isAnchorSpawn);
+            // CraftBukkit end
         }
 
         private static float calculateLookAtYaw(Vec3 respawnPos, BlockPos currentPos) {
-            Vec3 vec3 = Vec3.atBottomCenterOf(currentPos).subtract(respawnPos).normalize();
-            return (float)Mth.wrapDegrees(Mth.atan2(vec3.z, vec3.x) * 180.0F / (float)Math.PI - 90.0);
+            Vec3 vec3d1 = Vec3.atBottomCenterOf(currentPos).subtract(respawnPos).normalize();
+
+            return (float) Mth.wrapDegrees(Mth.atan2(vec3d1.z, vec3d1.x) * 57.2957763671875D - 90.0D);
+        }
+    }
+
+    // CraftBukkit start - Add per-player time and weather.
+    public long timeOffset = 0;
+    public boolean relativeTime = true;
+
+    public long getPlayerTime() {
+        if (this.relativeTime) {
+            // Adds timeOffset to the current server time.
+            return this.level().getDayTime() + this.timeOffset;
+        } else {
+            // Adds timeOffset to the beginning of this day.
+            return this.level().getDayTime() - (this.level().getDayTime() % 24000) + this.timeOffset;
+        }
+    }
+
+    public WeatherType weather = null;
+
+    public WeatherType getPlayerWeather() {
+        return this.weather;
+    }
+
+    public void setPlayerWeather(WeatherType type, boolean plugin) {
+        if (!plugin && this.weather != null) {
+            return;
+        }
+
+        if (plugin) {
+            this.weather = type;
+        }
+
+        if (type == WeatherType.DOWNFALL) {
+            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0));
+        } else {
+            this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0));
+        }
+    }
+
+    private float pluginRainPosition;
+    private float pluginRainPositionPrevious;
+
+    public void updateWeather(float oldRain, float newRain, float oldThunder, float newThunder) {
+        if (this.weather == null) {
+            // Vanilla
+            if (oldRain != newRain) {
+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, newRain));
+            }
+        } else {
+            // Plugin
+            if (this.pluginRainPositionPrevious != this.pluginRainPosition) {
+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, this.pluginRainPosition));
+            }
+        }
+
+        if (oldThunder != newThunder) {
+            if (this.weather == WeatherType.DOWNFALL || this.weather == null) {
+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, newThunder));
+            } else {
+                this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0));
+            }
+        }
+    }
+
+    public void tickWeather() {
+        if (this.weather == null) return;
+
+        this.pluginRainPositionPrevious = this.pluginRainPosition;
+        if (this.weather == WeatherType.DOWNFALL) {
+            this.pluginRainPosition += 0.01;
+        } else {
+            this.pluginRainPosition -= 0.01;
+        }
+
+        this.pluginRainPosition = Mth.clamp(this.pluginRainPosition, 0.0F, 1.0F);
+    }
+
+    public void resetPlayerWeather() {
+        this.weather = null;
+        this.setPlayerWeather(this.level().getLevelData().isRaining() ? WeatherType.DOWNFALL : WeatherType.CLEAR, false);
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + "(" + this.getScoreboardName() + " at " + this.getX() + "," + this.getY() + "," + this.getZ() + ")";
+    }
+
+    // SPIGOT-1903, MC-98153
+    public void forceSetPositionRotation(double x, double y, double z, float yaw, float pitch) {
+        this.moveTo(x, y, z, yaw, pitch);
+        this.connection.resetPosition();
+    }
+
+    @Override
+    public boolean isImmobile() {
+        return super.isImmobile() || (this.connection != null && this.connection.isDisconnected()); // Paper - Fix duplication bugs
+    }
+
+    @Override
+    public Scoreboard getScoreboard() {
+        return this.getBukkitEntity().getScoreboard().getHandle();
+    }
+
+    public void reset() {
+        float exp = 0;
+        boolean keepInventory = this.level().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY);
+
+        if (this.keepLevel) { // CraftBukkit - SPIGOT-6687: Only use keepLevel (was pre-set with RULE_KEEPINVENTORY value in PlayerDeathEvent)
+            exp = this.experienceProgress;
+            this.newTotalExp = this.totalExperience;
+            this.newLevel = this.experienceLevel;
         }
+
+        this.setHealth(this.getMaxHealth());
+        this.stopUsingItem(); // CraftBukkit - SPIGOT-6682: Clear active item on reset
+        this.setAirSupply(this.getMaxAirSupply()); // Paper - Reset players airTicks on respawn
+        this.setRemainingFireTicks(0);
+        this.fallDistance = 0;
+        this.foodData = new FoodData(this);
+        this.experienceLevel = this.newLevel;
+        this.totalExperience = this.newTotalExp;
+        this.experienceProgress = 0;
+        this.deathTime = 0;
+        this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent
+        this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH);
+        this.effectsDirty = true;
+        this.containerMenu = this.inventoryMenu;
+        this.lastHurtByPlayer = null;
+        this.lastHurtByMob = null;
+        this.combatTracker = new CombatTracker(this);
+        this.lastSentExp = -1;
+        if (this.keepLevel) { // CraftBukkit - SPIGOT-6687: Only use keepLevel (was pre-set with RULE_KEEPINVENTORY value in PlayerDeathEvent)
+            this.experienceProgress = exp;
+        } else {
+            this.giveExperiencePoints(this.newExp);
+        }
+        this.keepLevel = false;
+        this.setDeltaMovement(0, 0, 0); // CraftBukkit - SPIGOT-6948: Reset velocity on death
+        this.skipDropExperience = false; // CraftBukkit - SPIGOT-7462: Reset experience drop skip, so that further deaths drop xp
     }
+
+    @Override
+    public CraftPlayer getBukkitEntity() {
+        return (CraftPlayer) super.getBukkitEntity();
+    }
+    // CraftBukkit end
 }
