001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.bukkit.listener; 020 021import com.destroystokyo.paper.event.block.BeaconEffectEvent; 022import com.destroystokyo.paper.event.block.BlockDestroyEvent; 023import com.destroystokyo.paper.event.entity.EntityPathfindEvent; 024import com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent; 025import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent; 026import com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent; 027import com.destroystokyo.paper.event.entity.SlimePathfindEvent; 028import com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent; 029import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; 030import com.google.inject.Inject; 031import com.plotsquared.bukkit.util.BukkitUtil; 032import com.plotsquared.core.PlotSquared; 033import com.plotsquared.core.command.Command; 034import com.plotsquared.core.command.MainCommand; 035import com.plotsquared.core.configuration.Settings; 036import com.plotsquared.core.configuration.caption.TranslatableCaption; 037import com.plotsquared.core.location.Location; 038import com.plotsquared.core.permissions.Permission; 039import com.plotsquared.core.player.PlotPlayer; 040import com.plotsquared.core.plot.Plot; 041import com.plotsquared.core.plot.PlotArea; 042import com.plotsquared.core.plot.PlotAreaType; 043import com.plotsquared.core.plot.flag.FlagContainer; 044import com.plotsquared.core.plot.flag.implementations.BeaconEffectsFlag; 045import com.plotsquared.core.plot.flag.implementations.DoneFlag; 046import com.plotsquared.core.plot.flag.implementations.FishingFlag; 047import com.plotsquared.core.plot.flag.implementations.ProjectilesFlag; 048import com.plotsquared.core.plot.flag.implementations.TileDropFlag; 049import com.plotsquared.core.plot.flag.types.BooleanFlag; 050import com.plotsquared.core.plot.world.PlotAreaManager; 051import com.plotsquared.core.util.PlotFlagUtil; 052import io.papermc.paper.event.entity.EntityMoveEvent; 053import io.papermc.paper.event.world.StructuresLocateEvent; 054import net.kyori.adventure.text.Component; 055import net.kyori.adventure.text.minimessage.tag.Tag; 056import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 057import org.bukkit.Chunk; 058import org.bukkit.NamespacedKey; 059import org.bukkit.block.Block; 060import org.bukkit.block.TileState; 061import org.bukkit.entity.Entity; 062import org.bukkit.entity.EntityType; 063import org.bukkit.entity.Player; 064import org.bukkit.entity.Projectile; 065import org.bukkit.entity.Slime; 066import org.bukkit.event.Cancellable; 067import org.bukkit.event.EventHandler; 068import org.bukkit.event.EventPriority; 069import org.bukkit.event.Listener; 070import org.bukkit.event.block.BlockPlaceEvent; 071import org.bukkit.event.entity.CreatureSpawnEvent; 072import org.bukkit.projectiles.ProjectileSource; 073import org.checkerframework.checker.nullness.qual.NonNull; 074 075import java.util.ArrayList; 076import java.util.Collection; 077import java.util.List; 078import java.util.Locale; 079import java.util.regex.Pattern; 080 081/** 082 * Events specific to Paper. Some toit nups here 083 */ 084@SuppressWarnings("unused") 085public class PaperListener implements Listener { 086 087 private static final NamespacedKey ITEM = NamespacedKey.minecraft("item"); 088 private static final NamespacedKey FISHING_BOBBER = NamespacedKey.minecraft("fishing_bobber"); 089 090 private final PlotAreaManager plotAreaManager; 091 private Chunk lastChunk; 092 093 @Inject 094 public PaperListener(final @NonNull PlotAreaManager plotAreaManager) { 095 this.plotAreaManager = plotAreaManager; 096 } 097 098 @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) 099 public void onBlockDestroy(final BlockDestroyEvent event) { 100 Location location = BukkitUtil.adapt(event.getBlock().getLocation()); 101 PlotArea area = location.getPlotArea(); 102 if (area == null) { 103 return; 104 } 105 Plot plot = area.getPlot(location); 106 if (plot != null) { 107 // Prevent dropping blocks which normally would not be dropped. 108 if (!event.willDrop()) { 109 return; 110 } 111 event.setWillDrop(plot.getFlag(TileDropFlag.class)); 112 } 113 } 114 115 @EventHandler 116 public void onEntityPathfind(EntityPathfindEvent event) { 117 if (!Settings.Paper_Components.ENTITY_PATHING) { 118 return; 119 } 120 handleEntityMovement(event, event.getEntity().getLocation(), event.getLoc()); 121 } 122 123 @EventHandler 124 public void onEntityPathfind(SlimePathfindEvent event) { 125 if (!Settings.Paper_Components.ENTITY_PATHING) { 126 return; 127 } 128 Slime slime = event.getEntity(); 129 130 Block b = slime.getTargetBlockExact(4); 131 if (b == null) { 132 return; 133 } 134 135 handleEntityMovement(event, event.getEntity().getLocation(), b.getLocation()); 136 } 137 138 @EventHandler 139 public void onEntityMove(EntityMoveEvent event) { 140 if (!Settings.Paper_Components.ENTITY_MOVEMENT) { 141 return; 142 } 143 if (!event.hasExplicitlyChangedBlock()) { 144 return; 145 } 146 handleEntityMovement(event, event.getFrom(), event.getTo()); 147 } 148 149 private static void handleEntityMovement(Cancellable event, org.bukkit.Location from, org.bukkit.Location target) { 150 Location toLoc = BukkitUtil.adapt(target); 151 Location fromLoc = BukkitUtil.adapt(from); 152 PlotArea tarea = toLoc.getPlotArea(); 153 if (tarea == null) { 154 return; 155 } 156 PlotArea farea = fromLoc.getPlotArea(); 157 if (farea == null) { 158 return; 159 } 160 if (tarea != farea) { 161 event.setCancelled(true); 162 return; 163 } 164 Plot tplot = toLoc.getPlot(); 165 Plot fplot = fromLoc.getPlot(); 166 if (tplot == null ^ fplot == null) { 167 event.setCancelled(true); 168 return; 169 } 170 if (tplot == null || tplot.getId().equals(fplot.getId())) { 171 return; 172 } 173 if (fplot.isMerged() && fplot.getConnectedPlots().contains(tplot)) { 174 return; 175 } 176 event.setCancelled(true); 177 } 178 179 @EventHandler 180 public void onPreCreatureSpawnEvent(PreCreatureSpawnEvent event) { 181 if (!Settings.Paper_Components.CREATURE_SPAWN) { 182 return; 183 } 184 Location location = BukkitUtil.adapt(event.getSpawnLocation()); 185 PlotArea area = location.getPlotArea(); 186 if (area == null) { 187 return; 188 } 189 // Armour-stands are handled elsewhere and should not be handled by area-wide entity-spawn options 190 if (event.getType() == EntityType.ARMOR_STAND) { 191 return; 192 } 193 // If entities are spawning... the chunk should be loaded? 194 Entity[] entities = event.getSpawnLocation().getChunk().getEntities(); 195 if (entities.length >= Settings.Chunk_Processor.MAX_ENTITIES) { 196 event.setShouldAbortSpawn(true); 197 event.setCancelled(true); 198 return; 199 } 200 CreatureSpawnEvent.SpawnReason reason = event.getReason(); 201 switch (reason.toString()) { 202 case "DISPENSE_EGG", "EGG", "OCELOT_BABY", "SPAWNER_EGG" -> { 203 if (!area.isSpawnEggs()) { 204 event.setShouldAbortSpawn(true); 205 event.setCancelled(true); 206 return; 207 } 208 } 209 case "REINFORCEMENTS", "NATURAL", "MOUNT", "PATROL", "RAID", "SILVERFISH_BLOCK", "ENDER_PEARL", "TRAP", "VILLAGE_DEFENSE", "VILLAGE_INVASION", "BEEHIVE", "CHUNK_GEN" -> { 210 if (!area.isMobSpawning()) { 211 event.setShouldAbortSpawn(true); 212 event.setCancelled(true); 213 return; 214 } 215 } 216 case "BREEDING" -> { 217 if (!area.isSpawnBreeding()) { 218 event.setShouldAbortSpawn(true); 219 event.setCancelled(true); 220 return; 221 } 222 } 223 case "BUILD_IRONGOLEM", "BUILD_SNOWMAN", "BUILD_WITHER", "CUSTOM" -> { 224 if (!area.isSpawnCustom()) { 225 event.setShouldAbortSpawn(true); 226 event.setCancelled(true); 227 return; 228 } 229 } 230 case "SPAWNER" -> { 231 if (!area.isMobSpawnerSpawning()) { 232 event.setShouldAbortSpawn(true); 233 event.setCancelled(true); 234 return; 235 } 236 } 237 } 238 Plot plot = location.getOwnedPlotAbs(); 239 if (plot == null) { 240 EntityType type = event.getType(); 241 // PreCreatureSpawnEvent **should** not be called for DROPPED_ITEM, just for the sake of consistency 242 if (type.getKey().equals(ITEM)) { 243 if (Settings.Enabled_Components.KILL_ROAD_ITEMS) { 244 event.setCancelled(true); 245 } 246 return; 247 } 248 if (!area.isMobSpawning()) { 249 if (type == EntityType.PLAYER) { 250 return; 251 } 252 if (type.isAlive()) { 253 event.setShouldAbortSpawn(true); 254 event.setCancelled(true); 255 } 256 } 257 if (!area.isMiscSpawnUnowned() && !type.isAlive()) { 258 event.setShouldAbortSpawn(true); 259 event.setCancelled(true); 260 } 261 return; 262 } 263 if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) { 264 event.setShouldAbortSpawn(true); 265 event.setCancelled(true); 266 } 267 } 268 269 @EventHandler 270 public void onPlayerNaturallySpawnCreaturesEvent(PlayerNaturallySpawnCreaturesEvent event) { 271 if (Settings.Paper_Components.CANCEL_CHUNK_SPAWN) { 272 Location location = BukkitUtil.adapt(event.getPlayer().getLocation()); 273 PlotArea area = location.getPlotArea(); 274 if (area != null && !area.isMobSpawning()) { 275 event.setCancelled(true); 276 } 277 } 278 } 279 280 @EventHandler 281 public void onPreSpawnerSpawnEvent(PreSpawnerSpawnEvent event) { 282 if (Settings.Paper_Components.SPAWNER_SPAWN) { 283 Location location = BukkitUtil.adapt(event.getSpawnerLocation()); 284 PlotArea area = location.getPlotArea(); 285 if (area != null && !area.isMobSpawnerSpawning()) { 286 event.setCancelled(true); 287 event.setShouldAbortSpawn(true); 288 } 289 } 290 } 291 292 @EventHandler(priority = EventPriority.HIGHEST) 293 public void onBlockPlace(BlockPlaceEvent event) { 294 if (!Settings.Paper_Components.TILE_ENTITY_CHECK || !Settings.Enabled_Components.CHUNK_PROCESSOR) { 295 return; 296 } 297 if (!(event.getBlock().getState(false) instanceof TileState)) { 298 return; 299 } 300 final Location location = BukkitUtil.adapt(event.getBlock().getLocation()); 301 final PlotArea plotArea = location.getPlotArea(); 302 if (plotArea == null) { 303 return; 304 } 305 final int tileEntityCount = event.getBlock().getChunk().getTileEntities(false).length; 306 if (tileEntityCount >= Settings.Chunk_Processor.MAX_TILES) { 307 final PlotPlayer<?> plotPlayer = BukkitUtil.adapt(event.getPlayer()); 308 plotPlayer.sendMessage( 309 TranslatableCaption.of("errors.tile_entity_cap_reached"), 310 TagResolver.resolver("amount", Tag.inserting(Component.text(Settings.Chunk_Processor.MAX_TILES))) 311 ); 312 event.setCancelled(true); 313 event.setBuild(false); 314 } 315 } 316 317 /** 318 * Unsure if this will be any performance improvement over the spigot version, 319 * but here it is anyway :) 320 * 321 * @param event Paper's PlayerLaunchProjectileEvent 322 */ 323 @EventHandler 324 public void onProjectileLaunch(PlayerLaunchProjectileEvent event) { 325 if (!Settings.Paper_Components.PLAYER_PROJECTILE) { 326 return; 327 } 328 Projectile entity = event.getProjectile(); 329 ProjectileSource shooter = entity.getShooter(); 330 if (!(shooter instanceof Player)) { 331 return; 332 } 333 Location location = BukkitUtil.adapt(entity.getLocation()); 334 PlotArea area = location.getPlotArea(); 335 if (area == null) { 336 return; 337 } 338 PlotPlayer<Player> pp = BukkitUtil.adapt((Player) shooter); 339 Plot plot = location.getOwnedPlot(); 340 341 if (plot == null) { 342 if (!PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, ProjectilesFlag.class, true) && !pp.hasPermission( 343 Permission.PERMISSION_ADMIN_PROJECTILE_ROAD 344 )) { 345 pp.sendMessage( 346 TranslatableCaption.of("permission.no_permission_event"), 347 TagResolver.resolver( 348 "node", 349 Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_ROAD) 350 ) 351 ); 352 entity.remove(); 353 event.setCancelled(true); 354 } 355 } else if (!plot.hasOwner()) { 356 if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)) { 357 pp.sendMessage( 358 TranslatableCaption.of("permission.no_permission_event"), 359 TagResolver.resolver( 360 "node", 361 Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED) 362 ) 363 ); 364 entity.remove(); 365 event.setCancelled(true); 366 } 367 } else if (!plot.isAdded(pp.getUUID())) { 368 if (entity.getType().getKey().equals(FISHING_BOBBER)) { 369 if (plot.getFlag(FishingFlag.class)) { 370 return; 371 } 372 } 373 if (!plot.getFlag(ProjectilesFlag.class)) { 374 if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)) { 375 pp.sendMessage( 376 TranslatableCaption.of("permission.no_permission_event"), 377 TagResolver.resolver( 378 "node", 379 Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER) 380 ) 381 ); 382 entity.remove(); 383 event.setCancelled(true); 384 } 385 } 386 } 387 } 388 389 @EventHandler 390 public void onAsyncTabCompletion(final AsyncTabCompleteEvent event) { 391 if (!Settings.Paper_Components.ASYNC_TAB_COMPLETION) { 392 return; 393 } 394 String buffer = event.getBuffer(); 395 if (!(event.getSender() instanceof Player)) { 396 return; 397 } 398 if ((!event.isCommand() && !buffer.startsWith("/")) || buffer.indexOf(' ') == -1) { 399 return; 400 } 401 if (buffer.startsWith("/")) { 402 buffer = buffer.substring(1); 403 } 404 final String[] unprocessedArgs = buffer.split(Pattern.quote(" ")); 405 if (unprocessedArgs.length == 1) { 406 return; // We don't do anything in this case 407 } else if (!Settings.Enabled_Components.TAB_COMPLETED_ALIASES 408 .contains(unprocessedArgs[0].toLowerCase(Locale.ENGLISH))) { 409 return; 410 } 411 final String[] args = new String[unprocessedArgs.length - 1]; 412 System.arraycopy(unprocessedArgs, 1, args, 0, args.length); 413 try { 414 final PlotPlayer<?> player = BukkitUtil.adapt((Player) event.getSender()); 415 final Collection<Command> objects = MainCommand.getInstance().tab(player, args, buffer.endsWith(" ")); 416 if (objects == null) { 417 return; 418 } 419 final List<String> result = new ArrayList<>(); 420 for (final com.plotsquared.core.command.Command o : objects) { 421 result.add(o.toString()); 422 } 423 event.setCompletions(result); 424 event.setHandled(true); 425 } catch (final Exception ignored) { 426 } 427 } 428 429 @EventHandler(ignoreCancelled = true) 430 public void onBeaconEffect(final BeaconEffectEvent event) { 431 Block block = event.getBlock(); 432 Location beaconLocation = BukkitUtil.adapt(block.getLocation()); 433 Plot beaconPlot = beaconLocation.getPlot(); 434 435 PlotArea area = beaconLocation.getPlotArea(); 436 if (area == null) { 437 return; 438 } 439 440 Player player = event.getPlayer(); 441 Location playerLocation = BukkitUtil.adapt(player.getLocation()); 442 443 PlotPlayer<Player> plotPlayer = BukkitUtil.adapt(player); 444 Plot playerStandingPlot = playerLocation.getPlot(); 445 if (playerStandingPlot == null) { 446 FlagContainer container = area.getRoadFlagContainer(); 447 if (!getBooleanFlagValue(container, BeaconEffectsFlag.class, true) || 448 (beaconPlot != null && Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW)) { 449 event.setCancelled(true); 450 } 451 return; 452 } 453 454 FlagContainer container = playerStandingPlot.getFlagContainer(); 455 boolean plotBeaconEffects = getBooleanFlagValue(container, BeaconEffectsFlag.class, true); 456 if (playerStandingPlot.equals(beaconPlot)) { 457 if (!plotBeaconEffects) { 458 event.setCancelled(true); 459 } 460 return; 461 } 462 463 if (!plotBeaconEffects || Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW) { 464 event.setCancelled(true); 465 } 466 } 467 468 /** 469 * Don't let the server die when populating cartographers (villager offering maps) in classic plot worlds 470 * (as those don't generate POIs) 471 */ 472 @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) 473 public void onStructuresLocate(StructuresLocateEvent event) { 474 if (!PlotSquared.get().getPlotAreaManager().hasPlotArea(event.getWorld().getName())) { 475 return; 476 } 477 final PlotArea area = PlotSquared.get().getPlotAreaManager().getPlotAreaByString(event.getWorld().getName()); 478 if (area != null && area.getType() == PlotAreaType.NORMAL) { 479 event.setCancelled(true); 480 } 481 } 482 483 private boolean getBooleanFlagValue( 484 @NonNull FlagContainer container, 485 @NonNull Class<? extends BooleanFlag<?>> flagClass, 486 boolean defaultValue 487 ) { 488 BooleanFlag<?> flag = container.getFlag(flagClass); 489 return flag == null ? defaultValue : flag.getValue(); 490 } 491 492}