diff --git a/src/main/java/net/glowstone/GlowWorld.java b/src/main/java/net/glowstone/GlowWorld.java index e3144583c5..4c2abe76e3 100644 --- a/src/main/java/net/glowstone/GlowWorld.java +++ b/src/main/java/net/glowstone/GlowWorld.java @@ -60,6 +60,7 @@ import net.glowstone.net.message.play.player.ServerDifficultyMessage; import net.glowstone.util.BlockStateDelegate; import net.glowstone.util.GameRuleManager; +import net.glowstone.util.GlowTravelAgent; import net.glowstone.util.RayUtil; import net.glowstone.util.TickUtil; import net.glowstone.util.collection.ConcurrentSet; @@ -376,6 +377,11 @@ public LightningStrike strikeLightningEffect(Location loc, boolean isSilent) { */ @Getter private boolean initialized; + /** + * The TravelAgent of this world. Currently is null when this world is a THE_END world. + */ + @Getter + private GlowTravelAgent travelAgent; /** * Creates a new world from the options in the given WorldCreator. @@ -391,6 +397,10 @@ public GlowWorld(GlowServer server, WorldCreator creator, // set up values from WorldCreator name = creator.name(); environment = creator.environment(); + // end portals are not implemented yet + if (environment != Environment.THE_END) { + travelAgent = new GlowTravelAgent(this); + } worldType = creator.type(); generateStructures = creator.generateStructures(); @@ -766,6 +776,22 @@ public double getMoonPhase() { return 0; } + /** + * Determines if a nether portal can port to a certain world. + * If the nether is allowed, both NORMAL and NETHER environments are valid, else only NORMAL ones are allowed. + * @param destination the world to check for. May be null. + * + * @return whether a nether portal can port to a certain world + */ + public boolean isNetherPortalDestinationValid(GlowWorld destination) { + // Do not allow Nether portals to be accessed from the end + if (destination == null || environment == Environment.THE_END) { + return false; + } + Environment environment = destination.environment; + return environment == Environment.NORMAL || (environment == Environment.NETHER && server.getAllowNether()); + } + public Collection getRawPlayers() { return entityManager.getAll(GlowPlayer.class); } diff --git a/src/main/java/net/glowstone/block/ItemTable.java b/src/main/java/net/glowstone/block/ItemTable.java index 51e24fe73c..a2bdb956fd 100644 --- a/src/main/java/net/glowstone/block/ItemTable.java +++ b/src/main/java/net/glowstone/block/ItemTable.java @@ -68,6 +68,7 @@ import net.glowstone.block.blocktype.BlockObserver; import net.glowstone.block.blocktype.BlockOre; import net.glowstone.block.blocktype.BlockPiston; +import net.glowstone.block.blocktype.BlockPortal; import net.glowstone.block.blocktype.BlockPotato; import net.glowstone.block.blocktype.BlockPumpkin; import net.glowstone.block.blocktype.BlockPumpkinBase; @@ -273,6 +274,7 @@ private void registerBuiltins() { reg(Material.WEB, new BlockWeb()); reg(Material.FIRE, new BlockFire()); reg(Material.ENDER_PORTAL_FRAME, new BlockEnderPortalFrame()); + reg(Material.PORTAL, new BlockPortal()); reg(Material.FENCE_GATE, new BlockFenceGate()); reg(Material.ACACIA_FENCE_GATE, new BlockFenceGate()); reg(Material.BIRCH_FENCE_GATE, new BlockFenceGate()); diff --git a/src/main/java/net/glowstone/block/blocktype/BlockEnderPortalFrame.java b/src/main/java/net/glowstone/block/blocktype/BlockEnderPortalFrame.java index b01cddb92e..eb1bce2771 100644 --- a/src/main/java/net/glowstone/block/blocktype/BlockEnderPortalFrame.java +++ b/src/main/java/net/glowstone/block/blocktype/BlockEnderPortalFrame.java @@ -55,7 +55,7 @@ public boolean blockInteract(GlowPlayer player, GlowBlock block, BlockFace face, block.setData((byte) (block.getData() | 0x4)); if (block.getWorld().getEnvironment() != Environment.THE_END) { - searchForCompletedPortal(player, block); + searchForCompletedEnderPortal(player, block); } return true; } @@ -65,13 +65,13 @@ public boolean blockInteract(GlowPlayer player, GlowBlock block, BlockFace face, /** * Checks for a completed portal at all relevant positions. */ - private void searchForCompletedPortal(GlowPlayer player, GlowBlock changed) { + private void searchForCompletedEnderPortal(GlowPlayer player, GlowBlock changed) { for (int i = 0; i < 4; i++) { for (int j = -1; j <= 1; j++) { GlowBlock center = changed.getRelative(SIDES[i], 2) .getRelative(SIDES[(i + 1) % 4], j); - if (isCompletedPortal(center)) { - createPortal(player, center); + if (isCompletedEnderPortal(center)) { + createEnderPortal(player, center); return; } } @@ -81,7 +81,7 @@ private void searchForCompletedPortal(GlowPlayer player, GlowBlock changed) { /** * Check whether there is a completed portal with the specified center. */ - private boolean isCompletedPortal(GlowBlock center) { + private boolean isCompletedEnderPortal(GlowBlock center) { for (int i = 0; i < 4; i++) { for (int j = -1; j <= 1; j++) { GlowBlock block = center.getRelative(SIDES[i], 2) @@ -98,7 +98,7 @@ private boolean isCompletedPortal(GlowBlock center) { /** * Spawn the portal and call the {@link EntityCreatePortalEvent}. */ - private void createPortal(GlowPlayer player, GlowBlock center) { + private void createEnderPortal(GlowPlayer player, GlowBlock center) { List blocks = new ArrayList<>(9); for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { diff --git a/src/main/java/net/glowstone/block/blocktype/BlockPortal.java b/src/main/java/net/glowstone/block/blocktype/BlockPortal.java new file mode 100644 index 0000000000..56e4d28f83 --- /dev/null +++ b/src/main/java/net/glowstone/block/blocktype/BlockPortal.java @@ -0,0 +1,93 @@ +package net.glowstone.block.blocktype; + +import java.util.concurrent.ThreadLocalRandom; +import net.glowstone.GlowServer; +import net.glowstone.GlowWorld; +import net.glowstone.block.GlowBlock; +import net.glowstone.entity.monster.GlowPigZombie; +import net.glowstone.entity.physics.BoundingBox; +import net.glowstone.util.pattern.PortalShape; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockFace; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.util.Vector; + +public class BlockPortal extends BlockType { + + /** + * Gets the Axis-specific bounding box of the specified block. Only works for Portal blocks. + * + * @param block the block to get the bounding box from. + * @return the resulting bounding box, axis alignment dependent. + */ + public static BoundingBox getBoundingBox(GlowBlock block) { + boolean north = getFace(block.getData()) == BlockFace.NORTH; + Vector base = new Vector(north ? .375 : 0, 0, north ? 0 : .375); + Vector size = new Vector(north ? .25 : 1, 1, north ? 1 : .25); + return BoundingBox.fromPositionAndSize(block.getLocation().toVector().add(base), size); + } + + private static BlockFace getFace(int blockData) { + int faceData = blockData & 3; + return faceData == 1 ? BlockFace.WEST : faceData == 2 ? BlockFace.NORTH : null; + } + + @Override + public void onNearBlockChanged(final GlowBlock block, BlockFace face, GlowBlock changedBlock, + Material oldType, byte oldData, Material newType, byte newData) { + if (newType == oldType || (oldType != Material.PORTAL && oldType != Material.OBSIDIAN)) { + return; + } + BlockFace left = getFace(block.getData()); + if (left == null) { + return; + } + PortalShape shape = new PortalShape(block.getLocation(), left); + if (shape.validate() + && shape.getPortalBlockCount() == shape.getHeight() * shape.getWidth()) { + return; + } + block.setType(Material.AIR); + } + + @Override + public void updateBlock(GlowBlock block) { + // remove invalid portal blocks + if ((block.getData() & 3) == 0) { + block.setType(Material.AIR); + System.out.println(block.getLocation()); + return; + } + GlowWorld world = block.getWorld(); + GlowServer server = world.getServer(); + // No pigman spawns without nether + if (!server.getAllowNether() + // Pigmen only spawn in overworld + || world.getEnvironment() != World.Environment.NORMAL + // Pigmen spawning explicitly disabled + // TODO: Uncomment after implementing Spigot config + //|| !server.spigot().getSpigotConfig(). + //getBoolean("enable-zombie-pigmen-portal-spawns") + // Increasing spawn chance with increasing difficulty. + // If random * 2000 is 0, it is still not bigger than the ordinal of peaceful (0) + || ThreadLocalRandom.current().nextInt(2000) > world.getDifficulty().ordinal() + ) { + return; + } + + Location location = block.getLocation(); + //move down to the bottom of the portal + while (location.getBlock().getType() == Material.PORTAL && location.getY() > 0) { + location.subtract(0, 1, 0); + } + world.spawn(location.add(.5, 2.1, .5), GlowPigZombie.class, + CreatureSpawnEvent.SpawnReason.NETHER_PORTAL); + } + + @Override + public boolean canTickRandomly() { + return true; + } +} diff --git a/src/main/java/net/glowstone/block/itemtype/ItemFlintAndSteel.java b/src/main/java/net/glowstone/block/itemtype/ItemFlintAndSteel.java index a941afbced..be6ee8e1c9 100644 --- a/src/main/java/net/glowstone/block/itemtype/ItemFlintAndSteel.java +++ b/src/main/java/net/glowstone/block/itemtype/ItemFlintAndSteel.java @@ -5,6 +5,8 @@ import net.glowstone.block.ItemTable; import net.glowstone.block.blocktype.BlockTnt; import net.glowstone.entity.GlowPlayer; +import net.glowstone.util.pattern.PortalShape; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.BlockFace; import org.bukkit.event.block.BlockIgniteEvent; @@ -21,15 +23,16 @@ public ItemFlintAndSteel() { @Override public boolean onToolRightClick(GlowPlayer player, GlowBlock target, BlockFace face, - ItemStack holding, Vector clickedLoc, EquipmentSlot hand) { - if (target.getType() == Material.OBSIDIAN) { - fireNetherPortal(target, face); - return true; - } + ItemStack holding, Vector clickedLoc, EquipmentSlot hand) { if (target.getType() == Material.TNT) { fireTnt(target, player); return true; } + + if (tryFireNetherPortal(target, face)) { + return true; + } + if (target.isFlammable() || target.getType().isOccluding()) { setBlockOnFire(player, target, face, holding, clickedLoc, hand); return true; @@ -37,36 +40,49 @@ public boolean onToolRightClick(GlowPlayer player, GlowBlock target, BlockFace f return false; } - private void fireNetherPortal(GlowBlock target, BlockFace face) { - if (face == BlockFace.UP || face == BlockFace.DOWN) { - target = target.getRelative(face); - int limit = 0; - while (target.getType() == Material.AIR && limit < 23) { - target.setType(Material.PORTAL); - target = target.getRelative(face); - limit++; + /** + * Try to fire a nether portal at the given position. + * + * @param target the target block + * @param face the face from which the block was fired + * @return whether a portal could be fired + */ + private boolean tryFireNetherPortal(GlowBlock target, BlockFace face) { + // Where fire would be placed if this is not a portal + Location fireLocation = + target.getLocation().add(face.getModX(), face.getModY(), face.getModZ()); + + PortalShape shape = new PortalShape(fireLocation, BlockFace.WEST); + if (!shape.validate() || shape.getPortalBlockCount() != 0) { + shape = new PortalShape(fireLocation, BlockFace.NORTH); + if (!shape.validate() || shape.getPortalBlockCount() != 0) { + return false; } + shape.placePortalBlocks(); + return true; } + shape.placePortalBlocks(); + return true; } - private void fireTnt(GlowBlock tnt,GlowPlayer player) { + private void fireTnt(GlowBlock tnt, GlowPlayer player) { BlockTnt.igniteBlock(tnt, false, player); } private boolean setBlockOnFire(GlowPlayer player, GlowBlock clicked, BlockFace face, - ItemStack holding, Vector clickedLoc, EquipmentSlot hand) { + ItemStack holding, Vector clickedLoc, EquipmentSlot hand) { GlowBlock fireBlock = clicked.getRelative(face); if (fireBlock.getType() != Material.AIR) { return true; } if (!clicked.isFlammable() - && clicked.getRelative(BlockFace.DOWN).getType() == Material.AIR) { + && clicked.getRelative(BlockFace.DOWN).getType() == Material.AIR) { return true; } - BlockIgniteEvent event = EventFactory.getInstance() - .callEvent(new BlockIgniteEvent(fireBlock, IgniteCause.FLINT_AND_STEEL, player, null)); + BlockIgniteEvent event = EventFactory.getInstance().callEvent( + new BlockIgniteEvent(fireBlock, IgniteCause.FLINT_AND_STEEL, player, null)); if (event.isCancelled()) { player.setItemInHand(holding); return false; @@ -74,7 +90,7 @@ private boolean setBlockOnFire(GlowPlayer player, GlowBlock clicked, BlockFace f // clone holding to avoid decreasing of the item's amount ItemTable.instance().getBlock(Material.FIRE) - .rightClickBlock(player, clicked, face, holding.clone(), clickedLoc, hand); + .rightClickBlock(player, clicked, face, holding.clone(), clickedLoc, hand); return true; } diff --git a/src/main/java/net/glowstone/entity/GlowEntity.java b/src/main/java/net/glowstone/entity/GlowEntity.java index fb1be1353c..9a8ba4712d 100644 --- a/src/main/java/net/glowstone/entity/GlowEntity.java +++ b/src/main/java/net/glowstone/entity/GlowEntity.java @@ -24,6 +24,8 @@ import net.glowstone.EventFactory; import net.glowstone.GlowServer; import net.glowstone.GlowWorld; +import net.glowstone.block.GlowBlock; +import net.glowstone.block.blocktype.BlockPortal; import net.glowstone.chunk.GlowChunk; import net.glowstone.entity.meta.MetadataIndex; import net.glowstone.entity.meta.MetadataIndex.StatusFlags; @@ -51,6 +53,7 @@ import org.bukkit.EntityEffect; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.TravelAgent; import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.block.Block; @@ -292,6 +295,10 @@ public abstract class GlowEntity implements Entity { @Getter @Setter private int portalCooldown; + + @Getter + private boolean isInPortal; + /** * Whether this entity has operator permissions. */ @@ -573,6 +580,9 @@ public void pulse() { if (fireTicks > 0) { --fireTicks; } + if (portalCooldown > 0) { + --portalCooldown; + } metadata.setBit(MetadataIndex.STATUS, StatusFlags.ON_FIRE, fireTicks > 0); // resend position if it's been a while, causes ItemFrames to disappear and GlowPaintings @@ -618,6 +628,52 @@ public void pulse() { } } } + GlowBlock block = world.getBlockAt(location); + // Nether portal handling + if (block.getType().equals(Material.PORTAL) + && BlockPortal.getBoundingBox(block).intersects(boundingBox)) { + if (!isInPortal) { + //Entity just entered the Portal, fire EnterEvent + EventFactory.getInstance().callEvent( + new EntityPortalEnterEvent(this, block.getLocation())); + // prevent EnterEvent being fired again + isInPortal = true; + } + // only try porting if the nether exists + if (server.getAllowNether() && portalCooldown <= 0) { + // determine destination world + GlowWorld destination = getWorld().getEnvironment().equals(Environment.NETHER) + ? server.getWorld("world") : server.getWorld("world_nether"); + if (world.isNetherPortalDestinationValid(destination)) { + TravelAgent agent = destination.getTravelAgent(); + boolean destIsNether = destination.getEnvironment().equals(Environment.NETHER); + // Destination Coordinates: NetherX * 8 = OverworldX etc. + int destX = destIsNether ? location.getBlockX() / 8 : location.getBlockX() * 8; + int destY = destIsNether ? location.getBlockY() / 8 : location.getBlockY() * 8; + int destZ = destIsNether ? location.getBlockZ() / 8 : location.getBlockZ() * 8; + Location requested = new Location(destination, destX, destY, destZ); + if (agent.getCanCreatePortal() || agent.findPortal(requested) != null) { + Location teleportLocation = agent.findOrCreate(requested); + //attempt teleportation: fire porting event, abort if cancelled + EntityPortalEvent p = EventFactory.getInstance().callEvent( + new EntityPortalEvent(this, + location.clone(), teleportLocation.clone(), agent)); + if (!p.isCancelled()) { + // if not, teleport the Entity to the location specified by the event + teleport(p.getTo()); + setPortalCooldown(300); + // change the velocity based on the portal exit event + setVelocity(EventFactory.getInstance().callEvent( + new EntityPortalExitEvent(this, previousLocation, + location.clone(), velocity.clone(), new Vector())).getAfter()); + } + } + } + } + } else { + // entity has left the portal + isInPortal = false; + } if (leashHolderUniqueId != null && ticksLived < 2) { Optional any = world.getEntityManager().getAll().stream() diff --git a/src/main/java/net/glowstone/util/GlowTravelAgent.java b/src/main/java/net/glowstone/util/GlowTravelAgent.java new file mode 100644 index 0000000000..127ae9da7b --- /dev/null +++ b/src/main/java/net/glowstone/util/GlowTravelAgent.java @@ -0,0 +1,320 @@ +package net.glowstone.util; + +import java.util.ArrayList; +import java.util.concurrent.ThreadLocalRandom; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.glowstone.EventFactory; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.TravelAgent; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.event.world.PortalCreateEvent; + +@Accessors(chain = true) +public class GlowTravelAgent implements TravelAgent { + + @Getter + @Setter + private int searchRadius = 128; + @Getter + private int creationRadius = 16; + @Accessors(chain = false) + @Setter + private boolean canCreatePortal = true; + private World world; + + /** + * Construct a new TravelAgent. + * @param world the world this agent operates in + */ + public GlowTravelAgent(World world) { + if (world.getEnvironment() == World.Environment.THE_END) { + throw new UnsupportedOperationException("End Portals are not supported yet."); + } + this.world = world; + } + + @Override + public Location findPortal(Location destination) { + Location minLoc = null; + double minDistance = Double.MAX_VALUE; + + final int maxY = world.getEnvironment() == World.Environment.NETHER ? 127 : 255; + final int blockX = destination.getBlockX(); + final int blockZ = destination.getBlockZ(); + + for (int x = blockX - searchRadius; x < (blockX + searchRadius); x++) { + for (int z = blockZ - searchRadius; z < (blockZ + searchRadius); z++) { + for (int y = maxY; y >= 0; y--) { + final Block toCompare = world.getBlockAt(x, y, z); + + if (toCompare.getType() == Material.PORTAL + && toCompare.getRelative(BlockFace.DOWN).getType() != Material.PORTAL + ) { + final Location location = toCompare.getLocation(); + double distance = RayUtil.getRayBetween(location, destination).length(); + if (distance < minDistance) { + minDistance = distance; + minLoc = location; + } + } + } + } + } + return minLoc; + } + + @Override + public Location findOrCreate(Location location) { + Location found = findPortal(location); + if (found == null) { + if (canCreatePortal && createPortal(location)) { + found = findPortal(location); + } else { + found = location; + } + } + return found; // Bukkit JDoc says to return the input location if no portal could be found + } + + @Override + public boolean getCanCreatePortal() { + return canCreatePortal; + } + + @Override + public TravelAgent setCreationRadius(int creationRadius) { + // Prevent too-low values + this.creationRadius = creationRadius < 2 ? 0 : creationRadius; + return this; + } + + @Override + public boolean createPortal(Location destination) { + if (world.getEnvironment() == World.Environment.THE_END) { + return false; + } + double distance = -1.0D; + //requested location data + final int reqX = destination.getBlockX(); + final int reqY = destination.getBlockY(); + final int reqZ = destination.getBlockZ(); + //actual portal location + int actualX = reqX; + int actualY = reqY; + int actualZ = reqZ; + //axis of the portal. 0 -> X axis, 1, -1 -> Z axis + int axis = 0; + int random = ThreadLocalRandom.current().nextInt(4); + int worldHeight = world.getEnvironment() == World.Environment.NETHER ? 128 : 256; + + //try to find a suitable area for building the portal in range between + // x - 16, z - 16 and x + 16, z + 16. + + //first attempt: find a portal spot that not only guarantees space for the portal, but also + //space around it + // iteration from x - 16 to x + 16 + for (int tempX = reqX - 16; tempX <= reqX + 16; ++tempX) { + // calculation of xOffset from the requested location + double offsetX = (double) tempX + 0.5D - reqX; + // iteration from z - 16 to z + 16 + for (int tempZ = reqZ - 16; tempZ <= reqZ + 16; ++tempZ) { + // calculation of zOffset from the requested location + double offsetZ = (double) tempZ + 0.5D - reqZ; + // search for suitable build location + yLoop: + for (int tempY = worldHeight - 1; tempY >= 0; --tempY) { + // new Location to test suitability + Location location = new Location(world, tempX, tempY, tempZ); + + // Continue if block is not air + if (location.getBlock().getType() != Material.AIR) { + continue; + } + // Move to the ground + while (tempY > 0 && location.getBlock() + .getRelative(BlockFace.DOWN).getType() == Material.AIR) { + --tempY; + location.subtract(0, 1, 0); + } + + // try portals in all four directions + for (int riv = random; riv < random + 4; ++riv) { + // coefficients to easily check whether the environment + // is "save" (solid blocks at the ground, air around the portal) + int coEff1 = riv % 2; + int coEff2 = 1 - coEff1; + if (riv % 4 >= 2) { + coEff1 = -coEff1; + coEff2 = -coEff2; + } + + // guarantee space before portal + for (int safeSpace1 = 0; safeSpace1 < 3; ++safeSpace1) { + // guarantee portal width + for (int safeSpace2 = -1; safeSpace2 < 3; ++safeSpace2) { + // -1 is ground, 0-3 room above -> nether portal is 5 blocks high + for (int height = -1; height < 4; ++height) { + location.setX( + tempX + safeSpace2 * coEff1 + safeSpace1 * coEff2); + location.setY(tempY + height); + location.setZ( + tempZ + safeSpace2 * coEff2 - safeSpace1 * coEff1); + Material material = location.getBlock().getType(); + if (height < 0 && !material.isSolid() + || height >= 0 && material != Material.AIR) { + continue yLoop; + } + } + } + } + // calculate yOffset from requested location + double offsetY = (double) tempY + 0.5D - reqY; + // calculate portal distance from requested location + double newDist = offsetX * offsetX + offsetY * offsetY + offsetZ * offsetZ; + // if the new portal location is closer to the requested one, yield it for + // portal placement + if (distance < 0.0D || newDist < distance) { + distance = newDist; + actualX = tempX; + actualY = tempY; + actualZ = tempZ; + axis = riv % 4; + } + } + } + } + } + + // If no portal spot is found this way, try to find a position where at least portal width + // and height are guaranteed + if (distance < 0.0D) { + // iteration from x - 16 to x + 16 + for (int tempX = reqX - 16; tempX <= reqX + 16; ++tempX) { + // calculation of xOffset from the requested location + double offsetX = (double) tempX + 0.5D - reqX; + // iteration from z - 16 to z + 16 + for (int tempZ = reqZ - 16; tempZ <= reqZ + 16; ++tempZ) { + // calculation of zOffset from the requested location + double offsetZ = (double) tempZ + 0.5D - reqZ; + // search for suitable build location + yLoop2: + for (int tempY = worldHeight - 1; tempY >= 0; --tempY) { + // new Location to test suitability + Location location = new Location(world, tempX, tempY, tempZ); + + // Continue if block is not air + if (location.getBlock().getType() != Material.AIR) { + continue; + } + // if block is air, move to the ground + while (tempY > 0 && location + .getBlock().getRelative(BlockFace.DOWN).getType() == Material.AIR) { + location.subtract(0, 1, 0); + --tempY; + } + + // try portals in x and z direction, just needs to be done 2 times as we + // don't require extra space around the portal + for (int riv = random; riv < random + 2; ++riv) { + // coefficients for easier checking + int coEff1 = riv % 2; + int coEff2 = 1 - coEff1; + + // just guarantee portal width + for (int safeSpace = -1; safeSpace < 3; ++safeSpace) { + // -1 is ground, 0-3 room above -> nether portal is 5 blocks high + for (int height = -1; height < 4; ++height) { + location.setX(tempX + safeSpace * coEff1); + location.setY(tempY + height); + location.setZ(tempZ + safeSpace * coEff2); + + Material material = location.getBlock().getType(); + if (height < 0 && !material.isSolid() + || height >= 0 && material != Material.AIR) { + continue yLoop2; + } + } + } + + // calculate yOffset from requested location + double offsetY = (double) tempY + 0.5D - reqY; + // calculate portal distance from requested location + double newDist = + offsetX * offsetX + offsetY * offsetY + offsetZ * offsetZ; + + // if the new portal location is closer to the requested one, yield it + // for portal placement + if (distance < 0.0D || newDist < distance) { + distance = newDist; + actualX = tempX; + actualY = tempY; + actualZ = tempZ; + axis = riv % 2; + } + } + } + } + } + } + + int finalX = actualX; + int finalY = actualY; + int finalZ = actualZ; + int coEff1 = axis % 2; + int coEff2 = 1 - coEff1; + + if (axis % 4 >= 2) { + coEff1 = -coEff1; + coEff2 = -coEff2; + } + + // Id still no place is found, create an obsidian + // platform in the void to place the portal onto + if (distance < 0.0D) { + actualY = Math.min(Math.max(actualY, 70), worldHeight - 10); + finalY = actualY; + + for (int safeBeforeAfter = -1; safeBeforeAfter <= 1; ++safeBeforeAfter) { + for (int safeWidth = 0; safeWidth < 2; ++safeWidth) { + for (int height = -1; height < 3; ++height) { + int curX = finalX + safeWidth * coEff1 + safeBeforeAfter * coEff2; + int curY = finalY + height; + int curZ = finalZ + safeWidth * coEff2 - safeBeforeAfter * coEff1; + new Location(world, curX, curY, curZ).getBlock() + .setType(height < 0 ? Material.OBSIDIAN : Material.AIR); + } + } + } + } + + // calculate portal axis based on coefficient 1 + byte meta = (byte) (coEff1 == 0 ? 2 : 1); + ArrayList blocks = new ArrayList<>(6); + for (int width = -1; width < 3; ++width) { + for (int height = -1; height < 4; ++height) { + int curX = finalX + width * coEff1; + int curY = finalY + height; + int curZ = finalZ + width * coEff2; + Block block = new Location(world, curX, curY, curZ).getBlock(); + if (width == -1 || width == 2 || height == -1 || height == 3) { + block.setType(Material.OBSIDIAN); + } else { + blocks.add(block); + } + } + } + PortalCreateEvent event = + new PortalCreateEvent(blocks, world, PortalCreateEvent.CreateReason.OBC_DESTINATION); + if (!EventFactory.getInstance().callEvent(event).isCancelled()) { + event.getBlocks().forEach(block -> + block.setTypeIdAndData(Material.PORTAL.getId(), meta, false)); + return true; + } + return false; + } +} diff --git a/src/main/java/net/glowstone/util/pattern/PortalShape.java b/src/main/java/net/glowstone/util/pattern/PortalShape.java new file mode 100644 index 0000000000..457e507a99 --- /dev/null +++ b/src/main/java/net/glowstone/util/pattern/PortalShape.java @@ -0,0 +1,263 @@ +package net.glowstone.util.pattern; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import net.glowstone.EventFactory; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.event.world.PortalCreateEvent; + +/** + * A PortalShape functions as a validator for Nether Portals. + */ +public class PortalShape { + + /** + * Max portal width or height. Inclusive. + */ + private static final int MAX_PORTAL_WIDTH_HEIGHT = 21; + + /** + * Min portal height. Inclusive. + */ + private static final int MIN_PORTAL_HEIGHT = 3; + + /** + * Min portal width. Inclusive. + */ + private static final int MIN_PORTAL_WIDTH = 2; + + /** + * The direction that is considered left. + */ + private final BlockFace left; + /** + * All blocks that already are portal blocks. + */ + @Getter + private int portalBlockCount; + /** + * The bottom leftmost location. + */ + private Location bottomLeft; + /** + * The height oft this portal. + */ + @Getter + private int height; + /** + * The width of this portal. + */ + @Getter + private int width; + + public PortalShape(Location buildLocation, BlockFace portalFace) { + if (portalFace == BlockFace.WEST || portalFace == BlockFace.EAST) { + left = BlockFace.WEST; + } else if (portalFace == BlockFace.NORTH || portalFace == BlockFace.SOUTH) { + left = BlockFace.NORTH; + } else { + throw new IllegalArgumentException( + "Invalid Blockface: " + portalFace + + ". Supported are only NORTH, SOUTH, EAST, WEST"); + } + + //Locations are mutable so we have to clone to compare y-values + Location location = buildLocation.clone(); + + // calculations start on the lower part, so we have to move down + while (location.getY() > buildLocation.getY() - MAX_PORTAL_WIDTH_HEIGHT + && location.getY() > 0 && canBuildThrough(downImmutable(location).getBlock().getType())) { + // move downwards + location.subtract(0, 1, 0); + } + + // No obsidian in 23 block range + if (downImmutable(location).getBlock().getType() != Material.OBSIDIAN) { + return; + } + + // get distance to leftmost edge + int i = getDistanceUntilEdge(location, left); + + // No edge -> no portal + if (i < 0) { + return; + } + + // Set the bottom leftmost portal bloc + bottomLeft = offsetImmutable(location, left, i); + + // Calculate the portal width + width = getDistanceUntilEdge(bottomLeft, left.getOppositeFace()) + 1; + + // Portal too big / too small + if (width < MIN_PORTAL_WIDTH || width > MAX_PORTAL_WIDTH_HEIGHT) { + return; + } + + // calculate portal height + height = calculatePortalHeight(); + } + + /** + * Check whether a given Material counts as empty in a portal. + * + * @param material the material to check for + * @return whether the material counts as empty + */ + private static boolean canBuildThrough(Material material) { + return material == Material.AIR || material == Material.FIRE || material == Material.PORTAL; + } + + /** + * Go down 1 block from a Location without changing the Location itself. + * Needed to perform a "look-ahead" while preserving the original location. + * + * @param in the location to go down from + * @return a new Location with the y-value decreased by one + */ + private static Location downImmutable(Location in) { + return new Location(in.getWorld(), in.getX(), in.getY() - 1, in.getZ()); + } + + /** + * Offset from a given location in a certain direction by a specific amount of times. + * Needed to preserve the original location while offsetting. + * + * @param in the original location to offset from + * @param face the direction in which to offset + * @param offset how many times to offset + * @return a new Location with the specified offset + */ + private static Location offsetImmutable(Location in, BlockFace face, int offset) { + return new Location( + in.getWorld(), + in.getX() + (face.getModX() * offset), + in.getY() + (face.getModY() * offset), + in.getZ() + (face.getModZ() * offset) + ); + } + + /** + * Get the distance to one edge of the possible portal. Returns -1 if no end is reached. + * + * @param location the location from where to check. Not modified in process. + * @param face the side of the edge to check for + * @return the distance or -1 in case of an invalid shape + */ + private int getDistanceUntilEdge(Location location, BlockFace face) { + // Clone the location as Location objects are mutable + Location destLoc = location.clone(); + + int distance; + for (distance = 0; distance <= MAX_PORTAL_WIDTH_HEIGHT; ++distance) { + // Move onwards + destLoc.add(face.getModX(), face.getModY(), face.getModZ()); + // Break if either an obstacle or the end of the obsidian "line" is reached + if (!canBuildThrough(destLoc.getBlock().getType()) + || downImmutable(destLoc).getBlock().getType() != Material.OBSIDIAN) { + break; + } + } + // If the target block is not obsidian, the portal is invalid. + return destLoc.getBlock().getType() == Material.OBSIDIAN ? distance : -1; + } + + /** + * Calculate the portals height. + * + * @return the height or 0 in case of failure + */ + private int calculatePortalHeight() { + for (height = 0; height < MAX_PORTAL_WIDTH_HEIGHT; ++height) { + boolean flag = false; + for (int i = 0; i < width; ++i) { + Location location = offsetImmutable(bottomLeft, left.getOppositeFace(), i) + .add(0, height, 0); + Material material = location.getBlock().getType(); + + if (!canBuildThrough(material)) { + flag = true; + break; + } + + if (material == Material.PORTAL) { + ++portalBlockCount; + } + + if (i == 0) { + material = offsetImmutable(location, left, 1).getBlock().getType(); + if (material != Material.OBSIDIAN) { + flag = true; + break; + } + } else if (i == width - 1) { + material = offsetImmutable(location, left, -1).getBlock().getType(); + if (material != Material.OBSIDIAN) { + flag = true; + break; + } + } + } + if (flag) { + break; + } + } + + for (int j = 0; j < width; ++j) { + if (offsetImmutable(bottomLeft, left, -j).add(0, height, 0).getBlock().getType() + != Material.OBSIDIAN) { + height = 0; + break; + } + } + + if (height <= MAX_PORTAL_WIDTH_HEIGHT && height >= MIN_PORTAL_HEIGHT) { + return height; + } + + bottomLeft = null; + width = 0; + height = 0; + return 0; + } + + /** + * Validate the portal shape. + * + * @return whether the portal shape is valid + */ + public boolean validate() { + return bottomLeft != null && width >= MIN_PORTAL_WIDTH && width <= MAX_PORTAL_WIDTH_HEIGHT + && height >= MIN_PORTAL_HEIGHT && height <= MAX_PORTAL_WIDTH_HEIGHT; + } + + /** + * Place the portal blocks. + */ + @SuppressWarnings("deprecation") + public void placePortalBlocks() { + List portalBlocks = new ArrayList<>(6); + for (int i = 0; i < width; ++i) { + //we have to go down 1 block as Locations are mutable. + Location loc = offsetImmutable(bottomLeft, left.getOppositeFace(), i).subtract(0, 1, 0); + for (int j = 0; j < height; ++j) { + portalBlocks.add(loc.add(0, 1, 0).getBlock()); + } + } + PortalCreateEvent event = new PortalCreateEvent(portalBlocks, bottomLeft.getWorld(), + PortalCreateEvent.CreateReason.FIRE); + if (!EventFactory.getInstance().callEvent(event).isCancelled()) { + // Dirty hack: directly calculate the metadata; Bukkit does not have portal material data :/ + byte meta = (byte) (left == BlockFace.WEST ? 1 : 2); + event.getBlocks().forEach(block -> { + block.setType(Material.PORTAL); + block.setData(meta); + }); + } + } +}