diff --git a/src/com/massivecraft/factions/Conf.java b/src/com/massivecraft/factions/Conf.java index fdf6e7ee..a1b4a728 100644 --- a/src/com/massivecraft/factions/Conf.java +++ b/src/com/massivecraft/factions/Conf.java @@ -129,7 +129,10 @@ public class Conf public static boolean claimsCanBeUnconnectedIfOwnedByOtherFaction = true; public static int claimsRequireMinFactionMembers = 1; public static int claimedLandsMax = 0; - + + // if someone is doing a radius claim and the process fails to claim land this many times in a row, it will exit + public static int radiusClaimFailureLimit = 9; + //public static double considerFactionsReallyOfflineAfterXMinutes = 0.0; public static int actionDeniedPainAmount = 2; diff --git a/src/com/massivecraft/factions/FLocation.java b/src/com/massivecraft/factions/FLocation.java index 856d0e03..7e419c36 100644 --- a/src/com/massivecraft/factions/FLocation.java +++ b/src/com/massivecraft/factions/FLocation.java @@ -40,9 +40,7 @@ public class FLocation public FLocation(Location location) { -// this(location.getWorld().getName(), (int) Math.floor(location.getX() / cellSize) , (int) Math.floor(location.getZ() / cellSize)); - // handy dandy rapid bitshifting instead of division - this(location.getWorld().getName(), location.getBlockX() >> 4, location.getBlockZ() >> 4); + this( location.getWorld().getName(), blockToChunk(location.getBlockX()), blockToChunk(location.getBlockZ()) ); } public FLocation(Player player) @@ -109,6 +107,41 @@ public class FLocation return "["+this.getWorldName()+","+this.getCoordString()+"]"; } + //----------------------------------------------// + // Block/Chunk/Region Value Transformation + //----------------------------------------------// + + // bit-shifting is used because it's much faster than standard division and multiplication + public static int blockToChunk(int blockVal) + { // 1 chunk is 16x16 blocks + return blockVal >> 4; // ">> 4" == "/ 16" + } + + public static int blockToRegion(int blockVal) + { // 1 region is 512x512 blocks + return blockVal >> 9; // ">> 9" == "/ 512" + } + + public static int chunkToRegion(int chunkVal) + { // 1 region is 32x32 chunks + return chunkVal >> 5; // ">> 5" == "/ 32" + } + + public static int chunkToBlock(int chunkVal) + { + return chunkVal << 4; // "<< 4" == "* 16" + } + + public static int regionToBlock(int regionVal) + { + return regionVal << 9; // "<< 9" == "* 512" + } + + public static int regionToChunk(int regionVal) + { + return regionVal << 5; // "<< 5" == "* 32" + } + //----------------------------------------------// // Misc Geometry //----------------------------------------------// @@ -175,12 +208,9 @@ public class FLocation @Override public int hashCode() { - int hash = 3; - hash = 19 * hash + (this.worldName != null ? this.worldName.hashCode() : 0); - hash = 19 * hash + this.x; - hash = 19 * hash + this.z; - return hash; - }; + // should be fast, with good range and few hash collisions: (x * 512) + z + worldName.hashCode + return (this.x << 9) + this.z + (this.worldName != null ? this.worldName.hashCode() : 0); + } @Override public boolean equals(Object obj) diff --git a/src/com/massivecraft/factions/FPlayer.java b/src/com/massivecraft/factions/FPlayer.java index e4fe7ccb..4c0a9074 100644 --- a/src/com/massivecraft/factions/FPlayer.java +++ b/src/com/massivecraft/factions/FPlayer.java @@ -648,12 +648,10 @@ public class FPlayer extends PlayerEntity implements EconomyParticipator { Faction faction = this.getFaction(); if ( ! Econ.modifyMoney(faction, -cost, "to claim this land", "for claiming this land")) return false; - } else { if ( ! Econ.modifyMoney(this, -cost, "to claim this land", "for claiming this land")) return false; - } } diff --git a/src/com/massivecraft/factions/cmd/CmdClaim.java b/src/com/massivecraft/factions/cmd/CmdClaim.java index 90118d8b..dbc0b525 100644 --- a/src/com/massivecraft/factions/cmd/CmdClaim.java +++ b/src/com/massivecraft/factions/cmd/CmdClaim.java @@ -1,12 +1,11 @@ package com.massivecraft.factions.cmd; -import java.util.Set; - -import org.bukkit.Location; - +import com.massivecraft.factions.Conf; import com.massivecraft.factions.FLocation; import com.massivecraft.factions.Faction; import com.massivecraft.factions.struct.Permission; +import com.massivecraft.factions.util.SpiralTask; + public class CmdClaim extends FCommand { @@ -33,34 +32,43 @@ public class CmdClaim extends FCommand public void perform() { // Read and validate input - Faction forFaction = this.argAsFaction(0, myFaction); + final Faction forFaction = this.argAsFaction(0, myFaction); + int radius = this.argAsInt(1, 1); - // just to cut the unauthorized off immediately instead of going on to do radius calculations - if (! fme.canClaimForFactionAtLocation(forFaction, me.getLocation(), false)) + if (radius < 1) { - msg("You do not currently have permission to claim land for the faction "+forFaction.describeTo(fme) +"."); + msg("If you specify a radius, it must be at least 1."); return; } - double radius = this.argAsDouble(1, 1d); - radius -= 0.5; - if (radius <= 0) + if (radius < 2) { - msg("That radius is to small."); - return; + // single chunk + fme.attemptClaim(forFaction, me.getLocation(), true); } - else if (radius > 100) // huge radius can crash server + else { - msg("That radius is overly large. Remember that the radius is in chunks (16x16 blocks), not individual blocks."); - return; - } - - // Get the FLocations - Set flocs = new FLocation(me).getCircle(radius); - for (FLocation floc : flocs) - { - fme.attemptClaim(forFaction, new Location(floc.getWorld(), floc.getX() << 4, 1, floc.getZ() << 4), true); + // radius claim + new SpiralTask(new FLocation(me), radius) + { + private int failCount = 0; + private final int limit = Conf.radiusClaimFailureLimit - 1; + + @Override + public boolean work() + { + boolean success = fme.attemptClaim(forFaction, this.currentLocation(), true); + if (success) + failCount = 0; + else if ( ! success && failCount++ >= limit) + { + this.stop(); + return false; + } + + return true; + } + }; } } - } diff --git a/src/com/massivecraft/factions/util/SpiralTask.java b/src/com/massivecraft/factions/util/SpiralTask.java new file mode 100644 index 00000000..98d5927a --- /dev/null +++ b/src/com/massivecraft/factions/util/SpiralTask.java @@ -0,0 +1,213 @@ +package com.massivecraft.factions.util; + +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import com.massivecraft.factions.FLocation; +import com.massivecraft.factions.P; + + +/* + * reference diagram, task should move in this pattern out from chunk 0 in the center. + * 8 [>][>][>][>][>] etc. + * [^][6][>][>][>][>][>][6] + * [^][^][4][>][>][>][4][v] + * [^][^][^][2][>][2][v][v] + * [^][^][^][^][0][v][v][v] + * [^][^][^][1][1][v][v][v] + * [^][^][3][<][<][3][v][v] + * [^][5][<][<][<][<][5][v] + * [7][<][<][<][<][<][<][7] + */ + +public abstract class SpiralTask implements Runnable +{ + // general task-related reference data + private transient World world = null; + private transient boolean readyToGo = false; + private transient int taskID = -1; + private transient int limit = 0; + + // values for the spiral pattern routine + private transient int x = 0; + private transient int z = 0; + private transient boolean isZLeg = false; + private transient boolean isNeg = false; + private transient int length = -1; + private transient int current = 0; + + @SuppressWarnings("LeakingThisInConstructor") + public SpiralTask(FLocation fLocation, int radius) + { + // limit is determined based on spiral leg length for given radius; see insideRadius() + this.limit = (radius - 1) * 2; + + this.world = Bukkit.getWorld(fLocation.getWorldName()); + if (this.world == null) + { + P.p.log(Level.WARNING, "[SpiralTask] A valid world must be specified!"); + this.stop(); + return; + } + + this.x = (int)fLocation.getX(); + this.z = (int)fLocation.getZ(); + + this.readyToGo = true; + + // get this party started + this.setTaskID(Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(P.p, this, 2, 2)); + } + +/* + * This is where the necessary work is done; you'll need to override this method with whatever you want + * done at each chunk in the spiral pattern. + * Return false if the entire task needs to be aborted, otherwise return true to continue. + */ + public abstract boolean work(); + +/* + * Returns an FLocation pointing at the current chunk X and Z values. + */ + public final FLocation currentFLocation() + { + return new FLocation(world.getName(), x, z); + } +/* + * Returns a Location pointing at the current chunk X and Z values. + * note that the Location is at the corner of the chunk, not the center. + */ + public final Location currentLocation() + { + return new Location(world, FLocation.chunkToBlock(x), 65.0, FLocation.chunkToBlock(z)); + } +/* + * Returns current chunk X and Z values. + */ + public final int getX() + { + return x; + } + public final int getZ() + { + return z; + } + + + +/* + * Below are the guts of the class, which you normally wouldn't need to mess with. + */ + + public final void setTaskID(int ID) + { + if (ID == -1) + this.stop(); + taskID = ID; + } + + public final void run() + { + if (!this.valid() || !readyToGo) return; + + // this is set so it only does one iteration at a time, no matter how frequently the timer fires + readyToGo = false; + + // make sure we're still inside the specified radius + if ( ! this.insideRadius()) return; + + // track this to keep one iteration from dragging on too long and possibly choking the system + long loopStartTime = now(); + + // keep going until the task has been running for 20ms or more, then stop to take a breather + while (now() < loopStartTime + 20) + { + // run the primary task on the current X/Z coordinates + if ( ! this.work()) + { + this.finish(); + return; + } + + // move on to next chunk in spiral + if ( ! this.moveToNext()) + return; + } + + // ready for the next iteration to run + readyToGo = true; + } + + // step through chunks in spiral pattern from center; returns false if we're done, otherwise returns true + public final boolean moveToNext() + { + if ( ! this.valid()) return false; + + // make sure we don't need to turn down the next leg of the spiral + if (current < length) + { + current++; + + // if we're outside the radius, we're done + if ( ! this.insideRadius()) return false; + } + else + { // one leg/side of the spiral down... + current = 0; + isZLeg ^= true; + // every second leg (between X and Z legs, negative or positive), length increases + if (isZLeg) + { + isNeg ^= true; + length++; + } + } + + // move one chunk further in the appropriate direction + if (isZLeg) + z += (isNeg) ? -1 : 1; + else + x += (isNeg) ? -1 : 1; + + return true; + } + + public final boolean insideRadius() + { + boolean inside = current < limit; + if (!inside) + this.finish(); + return inside; + } + + // for successful completion + public void finish() + { +// P.p.log("SpiralTask successfully completed!"); + this.stop(); + } + + // we're done, whether finished or cancelled + public final void stop() + { + if (!this.valid()) return; + + readyToGo = false; + Bukkit.getServer().getScheduler().cancelTask(taskID); + taskID = -1; + } + + // is this task still valid/workable? + public final boolean valid() + { + return taskID != -1; + } + + private static long now() + { + return System.currentTimeMillis(); + } +}