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