From 1c62897a7f001598721e4afd74546dd9dfc98e8c Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:09:34 +0200 Subject: [PATCH 1/4] fix: move players out of blocks on join and place graves nearest first A player logged in inside planks someone had built where they logged out and suffocated while the resource pack loaded. On join, anyone whose head is in a suffocating block and who cannot crawl is moved to the nearest safe spot. Graves that cannot go at the death spot now use the nearest free block instead of the first corner of the 5x5 search. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../rpcharacters/RPCharacters.java | 2 + .../rpcharacters/grave/GraveManager.java | 33 +++-- .../joinsafety/JoinUnstuckListener.java | 135 ++++++++++++++++++ .../joinsafety/SafeSpotSearch.java | 61 ++++++++ .../grave/GraveSearchOffsetsTest.java | 34 +++++ .../joinsafety/SafeSpotSearchTest.java | 62 ++++++++ 6 files changed, 315 insertions(+), 12 deletions(-) create mode 100644 src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java create mode 100644 src/main/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearch.java create mode 100644 src/test/java/net/tfminecraft/rpcharacters/grave/GraveSearchOffsetsTest.java create mode 100644 src/test/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearchTest.java diff --git a/src/main/java/net/tfminecraft/rpcharacters/RPCharacters.java b/src/main/java/net/tfminecraft/rpcharacters/RPCharacters.java index 4dd556e..c2741d0 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/RPCharacters.java +++ b/src/main/java/net/tfminecraft/rpcharacters/RPCharacters.java @@ -96,6 +96,7 @@ import net.tfminecraft.rpcharacters.grave.GraveManager; import net.tfminecraft.rpcharacters.grave.GraveVisualManager; import net.tfminecraft.rpcharacters.grave.LastSolidTracker; +import net.tfminecraft.rpcharacters.joinsafety.JoinUnstuckListener; import net.tfminecraft.rpcharacters.playerlist.PlayerListCommand; import net.tfminecraft.rpcharacters.playerlist.QuickActionPack; import net.tfminecraft.rpcharacters.pvp.PvpCommand; @@ -347,6 +348,7 @@ public void registerListeners() { getServer().getPluginManager().registerEvents(new GraveInteractListener(), this); getServer().getPluginManager().registerEvents(new GraveInsuranceListener(), this); getServer().getPluginManager().registerEvents(new EvilRpListener(), this); + getServer().getPluginManager().registerEvents(new JoinUnstuckListener(), this); } public void startManagers() { playerManager.start(); diff --git a/src/main/java/net/tfminecraft/rpcharacters/grave/GraveManager.java b/src/main/java/net/tfminecraft/rpcharacters/grave/GraveManager.java index a78a911..1c02a91 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/grave/GraveManager.java +++ b/src/main/java/net/tfminecraft/rpcharacters/grave/GraveManager.java @@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.UUID; @@ -36,6 +37,7 @@ public final class GraveManager { private static final GraveManager INSTANCE = new GraveManager(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final int SEARCH_RADIUS = 2; + private static final List SEARCH_OFFSETS = searchOffsets(SEARCH_RADIUS); private final Map byId = new ConcurrentHashMap<>(); private final Map byBlock = new ConcurrentHashMap<>(); @@ -409,23 +411,30 @@ private static boolean isWaterLike(Block block) { private Block searchNearby(Block origin) { World world = origin.getWorld(); - int ox = origin.getX(); - int oy = origin.getY(); - int oz = origin.getZ(); + for (int[] offset : SEARCH_OFFSETS) { + Block candidate = world.getBlockAt(origin.getX() + offset[0], origin.getY() + offset[1], + origin.getZ() + offset[2]); + if (canPlace(candidate)) { + return candidate; + } + } + return null; + } + + /** Nearest first, so the grave lands next to the death spot rather than in a far corner. */ + static List searchOffsets(int radius) { + List offsets = new ArrayList<>(); for (int dy = 0; dy <= 1; dy++) { - for (int dx = -SEARCH_RADIUS; dx <= SEARCH_RADIUS; dx++) { - for (int dz = -SEARCH_RADIUS; dz <= SEARCH_RADIUS; dz++) { - if (dx == 0 && dy == 0 && dz == 0) { - continue; - } - Block candidate = world.getBlockAt(ox + dx, oy + dy, oz + dz); - if (canPlace(candidate)) { - return candidate; + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + if (dx != 0 || dy != 0 || dz != 0) { + offsets.add(new int[] { dx, dy, dz }); } } } } - return null; + offsets.sort(Comparator.comparingInt(o -> o[0] * o[0] + o[1] * o[1] + o[2] * o[2])); + return offsets; } private boolean canPlace(Block block) { diff --git a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java new file mode 100644 index 0000000..416c96d --- /dev/null +++ b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java @@ -0,0 +1,135 @@ +package net.tfminecraft.rpcharacters.joinsafety; + +import java.util.EnumSet; +import java.util.Set; + +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.util.BoundingBox; + +import net.tfminecraft.rpcharacters.RPCharacters; + +/** + * Moves players out of blocks when they log in. Someone may have built where + * they logged out, and they would suffocate while the resource pack loads. + */ +public final class JoinUnstuckListener implements Listener { + + private static final String MOVED_MESSAGE = + "§eYou logged in inside a block, so you were moved to a safe spot nearby."; + + private static final Set HAZARDS = EnumSet.of( + Material.LAVA, Material.FIRE, Material.SOUL_FIRE, Material.MAGMA_BLOCK, + Material.CAMPFIRE, Material.SOUL_CAMPFIRE, Material.CACTUS, Material.SWEET_BERRY_BUSH, + Material.POWDER_SNOW, Material.WITHER_ROSE, Material.POINTED_DRIPSTONE); + + // LOWEST so PlayerManager (NORMAL) stores the no-character freeze spot after the move. + @EventHandler(priority = EventPriority.LOWEST) + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + GameMode mode = player.getGameMode(); + if (mode == GameMode.CREATIVE || mode == GameMode.SPECTATOR || player.isInsideVehicle()) { + return; + } + // Vanilla drops a player who still fits crawling into the swimming pose, so leave them be. + if (!isInWall(player) || !crawlSpaceBlocked(player)) { + return; + } + Location from = player.getLocation(); + Location target = findSafeSpot(from); + if (target == null) { + return; + } + player.teleport(target); + player.sendMessage(MOVED_MESSAGE); + RPCharacters.plugin.getLogger().info("Moved " + player.getName() + " out of blocks on join: " + + describe(from) + " -> " + describe(target)); + } + + /** Same test vanilla uses for suffocation damage: a thin box at eye level. */ + static boolean isInWall(Player player) { + Location eye = player.getEyeLocation(); + double half = player.getWidth() * 0.4; + BoundingBox box = new BoundingBox( + eye.getX() - half, eye.getY() - 1.0E-6, eye.getZ() - half, + eye.getX() + half, eye.getY() + 1.0E-6, eye.getZ() + half); + return collides(player.getWorld(), box, true); + } + + /** True when blocks fill the space a crawling (swimming pose) player would need. */ + static boolean crawlSpaceBlocked(Player player) { + Location feet = player.getLocation(); + double half = player.getWidth() / 2.0 - 1.0E-6; + BoundingBox box = new BoundingBox( + feet.getX() - half, feet.getY() + 1.0E-6, feet.getZ() - half, + feet.getX() + half, feet.getY() + 0.6, feet.getZ() + half); + return collides(player.getWorld(), box, false); + } + + private static boolean collides(World world, BoundingBox box, boolean suffocatingOnly) { + for (int x = floor(box.getMinX()); x <= floor(box.getMaxX()); x++) { + for (int y = floor(box.getMinY()); y <= floor(box.getMaxY()); y++) { + for (int z = floor(box.getMinZ()); z <= floor(box.getMaxZ()); z++) { + Block block = world.getBlockAt(x, y, z); + if (suffocatingOnly && !block.isSuffocating()) { + continue; + } + if (block.getCollisionShape().overlaps(box.clone().shift(-x, -y, -z))) { + return true; + } + } + } + } + return false; + } + + private static Location findSafeSpot(Location from) { + World world = from.getWorld(); + if (world == null) { + return null; + } + int[] spot = SafeSpotSearch.find(from.getBlockX(), from.getBlockY(), from.getBlockZ(), + (x, y, z) -> canStand(world, x, y, z)); + Location target; + if (spot != null) { + target = new Location(world, spot[0] + 0.5, spot[1], spot[2] + 0.5); + } else { + Block top = world.getHighestBlockAt(from.getBlockX(), from.getBlockZ()); + target = new Location(world, top.getX() + 0.5, top.getY() + 1, top.getZ() + 0.5); + } + target.setYaw(from.getYaw()); + target.setPitch(from.getPitch()); + return target; + } + + private static boolean canStand(World world, int x, int y, int z) { + if (y - 1 < world.getMinHeight() || y + 1 >= world.getMaxHeight()) { + return false; + } + Block floor = world.getBlockAt(x, y - 1, z); + if (!floor.getType().isSolid() || HAZARDS.contains(floor.getType())) { + return false; + } + return isOpen(world.getBlockAt(x, y, z)) && isOpen(world.getBlockAt(x, y + 1, z)); + } + + private static boolean isOpen(Block block) { + return block.isPassable() && !block.isLiquid() && !HAZARDS.contains(block.getType()); + } + + private static int floor(double value) { + return (int) Math.floor(value); + } + + private static String describe(Location loc) { + return loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ(); + } +} diff --git a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearch.java b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearch.java new file mode 100644 index 0000000..4909cec --- /dev/null +++ b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearch.java @@ -0,0 +1,61 @@ +package net.tfminecraft.rpcharacters.joinsafety; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Finds the nearest block position a player can stand at, checking closer offsets first. + */ +public final class SafeSpotSearch { + + public static final int HORIZONTAL_RADIUS = 8; + public static final int VERTICAL_RADIUS = 8; + + private static final List OFFSETS = buildOffsets(HORIZONTAL_RADIUS, VERTICAL_RADIUS); + + @FunctionalInterface + public interface Probe { + /** True when feet at (x, y, z) are safe: open feet and head blocks on a solid floor. */ + boolean canStand(int x, int y, int z); + } + + private SafeSpotSearch() { + } + + /** + * @return {x, y, z} of the nearest standable position, or {@code null} if none is in range + */ + public static int[] find(int x, int y, int z, Probe probe) { + if (probe == null) { + return null; + } + for (int[] offset : OFFSETS) { + int cx = x + offset[0]; + int cy = y + offset[1]; + int cz = z + offset[2]; + if (probe.canStand(cx, cy, cz)) { + return new int[] { cx, cy, cz }; + } + } + return null; + } + + /** Offsets ordered by distance; ties prefer less vertical movement, then moving up. */ + static List buildOffsets(int horizontal, int vertical) { + List offsets = new ArrayList<>(); + for (int dx = -horizontal; dx <= horizontal; dx++) { + for (int dy = -vertical; dy <= vertical; dy++) { + for (int dz = -horizontal; dz <= horizontal; dz++) { + offsets.add(new int[] { dx, dy, dz }); + } + } + } + offsets.sort(Comparator.comparingInt(o -> o[0] * o[0] + o[1] * o[1] + o[2] * o[2]) + .thenComparingInt(o -> Math.abs(o[1])) + .thenComparingInt(o -> -o[1]) + .thenComparingInt(o -> o[0]) + .thenComparingInt(o -> o[2])); + return offsets; + } +} diff --git a/src/test/java/net/tfminecraft/rpcharacters/grave/GraveSearchOffsetsTest.java b/src/test/java/net/tfminecraft/rpcharacters/grave/GraveSearchOffsetsTest.java new file mode 100644 index 0000000..34b400f --- /dev/null +++ b/src/test/java/net/tfminecraft/rpcharacters/grave/GraveSearchOffsetsTest.java @@ -0,0 +1,34 @@ +package net.tfminecraft.rpcharacters.grave; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +class GraveSearchOffsetsTest { + + private static int distanceSq(int[] o) { + return o[0] * o[0] + o[1] * o[1] + o[2] * o[2]; + } + + @Test + void coversRadiusTwoOnSameLevelAndOneAboveWithoutOrigin() { + List offsets = GraveManager.searchOffsets(2); + assertEquals(5 * 5 * 2 - 1, offsets.size()); + for (int[] o : offsets) { + assertTrue(o[0] != 0 || o[1] != 0 || o[2] != 0); + } + } + + @Test + void checksNearestBlocksFirst() { + List offsets = GraveManager.searchOffsets(2); + assertEquals(1, distanceSq(offsets.get(0))); + assertEquals(0, offsets.get(0)[1], "same level is tried before the block above"); + for (int i = 1; i < offsets.size(); i++) { + assertTrue(distanceSq(offsets.get(i - 1)) <= distanceSq(offsets.get(i))); + } + } +} diff --git a/src/test/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearchTest.java b/src/test/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearchTest.java new file mode 100644 index 0000000..4b1c8d3 --- /dev/null +++ b/src/test/java/net/tfminecraft/rpcharacters/joinsafety/SafeSpotSearchTest.java @@ -0,0 +1,62 @@ +package net.tfminecraft.rpcharacters.joinsafety; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +class SafeSpotSearchTest { + + private static SafeSpotSearch.Probe spots(int[]... positions) { + Set open = new HashSet<>(); + for (int[] p : positions) { + open.add(p[0] + ":" + p[1] + ":" + p[2]); + } + return (x, y, z) -> open.contains(x + ":" + y + ":" + z); + } + + @Test + void keepsOriginWhenItIsStandable() { + int[] found = SafeSpotSearch.find(4079, 196, 1663, spots(new int[] { 4079, 196, 1663 })); + assertArrayEquals(new int[] { 4079, 196, 1663 }, found); + } + + @Test + void picksNearestSpot() { + int[] found = SafeSpotSearch.find(4079, 196, 1663, spots( + new int[] { 4077, 196, 1661 }, + new int[] { 4079, 196, 1662 }, + new int[] { 4083, 196, 1663 })); + assertArrayEquals(new int[] { 4079, 196, 1662 }, found); + } + + @Test + void prefersUpOverDownAtEqualDistance() { + int[] found = SafeSpotSearch.find(0, 64, 0, spots(new int[] { 0, 63, 0 }, new int[] { 0, 65, 0 })); + assertArrayEquals(new int[] { 0, 65, 0 }, found); + } + + @Test + void prefersSidewaysOverVerticalAtEqualDistance() { + int[] found = SafeSpotSearch.find(0, 64, 0, spots(new int[] { 0, 65, 0 }, new int[] { 1, 64, 0 })); + assertArrayEquals(new int[] { 1, 64, 0 }, found); + } + + @Test + void returnsNullWhenNothingInRange() { + int far = SafeSpotSearch.HORIZONTAL_RADIUS + 1; + assertNull(SafeSpotSearch.find(0, 64, 0, spots(new int[] { far, 64, 0 }))); + } + + @Test + void offsetsCoverTheWholeBoxOnce() { + List offsets = SafeSpotSearch.buildOffsets(2, 1); + assertEquals(5 * 3 * 5, offsets.size()); + assertArrayEquals(new int[] { 0, 0, 0 }, offsets.get(0)); + } +} From ca54164cfc729605d4abc024eca3c7c86c78e0f4 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:16:03 +0200 Subject: [PATCH 2/4] fix: unstick on the tick after join so the teleport sticks A teleport inside PlayerJoinEvent was lost when the client's first position synced (seen on TFMCDev01: logged as moved, still suffocated). Run it on the next tick, which is before the player ticks, and let PlayerManager re-capture the no-character freeze spot after the move. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../joinsafety/JoinUnstuckListener.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java index 416c96d..4cd66a4 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java +++ b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java @@ -3,6 +3,7 @@ import java.util.EnumSet; import java.util.Set; +import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; @@ -10,7 +11,6 @@ import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.util.BoundingBox; @@ -24,17 +24,25 @@ public final class JoinUnstuckListener implements Listener { private static final String MOVED_MESSAGE = - "§eYou logged in inside a block, so you were moved to a safe spot nearby."; + "\u00A7eYou logged in inside a block, so you were moved to a safe spot nearby."; private static final Set HAZARDS = EnumSet.of( Material.LAVA, Material.FIRE, Material.SOUL_FIRE, Material.MAGMA_BLOCK, Material.CAMPFIRE, Material.SOUL_CAMPFIRE, Material.CACTUS, Material.SWEET_BERRY_BUSH, Material.POWDER_SNOW, Material.WITHER_ROSE, Material.POINTED_DRIPSTONE); - // LOWEST so PlayerManager (NORMAL) stores the no-character freeze spot after the move. - @EventHandler(priority = EventPriority.LOWEST) + @EventHandler public void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); + // A teleport inside the join event is lost when the client's first position syncs. + // Scheduled tasks run before players tick, so this still lands before suffocation damage. + Bukkit.getScheduler().runTask(RPCharacters.plugin, () -> unstick(player)); + } + + private static void unstick(Player player) { + if (!player.isOnline()) { + return; + } GameMode mode = player.getGameMode(); if (mode == GameMode.CREATIVE || mode == GameMode.SPECTATOR || player.isInsideVehicle()) { return; @@ -49,6 +57,8 @@ public void onJoin(PlayerJoinEvent event) { return; } player.teleport(target); + // PlayerManager froze no-character players in the wall on join; let it re-capture here. + RPCharacters.getPlayerManager().releaseFreeze(player); player.sendMessage(MOVED_MESSAGE); RPCharacters.plugin.getLogger().info("Moved " + player.getName() + " out of blocks on join: " + describe(from) + " -> " + describe(target)); From 6dd2f511d3833f1cacbd50e05e51b0e4828debe4 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:18:50 +0200 Subject: [PATCH 3/4] fix: only report a join move after a safe, successful teleport Co-Authored-By: Claude Opus 5.5 (1M context) --- .../joinsafety/JoinUnstuckListener.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java index 4cd66a4..d9d411b 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java +++ b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java @@ -56,7 +56,9 @@ private static void unstick(Player player) { if (target == null) { return; } - player.teleport(target); + if (!player.teleport(target)) { + return; + } // PlayerManager froze no-character players in the wall on join; let it re-capture here. RPCharacters.getPlayerManager().releaseFreeze(player); player.sendMessage(MOVED_MESSAGE); @@ -108,13 +110,14 @@ private static Location findSafeSpot(Location from) { } int[] spot = SafeSpotSearch.find(from.getBlockX(), from.getBlockY(), from.getBlockZ(), (x, y, z) -> canStand(world, x, y, z)); - Location target; - if (spot != null) { - target = new Location(world, spot[0] + 0.5, spot[1], spot[2] + 0.5); - } else { + if (spot == null) { Block top = world.getHighestBlockAt(from.getBlockX(), from.getBlockZ()); - target = new Location(world, top.getX() + 0.5, top.getY() + 1, top.getZ() + 0.5); + if (!canStand(world, top.getX(), top.getY() + 1, top.getZ())) { + return null; + } + spot = new int[] { top.getX(), top.getY() + 1, top.getZ() }; } + Location target = new Location(world, spot[0] + 0.5, spot[1], spot[2] + 0.5); target.setYaw(from.getYaw()); target.setPitch(from.getPitch()); return target; From dc552c7c1c47e8af59ee2b16d389fd226cf7d167 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:33:36 +0200 Subject: [PATCH 4/4] fix: re-capture the freeze spot right after the join move Co-Authored-By: Claude Opus 5.5 (1M context) --- .../rpcharacters/joinsafety/JoinUnstuckListener.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java index d9d411b..88ed586 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java +++ b/src/main/java/net/tfminecraft/rpcharacters/joinsafety/JoinUnstuckListener.java @@ -59,8 +59,9 @@ private static void unstick(Player player) { if (!player.teleport(target)) { return; } - // PlayerManager froze no-character players in the wall on join; let it re-capture here. + // PlayerManager froze no-character players in the wall on join; re-capture it here. RPCharacters.getPlayerManager().releaseFreeze(player); + RPCharacters.getPlayerManager().reevaluateFreeze(player); player.sendMessage(MOVED_MESSAGE); RPCharacters.plugin.getLogger().info("Moved " + player.getName() + " out of blocks on join: " + describe(from) + " -> " + describe(target));