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.google.inject.Inject;
022import com.plotsquared.bukkit.player.BukkitPlayer;
023import com.plotsquared.bukkit.util.BukkitEntityUtil;
024import com.plotsquared.bukkit.util.BukkitUtil;
025import com.plotsquared.core.PlotSquared;
026import com.plotsquared.core.configuration.Settings;
027import com.plotsquared.core.listener.PlayerBlockEventType;
028import com.plotsquared.core.location.Location;
029import com.plotsquared.core.permissions.Permission;
030import com.plotsquared.core.player.PlotPlayer;
031import com.plotsquared.core.plot.Plot;
032import com.plotsquared.core.plot.PlotArea;
033import com.plotsquared.core.plot.PlotHandler;
034import com.plotsquared.core.plot.flag.implementations.DisablePhysicsFlag;
035import com.plotsquared.core.plot.flag.implementations.EntityChangeBlockFlag;
036import com.plotsquared.core.plot.flag.implementations.ExplosionFlag;
037import com.plotsquared.core.plot.flag.implementations.InvincibleFlag;
038import com.plotsquared.core.plot.flag.implementations.ProjectileChangeBlockFlag;
039import com.plotsquared.core.plot.world.PlotAreaManager;
040import com.plotsquared.core.util.EventDispatcher;
041import com.plotsquared.core.util.PlotFlagUtil;
042import com.sk89q.worldedit.bukkit.BukkitAdapter;
043import com.sk89q.worldedit.world.block.BlockType;
044import org.bukkit.Material;
045import org.bukkit.Particle;
046import org.bukkit.World;
047import org.bukkit.block.Block;
048import org.bukkit.entity.Ageable;
049import org.bukkit.entity.Boat;
050import org.bukkit.entity.Entity;
051import org.bukkit.entity.EntityType;
052import org.bukkit.entity.FallingBlock;
053import org.bukkit.entity.Player;
054import org.bukkit.entity.Projectile;
055import org.bukkit.entity.TNTPrimed;
056import org.bukkit.entity.Vehicle;
057import org.bukkit.event.EventHandler;
058import org.bukkit.event.EventPriority;
059import org.bukkit.event.Listener;
060import org.bukkit.event.entity.CreatureSpawnEvent;
061import org.bukkit.event.entity.EntityChangeBlockEvent;
062import org.bukkit.event.entity.EntityCombustByEntityEvent;
063import org.bukkit.event.entity.EntityDamageByEntityEvent;
064import org.bukkit.event.entity.EntityDamageEvent;
065import org.bukkit.event.entity.EntityExplodeEvent;
066import org.bukkit.event.entity.ExplosionPrimeEvent;
067import org.bukkit.event.vehicle.VehicleCreateEvent;
068import org.bukkit.metadata.FixedMetadataValue;
069import org.bukkit.metadata.MetadataValue;
070import org.bukkit.plugin.Plugin;
071import org.bukkit.projectiles.BlockProjectileSource;
072import org.bukkit.projectiles.ProjectileSource;
073import org.checkerframework.checker.nullness.qual.NonNull;
074
075import java.util.Iterator;
076import java.util.List;
077
078@SuppressWarnings("unused")
079public class EntityEventListener implements Listener {
080
081    private final PlotAreaManager plotAreaManager;
082    private final EventDispatcher eventDispatcher;
083    private float lastRadius;
084
085    @Inject
086    public EntityEventListener(
087            final @NonNull PlotAreaManager plotAreaManager,
088            final @NonNull EventDispatcher eventDispatcher
089    ) {
090        this.plotAreaManager = plotAreaManager;
091        this.eventDispatcher = eventDispatcher;
092    }
093
094    @EventHandler(priority = EventPriority.HIGHEST)
095    public void onEntityCombustByEntity(EntityCombustByEntityEvent event) {
096        EntityDamageByEntityEvent eventChange =
097                new EntityDamageByEntityEvent(
098                        event.getCombuster(),
099                        event.getEntity(),
100                        EntityDamageEvent.DamageCause.FIRE_TICK,
101                        event.getDuration()
102                );
103        onEntityDamageByEntityEvent(eventChange);
104        if (eventChange.isCancelled()) {
105            event.setCancelled(true);
106        }
107    }
108
109    @EventHandler(priority = EventPriority.HIGHEST)
110    public void onEntityDamageByEntityEvent(EntityDamageByEntityEvent event) {
111        Entity damager = event.getDamager();
112        Location location = BukkitUtil.adapt(damager.getLocation());
113        if (!this.plotAreaManager.hasPlotArea(location.getWorldName())) {
114            return;
115        }
116        Entity victim = event.getEntity();
117/*
118        if (victim.getType().equals(EntityType.ITEM_FRAME)) {
119            Plot plot = BukkitUtil.getLocation(victim).getPlot();
120            if (plot != null && !plot.isAdded(damager.getUniqueId())) {
121                event.setCancelled(true);
122                return;
123            }
124        }
125*/
126        if (!BukkitEntityUtil.entityDamage(damager, victim, event.getCause())) {
127            if (event.isCancelled()) {
128                if (victim instanceof Ageable ageable) {
129                    if (ageable.getAge() == -24000) {
130                        ageable.setAge(0);
131                        ageable.setAdult();
132                    }
133                }
134            }
135            event.setCancelled(true);
136        }
137    }
138
139    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
140    public void creatureSpawnEvent(CreatureSpawnEvent event) {
141        Entity entity = event.getEntity();
142        Location location = BukkitUtil.adapt(entity.getLocation());
143        PlotArea area = location.getPlotArea();
144        if (area == null) {
145            return;
146        }
147        // Armour-stands are handled elsewhere and should not be handled by area-wide entity-spawn options
148        if (entity.getType() == EntityType.ARMOR_STAND) {
149            return;
150        }
151        CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason();
152        switch (reason.toString()) {
153            case "DISPENSE_EGG", "EGG", "OCELOT_BABY", "SPAWNER_EGG" -> {
154                if (!area.isSpawnEggs()) {
155                    event.setCancelled(true);
156                    return;
157                }
158            }
159            case "REINFORCEMENTS", "NATURAL", "MOUNT", "PATROL", "RAID", "SHEARED", "SILVERFISH_BLOCK", "ENDER_PEARL",
160                    "TRAP", "VILLAGE_DEFENSE", "VILLAGE_INVASION", "BEEHIVE", "CHUNK_GEN", "NETHER_PORTAL",
161                    "DUPLICATION", "FROZEN", "SPELL", "DEFAULT" -> {
162                if (!area.isMobSpawning()) {
163                    event.setCancelled(true);
164                    return;
165                }
166            }
167            case "BREEDING" -> {
168                if (!area.isSpawnBreeding()) {
169                    event.setCancelled(true);
170                    return;
171                }
172            }
173            case "BUILD_IRONGOLEM", "BUILD_SNOWMAN", "BUILD_WITHER", "CUSTOM" -> {
174                if (!area.isSpawnCustom()) {
175                    event.setCancelled(true);
176                    return;
177                }
178            }
179            case "SPAWNER" -> {
180                if (!area.isMobSpawnerSpawning()) {
181                    event.setCancelled(true);
182                    return;
183                }
184            }
185        }
186        Plot plot = area.getOwnedPlotAbs(location);
187        if (plot == null) {
188            if (!area.isMobSpawning()) {
189                event.setCancelled(true);
190            }
191            return;
192        }
193        if (BukkitEntityUtil.checkEntity(entity, plot.getBasePlot(false))) {
194            event.setCancelled(true);
195        }
196    }
197
198    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
199    public void onEntityFall(EntityChangeBlockEvent event) {
200        if (event.getEntityType() != EntityType.FALLING_BLOCK) {
201            return;
202        }
203        Block block = event.getBlock();
204        World world = block.getWorld();
205        String worldName = world.getName();
206        if (!this.plotAreaManager.hasPlotArea(worldName)) {
207            return;
208        }
209        Location location = BukkitUtil.adapt(block.getLocation());
210        PlotArea area = location.getPlotArea();
211        if (area == null) {
212            return;
213        }
214        Plot plot = area.getOwnedPlotAbs(location);
215        if (plot == null || plot.getFlag(DisablePhysicsFlag.class)) {
216            event.setCancelled(true);
217            if (plot != null) {
218                if (block.getType().hasGravity()) {
219                    BlockEventListener.sendBlockChange(block.getLocation(), block.getBlockData());
220                }
221                plot.debug("Falling block event was cancelled because disable-physics = true");
222            }
223            return;
224        }
225        if (event.getTo().hasGravity()) {
226            Entity entity = event.getEntity();
227            List<MetadataValue> meta = entity.getMetadata("plot");
228            if (meta.isEmpty()) {
229                return;
230            }
231            Plot origin = (Plot) meta.get(0).value();
232            if (origin != null && !origin.equals(plot)) {
233                event.setCancelled(true);
234                entity.remove();
235            }
236        } else if (event.getTo() == Material.AIR) {
237            event.getEntity().setMetadata("plot", new FixedMetadataValue((Plugin) PlotSquared.platform(), plot));
238        }
239    }
240
241    @EventHandler(priority = EventPriority.HIGH)
242    public void onDamage(EntityDamageEvent event) {
243        if (event.getEntityType() != EntityType.PLAYER) {
244            return;
245        }
246        Location location = BukkitUtil.adapt(event.getEntity().getLocation());
247        PlotArea area = location.getPlotArea();
248        if (area == null) {
249            return;
250        }
251        Plot plot = location.getOwnedPlot();
252        if (plot == null) {
253            if (PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, InvincibleFlag.class, true)) {
254                event.setCancelled(true);
255            }
256            return;
257        }
258        if (plot.getFlag(InvincibleFlag.class)) {
259            plot.debug(event.getEntity().getName() + " could not take damage because invincible = true");
260            event.setCancelled(true);
261        }
262    }
263
264    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
265    public void onBigBoom(EntityExplodeEvent event) {
266        Location location = BukkitUtil.adapt(event.getLocation());
267        PlotArea area = location.getPlotArea();
268        boolean plotArea = location.isPlotArea();
269        if (!plotArea) {
270            if (!this.plotAreaManager.hasPlotArea(location.getWorldName())) {
271                return;
272            }
273            return;
274        }
275        Plot plot = area.getOwnedPlot(location);
276        if (plot != null) {
277            if (plot.getFlag(ExplosionFlag.class)) {
278                List<MetadataValue> meta = event.getEntity().getMetadata("plot");
279                Plot origin;
280                if (meta.isEmpty()) {
281                    origin = plot;
282                } else {
283                    origin = (Plot) meta.get(0).value();
284                }
285                if (this.lastRadius != 0) {
286                    List<Entity> nearby = event.getEntity().getNearbyEntities(this.lastRadius, this.lastRadius, this.lastRadius);
287                    for (Entity near : nearby) {
288                        if (near instanceof TNTPrimed || near.getType().equals(EntityType.MINECART_TNT)) {
289                            if (!near.hasMetadata("plot")) {
290                                near.setMetadata("plot", new FixedMetadataValue((Plugin) PlotSquared.platform(), plot));
291                            }
292                        }
293                    }
294                    this.lastRadius = 0;
295                }
296                Iterator<Block> iterator = event.blockList().iterator();
297                while (iterator.hasNext()) {
298                    Block block = iterator.next();
299                    location = BukkitUtil.adapt(block.getLocation());
300                    if (!area.contains(location.getX(), location.getZ()) || !origin.equals(area.getOwnedPlot(location))) {
301                        iterator.remove();
302                    }
303                }
304                return;
305            } else {
306                plot.debug("Explosion was cancelled because explosion = false");
307            }
308        }
309        event.setCancelled(true);
310        //Spawn Explosion Particles when enabled in settings
311        if (Settings.General.ALWAYS_SHOW_EXPLOSIONS) {
312            event.getLocation().getWorld().spawnParticle(Particle.EXPLOSION_HUGE, event.getLocation(), 0);
313        }
314    }
315
316    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
317    public void onPeskyMobsChangeTheWorldLikeWTFEvent(EntityChangeBlockEvent event) {
318        Entity e = event.getEntity();
319        Material type = event.getBlock().getType();
320        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
321        PlotArea area = location.getPlotArea();
322        if (area == null) {
323            return;
324        }
325        if (e instanceof FallingBlock) {
326            // allow falling blocks converting to blocks and vice versa
327            return;
328        } else if (e instanceof Boat) {
329            // allow boats destroying lily pads
330            if (type == Material.LILY_PAD) {
331                return;
332            }
333        } else if (e instanceof Player player) {
334            BukkitPlayer pp = BukkitUtil.adapt(player);
335            if (type.toString().equals("POWDER_SNOW")) {
336                // Burning player evaporating powder snow. Use same checks as
337                // trampling farmland
338                BlockType blockType = BukkitAdapter.asBlockType(type);
339                if (!this.eventDispatcher.checkPlayerBlockEvent(pp,
340                        PlayerBlockEventType.TRIGGER_PHYSICAL, location, blockType, true
341                )) {
342                    event.setCancelled(true);
343                }
344                return;
345            } else {
346                // already handled by other flags (mainly the 'use' flag):
347                // - player tilting big dripleaf by standing on it
348                // - player picking glow berries from cave vine
349                // - player trampling farmland
350                // - player standing on or clicking redstone ore
351                return;
352            }
353        } else if (e instanceof Projectile entity) {
354            // Exact same as the ProjectileHitEvent listener, except that we let
355            // the entity-change-block determine what to do with shooters that
356            // aren't players and aren't blocks
357            Plot plot = area.getPlot(location);
358            ProjectileSource shooter = entity.getShooter();
359            if (shooter instanceof Player) {
360                PlotPlayer<?> pp = BukkitUtil.adapt((Player) shooter);
361                if (plot == null) {
362                    if (area.isRoadFlags() && !area.getRoadFlag(ProjectileChangeBlockFlag.class) && !pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)) {
363                        entity.remove();
364                        event.setCancelled(true);
365                    }
366                    return;
367                }
368                if (plot.isAdded(pp.getUUID()) || plot.getFlag(ProjectileChangeBlockFlag.class) || pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)) {
369                    return;
370                }
371                entity.remove();
372                event.setCancelled(true);
373                return;
374            }
375            if (!(shooter instanceof Entity) && shooter != null) {
376                if (plot == null) {
377                    entity.remove();
378                    event.setCancelled(true);
379                    return;
380                }
381                Location sLoc =
382                        BukkitUtil.adapt(((BlockProjectileSource) shooter).getBlock().getLocation());
383                if (!area.contains(sLoc.getX(), sLoc.getZ())) {
384                    entity.remove();
385                    event.setCancelled(true);
386                    return;
387                }
388                Plot sPlot = area.getOwnedPlotAbs(sLoc);
389                if (sPlot == null || !PlotHandler.sameOwners(plot, sPlot)) {
390                    entity.remove();
391                    event.setCancelled(true);
392                }
393                return;
394            }
395            // fall back to entity-change-block flag
396        }
397
398        Plot plot = area.getOwnedPlot(location);
399        if (plot != null && !plot.getFlag(EntityChangeBlockFlag.class)) {
400            plot.debug(e.getType() + " could not change block because entity-change-block = false");
401            event.setCancelled(true);
402        }
403    }
404
405    @EventHandler
406    public void onPrime(ExplosionPrimeEvent event) {
407        this.lastRadius = event.getRadius() + 1;
408    }
409
410    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
411    public void onVehicleCreate(VehicleCreateEvent event) {
412        Vehicle entity = event.getVehicle();
413        Location location = BukkitUtil.adapt(entity.getLocation());
414        PlotArea area = location.getPlotArea();
415        if (area == null) {
416            return;
417        }
418        Plot plot = area.getOwnedPlotAbs(location);
419        if (plot == null || BukkitEntityUtil.checkEntity(entity, plot)) {
420            entity.remove();
421            return;
422        }
423        if (Settings.Enabled_Components.KILL_ROAD_VEHICLES) {
424            entity.setMetadata("plot", new FixedMetadataValue((Plugin) PlotSquared.platform(), plot));
425        }
426    }
427
428}