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 event.setWillDrop(plot.getFlag(TileDropFlag.class)); 108 } 109 } 110 111 @EventHandler 112 public void onEntityPathfind(EntityPathfindEvent event) { 113 if (!Settings.Paper_Components.ENTITY_PATHING) { 114 return; 115 } 116 handleEntityMovement(event, event.getEntity().getLocation(), event.getLoc()); 117 } 118 119 @EventHandler 120 public void onEntityPathfind(SlimePathfindEvent event) { 121 if (!Settings.Paper_Components.ENTITY_PATHING) { 122 return; 123 } 124 Slime slime = event.getEntity(); 125 126 Block b = slime.getTargetBlockExact(4); 127 if (b == null) { 128 return; 129 } 130 131 handleEntityMovement(event, event.getEntity().getLocation(), b.getLocation()); 132 } 133 134 @EventHandler 135 public void onEntityMove(EntityMoveEvent event) { 136 if (!Settings.Paper_Components.ENTITY_MOVEMENT) { 137 return; 138 } 139 if (!event.hasExplicitlyChangedBlock()) { 140 return; 141 } 142 handleEntityMovement(event, event.getFrom(), event.getTo()); 143 } 144 145 private static void handleEntityMovement(Cancellable event, org.bukkit.Location from, org.bukkit.Location target) { 146 Location toLoc = BukkitUtil.adapt(target); 147 Location fromLoc = BukkitUtil.adapt(from); 148 PlotArea tarea = toLoc.getPlotArea(); 149 if (tarea == null) { 150 return; 151 } 152 PlotArea farea = fromLoc.getPlotArea(); 153 if (farea == null) { 154 return; 155 } 156 if (tarea != farea) { 157 event.setCancelled(true); 158 return; 159 } 160 Plot tplot = toLoc.getPlot(); 161 Plot fplot = fromLoc.getPlot(); 162 if (tplot == null ^ fplot == null) { 163 event.setCancelled(true); 164 return; 165 } 166 if (tplot == null || tplot.getId().equals(fplot.getId())) { 167 return; 168 } 169 if (fplot.isMerged() && fplot.getConnectedPlots().contains(tplot)) { 170 return; 171 } 172 event.setCancelled(true); 173 } 174 175 @EventHandler 176 public void onPreCreatureSpawnEvent(PreCreatureSpawnEvent event) { 177 if (!Settings.Paper_Components.CREATURE_SPAWN) { 178 return; 179 } 180 Location location = BukkitUtil.adapt(event.getSpawnLocation()); 181 PlotArea area = location.getPlotArea(); 182 if (area == null) { 183 return; 184 } 185 // Armour-stands are handled elsewhere and should not be handled by area-wide entity-spawn options 186 if (event.getType() == EntityType.ARMOR_STAND) { 187 return; 188 } 189 // If entities are spawning... the chunk should be loaded? 190 Entity[] entities = event.getSpawnLocation().getChunk().getEntities(); 191 if (entities.length >= Settings.Chunk_Processor.MAX_ENTITIES) { 192 event.setShouldAbortSpawn(true); 193 event.setCancelled(true); 194 return; 195 } 196 CreatureSpawnEvent.SpawnReason reason = event.getReason(); 197 switch (reason.toString()) { 198 case "DISPENSE_EGG", "EGG", "OCELOT_BABY", "SPAWNER_EGG" -> { 199 if (!area.isSpawnEggs()) { 200 event.setShouldAbortSpawn(true); 201 event.setCancelled(true); 202 return; 203 } 204 } 205 case "REINFORCEMENTS", "NATURAL", "MOUNT", "PATROL", "RAID", "SILVERFISH_BLOCK", "ENDER_PEARL", "TRAP", "VILLAGE_DEFENSE", "VILLAGE_INVASION", "BEEHIVE", "CHUNK_GEN" -> { 206 if (!area.isMobSpawning()) { 207 event.setShouldAbortSpawn(true); 208 event.setCancelled(true); 209 return; 210 } 211 } 212 case "BREEDING" -> { 213 if (!area.isSpawnBreeding()) { 214 event.setShouldAbortSpawn(true); 215 event.setCancelled(true); 216 return; 217 } 218 } 219 case "BUILD_IRONGOLEM", "BUILD_SNOWMAN", "BUILD_WITHER", "CUSTOM" -> { 220 if (!area.isSpawnCustom()) { 221 event.setShouldAbortSpawn(true); 222 event.setCancelled(true); 223 return; 224 } 225 } 226 case "SPAWNER" -> { 227 if (!area.isMobSpawnerSpawning()) { 228 event.setShouldAbortSpawn(true); 229 event.setCancelled(true); 230 return; 231 } 232 } 233 } 234 Plot plot = location.getOwnedPlotAbs(); 235 if (plot == null) { 236 EntityType type = event.getType(); 237 // PreCreatureSpawnEvent **should** not be called for DROPPED_ITEM, just for the sake of consistency 238 if (type.getKey().equals(ITEM)) { 239 if (Settings.Enabled_Components.KILL_ROAD_ITEMS) { 240 event.setCancelled(true); 241 } 242 return; 243 } 244 if (!area.isMobSpawning()) { 245 if (type == EntityType.PLAYER) { 246 return; 247 } 248 if (type.isAlive()) { 249 event.setShouldAbortSpawn(true); 250 event.setCancelled(true); 251 } 252 } 253 if (!area.isMiscSpawnUnowned() && !type.isAlive()) { 254 event.setShouldAbortSpawn(true); 255 event.setCancelled(true); 256 } 257 return; 258 } 259 if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) { 260 event.setShouldAbortSpawn(true); 261 event.setCancelled(true); 262 } 263 } 264 265 @EventHandler 266 public void onPlayerNaturallySpawnCreaturesEvent(PlayerNaturallySpawnCreaturesEvent event) { 267 if (Settings.Paper_Components.CANCEL_CHUNK_SPAWN) { 268 Location location = BukkitUtil.adapt(event.getPlayer().getLocation()); 269 PlotArea area = location.getPlotArea(); 270 if (area != null && !area.isMobSpawning()) { 271 event.setCancelled(true); 272 } 273 } 274 } 275 276 @EventHandler 277 public void onPreSpawnerSpawnEvent(PreSpawnerSpawnEvent event) { 278 if (Settings.Paper_Components.SPAWNER_SPAWN) { 279 Location location = BukkitUtil.adapt(event.getSpawnerLocation()); 280 PlotArea area = location.getPlotArea(); 281 if (area != null && !area.isMobSpawnerSpawning()) { 282 event.setCancelled(true); 283 event.setShouldAbortSpawn(true); 284 } 285 } 286 } 287 288 @EventHandler(priority = EventPriority.HIGHEST) 289 public void onBlockPlace(BlockPlaceEvent event) { 290 if (!Settings.Paper_Components.TILE_ENTITY_CHECK || !Settings.Enabled_Components.CHUNK_PROCESSOR) { 291 return; 292 } 293 if (!(event.getBlock().getState(false) instanceof TileState)) { 294 return; 295 } 296 final Location location = BukkitUtil.adapt(event.getBlock().getLocation()); 297 final PlotArea plotArea = location.getPlotArea(); 298 if (plotArea == null) { 299 return; 300 } 301 final int tileEntityCount = event.getBlock().getChunk().getTileEntities(false).length; 302 if (tileEntityCount >= Settings.Chunk_Processor.MAX_TILES) { 303 final PlotPlayer<?> plotPlayer = BukkitUtil.adapt(event.getPlayer()); 304 plotPlayer.sendMessage( 305 TranslatableCaption.of("errors.tile_entity_cap_reached"), 306 TagResolver.resolver("amount", Tag.inserting(Component.text(Settings.Chunk_Processor.MAX_TILES))) 307 ); 308 event.setCancelled(true); 309 event.setBuild(false); 310 } 311 } 312 313 /** 314 * Unsure if this will be any performance improvement over the spigot version, 315 * but here it is anyway :) 316 * 317 * @param event Paper's PlayerLaunchProjectileEvent 318 */ 319 @EventHandler 320 public void onProjectileLaunch(PlayerLaunchProjectileEvent event) { 321 if (!Settings.Paper_Components.PLAYER_PROJECTILE) { 322 return; 323 } 324 Projectile entity = event.getProjectile(); 325 ProjectileSource shooter = entity.getShooter(); 326 if (!(shooter instanceof Player)) { 327 return; 328 } 329 Location location = BukkitUtil.adapt(entity.getLocation()); 330 PlotArea area = location.getPlotArea(); 331 if (area == null) { 332 return; 333 } 334 PlotPlayer<Player> pp = BukkitUtil.adapt((Player) shooter); 335 Plot plot = location.getOwnedPlot(); 336 337 if (plot == null) { 338 if (!PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, ProjectilesFlag.class, true) && !pp.hasPermission( 339 Permission.PERMISSION_ADMIN_PROJECTILE_ROAD 340 )) { 341 pp.sendMessage( 342 TranslatableCaption.of("permission.no_permission_event"), 343 TagResolver.resolver( 344 "node", 345 Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_ROAD) 346 ) 347 ); 348 entity.remove(); 349 event.setCancelled(true); 350 } 351 } else if (!plot.hasOwner()) { 352 if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)) { 353 pp.sendMessage( 354 TranslatableCaption.of("permission.no_permission_event"), 355 TagResolver.resolver( 356 "node", 357 Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED) 358 ) 359 ); 360 entity.remove(); 361 event.setCancelled(true); 362 } 363 } else if (!plot.isAdded(pp.getUUID())) { 364 if (entity.getType().getKey().equals(FISHING_BOBBER)) { 365 if (plot.getFlag(FishingFlag.class)) { 366 return; 367 } 368 } 369 if (!plot.getFlag(ProjectilesFlag.class)) { 370 if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)) { 371 pp.sendMessage( 372 TranslatableCaption.of("permission.no_permission_event"), 373 TagResolver.resolver( 374 "node", 375 Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER) 376 ) 377 ); 378 entity.remove(); 379 event.setCancelled(true); 380 } 381 } 382 } 383 } 384 385 @EventHandler 386 public void onAsyncTabCompletion(final AsyncTabCompleteEvent event) { 387 if (!Settings.Paper_Components.ASYNC_TAB_COMPLETION) { 388 return; 389 } 390 String buffer = event.getBuffer(); 391 if (!(event.getSender() instanceof Player)) { 392 return; 393 } 394 if ((!event.isCommand() && !buffer.startsWith("/")) || buffer.indexOf(' ') == -1) { 395 return; 396 } 397 if (buffer.startsWith("/")) { 398 buffer = buffer.substring(1); 399 } 400 final String[] unprocessedArgs = buffer.split(Pattern.quote(" ")); 401 if (unprocessedArgs.length == 1) { 402 return; // We don't do anything in this case 403 } else if (!Settings.Enabled_Components.TAB_COMPLETED_ALIASES 404 .contains(unprocessedArgs[0].toLowerCase(Locale.ENGLISH))) { 405 return; 406 } 407 final String[] args = new String[unprocessedArgs.length - 1]; 408 System.arraycopy(unprocessedArgs, 1, args, 0, args.length); 409 try { 410 final PlotPlayer<?> player = BukkitUtil.adapt((Player) event.getSender()); 411 final Collection<Command> objects = MainCommand.getInstance().tab(player, args, buffer.endsWith(" ")); 412 if (objects == null) { 413 return; 414 } 415 final List<String> result = new ArrayList<>(); 416 for (final com.plotsquared.core.command.Command o : objects) { 417 result.add(o.toString()); 418 } 419 event.setCompletions(result); 420 event.setHandled(true); 421 } catch (final Exception ignored) { 422 } 423 } 424 425 @EventHandler(ignoreCancelled = true) 426 public void onBeaconEffect(final BeaconEffectEvent event) { 427 Block block = event.getBlock(); 428 Location beaconLocation = BukkitUtil.adapt(block.getLocation()); 429 Plot beaconPlot = beaconLocation.getPlot(); 430 431 PlotArea area = beaconLocation.getPlotArea(); 432 if (area == null) { 433 return; 434 } 435 436 Player player = event.getPlayer(); 437 Location playerLocation = BukkitUtil.adapt(player.getLocation()); 438 439 PlotPlayer<Player> plotPlayer = BukkitUtil.adapt(player); 440 Plot playerStandingPlot = playerLocation.getPlot(); 441 if (playerStandingPlot == null) { 442 FlagContainer container = area.getRoadFlagContainer(); 443 if (!getBooleanFlagValue(container, BeaconEffectsFlag.class, true) || 444 (beaconPlot != null && Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW)) { 445 event.setCancelled(true); 446 } 447 return; 448 } 449 450 FlagContainer container = playerStandingPlot.getFlagContainer(); 451 boolean plotBeaconEffects = getBooleanFlagValue(container, BeaconEffectsFlag.class, true); 452 if (playerStandingPlot.equals(beaconPlot)) { 453 if (!plotBeaconEffects) { 454 event.setCancelled(true); 455 } 456 return; 457 } 458 459 if (!plotBeaconEffects || Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW) { 460 event.setCancelled(true); 461 } 462 } 463 464 /** 465 * Don't let the server die when populating cartographers (villager offering maps) in classic plot worlds 466 * (as those don't generate POIs) 467 */ 468 @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) 469 public void onStructuresLocate(StructuresLocateEvent event) { 470 if (!PlotSquared.get().getPlotAreaManager().hasPlotArea(event.getWorld().getName())) { 471 return; 472 } 473 final PlotArea area = PlotSquared.get().getPlotAreaManager().getPlotAreaByString(event.getWorld().getName()); 474 if (area != null && area.getType() == PlotAreaType.NORMAL) { 475 event.setCancelled(true); 476 } 477 } 478 479 private boolean getBooleanFlagValue( 480 @NonNull FlagContainer container, 481 @NonNull Class<? extends BooleanFlag<?>> flagClass, 482 boolean defaultValue 483 ) { 484 BooleanFlag<?> flag = container.getFlag(flagClass); 485 return flag == null ? defaultValue : flag.getValue(); 486 } 487 488}