7474import org .bukkit .event .player .PlayerItemHeldEvent ;
7575import org .bukkit .event .player .PlayerJoinEvent ;
7676import org .bukkit .event .player .PlayerKickEvent ;
77+ import org .bukkit .event .entity .CreatureSpawnEvent ;
78+ import org .bukkit .event .entity .CreatureSpawnEvent .SpawnReason ;
7779import org .bukkit .event .entity .PlayerLeashEntityEvent ;
7880import org .bukkit .event .entity .ProjectileHitEvent ;
7981import 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