Skip to content

Commit d8d87fb

Browse files
committed
Folia fix: Prevent endermites spawning and damaging entities when using enderpearls in claims
1 parent 24366d5 commit d8d87fb

File tree

2 files changed

+122
-108
lines changed

2 files changed

+122
-108
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<groupId>com.griefprevention</groupId>
1010
<artifactId>GriefPrevention</artifactId>
11-
<version>17.2.3</version>
11+
<version>17.2.4</version>
1212

1313
<name>GriefPrevention</name>
1414
<description>The official self-service anti-griefing Bukkit plugin for Minecraft servers since 2011.</description>

src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java

Lines changed: 121 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
import org.bukkit.event.player.PlayerItemHeldEvent;
7575
import org.bukkit.event.player.PlayerJoinEvent;
7676
import org.bukkit.event.player.PlayerKickEvent;
77+
import org.bukkit.event.entity.CreatureSpawnEvent;
78+
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
7779
import org.bukkit.event.entity.PlayerLeashEntityEvent;
7880
import org.bukkit.event.entity.ProjectileHitEvent;
7981
import org.bukkit.event.inventory.InventoryType;
@@ -129,8 +131,10 @@ class PlayerEventHandler implements Listener {
129131
// timestamps of login and logout notifications in the last minute
130132
private final ArrayList<Long> recentLoginLogoutNotifications = new ArrayList<>();
131133

132-
// prevent duplicate rollbacks when multiple tick checks find player at denied destination
133-
private final Set<UUID> pendingEnderPearlRollbacks = ConcurrentHashMap.newKeySet();
134+
// Canvas fallback: track refunded pearl entity UUIDs to prevent dupe (one refund per pearl)
135+
private final Set<UUID> refundedEnderPearlEntities = ConcurrentHashMap.newKeySet();
136+
// Track players who were just rolled back from denied pearl - prevents endermite spawn at rollback location
137+
private final Set<UUID> recentPearlRollbackPlayers = ConcurrentHashMap.newKeySet();
134138

135139
// regex pattern for the "how do i claim land?" scanner
136140
private Pattern howToClaimPattern = null;
@@ -1056,131 +1060,35 @@ void onPlayerPortal(PlayerPortalEvent event) {
10561060
}
10571061
}
10581062

1059-
/** Folia/Canvas-safe teleport: uses teleportAsync when in region threading, else teleport. */
1060-
private static void teleportFoliaSafe(Player player, Location location) {
1061-
try {
1062-
player.getClass().getMethod("teleportAsync", Location.class).invoke(player, location);
1063-
} catch (Exception e) {
1064-
player.teleport(location);
1065-
}
1066-
}
1067-
1068-
// Fallback for Canvas/Folia: PlayerTeleportEvent may not fire for ender pearls. Use
1069-
// ProjectileHitEvent to detect the landing, then verify/rollback on next tick.
1070-
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false)
1071-
public void onProjectileHitEnderPearl(ProjectileHitEvent event) {
1072-
if (event.getEntity().getType() != EntityType.ENDER_PEARL) return;
1073-
if (!instance.config_claims_enderPearlsRequireAccessTrust) return;
1074-
if (!(event.getEntity().getShooter() instanceof Player shooter)) return;
1075-
if (!instance.claimsEnabledForWorld(event.getEntity().getWorld())) return;
1076-
1077-
// Player lands on top of hit block (or at pearl location if hit entity)
1078-
Block hitBlock = event.getHitBlock();
1079-
Location destLoc = hitBlock != null
1080-
? hitBlock.getLocation().add(0.5, 1, 0.5)
1081-
: event.getEntity().getLocation();
1082-
Location fromLoc = shooter.getLocation().clone();
1083-
UUID playerID = shooter.getUniqueId();
1084-
// Canvas may teleport the player several ticks after ProjectileHitEvent. Run check at
1085-
// 1, 2, 3 ticks until we find the player at destination (tick+2 typically on Canvas).
1086-
for (long delay = 1; delay <= 3; delay++) {
1087-
final long d = delay;
1088-
SchedulerUtil.runLaterGlobal(instance, () -> {
1089-
Player p = instance.getServer().getPlayer(playerID);
1090-
if (p == null || !p.isOnline()) return;
1091-
Location now = p.getLocation();
1092-
if (now.getWorld() != destLoc.getWorld()) return;
1093-
double distSq = now.distanceSquared(destLoc);
1094-
if (distSq > 25) return; // not at pearl landing (5 block radius)
1095-
Supplier<String> noAccessReason = ProtectionHelper.checkPermission(p, now,
1096-
ClaimPermission.Access, null);
1097-
if (noAccessReason != null) {
1098-
// Only rollback once per pearl (multiple tick checks can fire before teleportAsync completes)
1099-
if (!pendingEnderPearlRollbacks.add(playerID)) return;
1100-
teleportFoliaSafe(p, fromLoc);
1101-
GriefPrevention.sendMessage(p, TextMode.Err, noAccessReason.get());
1102-
if (instance.config_claims_refundDeniedEnderPearls) {
1103-
p.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
1104-
}
1105-
SchedulerUtil.runLaterGlobal(instance, () -> pendingEnderPearlRollbacks.remove(playerID), 15L);
1106-
}
1107-
}, d);
1108-
}
1109-
}
1110-
11111063
// when a player teleports
11121064
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
11131065
public void onPlayerTeleport(PlayerTeleportEvent event) {
11141066
Player player = event.getPlayer();
11151067
PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
11161068

1117-
TeleportCause cause = event.getCause();
1118-
11191069
// Get the claim at the destination
11201070
Claim toClaim = this.dataStore.getClaimAt(event.getTo(), false, playerData.lastClaim);
11211071

11221072
// Get the claim at the original location
11231073
Claim fromClaim = playerData.lastClaim;
11241074

11251075
// Special handling for ender pearls and chorus fruit to prevent gaining access
1126-
// to secured claims. On Folia/Canvas, the event may fire from a region thread
1127-
// where claim lookup fails; run the check on GlobalRegionScheduler so it executes
1128-
// in a context where getClaimAt returns correct results.
1076+
// to secured claims. Must run before updating lastClaim so we don't corrupt
1077+
// player state when cancelling. Use ProtectionHelper for proper 3D claim and
1078+
// parent inheritance handling.
11291079
if (instance.config_claims_enderPearlsRequireAccessTrust) {
1080+
TeleportCause cause = event.getCause();
11301081
if (cause == TeleportCause.CHORUS_FRUIT || cause == TeleportCause.ENDER_PEARL) {
1131-
Location to = event.getTo();
1132-
if (to == null || to.getWorld() == null) {
1133-
// On Folia/Canvas, getTo() can be null or invalid from region thread
1082+
Supplier<String> noAccessReason = ProtectionHelper.checkPermission(player, event.getTo(),
1083+
ClaimPermission.Access, event);
1084+
if (noAccessReason != null) {
1085+
GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason.get());
11341086
event.setCancelled(true);
11351087
if (cause == TeleportCause.ENDER_PEARL && instance.config_claims_refundDeniedEnderPearls) {
11361088
player.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
11371089
}
1138-
return;
1090+
return; // Don't update lastClaim when teleport is cancelled
11391091
}
1140-
event.setCancelled(true);
1141-
Location destination = to.clone();
1142-
Location fromLoc = player.getLocation().clone();
1143-
UUID playerID = player.getUniqueId();
1144-
boolean isEnderPearl = (cause == TeleportCause.ENDER_PEARL);
1145-
if (instance.config_logs_debugEnabled) {
1146-
GriefPrevention.AddLogEntry("[DEBUG] EnderPearl/Chorus: cancelled event, scheduling GlobalRegionScheduler check for "
1147-
+ player.getName() + " -> " + GriefPrevention.getfriendlyLocationString(destination),
1148-
CustomLogEntryTypes.Debug, true);
1149-
}
1150-
SchedulerUtil.runLaterGlobal(instance, () -> {
1151-
Player p = instance.getServer().getPlayer(playerID);
1152-
if (p == null || !p.isOnline()) return;
1153-
PlayerData data = this.dataStore.getPlayerData(playerID);
1154-
// Use actual location - on Folia/Canvas event.getTo() may be wrong; if cancel
1155-
// didn't work, player has already teleported
1156-
Location actualLoc = p.getLocation();
1157-
Claim destClaim = this.dataStore.getClaimAt(actualLoc, false, data.lastClaim);
1158-
Supplier<String> noAccessReason = ProtectionHelper.checkPermission(p, actualLoc,
1159-
ClaimPermission.Access, null);
1160-
if (instance.config_logs_debugEnabled) {
1161-
GriefPrevention.AddLogEntry("[DEBUG] EnderPearl/Chorus: check ran for " + p.getName()
1162-
+ " destClaim=" + (destClaim != null ? "id=" + destClaim.id : "null")
1163-
+ " noAccessReason=" + (noAccessReason != null ? noAccessReason.get() : "null (allowed)"),
1164-
CustomLogEntryTypes.Debug, true);
1165-
}
1166-
if (noAccessReason != null) {
1167-
teleportFoliaSafe(p, fromLoc);
1168-
GriefPrevention.sendMessage(p, TextMode.Err, noAccessReason.get());
1169-
if (isEnderPearl && instance.config_claims_refundDeniedEnderPearls) {
1170-
p.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
1171-
}
1172-
return;
1173-
}
1174-
// Only teleport if cancel worked (player still at origin)
1175-
if (actualLoc.distanceSquared(fromLoc) < 1) {
1176-
teleportFoliaSafe(p, destination);
1177-
}
1178-
data.lastClaim = this.dataStore.getClaimAt(p.getLocation(), false, data.lastClaim);
1179-
if (data.lastClaim != fromClaim) {
1180-
p.updateCommands();
1181-
}
1182-
}, 1L);
1183-
return;
11841092
}
11851093
}
11861094

@@ -1194,6 +1102,112 @@ public void onPlayerTeleport(PlayerTeleportEvent event) {
11941102
}
11951103
}
11961104

1105+
// Prevent endermite spawn at rollback location when pearl was denied in claim (Canvas: endermite spawns at player).
1106+
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
1107+
public void onCreatureSpawnEndermite(CreatureSpawnEvent event) {
1108+
if (event.getEntityType() != EntityType.ENDERMITE) return;
1109+
if (event.getSpawnReason() != SpawnReason.ENDER_PEARL) return;
1110+
if (!instance.config_claims_enderPearlsRequireAccessTrust) return;
1111+
if (!instance.claimsEnabledForWorld(event.getLocation().getWorld())) return;
1112+
1113+
Location spawnLoc = event.getLocation();
1114+
Claim claim = this.dataStore.getClaimAt(spawnLoc, false, null);
1115+
if (claim == null) return;
1116+
1117+
for (UUID playerID : recentPearlRollbackPlayers) {
1118+
Player p = instance.getServer().getPlayer(playerID);
1119+
if (p != null && p.isOnline() && p.getWorld().equals(spawnLoc.getWorld())
1120+
&& p.getLocation().distanceSquared(spawnLoc) <= 25) {
1121+
event.setCancelled(true);
1122+
return;
1123+
}
1124+
}
1125+
}
1126+
1127+
// Cancel projectile landing when access denied - prevents endermite spawn and entity damage.
1128+
// Upstream uses PlayerTeleportEvent cancel; on Canvas that doesn't fire, so we cancel here.
1129+
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
1130+
public void onProjectileHitEnderPearlCancel(ProjectileHitEvent event) {
1131+
if (event.getEntity().getType() != EntityType.ENDER_PEARL) return;
1132+
if (!instance.config_claims_enderPearlsRequireAccessTrust) return;
1133+
if (!(event.getEntity().getShooter() instanceof Player shooter)) return;
1134+
if (!instance.claimsEnabledForWorld(event.getEntity().getWorld())) return;
1135+
1136+
Block hitBlock = event.getHitBlock();
1137+
Location destLoc = hitBlock != null
1138+
? hitBlock.getLocation().add(0.5, 1, 0.5)
1139+
: event.getEntity().getLocation();
1140+
Supplier<String> noAccessReason = ProtectionHelper.checkPermission(shooter, destLoc,
1141+
ClaimPermission.Access, null);
1142+
if (noAccessReason != null) {
1143+
event.setCancelled(true);
1144+
// Only message/refund once - ProjectileHitEvent can fire multiple times when pearl hits entity
1145+
UUID pearlID = event.getEntity().getUniqueId();
1146+
if (refundedEnderPearlEntities.add(pearlID)) {
1147+
recentPearlRollbackPlayers.add(shooter.getUniqueId());
1148+
SchedulerUtil.runLaterGlobal(instance, () -> recentPearlRollbackPlayers.remove(shooter.getUniqueId()), 20L);
1149+
GriefPrevention.sendMessage(shooter, TextMode.Err, noAccessReason.get());
1150+
if (instance.config_claims_refundDeniedEnderPearls) {
1151+
shooter.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
1152+
}
1153+
// Never remove - stasis chambers keep pearl in motion, triggering many events over time
1154+
}
1155+
}
1156+
}
1157+
1158+
/** Folia/Canvas: use teleportAsync when in region threading. */
1159+
private static void teleportFoliaSafe(Player player, Location location) {
1160+
try {
1161+
player.getClass().getMethod("teleportAsync", Location.class).invoke(player, location);
1162+
} catch (Exception e) {
1163+
player.teleport(location);
1164+
}
1165+
}
1166+
1167+
// Canvas fallback: PlayerTeleportEvent may not fire for ender pearls. Run rollback at tick+1
1168+
// and tick+2 to minimize the window where the player can interact with boats/minecarts.
1169+
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false)
1170+
public void onProjectileHitEnderPearl(ProjectileHitEvent event) {
1171+
if (event.getEntity().getType() != EntityType.ENDER_PEARL) return;
1172+
if (!instance.config_claims_enderPearlsRequireAccessTrust) return;
1173+
if (!(event.getEntity().getShooter() instanceof Player shooter)) return;
1174+
if (!instance.claimsEnabledForWorld(event.getEntity().getWorld())) return;
1175+
1176+
Block hitBlock = event.getHitBlock();
1177+
Location destLoc = hitBlock != null
1178+
? hitBlock.getLocation().add(0.5, 1, 0.5)
1179+
: event.getEntity().getLocation();
1180+
Location fromLoc = shooter.getLocation().clone();
1181+
UUID playerID = shooter.getUniqueId();
1182+
UUID pearlEntityID = event.getEntity().getUniqueId();
1183+
1184+
Runnable rollbackTask = () -> {
1185+
Player p = instance.getServer().getPlayer(playerID);
1186+
if (p == null || !p.isOnline()) return;
1187+
Location now = p.getLocation();
1188+
if (now.getWorld() != destLoc.getWorld()) return;
1189+
if (now.distanceSquared(destLoc) > 25) return;
1190+
Supplier<String> noAccessReason = ProtectionHelper.checkPermission(p, now,
1191+
ClaimPermission.Access, null);
1192+
if (noAccessReason != null) {
1193+
recentPearlRollbackPlayers.add(playerID);
1194+
SchedulerUtil.runLaterGlobal(instance, () -> recentPearlRollbackPlayers.remove(playerID), 20L);
1195+
// Run teleport on player's entity region for fastest possible rollback
1196+
SchedulerUtil.runLaterEntity(instance, p, () -> teleportFoliaSafe(p, fromLoc), 0L);
1197+
if (!refundedEnderPearlEntities.contains(pearlEntityID)) {
1198+
GriefPrevention.sendMessage(p, TextMode.Err, noAccessReason.get());
1199+
if (instance.config_claims_refundDeniedEnderPearls) {
1200+
refundedEnderPearlEntities.add(pearlEntityID);
1201+
p.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
1202+
// Never remove - stasis chambers keep pearl in motion, triggering many events
1203+
}
1204+
}
1205+
}
1206+
};
1207+
SchedulerUtil.runLaterGlobal(instance, rollbackTask, 1L);
1208+
SchedulerUtil.runLaterGlobal(instance, rollbackTask, 2L);
1209+
}
1210+
11971211
// when a player triggers a raid (in a claim)
11981212
@EventHandler(priority = EventPriority.LOWEST)
11991213
public void onPlayerTriggerRaid(RaidTriggerEvent event) {

0 commit comments

Comments
 (0)