package tc.oc.pgm.blockdrops; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.FallingBlock; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.entity.EntityChangeBlockEvent; import org.bukkit.event.entity.EntityDespawnInVoidEvent; import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.material.MaterialData; import org.bukkit.util.RayBlockIntersection; import org.bukkit.util.Vector; import tc.oc.commons.bukkit.util.BlockUtils; import tc.oc.commons.bukkit.util.NMSHacks; import tc.oc.commons.core.util.Pair; import tc.oc.pgm.kits.KitPlayerFacet; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.commons.bukkit.event.BlockPunchEvent; import tc.oc.commons.bukkit.event.BlockTrampleEvent; import tc.oc.pgm.events.BlockTransformEvent; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.ParticipantBlockTransformEvent; import tc.oc.pgm.match.MatchModule; import tc.oc.commons.bukkit.util.Materials; import javax.annotation.Nullable; import java.util.HashSet; import java.util.Random; import java.util.Set; @ListenerScope(MatchScope.RUNNING) public class BlockDropsMatchModule extends MatchModule implements Listener { private static final double BASE_FALL_SPEED = 3d; private final BlockDropsRuleSet ruleSet; // Tracks FallingBlocks created by explosions that have been randomly chosen // to not form a block when they land. We need to track them from the time // they are created because we don't want this to affect FallingBlocks created // in the normal vanilla way. // // This WILL leak a few entities now and then, because there are ways they can // die that do not fire an event e.g. the tick age limit, but this should be // rare and they will only leak until the end of the match. private final Set<FallingBlock> fallingBlocksThatWillNotLand = new HashSet<>(); public BlockDropsMatchModule(Match match, BlockDropsRuleSet ruleSet) { super(match); this.ruleSet = ruleSet; } public BlockDropsRuleSet getRuleSet() { return ruleSet; } public static boolean causesDrops(final Event event) { return event instanceof BlockBreakEvent || event instanceof EntityExplodeEvent; } @EventHandler(priority = EventPriority.LOW) public void initializeDrops(BlockTransformEvent event) { if(!causesDrops(event.getCause())) { return; } BlockDrops drops = this.ruleSet.getDrops(event, event.getOldState(), ParticipantBlockTransformEvent.getPlayerState(event)); if(drops != null) { event.setDrops(drops); } } private void dropItems(BlockDrops drops, @Nullable MatchPlayer player, Location location, double yield) { if(player == null || player.getBukkit().getGameMode() != GameMode.CREATIVE) { Random random = getMatch().getRandom(); for (Pair<Double, ItemStack> entry : drops.items) { if (random.nextFloat() < yield * entry.first) { location.getWorld().dropItemNaturally(BlockUtils.center(location), entry.second); } } } } private void dropExperience(BlockDrops drops, Location location) { if(drops.experience != 0) { ExperienceOrb expOrb = (ExperienceOrb) location.getWorld().spawnEntity(BlockUtils.center(location), EntityType.EXPERIENCE_ORB); if(expOrb != null) { expOrb.setExperience(drops.experience); } } } private void giveKit(BlockDrops drops, MatchPlayer player) { if(player != null && player.isParticipating() && player.isSpawned() && drops.kit != null) { player.facet(KitPlayerFacet.class).applyKit(drops.kit, false); } } private void dropObjects(BlockDrops drops, @Nullable MatchPlayer player, Location location, double yield, boolean explosion) { giveKit(drops, player); if(explosion) { match.getScheduler(MatchScope.RUNNING).createTask(() -> dropItems(drops, player, location, yield)); } else { dropItems(drops, player, location, yield); dropExperience(drops, location); } } private void replaceBlock(BlockDrops drops, Block block, MatchPlayer player) { if(drops.replacement != null) { EntityChangeBlockEvent event = new EntityChangeBlockEvent(player.getBukkit(), block, drops.replacement); getMatch().callEvent(event); if(!event.isCancelled()) { BlockState state = block.getState(); state.setType(drops.replacement.getItemType()); state.setData(drops.replacement); state.update(true, true); } } } /** * This is not an event handler. It is called explicitly by BlockTransformListener * after all event handlers have been called. */ @SuppressWarnings("deprecation") public void doBlockDrops(final BlockTransformEvent event) { if(!causesDrops(event.getCause())) { return; } final BlockDrops drops = event.getDrops(); if(drops != null) { event.setCancelled(true); final BlockState oldState = event.getOldState(); final BlockState newState = event.getNewState(); final Block block = event.getOldState().getBlock(); final int newTypeId = newState.getTypeId(); final byte newData = newState.getRawData(); block.setTypeIdAndData(newTypeId, newData, true); boolean explosion = false; MatchPlayer player = ParticipantBlockTransformEvent.getParticipant(event); if(event.getCause() instanceof EntityExplodeEvent) { EntityExplodeEvent explodeEvent = (EntityExplodeEvent) event.getCause(); explosion = true; if(drops.fallChance != null && oldState.getType().isBlock() && oldState.getType() != Material.AIR && this.getMatch().getRandom().nextFloat() < drops.fallChance) { FallingBlock fallingBlock = event.getOldState().spawnFallingBlock(); fallingBlock.setDropItem(false); if(drops.landChance != null && this.getMatch().getRandom().nextFloat() >= drops.landChance) { this.fallingBlocksThatWillNotLand.add(fallingBlock); } Vector v = fallingBlock.getLocation().subtract(explodeEvent.getLocation()).toVector(); double distance = v.length(); v.normalize().multiply(BASE_FALL_SPEED * drops.fallSpeed / Math.max(1d, distance)); // A very simple deflection model. Check for a solid // neighbor block and "bounce" the velocity off of it. Block west = block.getRelative(BlockFace.WEST); Block east = block.getRelative(BlockFace.EAST); Block down = block.getRelative(BlockFace.DOWN); Block up = block.getRelative(BlockFace.UP); Block north = block.getRelative(BlockFace.NORTH); Block south = block.getRelative(BlockFace.SOUTH); if((v.getX() < 0 && west != null && Materials.isColliding(west.getType())) || v.getX() > 0 && east != null && Materials.isColliding(east.getType())) { v.setX(-v.getX()); } if((v.getY() < 0 && down != null && Materials.isColliding(down.getType())) || v.getY() > 0 && up != null && Materials.isColliding(up.getType())) { v.setY(-v.getY()); } if((v.getZ() < 0 && north != null && Materials.isColliding(north.getType())) || v.getZ() > 0 && south != null && Materials.isColliding(south.getType())) { v.setZ(-v.getZ()); } fallingBlock.setVelocity(v); } } dropObjects(drops, player, newState.getLocation(), 1d, explosion); } } @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onFallingBlockLand(BlockTransformEvent event) { if(event.getCause() instanceof EntityChangeBlockEvent) { Entity entity = ((EntityChangeBlockEvent) event.getCause()).getEntity(); if(entity instanceof FallingBlock && this.fallingBlocksThatWillNotLand.remove(entity)) { event.setCancelled(true); } } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockFallInVoid(EntityDespawnInVoidEvent event) { if(event.getEntity() instanceof FallingBlock) { this.fallingBlocksThatWillNotLand.remove(event.getEntity()); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockPunch(BlockPunchEvent event) { final MatchPlayer player = getMatch().getPlayer(event.getPlayer()); if(player == null) return; RayBlockIntersection hit = event.getIntersection(); BlockDrops drops = getRuleSet().getDrops(event, hit.getBlock().getState(), player.getParticipantState()); if(drops == null) return; MaterialData oldMaterial = hit.getBlock().getState().getData(); replaceBlock(drops, hit.getBlock(), player); // Play a fake punching effect if the block is punchable. Use raw particles instead of // playBlockBreakEffect so the position is precise rather than in the block center. Object packet = NMSHacks.blockCrackParticlesPacket(oldMaterial, false, hit.getPosition(), new Vector(), 0, 5); for(MatchPlayer viewer : getMatch().getPlayers()) { if(viewer.getBukkit().getEyeLocation().toVector().distanceSquared(hit.getPosition()) < 16 * 16) { NMSHacks.sendPacket(viewer.getBukkit(), packet); } } NMSHacks.playBlockPlaceSound(hit.getBlock().getWorld(), hit.getPosition(), oldMaterial.getItemType(), 1); dropObjects(drops, player, hit.getPosition().toLocation(hit.getBlock().getWorld()), 1d, false); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockTrample(BlockTrampleEvent event) { final MatchPlayer player = getMatch().getPlayer(event.getPlayer()); if(player == null) return; BlockDrops drops = getRuleSet().getDrops(event, event.getBlock().getState(), player.getParticipantState()); if(drops == null) return; replaceBlock(drops, event.getBlock(), player); dropObjects(drops, player, player.getBukkit().getLocation(), 1d, false); } }