diff --git a/src/com/wimbli/WorldBorder/BorderData.java b/src/com/wimbli/WorldBorder/BorderData.java index ddbab4b..d8926f8 100644 --- a/src/com/wimbli/WorldBorder/BorderData.java +++ b/src/com/wimbli/WorldBorder/BorderData.java @@ -45,6 +45,11 @@ public class BorderData this.DefiniteSquare = Math.sqrt(.5 * this.radiusSquared); } + public BorderData copy() + { + return new BorderData(x, z, radius, shapeRound); + } + public double getX() { return x; @@ -178,12 +183,12 @@ public class BorderData } //these material IDs are acceptable for places to teleport player; breathable blocks and water - private static LinkedHashSet safeOpenBlocks = new LinkedHashSet(Arrays.asList( + private static final LinkedHashSet safeOpenBlocks = new LinkedHashSet(Arrays.asList( new Integer[] {0, 6, 8, 9, 27, 28, 31, 32, 37, 38, 39, 40, 50, 55, 59, 63, 64, 65, 66, 68, 69, 70, 71, 72, 75, 76, 77, 78, 83, 90, 93, 94} )); //these material IDs are ones we don't want to drop the player onto, like cactus or lava or fire - private static LinkedHashSet painfulBlocks = new LinkedHashSet(Arrays.asList( + private static final LinkedHashSet painfulBlocks = new LinkedHashSet(Arrays.asList( new Integer[] {10, 11, 51, 81} )); @@ -198,7 +203,7 @@ public class BorderData ); } - static final private int limTop = 120, limBot = 1; + private static final int limTop = 120, limBot = 1; // find closest safe Y position from the starting position private double getSafeY(World world, int X, int Y, int Z) diff --git a/src/com/wimbli/WorldBorder/Config.java b/src/com/wimbli/WorldBorder/Config.java index da25f0b..96f6a51 100644 --- a/src/com/wimbli/WorldBorder/Config.java +++ b/src/com/wimbli/WorldBorder/Config.java @@ -16,6 +16,9 @@ import org.bukkit.plugin.Plugin; import org.bukkit.util.config.Configuration; import org.bukkit.util.config.ConfigurationNode; +import org.bukkit.craftbukkit.command.ColouredConsoleSender; +import org.bukkit.craftbukkit.CraftServer; + import org.anjocaido.groupmanager.GroupManager; import com.nijiko.permissions.PermissionHandler; import com.nijikokun.bukkit.Permissions.Permissions; @@ -31,8 +34,11 @@ public class Config private static final Logger mcLog = Logger.getLogger("Minecraft"); public static DecimalFormat coord = new DecimalFormat("0.0"); private static int borderTask = -1; + public static WorldFillTask fillTask = null; public static Set movedPlayers = Collections.synchronizedSet(new HashSet()); - + private static Runtime rt = Runtime.getRuntime(); + private static ColouredConsoleSender console = null; + // actual configuration values which can be changed private static boolean shapeRound = false; private static Map borders = Collections.synchronizedMap(new LinkedHashMap()); @@ -41,13 +47,13 @@ public class Config private static double knockBack = 3.0; private static int timerTicks = 4; -/* // for monitoring plugin efficiency - public static long timeUsed = 0; + // for monitoring plugin efficiency +// public static long timeUsed = 0; + public static long Now() { - return Calendar.getInstance().getTimeInMillis(); + return System.currentTimeMillis(); } -*/ public static void setBorder(String world, BorderData border) { @@ -191,6 +197,39 @@ public class Config } + public static void StopFillTask() + { + if (fillTask != null && fillTask.valid()) + fillTask.cancel(); + } + + public static void StoreFillTask() + { + save(false, true); + } + public static void UnStoreFillTask() + { + save(false); + } + + public static void RestoreFillTask(String world, int fillDistance, int chunksPerRun, int tickFrequency, int x, int z, int length, int total) + { + fillTask = new WorldFillTask(plugin.getServer(), null, world, fillDistance, chunksPerRun, tickFrequency); + if (fillTask.valid()) + { + fillTask.continueProgress(x, z, length, total); + int task = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, fillTask, 20, tickFrequency); + fillTask.setTaskID(task); + } + } + + + public static int AvailableMemory() + { + return (int)((rt.maxMemory() - rt.totalMemory() + rt.freeMemory()) / 1024L / 1024L); + } + + public static void loadPermissions(WorldBorder plugin) { if (GroupPlugin != null || Permissions != null || plugin == null) @@ -224,29 +263,36 @@ public class Config return true; else if (player.isOp()) // Op, always permitted return true; - else if (GroupPlugin != null) // GroupManager plugin available + + if (GroupPlugin != null) // GroupManager plugin available { if (GroupPlugin.getWorldsHolder().getWorldPermissions(player).has(player, "worldborder." + request)) return true; - player.sendMessage("You do not have sufficient permissions to do that."); - return false; } else if (Permissions != null) // Permissions plugin available { if (Permissions.permission(player, "worldborder." + request)) return true; - player.sendMessage("You do not have sufficient permissions to do that."); - return false; } - else + if (player.hasPermission("worldborder." + request)) // built-in Bukkit superperms return true; + + player.sendMessage("You do not have sufficient permissions."); + return false; } + private static final String logName = "WorldBorder"; public static void Log(Level lvl, String text) { - String name = (plugin == null) ? "WorldBorder" : plugin.getDescription().getName(); - mcLog.log(lvl, String.format("[%s] %s", name, text)); + if (console != null) + { + if (lvl != Level.INFO) + text = "[" + lvl.getLocalizedName() + "] " + text; + console.sendMessage(String.format("[%s] %s", logName, text)); + } + else + mcLog.log(lvl, String.format("[%s] %s", logName, text)); } public static void Log(String text) { @@ -265,7 +311,8 @@ public class Config public static void load(WorldBorder master, boolean logIt) { // load config from file plugin = master; - cfg = plugin.getConfiguration(); + console = new ColouredConsoleSender((CraftServer)plugin.getServer()); + cfg = plugin.getConfiguration(); int cfgVersion = cfg.getInt("cfg-version", 1); @@ -311,6 +358,22 @@ public class Config } } + // if we have an unfinished fill task stored from a previous run, load it up + ConfigurationNode storedFillTask = cfg.getNode("fillTask"); + if (storedFillTask != null) + { + String worldName = storedFillTask.getString("world"); + int fillDistance = storedFillTask.getInt("fillDistance", 176); + int chunksPerRun = storedFillTask.getInt("chunksPerRun", 5); + int tickFrequency = storedFillTask.getInt("tickFrequency", 20); + int fillX = storedFillTask.getInt("x", 0); + int fillZ = storedFillTask.getInt("z", 0); + int fillLength = storedFillTask.getInt("length", 0); + int fillTotal = storedFillTask.getInt("total", 0); + RestoreFillTask(worldName, fillDistance, chunksPerRun, tickFrequency, fillX, fillZ, fillLength, fillTotal); + save(false); + } + if (logIt) LogConfig("Configuration loaded."); @@ -319,6 +382,10 @@ public class Config } public static void save(boolean logIt) + { + save(logIt, false); + } + public static void save(boolean logIt, boolean storeFillTask) { // save config to file if (cfg == null) return; @@ -345,6 +412,20 @@ public class Config cfg.setProperty("worlds." + name.replace(".", "¨") + ".shape-round", bord.getShape()); } + if (storeFillTask && fillTask != null && fillTask.valid()) + { + cfg.setProperty("fillTask.world", fillTask.refWorld()); + cfg.setProperty("fillTask.fillDistance", fillTask.refFillDistance()); + cfg.setProperty("fillTask.chunksPerRun", fillTask.refChunksPerRun()); + cfg.setProperty("fillTask.tickFrequency", fillTask.refTickFrequency()); + cfg.setProperty("fillTask.x", fillTask.refX()); + cfg.setProperty("fillTask.z", fillTask.refZ()); + cfg.setProperty("fillTask.length", fillTask.refLength()); + cfg.setProperty("fillTask.total", fillTask.refTotal()); + } + else + cfg.removeProperty("fillTask"); + cfg.save(); if (logIt) diff --git a/src/com/wimbli/WorldBorder/WBCommand.java b/src/com/wimbli/WorldBorder/WBCommand.java index bfc03d0..6c04370 100644 --- a/src/com/wimbli/WorldBorder/WBCommand.java +++ b/src/com/wimbli/WorldBorder/WBCommand.java @@ -32,7 +32,7 @@ public class WBCommand implements CommandExecutor Player player = (sender instanceof Player) ? (Player)sender : null; String cmd = clrCmd + ((player == null) ? "wb" : "/wb"); - String cmdW = clrCmd + ((player == null) ? "wb " + clrReq + "" : "/wb " + clrOpt + "[world]") + clrCmd; + String cmdW = clrCmd + ((player == null) ? "wb " + clrReq + "" : "/wb " + clrOpt + "[world]") + clrCmd; // "set" command from player or console, world specified if (split.length == 5 && split[1].equalsIgnoreCase("set")) @@ -305,13 +305,13 @@ public class WBCommand implements CommandExecutor } catch(NumberFormatException ex) { - sender.sendMessage(clrErr + "The knockback must be a decimal value above 0."); + sender.sendMessage(clrErr + "The knockback must be a decimal value of at least 1.0."); return true; } - if (numBlocks <= 0.0) + if (numBlocks < 1.0) { - sender.sendMessage(clrErr + "The knockback must be a decimal value above 0."); + sender.sendMessage(clrErr + "The knockback must be a decimal value of at least 1.0."); return true; } @@ -399,6 +399,60 @@ public class WBCommand implements CommandExecutor sender.sendMessage("Border shape for world \"" + world + "\" is now set to \"" + (shape == null ? "default" : (shape.booleanValue() ? "round" : "square")) + "\"."); } + // "fill" command from player or console, world specified + else if (split.length >= 2 && split[1].equalsIgnoreCase("fill")) + { + if (!Config.HasPermission(player, "fill")) return true; + + boolean cancel = false, confirm = false, pause = false; + String pad = "", frequency = ""; + if (split.length >= 3) + { + cancel = split[2].equalsIgnoreCase("cancel"); + confirm = split[2].equalsIgnoreCase("confirm"); + pause = split[2].equalsIgnoreCase("pause"); + if (!cancel && !confirm && !pause) + frequency = split[2]; + } + if (split.length >= 4) + pad = split[3]; + + String world = split[0]; + + cmdFill(sender, player, world, confirm, cancel, pause, pad, frequency); + } + + // "fill" command from player (or from console solely if using cancel or confirm), using current world + else if (split.length >= 1 && split[0].equalsIgnoreCase("fill")) + { + if (!Config.HasPermission(player, "fill")) return true; + + boolean cancel = false, confirm = false, pause = false; + String pad = "", frequency = ""; + if (split.length >= 2) + { + cancel = split[1].equalsIgnoreCase("cancel"); + confirm = split[1].equalsIgnoreCase("confirm"); + pause = split[1].equalsIgnoreCase("pause"); + if (!cancel && !confirm && !pause) + frequency = split[1]; + } + if (split.length >= 3) + pad = split[2]; + + String world = ""; + if (player != null) + world = player.getWorld().getName(); + + if (!cancel && !confirm && !pause && world.isEmpty()) + { + sender.sendMessage("You must specify a world! Example: " + cmdW+" fill " + clrOpt + "[freq] [pad]"); + return true; + } + + cmdFill(sender, player, world, confirm, cancel, pause, pad, frequency); + } + // we couldn't decipher any known commands, so show help else { @@ -418,7 +472,7 @@ public class WBCommand implements CommandExecutor page = 1; } - sender.sendMessage(clrHead + plugin.getDescription().getFullName() + " - commands (" + (player != null ? clrOpt + "[optional] " : "") + clrReq + "" + clrHead + ")" + (page > 0 ? " " + page + "/2" : "") + ":"); + sender.sendMessage(clrHead + plugin.getDescription().getFullName() + " - commands (" + clrReq + " " + clrOpt + "[optional]" + clrHead + ")" + (page > 0 ? " " + page + "/2" : "") + ":"); if (page == 0 || page == 1) { @@ -437,6 +491,7 @@ public class WBCommand implements CommandExecutor if (page == 0 || page == 2) { + sender.sendMessage(cmdW+" fill " + clrOpt + "[freq] [pad]" + clrDesc + " - generate world out to border."); sender.sendMessage(cmd+" wshape " + ((player == null) ? clrReq + "" : clrOpt + "[world]") + clrReq + " " + clrDesc + " - shape override."); sender.sendMessage(cmd+" getmsg" + clrDesc + " - display border message."); sender.sendMessage(cmd+" setmsg " + clrReq + "" + clrDesc + " - set border message."); @@ -471,4 +526,110 @@ public class WBCommand implements CommandExecutor Config.setBorder(world, radius, x, z); return true; } + + + private String fillWorld = ""; + private int fillPadding = 16 * 11; + private int fillFrequency = 20; + + private void fillDefaults() + { + fillWorld = ""; + fillFrequency = 20; + // with "view-distance=10" in server.properties and "Render Distance: Far" in client, hitting border during testing + // was loading 11 chunks beyond the border in a couple of directions (10 chunks in the other two directions); thus: + fillPadding = 16 * 11; + } + + private boolean cmdFill(CommandSender sender, Player player, String world, boolean confirm, boolean cancel, boolean pause, String pad, String frequency) + { + if (cancel) + { + sender.sendMessage(clrHead + "Cancelling the world map generation task."); + fillDefaults(); + Config.StopFillTask(); + return true; + } + + if (pause) + { + if (Config.fillTask == null || !Config.fillTask.valid()) + { + sender.sendMessage(clrHead + "The world map generation task is not currently running."); + return true; + } + Config.fillTask.pause(); + sender.sendMessage(clrHead + "The world map generation task is now " + (Config.fillTask.isPaused() ? "" : "un") + "paused."); + return true; + } + + if (Config.fillTask != null && Config.fillTask.valid()) + { + sender.sendMessage(clrHead + "The world map generation task is already running."); + return true; + } + + // set padding and/or delay if those were specified + try + { + if (!pad.isEmpty()) + fillPadding = Math.abs(Integer.parseInt(pad)); + if (!frequency.isEmpty()) + fillFrequency = Math.abs(Integer.parseInt(frequency)); + } + catch(NumberFormatException ex) + { + sender.sendMessage(clrErr + "The frequency and padding values must be integers."); + return false; + } + + // set world if it was specified + if (!world.isEmpty()) + fillWorld = world; + + if (confirm) + { // command confirmed, go ahead with it + if (fillWorld.isEmpty()) + { + sender.sendMessage(clrErr + "You must first use this command successfully without confirming."); + return false; + } + + if (player != null) + Config.Log("Filling out world to border at the command of player \"" + player.getName() + "\"."); + + int ticks = 1, repeats = 1; + if (fillFrequency > 20) + repeats = fillFrequency / 20; + else + ticks = 20 / fillFrequency; + + Config.fillTask = new WorldFillTask(plugin.getServer(), player, fillWorld, fillPadding, repeats, ticks); + if (Config.fillTask.valid()) + { + int task = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, Config.fillTask, ticks, ticks); + Config.fillTask.setTaskID(task); + sender.sendMessage("WorldBorder map generation task started."); + } + else + sender.sendMessage(clrErr + "The world map generation task failed to start."); + + fillDefaults(); + } + else + { + if (fillWorld.isEmpty()) + { + sender.sendMessage(clrErr + "You must first specify a valid world."); + return false; + } + + String cmd = clrCmd + ((player == null) ? "wb" : "/wb"); + sender.sendMessage(clrHead + "World generation task is ready for world \"" + fillWorld + "\", padding the map out to " + fillPadding + " blocks beyond the border (default " + (16 * 11) + "), and the task will try to process up to " + fillFrequency + " chunks per second (default 20)."); + sender.sendMessage(clrHead + "This process can take a very long time depending on the world's border size. Also, depending on the chunk processing rate, players will likely experience severe lag for the duration."); + sender.sendMessage(clrDesc + "You should now use " + cmd + " fill confirm" + clrDesc + " to start the process."); + sender.sendMessage(clrDesc + "You can cancel at any time with " + cmd + " fill cancel" + clrDesc + ", or pause/unpause with " + cmd + " fill pause" + clrDesc + "."); + } + return true; + } } \ No newline at end of file diff --git a/src/com/wimbli/WorldBorder/WorldBorder.java b/src/com/wimbli/WorldBorder/WorldBorder.java index d14d4b4..8c2f63f 100644 --- a/src/com/wimbli/WorldBorder/WorldBorder.java +++ b/src/com/wimbli/WorldBorder/WorldBorder.java @@ -40,6 +40,8 @@ public class WorldBorder extends JavaPlugin PluginDescriptionFile desc = this.getDescription(); System.out.println( desc.getName() + " version " + desc.getVersion() + " shutting down" ); Config.StopBorderTimer(); + Config.StoreFillTask(); + Config.StopFillTask(); } // for other plugins to hook into diff --git a/src/plugin.yml b/src/plugin.yml index a20a2c9..d174594 100644 --- a/src/plugin.yml +++ b/src/plugin.yml @@ -1,7 +1,7 @@ name: WorldBorder author: Brettflan description: Efficient, feature-rich plugin for limiting the size of your worlds. -version: 1.2.3 +version: 1.3.0 main: com.wimbli.WorldBorder.WorldBorder commands: wborder: @@ -20,4 +20,65 @@ commands: / setmsg - set border message. / knockback - how far to move the player back. / delay - time between border checks. - / wshape [world] - override shape. \ No newline at end of file + / wshape [world] - override shape. + / [world] fill [freq] [pad] - generate world out to border. +permissions: + worldborder.*: + description: Grants all WorldBorder permissions + children: + worldborder.set: true + worldborder.radius: true + worldborder.clear: true + worldborder.list: true + worldborder.shape: true + worldborder.getmsg: true + worldborder.setmsg: true + worldborder.reload: true + worldborder.debug: true + worldborder.knockback: true + worldborder.delay: true + worldborder.wshape: true + worldborder.fill: true + worldborder.help: true + worldborder.set: + description: Can set borders for any world + default: op + worldborder.radius: + description: Can set the radius of an existing border + default: op + worldborder.clear: + description: Can remove any border + default: op + worldborder.list: + description: Can view a list of all borders + default: op + worldborder.shape: + description: Can set the default shape (round or square) for all borders + default: op + worldborder.getmsg: + description: Can view the border crossing message + default: op + worldborder.setmsg: + description: Can set the border crossing message + default: op + worldborder.reload: + description: Can force the plugin to reload from the config file + default: op + worldborder.debug: + description: Can enable/disable debug output to console + default: op + worldborder.knockback: + description: Can set the knockback distance for border crossings + default: op + worldborder.delay: + description: Can set the frequency at which the plugin checks for border crossings + default: op + worldborder.wshape: + description: Can set an overriding border shape for a single world + default: op + worldborder.fill: + description: Can fill in (generate) any missing map chunks out to the border + default: op + worldborder.help: + description: Can view the command reference help pages + default: op