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