Util Locating the nearest Nether portal

Discussion in 'Resources' started by teej107, Jan 28, 2015.

Thread Status:
Not open for further replies.
  1. Offline

    teej107

    I created this resource because of this thread: http://bukkit.org/threads/travelagent-is-being-not-so-friendly.338211/
    Basically the problem is that CraftBukkit's TravelAgent wasn't always accurate when finding nether portals. So I had to create my own methods to achieve finding nether portals.
    NetherPortalFinder.java (open)

    Code:
    import org.bukkit.Location;
    import org.bukkit.Material;
    import org.bukkit.World;
    import org.bukkit.block.Block;
    import org.bukkit.block.BlockFace;
    import org.bukkit.util.Vector;
    
    import java.util.HashSet;
    import java.util.Set;
    
    import static org.bukkit.block.BlockFace.*;
    import static org.bukkit.World.Environment.*;
    
    public class NetherPortalFinder
    {
        private static final BlockFace[] BLOCK_FACES = new BlockFace[] {
                EAST, WEST, NORTH, SOUTH, UP, DOWN };
    
        /**
         * * Gets the nearest Nether portal within the specified radius in relation to the given location.
         *
         * @param location  Center of the search radius
         * @param radius    The search radius
         * @param minHeight Minimum height of search
         * @param maxHeight Maximum height of search
         * @return Returns location in the bottom center of the nearest nether portal if found. Otherwise returns null.
         */
        public static Location locate(Location location, int radius, int minHeight,
                int maxHeight)
        {
            //Collection of all the PortalGroups found
            Set<PortalGroup> portals = new HashSet<PortalGroup>();
            //Collection of all the Portal blocks found in the PortalGroups
            Set<Vector> stored = new HashSet<Vector>();
            World world = location.getWorld();
    
            //The nearest PortalGroup and its distance.
            PortalGroup nearest = null;
            double nearestDistance = Double.MAX_VALUE;
    
            //Setting starting and ending Y value
            int yStart = location.getBlockY() - radius;
            yStart = yStart < minHeight ? minHeight : yStart;
            int yEnd = location.getBlockY() + radius;
            yEnd = yEnd > maxHeight ? maxHeight : yEnd;
    
            for (int x = location.getBlockX() - radius;
                 x <= location.getBlockX() + radius; x++)
            {
                for (int y = yStart; y <= yEnd; y += 2)
                {
                    for (int z = location.getBlockZ() - radius;
                         z <= location.getBlockZ() + radius; z++)
                    {
                        //Check in a checkerboard-like fashion
                        if (x + z % 2 == 0)
                            break;
    
                        //Location being iterated over.
                        Location loc = new Location(world, x, y, z);
                        Vector vec = loc.toVector();
                        //Don't do anything if the Portal block is already stored.
                        if (!stored.contains(vec))
                        {
                            PortalGroup pg = getPortalBlocks(loc);
                            //Do nothing if there are no Portal blocks
                            if (pg != null)
                            {
                                //If the PortalGroup was added, store the Portal blocks in the Collection
                                if (portals.add(pg))
                                {
                                    stored.addAll(pg.getBlocks());
    
                                    //Getting the nearest PortalGroup
                                    double distanceSquared = pg
                                            .distanceSquared(vec);
                                    if (distanceSquared < nearestDistance)
                                    {
                                        nearestDistance = distanceSquared;
                                        nearest = pg;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            return nearest == null ? null : nearest.teleportTo();
        }
    
        public static Location locate(Location location)
        {
            World w = location.getWorld();
            World.Environment dimension = w.getEnvironment();
            int maxHeight = dimension == NORMAL ? w.getMaxHeight() - 1 : 127;
            return locate(location, 128, 1, maxHeight);
        }
    
        /**
         * Gets the Portal blocks that is part of the Nether portal.
         *
         * @param loc - Location to start the getting the portal blocks.
         * @return A PortalGroup of all the found Portal blocks. Otherwise returns null. This will return null if the amount of found blocks if below 6.
         */
        public static PortalGroup getPortalBlocks(Location loc)
        {
            if (loc.getBlock().getType() != Material.PORTAL)
                return null;
    
            PortalGroup pg = portalBlock(new PortalGroup(loc.getWorld()), loc);
            return pg.size() > 5 ? pg : null;
        }
    
        private static PortalGroup portalBlock(PortalGroup group, Location loc)
        {
            for (BlockFace face : BLOCK_FACES)
            {
                Block relative = loc.getBlock().getRelative(face);
                Location relLoc = relative.getLocation();
                if (group.add(relLoc.toVector()))
                {
                    portalBlock(group, relLoc);
                }
            }
            return group;
        }
    }
    

    The big method to call in this class is the locate method. It takes four parameters. The 1st being a Location which is the center of the block iteration. An example of a Location to use would be: PlayerMoveEvent#getTo() which is also used in the PlayerPortalEvent. The 2nd parameter is an int which is the search range. The third is the minimum search height. The fourth is the maximum search height. This method returns a Location of the bottom center of the Nether portal blocks so you can teleport Entities. You could also just call the other locate method which just takes a Location in the parameters.

    There is also another class that is needed to use with the above class
    PortalGroup.java (open)

    Code:
    import org.bukkit.Location;
    import org.bukkit.Material;
    import org.bukkit.World;
    import org.bukkit.util.Vector;
    
    import java.util.Collection;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Set;
    
    public class PortalGroup
    {
        private World world;
        private Set<Vector> portal;
        private HashMap<Integer, Set<Vector>> yBlock;
        private Location teleportTo;
        private double distanceSquared;
        private int bottom;
    
        /**
         * A group of Portal block for a Nether portal.
         *
         * @param world The world the Portal blocks resides in.
         */
        public PortalGroup(World world)
        {
            this.world = world;
            portal = new HashSet<Vector>();
            yBlock = new HashMap<Integer, Set<Vector>>();
            bottom = Integer.MAX_VALUE;
            distanceSquared = Double.MAX_VALUE;
        }
    
        /**
         * Adds the Location to the PortalGroup.
         *
         * @param vec Vector to add
         * @return if the Location was added. Otherwise false.
         */
        public boolean add(Vector vec)
        {
            //Check to see if the block is a Portal block.
            if (vec.toLocation(world).getBlock().getType() != Material.PORTAL)
                return false;
    
            boolean b = portal.add(vec);
            //If the location was added, do more actions.
            if (b)
            {
                int y = vec.getBlockY();
                if (y < bottom)
                {
                    //The bottom of the Nether portal
                    bottom = vec.getBlockY();
                }
                //Put the Location in a Map sorted by Y value.
                Set<Vector> set = yBlock.get(y);
                if (set == null)
                {
                    set = new HashSet<Vector>();
                    yBlock.put(y, set);
                }
                set.add(vec);
                //Reset the teleport location and distance squared since a new block was added.
                distanceSquared = Double.MAX_VALUE;
                teleportTo = null;
            }
            return b;
        }
    
        public int size()
        {
            return portal.size();
        }
    
        public Collection<Vector> getBlocks()
        {
            return Collections.unmodifiableCollection(portal);
        }
    
        /**
         * Gets the average distance squared of all the portal blocks.
         *
         * @param vector vector to compare.
         * @return distance squared
         */
        public double distanceSquared(Vector vector)
        {
            if (distanceSquared < Double.MAX_VALUE)
                return distanceSquared;
    
            double d = 0;
            for (Vector vec : portal)
            {
                d += vec.distanceSquared(vector);
            }
            distanceSquared = d / portal.size();
            return distanceSquared;
        }
    
        /**
         * Gets the location to teleport an entity to the Nether portal.
         *
         * @return The location in the bottom center of the Nether portal.
         */
        public Location teleportTo()
        {
            if (teleportTo != null)
                return teleportTo;
    
            if (portal.size() == 0)
                return null;
    
            Set<Vector> bottomY = yBlock.get(bottom);
            int x = 0;
            int z = 0;
            for (Vector loc : bottomY)
            {
                x += loc.getBlockX();
                z += loc.getBlockZ();
            }
            teleportTo = new Location(world, ((x / bottomY.size()) + 0.5),
                    bottom, ((z / bottomY.size()) + 0.5));
            return teleportTo;
        }
    
        @Override
        public int hashCode()
        {
            return portal.hashCode();
        }
    
        @Override
        public boolean equals(Object o)
        {
            if (o instanceof PortalGroup)
            {
                PortalGroup pg = (PortalGroup) o;
                return portal.equals(pg.portal);
            }
            return portal.equals(o);
        }
    }
    

    This class represents the group of Portal blocks that a Nether portal contains. You do not need to use this class. The locate method does use this class so it is necessary.

    These classes are made to search for the default sized nether portals as well as the larger built ones. Since the default Nether portal contains 6 Portal blocks, it won't locate any Nether portals that are smaller than the default sized ones.

    If you think that this code could be improved/more efficient, please say so. If you have any questions, don't be afraid to ask! Constructive criticism is always welcome.

    WARNING: Don't have the search radius be too big. 128 is fine, CraftBukkit's default TravelAgent uses that range. Using 1024 however crashed my server.
     
    Last edited: Jan 30, 2015
  2. Offline

    Experminator

    @teej107 Great work, Teej! :)
    You're welcome!
     
    Last edited: Jan 29, 2015
    teej107 likes this.
  3. Offline

    Funergy

    teej107 and Experminator like this.
  4. Offline

    _Filip

    Have you benchmarked this?
     
  5. Offline

    teej107

    @_Filip No, but I'm in the middle of fixing bugs and improving the code. Even though the max distance in the Overworld to connect to a portal in the Nether is < 1024 blocks, I am 100% sure that CB doesn't iterate over that many blocks. I am fairly certain it stores the block locations somewhere. That being said you could skip the block iterations by caching the PortalGroup objects. Though iterating over a 1024 block range crashed my server and when looking through the CB's own TravelAgent class, it doesn't even iterate over that many blocks. It iterates over a 128 block radius which doesn't cause a noticeable lag spike with my code if any. Iterating 1024 block radius definitely lagged the server for a couple of seconds (no surprise there).
     
  6. where can u use it in a command
     
  7. Offline

    teej107

Thread Status:
Not open for further replies.

Share This Page