Dateien nach "src/main/java/com/wimbli/WorldBorder" hochladen
This commit is contained in:
31
src/main/java/com/wimbli/WorldBorder/BlockPlaceListener.java
Normal file
31
src/main/java/com/wimbli/WorldBorder/BlockPlaceListener.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
|
||||
|
||||
public class BlockPlaceListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(BlockPlaceEvent event) {
|
||||
Location loc = event.getBlockPlaced().getLocation();
|
||||
if (loc == null) return;
|
||||
|
||||
World world = loc.getWorld();
|
||||
if (world == null) return;
|
||||
BorderData border = Config.Border(world.getName());
|
||||
if (border == null) return;
|
||||
|
||||
if (!border.insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
}
|
||||
161
src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java
Normal file
161
src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java
Normal file
@@ -0,0 +1,161 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
||||
public class BorderCheckTask implements Runnable {
|
||||
// track players who are being handled (moved back inside the border) already; needed since Bukkit is sometimes sending teleport events with the old (now incorrect) location still indicated, which can lead to a loop when we then teleport them thinking they're outside the border, triggering event again, etc.
|
||||
private static final Set<String> handlingPlayers = Collections.synchronizedSet(new LinkedHashSet<String>());
|
||||
|
||||
// set targetLoc only if not current player location; set returnLocationOnly to true to have new Location returned if they need to be moved to one, instead of directly handling it
|
||||
public static Location checkPlayer(Player player, Location targetLoc, boolean returnLocationOnly, boolean notify) {
|
||||
if (player == null || !player.isOnline()) return null;
|
||||
|
||||
Location loc = (targetLoc == null) ? player.getLocation().clone() : targetLoc;
|
||||
if (loc == null) return null;
|
||||
|
||||
World world = loc.getWorld();
|
||||
if (world == null) return null;
|
||||
BorderData border = Config.Border(world.getName());
|
||||
if (border == null) return null;
|
||||
|
||||
if (border.insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound()))
|
||||
return null;
|
||||
|
||||
// if player is in bypass list (from bypass command), allow them beyond border; also ignore players currently being handled already
|
||||
if (Config.isPlayerBypassing(player.getUniqueId()) || handlingPlayers.contains(player.getName().toLowerCase()))
|
||||
return null;
|
||||
|
||||
// tag this player as being handled so we can't get stuck in a loop due to Bukkit currently sometimes repeatedly providing incorrect location through teleport event
|
||||
handlingPlayers.add(player.getName().toLowerCase());
|
||||
|
||||
Location newLoc = newLocation(player, loc, border, notify);
|
||||
boolean handlingVehicle = false;
|
||||
|
||||
/*
|
||||
* since we need to forcibly eject players who are inside vehicles, that fires a teleport event (go figure) and
|
||||
* so would effectively double trigger for us, so we need to handle it here to prevent sending two messages and
|
||||
* two log entries etc.
|
||||
* after players are ejected we can wait a few ticks (long enough for their client to receive new entity location)
|
||||
* and then set them as passenger of the vehicle again
|
||||
*/
|
||||
if (player.isInsideVehicle()) {
|
||||
Entity ride = player.getVehicle();
|
||||
player.leaveVehicle();
|
||||
if (ride != null) { // vehicles need to be offset vertically and have velocity stopped
|
||||
double vertOffset = (ride instanceof LivingEntity) ? 0 : ride.getLocation().getY() - loc.getY();
|
||||
Location rideLoc = newLoc.clone();
|
||||
rideLoc.setY(newLoc.getY() + vertOffset);
|
||||
if (Config.Debug())
|
||||
Config.logWarn("Player was riding a \"" + ride.toString() + "\".");
|
||||
|
||||
ride.setVelocity(new Vector(0, 0, 0));
|
||||
ride.teleport(rideLoc, TeleportCause.PLUGIN);
|
||||
|
||||
|
||||
if (Config.RemountTicks() > 0) {
|
||||
setPassengerDelayed(ride, player, player.getName(), Config.RemountTicks());
|
||||
handlingVehicle = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if player has something (a pet, maybe?) riding them; only possible through odd plugins.
|
||||
// it can prevent all teleportation of the player completely, so it's very much not good and needs handling
|
||||
List<Entity> passengers = player.getPassengers();
|
||||
if (!passengers.isEmpty()) {
|
||||
player.eject();
|
||||
for (Entity rider : passengers) {
|
||||
rider.teleport(newLoc, TeleportCause.PLUGIN);
|
||||
if (Config.Debug())
|
||||
Config.logWarn("Player had a passenger riding on them: " + rider.getType());
|
||||
}
|
||||
player.sendMessage("Your passenger" + ((passengers.size() > 1) ? "s have" : " has") + " been ejected.");
|
||||
}
|
||||
|
||||
// give some particle and sound effects where the player was beyond the border, if "whoosh effect" is enabled
|
||||
Config.showWhooshEffect(loc);
|
||||
|
||||
if (!returnLocationOnly)
|
||||
player.teleport(newLoc, TeleportCause.PLUGIN);
|
||||
|
||||
if (!handlingVehicle)
|
||||
handlingPlayers.remove(player.getName().toLowerCase());
|
||||
|
||||
if (returnLocationOnly)
|
||||
return newLoc;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Location checkPlayer(Player player, Location targetLoc, boolean returnLocationOnly) {
|
||||
return checkPlayer(player, targetLoc, returnLocationOnly, true);
|
||||
}
|
||||
|
||||
private static Location newLocation(Player player, Location loc, BorderData border, boolean notify) {
|
||||
if (Config.Debug()) {
|
||||
Config.logWarn((notify ? "Border crossing" : "Check was run") + " in \"" + loc.getWorld().getName() + "\". Border " + border.toString());
|
||||
Config.logWarn("Player position X: " + Config.coord.format(loc.getX()) + " Y: " + Config.coord.format(loc.getY()) + " Z: " + Config.coord.format(loc.getZ()));
|
||||
}
|
||||
|
||||
Location newLoc = border.correctedPosition(loc, Config.ShapeRound(), player.isFlying());
|
||||
|
||||
// it's remotely possible (such as in the Nether) a suitable location isn't available, in which case...
|
||||
if (newLoc == null) {
|
||||
if (Config.Debug())
|
||||
Config.logWarn("Target new location unviable, using spawn or killing player.");
|
||||
if (Config.getIfPlayerKill()) {
|
||||
player.setHealth(0.0D);
|
||||
return null;
|
||||
}
|
||||
newLoc = player.getWorld().getSpawnLocation();
|
||||
}
|
||||
|
||||
if (Config.Debug())
|
||||
Config.logWarn("New position in world \"" + newLoc.getWorld().getName() + "\" at X: " + Config.coord.format(newLoc.getX()) + " Y: " + Config.coord.format(newLoc.getY()) + " Z: " + Config.coord.format(newLoc.getZ()));
|
||||
|
||||
if (notify) {
|
||||
if(!Config.Message().isBlank()) {
|
||||
player.sendMessage(Config.Message());
|
||||
}
|
||||
}
|
||||
|
||||
return newLoc;
|
||||
}
|
||||
|
||||
private static void setPassengerDelayed(final Entity vehicle, final Player player, final String playerName, long delay) {
|
||||
Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(WorldBorder.plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handlingPlayers.remove(playerName.toLowerCase());
|
||||
if (vehicle == null || player == null)
|
||||
return;
|
||||
|
||||
vehicle.addPassenger(player);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// if knockback is set to 0, simply return
|
||||
if (Config.KnockBack() == 0.0)
|
||||
return;
|
||||
|
||||
Collection<Player> players = ImmutableList.copyOf(Bukkit.getServer().getOnlinePlayers());
|
||||
|
||||
for (Player player : players) {
|
||||
checkPlayer(player, null, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
468
src/main/java/com/wimbli/WorldBorder/BorderData.java
Normal file
468
src/main/java/com/wimbli/WorldBorder/BorderData.java
Normal file
@@ -0,0 +1,468 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
|
||||
public class BorderData {
|
||||
//these material IDs are acceptable for places to teleport player; breathable blocks and water
|
||||
public static final EnumSet<Material> safeOpenBlocks = EnumSet.noneOf(Material.class);
|
||||
//these material IDs are ones we don't want to drop the player onto, like cactus or lava or fire or activated Ender portal
|
||||
public static final EnumSet<Material> painfulBlocks = EnumSet.noneOf(Material.class);
|
||||
private static final int limBot = 0;
|
||||
|
||||
static {
|
||||
safeOpenBlocks.add(Material.AIR);
|
||||
safeOpenBlocks.add(Material.CAVE_AIR);
|
||||
safeOpenBlocks.add(Material.OAK_SAPLING);
|
||||
safeOpenBlocks.add(Material.SPRUCE_SAPLING);
|
||||
safeOpenBlocks.add(Material.BIRCH_SAPLING);
|
||||
safeOpenBlocks.add(Material.JUNGLE_SAPLING);
|
||||
safeOpenBlocks.add(Material.ACACIA_SAPLING);
|
||||
safeOpenBlocks.add(Material.DARK_OAK_SAPLING);
|
||||
safeOpenBlocks.add(Material.RAIL);
|
||||
safeOpenBlocks.add(Material.POWERED_RAIL);
|
||||
safeOpenBlocks.add(Material.DETECTOR_RAIL);
|
||||
safeOpenBlocks.add(Material.ACTIVATOR_RAIL);
|
||||
safeOpenBlocks.add(Material.COBWEB);
|
||||
// safeOpenBlocks.add(Material.GRASS); // <-- ENTFERNT, DA ES VERALTET IST UND TALL_GRASS BEREITS VORHANDEN IST
|
||||
safeOpenBlocks.add(Material.FERN);
|
||||
safeOpenBlocks.add(Material.DEAD_BUSH);
|
||||
safeOpenBlocks.add(Material.DANDELION);
|
||||
safeOpenBlocks.add(Material.POPPY);
|
||||
safeOpenBlocks.add(Material.BLUE_ORCHID);
|
||||
safeOpenBlocks.add(Material.ALLIUM);
|
||||
safeOpenBlocks.add(Material.AZURE_BLUET);
|
||||
safeOpenBlocks.add(Material.RED_TULIP);
|
||||
safeOpenBlocks.add(Material.ORANGE_TULIP);
|
||||
safeOpenBlocks.add(Material.WHITE_TULIP);
|
||||
safeOpenBlocks.add(Material.PINK_TULIP);
|
||||
safeOpenBlocks.add(Material.OXEYE_DAISY);
|
||||
safeOpenBlocks.add(Material.BROWN_MUSHROOM);
|
||||
safeOpenBlocks.add(Material.RED_MUSHROOM);
|
||||
safeOpenBlocks.add(Material.TORCH);
|
||||
safeOpenBlocks.add(Material.WALL_TORCH);
|
||||
safeOpenBlocks.add(Material.REDSTONE_WIRE);
|
||||
safeOpenBlocks.add(Material.WHEAT);
|
||||
safeOpenBlocks.add(Material.LADDER);
|
||||
safeOpenBlocks.add(Material.LEVER);
|
||||
safeOpenBlocks.add(Material.LIGHT_WEIGHTED_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.HEAVY_WEIGHTED_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.STONE_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.OAK_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.SPRUCE_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.BIRCH_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.JUNGLE_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.ACACIA_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.DARK_OAK_PRESSURE_PLATE);
|
||||
safeOpenBlocks.add(Material.REDSTONE_TORCH);
|
||||
safeOpenBlocks.add(Material.REDSTONE_WALL_TORCH);
|
||||
safeOpenBlocks.add(Material.STONE_BUTTON);
|
||||
safeOpenBlocks.add(Material.SNOW);
|
||||
safeOpenBlocks.add(Material.SUGAR_CANE);
|
||||
safeOpenBlocks.add(Material.REPEATER);
|
||||
safeOpenBlocks.add(Material.COMPARATOR);
|
||||
safeOpenBlocks.add(Material.OAK_TRAPDOOR);
|
||||
safeOpenBlocks.add(Material.SPRUCE_TRAPDOOR);
|
||||
safeOpenBlocks.add(Material.BIRCH_TRAPDOOR);
|
||||
safeOpenBlocks.add(Material.JUNGLE_TRAPDOOR);
|
||||
safeOpenBlocks.add(Material.ACACIA_TRAPDOOR);
|
||||
safeOpenBlocks.add(Material.DARK_OAK_TRAPDOOR);
|
||||
safeOpenBlocks.add(Material.MELON_STEM);
|
||||
safeOpenBlocks.add(Material.ATTACHED_MELON_STEM);
|
||||
safeOpenBlocks.add(Material.PUMPKIN_STEM);
|
||||
safeOpenBlocks.add(Material.ATTACHED_PUMPKIN_STEM);
|
||||
safeOpenBlocks.add(Material.VINE);
|
||||
safeOpenBlocks.add(Material.NETHER_WART);
|
||||
safeOpenBlocks.add(Material.TRIPWIRE);
|
||||
safeOpenBlocks.add(Material.TRIPWIRE_HOOK);
|
||||
safeOpenBlocks.add(Material.CARROTS);
|
||||
safeOpenBlocks.add(Material.POTATOES);
|
||||
safeOpenBlocks.add(Material.OAK_BUTTON);
|
||||
safeOpenBlocks.add(Material.SPRUCE_BUTTON);
|
||||
safeOpenBlocks.add(Material.BIRCH_BUTTON);
|
||||
safeOpenBlocks.add(Material.JUNGLE_BUTTON);
|
||||
safeOpenBlocks.add(Material.ACACIA_BUTTON);
|
||||
safeOpenBlocks.add(Material.DARK_OAK_BUTTON);
|
||||
safeOpenBlocks.add(Material.SUNFLOWER);
|
||||
safeOpenBlocks.add(Material.LILAC);
|
||||
safeOpenBlocks.add(Material.ROSE_BUSH);
|
||||
safeOpenBlocks.add(Material.PEONY);
|
||||
safeOpenBlocks.add(Material.TALL_GRASS);
|
||||
safeOpenBlocks.add(Material.LARGE_FERN);
|
||||
safeOpenBlocks.add(Material.BEETROOTS);
|
||||
try { // signs in 1.14 can be different wood types
|
||||
safeOpenBlocks.add(Material.ACACIA_SIGN);
|
||||
safeOpenBlocks.add(Material.ACACIA_WALL_SIGN);
|
||||
safeOpenBlocks.add(Material.BIRCH_SIGN);
|
||||
safeOpenBlocks.add(Material.BIRCH_WALL_SIGN);
|
||||
safeOpenBlocks.add(Material.DARK_OAK_SIGN);
|
||||
safeOpenBlocks.add(Material.DARK_OAK_WALL_SIGN);
|
||||
safeOpenBlocks.add(Material.JUNGLE_SIGN);
|
||||
safeOpenBlocks.add(Material.JUNGLE_WALL_SIGN);
|
||||
safeOpenBlocks.add(Material.OAK_SIGN);
|
||||
safeOpenBlocks.add(Material.OAK_WALL_SIGN);
|
||||
safeOpenBlocks.add(Material.SPRUCE_SIGN);
|
||||
safeOpenBlocks.add(Material.SPRUCE_WALL_SIGN);
|
||||
} catch (NoSuchFieldError ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
painfulBlocks.add(Material.LAVA);
|
||||
painfulBlocks.add(Material.FIRE);
|
||||
painfulBlocks.add(Material.CACTUS);
|
||||
painfulBlocks.add(Material.END_PORTAL);
|
||||
painfulBlocks.add(Material.MAGMA_BLOCK);
|
||||
painfulBlocks.add(Material.CAMPFIRE);
|
||||
painfulBlocks.add(Material.POTTED_CACTUS);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// the main data interacted with
|
||||
private double x = 0;
|
||||
private double z = 0;
|
||||
private int radiusX = 0;
|
||||
private int radiusZ = 0;
|
||||
private Boolean shapeRound = null;
|
||||
private boolean wrapping = false;
|
||||
// some extra data kept handy for faster border checks
|
||||
private double maxX;
|
||||
private double minX;
|
||||
private double maxZ;
|
||||
private double minZ;
|
||||
private double radiusXSquared;
|
||||
private double radiusZSquared;
|
||||
private double DefiniteRectangleX;
|
||||
private double DefiniteRectangleZ;
|
||||
private double radiusSquaredQuotient;
|
||||
|
||||
public BorderData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound, boolean wrap) {
|
||||
setData(x, z, radiusX, radiusZ, shapeRound, wrap);
|
||||
}
|
||||
|
||||
public BorderData(double x, double z, int radiusX, int radiusZ) {
|
||||
setData(x, z, radiusX, radiusZ, null);
|
||||
}
|
||||
|
||||
public BorderData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound) {
|
||||
setData(x, z, radiusX, radiusZ, shapeRound);
|
||||
}
|
||||
|
||||
public BorderData(double x, double z, int radius) {
|
||||
setData(x, z, radius, null);
|
||||
}
|
||||
|
||||
public BorderData(double x, double z, int radius, Boolean shapeRound) {
|
||||
setData(x, z, radius, shapeRound);
|
||||
}
|
||||
|
||||
public final void setData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound, boolean wrap) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.shapeRound = shapeRound;
|
||||
this.wrapping = wrap;
|
||||
this.setRadiusX(radiusX);
|
||||
this.setRadiusZ(radiusZ);
|
||||
}
|
||||
|
||||
public final void setData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound) {
|
||||
setData(x, z, radiusX, radiusZ, shapeRound, false);
|
||||
}
|
||||
|
||||
public final void setData(double x, double z, int radius, Boolean shapeRound) {
|
||||
setData(x, z, radius, radius, shapeRound, false);
|
||||
}
|
||||
|
||||
public BorderData copy() {
|
||||
return new BorderData(x, z, radiusX, radiusZ, shapeRound, wrapping);
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
this.x = x;
|
||||
this.maxX = x + radiusX;
|
||||
this.minX = x - radiusX;
|
||||
}
|
||||
|
||||
public double getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
|
||||
// backwards-compatible methods from before elliptical/rectangular shapes were supported
|
||||
|
||||
public void setZ(double z) {
|
||||
this.z = z;
|
||||
this.maxZ = z + radiusZ;
|
||||
this.minZ = z - radiusZ;
|
||||
}
|
||||
|
||||
public int getRadiusX() {
|
||||
return radiusX;
|
||||
}
|
||||
|
||||
public void setRadiusX(int radiusX) {
|
||||
this.radiusX = radiusX;
|
||||
this.maxX = x + radiusX;
|
||||
this.minX = x - radiusX;
|
||||
this.radiusXSquared = (double) radiusX * (double) radiusX;
|
||||
this.radiusSquaredQuotient = this.radiusXSquared / this.radiusZSquared;
|
||||
this.DefiniteRectangleX = Math.sqrt(.5 * this.radiusXSquared);
|
||||
}
|
||||
|
||||
public int getRadiusZ() {
|
||||
return radiusZ;
|
||||
}
|
||||
|
||||
public void setRadiusZ(int radiusZ) {
|
||||
this.radiusZ = radiusZ;
|
||||
this.maxZ = z + radiusZ;
|
||||
this.minZ = z - radiusZ;
|
||||
this.radiusZSquared = (double) radiusZ * (double) radiusZ;
|
||||
this.radiusSquaredQuotient = this.radiusXSquared / this.radiusZSquared;
|
||||
this.DefiniteRectangleZ = Math.sqrt(.5 * this.radiusZSquared);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Replaced by {@link #getRadiusX()} and {@link #getRadiusZ()};
|
||||
* this method now returns an average of those two values and is thus imprecise
|
||||
*/
|
||||
public int getRadius() {
|
||||
return (radiusX + radiusZ) / 2; // average radius; not great, but probably best for backwards compatibility
|
||||
}
|
||||
|
||||
public void setRadius(int radius) {
|
||||
setRadiusX(radius);
|
||||
setRadiusZ(radius);
|
||||
}
|
||||
|
||||
public Boolean getShape() {
|
||||
return shapeRound;
|
||||
}
|
||||
|
||||
public void setShape(Boolean shapeRound) {
|
||||
this.shapeRound = shapeRound;
|
||||
}
|
||||
|
||||
public boolean getWrapping() {
|
||||
return wrapping;
|
||||
}
|
||||
|
||||
public void setWrapping(boolean wrap) {
|
||||
this.wrapping = wrap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "radius " + ((radiusX == radiusZ) ? radiusX : radiusX + "x" + radiusZ) + " at X: " + Config.coord.format(x) + " Z: " + Config.coord.format(z) + (shapeRound != null ? (" (shape override: " + Config.ShapeName(shapeRound.booleanValue()) + ")") : "") + (wrapping ? (" (wrapping)") : "");
|
||||
}
|
||||
|
||||
// This algorithm of course needs to be fast, since it will be run very frequently
|
||||
public boolean insideBorder(double xLoc, double zLoc, boolean round) {
|
||||
// if this border has a shape override set, use it
|
||||
if (shapeRound != null)
|
||||
round = shapeRound.booleanValue();
|
||||
|
||||
// square border
|
||||
if (!round)
|
||||
return !(xLoc < minX || xLoc > maxX || zLoc < minZ || zLoc > maxZ);
|
||||
|
||||
// round border
|
||||
else {
|
||||
// elegant round border checking algorithm is from rBorder by Reil with almost no changes, all credit to him for it
|
||||
double X = Math.abs(x - xLoc);
|
||||
double Z = Math.abs(z - zLoc);
|
||||
|
||||
if (X < DefiniteRectangleX && Z < DefiniteRectangleZ)
|
||||
return true; // Definitely inside
|
||||
else // Apparently outside, then
|
||||
if (X >= radiusX || Z >= radiusZ)
|
||||
return false; // Definitely outside
|
||||
else
|
||||
return X * X + Z * Z * radiusSquaredQuotient < radiusXSquared; // After further calculation, inside
|
||||
}
|
||||
}
|
||||
|
||||
public boolean insideBorder(double xLoc, double zLoc) {
|
||||
return insideBorder(xLoc, zLoc, Config.ShapeRound());
|
||||
}
|
||||
|
||||
public boolean insideBorder(Location loc) {
|
||||
return insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound());
|
||||
}
|
||||
|
||||
public boolean insideBorder(CoordXZ coord, boolean round) {
|
||||
return insideBorder(coord.x, coord.z, round);
|
||||
}
|
||||
|
||||
public boolean insideBorder(CoordXZ coord) {
|
||||
return insideBorder(coord.x, coord.z, Config.ShapeRound());
|
||||
}
|
||||
|
||||
public Location correctedPosition(Location loc, boolean round, boolean flying) {
|
||||
// if this border has a shape override set, use it
|
||||
if (shapeRound != null)
|
||||
round = shapeRound.booleanValue();
|
||||
|
||||
double xLoc = loc.getX();
|
||||
double zLoc = loc.getZ();
|
||||
double yLoc = loc.getY();
|
||||
|
||||
// square border
|
||||
if (!round) {
|
||||
if (wrapping) {
|
||||
if (xLoc <= minX)
|
||||
xLoc = maxX - Config.KnockBack();
|
||||
else if (xLoc >= maxX)
|
||||
xLoc = minX + Config.KnockBack();
|
||||
if (zLoc <= minZ)
|
||||
zLoc = maxZ - Config.KnockBack();
|
||||
else if (zLoc >= maxZ)
|
||||
zLoc = minZ + Config.KnockBack();
|
||||
} else {
|
||||
if (xLoc <= minX)
|
||||
xLoc = minX + Config.KnockBack();
|
||||
else if (xLoc >= maxX)
|
||||
xLoc = maxX - Config.KnockBack();
|
||||
if (zLoc <= minZ)
|
||||
zLoc = minZ + Config.KnockBack();
|
||||
else if (zLoc >= maxZ)
|
||||
zLoc = maxZ - Config.KnockBack();
|
||||
}
|
||||
}
|
||||
|
||||
// round border
|
||||
else {
|
||||
// algorithm originally from: http://stackoverflow.com/questions/300871/best-way-to-find-a-point-on-a-circle-closest-to-a-given-point
|
||||
// modified by Lang Lukas to support elliptical border shape
|
||||
|
||||
//Transform the ellipse to a circle with radius 1 (we need to transform the point the same way)
|
||||
double dX = xLoc - x;
|
||||
double dZ = zLoc - z;
|
||||
double dU = Math.sqrt(dX * dX + dZ * dZ); //distance of the untransformed point from the center
|
||||
double dT = Math.sqrt(dX * dX / radiusXSquared + dZ * dZ / radiusZSquared); //distance of the transformed point from the center
|
||||
double f = (1 / dT - Config.KnockBack() / dU); //"correction" factor for the distances
|
||||
if (wrapping) {
|
||||
xLoc = x - dX * f;
|
||||
zLoc = z - dZ * f;
|
||||
} else {
|
||||
xLoc = x + dX * f;
|
||||
zLoc = z + dZ * f;
|
||||
}
|
||||
}
|
||||
|
||||
int ixLoc = Location.locToBlock(xLoc);
|
||||
int izLoc = Location.locToBlock(zLoc);
|
||||
|
||||
// Make sure the chunk we're checking in is actually loaded
|
||||
Chunk tChunk = loc.getWorld().getChunkAt(CoordXZ.blockToChunk(ixLoc), CoordXZ.blockToChunk(izLoc));
|
||||
if (!tChunk.isLoaded())
|
||||
tChunk.load();
|
||||
|
||||
yLoc = getSafeY(loc.getWorld(), ixLoc, Location.locToBlock(yLoc), izLoc, flying);
|
||||
if (yLoc == -1)
|
||||
return null;
|
||||
|
||||
return new Location(loc.getWorld(), Math.floor(xLoc) + 0.5, yLoc, Math.floor(zLoc) + 0.5, loc.getYaw(), loc.getPitch());
|
||||
}
|
||||
|
||||
public Location correctedPosition(Location loc, boolean round) {
|
||||
return correctedPosition(loc, round, false);
|
||||
}
|
||||
|
||||
public Location correctedPosition(Location loc) {
|
||||
return correctedPosition(loc, Config.ShapeRound(), false);
|
||||
}
|
||||
|
||||
// check if a particular spot consists of 2 breathable blocks over something relatively solid
|
||||
private boolean isSafeSpot(World world, int X, int Y, int Z, boolean flying) {
|
||||
boolean safe =
|
||||
// target block open and safe or is above maximum Y coordinate
|
||||
(Y == world.getMaxHeight()
|
||||
|| (safeOpenBlocks.contains(world.getBlockAt(X, Y, Z).getType())
|
||||
// above target block open and safe or is above maximum Y coordinate
|
||||
&& (Y + 1 == world.getMaxHeight()
|
||||
|| safeOpenBlocks.contains(world.getBlockAt(X, Y + 1, Z).getType()))));
|
||||
if (!safe || flying)
|
||||
return safe;
|
||||
|
||||
Material below = world.getBlockAt(X, Y - 1, Z).getType();
|
||||
// below target block not open/breathable (so presumably solid), or is water
|
||||
// below target block not painful
|
||||
// below target block not painful
|
||||
return (!safeOpenBlocks.contains(below) || below == Material.WATER) && !painfulBlocks.contains(below);
|
||||
}
|
||||
|
||||
// find closest safe Y position from the starting position
|
||||
private double getSafeY(World world, int X, int Y, int Z, boolean flying) {
|
||||
// artificial height limit of 127 added for Nether worlds since CraftBukkit still incorrectly returns 255 for their max height, leading to players sent to the "roof" of the Nether
|
||||
final boolean isNether = world.getEnvironment() == World.Environment.NETHER;
|
||||
int limTop = isNether ? 125 : world.getMaxHeight();
|
||||
final int highestBlockBoundary = Math.min(world.getHighestBlockYAt(X, Z) + 1, limTop);
|
||||
|
||||
// if Y is larger than the world can be and user can fly, return Y - Unless we are in the Nether, we might not want players on the roof
|
||||
if (flying && Y > limTop && !isNether)
|
||||
return Y;
|
||||
|
||||
// make sure Y values are within the boundaries of the world.
|
||||
if (Y > limTop) {
|
||||
if (isNether)
|
||||
Y = limTop; // because of the roof, the nether can not rely on highestBlockBoundary, so limTop has to be used
|
||||
else {
|
||||
if (flying)
|
||||
Y = limTop;
|
||||
else
|
||||
Y = highestBlockBoundary; // there will never be a save block to stand on for Y values > highestBlockBoundary
|
||||
}
|
||||
}
|
||||
if (Y < limBot)
|
||||
Y = limBot;
|
||||
|
||||
// for non Nether worlds we don't need to check upwards to the world-limit, it is enough to check up to the highestBlockBoundary, unless player is flying
|
||||
if (!isNether && !flying)
|
||||
limTop = highestBlockBoundary + 1;
|
||||
// Expanding Y search method adapted from Acru's code in the Nether plugin
|
||||
|
||||
for (int y1 = Y, y2 = Y; (y1 > limBot) || (y2 < limTop); y1--, y2++) {
|
||||
// Look below.
|
||||
if (y1 > limBot) {
|
||||
if (isSafeSpot(world, X, y1, Z, flying))
|
||||
return y1;
|
||||
}
|
||||
|
||||
// Look above.
|
||||
if (y2 <= limTop && y2 != y1) {
|
||||
if (isSafeSpot(world, X, y2, Z, flying))
|
||||
return y2;
|
||||
}
|
||||
}
|
||||
|
||||
return -1.0; // no safe Y location?!?!? Must be a rare spot in a Nether world or something
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
else if (obj == null || obj.getClass() != this.getClass())
|
||||
return false;
|
||||
|
||||
BorderData test = (BorderData) obj;
|
||||
return test.x == this.x && test.z == this.z && test.radiusX == this.radiusX && test.radiusZ == this.radiusZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (((int) (this.x * 10) << 4) + (int) this.z + (this.radiusX << 2) + (this.radiusZ << 3));
|
||||
}
|
||||
}
|
||||
695
src/main/java/com/wimbli/WorldBorder/Config.java
Normal file
695
src/main/java/com/wimbli/WorldBorder/Config.java
Normal file
@@ -0,0 +1,695 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Effect;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class Config {
|
||||
public static final DecimalFormat coord = new DecimalFormat("0.0");
|
||||
private static final int currentCfgVersion = 12;
|
||||
private static final Runtime rt = Runtime.getRuntime();
|
||||
private static final Map<String, BorderData> borders = Collections.synchronizedMap(new LinkedHashMap<String, BorderData>());
|
||||
private static final Set<UUID> bypassPlayers = Collections.synchronizedSet(new LinkedHashSet<UUID>());
|
||||
public static volatile WorldFillTask fillTask = null;
|
||||
public static volatile WorldTrimTask trimTask = null;
|
||||
// private stuff used within this class
|
||||
private static WorldBorder plugin;
|
||||
private static FileConfiguration cfg = null;
|
||||
private static Logger wbLog = null;
|
||||
private static int borderTask = -1;
|
||||
// actual configuration values which can be changed
|
||||
private static boolean shapeRound = true;
|
||||
private static String message; // raw message without color code formatting
|
||||
private static String messageFmt; // message with color code formatting ("&" changed to funky sort-of-double-dollar-sign for legitimate color/formatting codes)
|
||||
private static String messageClean; // message cleaned of formatting codes
|
||||
private static boolean DEBUG = false;
|
||||
private static double knockBack = 1.0;
|
||||
private static int timerTicks = 4;
|
||||
private static boolean whooshEffect = false;
|
||||
private static boolean portalRedirection = true;
|
||||
private static boolean dynmapEnable = true;
|
||||
private static String dynmapLayerLabel;
|
||||
private static String dynmapMessage;
|
||||
private static int dynmapPriority = 0;
|
||||
private static boolean dynmapHideByDefault = false;
|
||||
private static int remountDelayTicks = 0;
|
||||
private static boolean killPlayer = false;
|
||||
private static boolean denyEnderpearl = false;
|
||||
private static int fillAutosaveFrequency = 30;
|
||||
private static int fillMemoryTolerance = 500;
|
||||
private static boolean preventBlockPlace = false;
|
||||
private static boolean preventMobSpawn = false;
|
||||
private static boolean noPlayersToggle = false;
|
||||
|
||||
// for monitoring plugin efficiency
|
||||
// public static long timeUsed = 0;
|
||||
|
||||
|
||||
public static long Now() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public static void setBorder(String world, BorderData border, boolean logIt) {
|
||||
borders.put(world, border);
|
||||
if (logIt)
|
||||
log("Border set. " + BorderDescription(world));
|
||||
save(true);
|
||||
DynMapFeatures.showBorder(world, border);
|
||||
}
|
||||
|
||||
public static void setBorder(String world, BorderData border) {
|
||||
setBorder(world, border, true);
|
||||
}
|
||||
|
||||
public static void setBorder(String world, int radiusX, int radiusZ, double x, double z, Boolean shapeRound) {
|
||||
BorderData old = Border(world);
|
||||
boolean oldWrap = (old != null) && old.getWrapping();
|
||||
setBorder(world, new BorderData(x, z, radiusX, radiusZ, shapeRound, oldWrap), true);
|
||||
}
|
||||
|
||||
public static void setBorder(String world, int radiusX, int radiusZ, double x, double z) {
|
||||
BorderData old = Border(world);
|
||||
Boolean oldShape = (old == null) ? null : old.getShape();
|
||||
boolean oldWrap = (old != null) && old.getWrapping();
|
||||
setBorder(world, new BorderData(x, z, radiusX, radiusZ, oldShape, oldWrap), true);
|
||||
}
|
||||
|
||||
// backwards-compatible methods from before elliptical/rectangular shapes were supported
|
||||
public static void setBorder(String world, int radius, double x, double z, Boolean shapeRound) {
|
||||
setBorder(world, new BorderData(x, z, radius, radius, shapeRound), true);
|
||||
}
|
||||
|
||||
public static void setBorder(String world, int radius, double x, double z) {
|
||||
setBorder(world, radius, radius, x, z);
|
||||
}
|
||||
|
||||
// set border based on corner coordinates
|
||||
public static void setBorderCorners(String world, double x1, double z1, double x2, double z2, Boolean shapeRound, boolean wrap) {
|
||||
double radiusX = Math.abs(x1 - x2) / 2;
|
||||
double radiusZ = Math.abs(z1 - z2) / 2;
|
||||
double x = (Math.min(x1, x2)) + radiusX;
|
||||
double z = (Math.min(z1, z2)) + radiusZ;
|
||||
setBorder(world, new BorderData(x, z, (int) Math.round(radiusX), (int) Math.round(radiusZ), shapeRound, wrap), true);
|
||||
}
|
||||
|
||||
public static void setBorderCorners(String world, double x1, double z1, double x2, double z2, Boolean shapeRound) {
|
||||
setBorderCorners(world, x1, z1, x2, z2, shapeRound, false);
|
||||
}
|
||||
|
||||
public static void setBorderCorners(String world, double x1, double z1, double x2, double z2) {
|
||||
BorderData old = Border(world);
|
||||
Boolean oldShape = (old == null) ? null : old.getShape();
|
||||
boolean oldWrap = (old != null) && old.getWrapping();
|
||||
setBorderCorners(world, x1, z1, x2, z2, oldShape, oldWrap);
|
||||
}
|
||||
|
||||
public static void removeBorder(String world) {
|
||||
borders.remove(world);
|
||||
log("Removed border for world \"" + world + "\".");
|
||||
save(true);
|
||||
DynMapFeatures.removeBorder(world);
|
||||
}
|
||||
|
||||
public static void removeAllBorders() {
|
||||
borders.clear();
|
||||
log("Removed all borders for all worlds.");
|
||||
save(true);
|
||||
DynMapFeatures.removeAllBorders();
|
||||
}
|
||||
|
||||
public static String BorderDescription(String world) {
|
||||
BorderData border = borders.get(world);
|
||||
if (border == null)
|
||||
return "No border was found for the world \"" + world + "\".";
|
||||
else
|
||||
return "World \"" + world + "\" has border " + border.toString();
|
||||
}
|
||||
|
||||
public static Set<String> BorderDescriptions() {
|
||||
Set<String> output = new HashSet<String>();
|
||||
|
||||
for (String worldName : borders.keySet()) {
|
||||
output.add(BorderDescription(worldName));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public static BorderData Border(String world) {
|
||||
return borders.get(world);
|
||||
}
|
||||
|
||||
public static Map<String, BorderData> getBorders() {
|
||||
return new LinkedHashMap<String, BorderData>(borders);
|
||||
}
|
||||
|
||||
public static void setMessage(String msg) {
|
||||
updateMessage(msg);
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static void updateMessage(String msg) {
|
||||
message = msg;
|
||||
messageFmt = replaceAmpColors(msg);
|
||||
messageClean = stripAmpColors(msg);
|
||||
}
|
||||
|
||||
public static String Message() {
|
||||
return messageFmt;
|
||||
}
|
||||
|
||||
public static String MessageRaw() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public static String MessageClean() {
|
||||
return messageClean;
|
||||
}
|
||||
|
||||
public static void setShape(boolean round) {
|
||||
shapeRound = round;
|
||||
log("Set default border shape to " + (ShapeName()) + ".");
|
||||
save(true);
|
||||
DynMapFeatures.showAllBorders();
|
||||
}
|
||||
|
||||
public static boolean ShapeRound() {
|
||||
return shapeRound;
|
||||
}
|
||||
|
||||
public static String ShapeName() {
|
||||
return ShapeName(shapeRound);
|
||||
}
|
||||
|
||||
public static String ShapeName(Boolean round) {
|
||||
if (round == null)
|
||||
return "default";
|
||||
return round ? "elliptic/round" : "rectangular/square";
|
||||
}
|
||||
|
||||
public static void setDebug(boolean debugMode) {
|
||||
DEBUG = debugMode;
|
||||
log("Debug mode " + (DEBUG ? "enabled" : "disabled") + ".");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static boolean Debug() {
|
||||
return DEBUG;
|
||||
}
|
||||
|
||||
public static void setWhooshEffect(boolean enable) {
|
||||
whooshEffect = enable;
|
||||
log("\"Whoosh\" knockback effect " + (enable ? "enabled" : "disabled") + ".");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static boolean whooshEffect() {
|
||||
return whooshEffect;
|
||||
}
|
||||
|
||||
public static void showWhooshEffect(Location loc) {
|
||||
if (!whooshEffect())
|
||||
return;
|
||||
|
||||
World world = loc.getWorld();
|
||||
assert world != null;
|
||||
world.playEffect(loc, Effect.ENDER_SIGNAL, 0);
|
||||
world.playEffect(loc, Effect.ENDER_SIGNAL, 0);
|
||||
world.playEffect(loc, Effect.SMOKE, 4);
|
||||
world.playEffect(loc, Effect.SMOKE, 4);
|
||||
world.playEffect(loc, Effect.SMOKE, 4);
|
||||
world.playEffect(loc, Effect.GHAST_SHOOT, 0);
|
||||
}
|
||||
|
||||
public static void setPreventBlockPlace(boolean enable) {
|
||||
if (preventBlockPlace != enable)
|
||||
WorldBorder.plugin.enableBlockPlaceListener(enable);
|
||||
|
||||
preventBlockPlace = enable;
|
||||
log("prevent block place " + (enable ? "enabled" : "disabled") + ".");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static void setPreventMobSpawn(boolean enable) {
|
||||
if (preventMobSpawn != enable)
|
||||
WorldBorder.plugin.enableMobSpawnListener(enable);
|
||||
|
||||
preventMobSpawn = enable;
|
||||
log("prevent mob spawn " + (enable ? "enabled" : "disabled") + ".");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static boolean preventBlockPlace() {
|
||||
return preventBlockPlace;
|
||||
}
|
||||
|
||||
public static boolean preventMobSpawn() {
|
||||
return preventMobSpawn;
|
||||
}
|
||||
|
||||
public static boolean getIfPlayerKill() {
|
||||
return killPlayer;
|
||||
}
|
||||
|
||||
public static boolean getDenyEnderpearl() {
|
||||
return denyEnderpearl;
|
||||
}
|
||||
|
||||
public static void setDenyEnderpearl(boolean enable) {
|
||||
denyEnderpearl = enable;
|
||||
log("Direct cancellation of ender pearls thrown past the border " + (enable ? "enabled" : "disabled") + ".");
|
||||
log("Direct cancellation of ender pearls thrown past the border " + (enable ? "enabled" : "disabled") + ".");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static void setPortalRedirection(boolean enable) {
|
||||
portalRedirection = enable;
|
||||
log("Portal redirection " + (enable ? "enabled" : "disabled") + ".");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static boolean portalRedirection() {
|
||||
return portalRedirection;
|
||||
}
|
||||
|
||||
public static void setKnockBack(double numBlocks) {
|
||||
knockBack = numBlocks;
|
||||
log("Knockback set to " + knockBack + " blocks inside the border.");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static double KnockBack() {
|
||||
return knockBack;
|
||||
}
|
||||
|
||||
public static boolean NoPlayersToggle() {
|
||||
return noPlayersToggle;
|
||||
}
|
||||
|
||||
public static void setTimerTicks(int ticks) {
|
||||
timerTicks = ticks;
|
||||
log("Timer delay set to " + timerTicks + " tick(s). That is roughly " + (timerTicks * 50) + "ms / " + (((double) timerTicks * 50.0) / 1000.0) + " seconds.");
|
||||
StartBorderTimer();
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static int TimerTicks() {
|
||||
return timerTicks;
|
||||
}
|
||||
|
||||
public static void setRemountTicks(int ticks) {
|
||||
remountDelayTicks = ticks;
|
||||
if (remountDelayTicks == 0)
|
||||
log("Remount delay set to 0. Players will be left dismounted when knocked back from the border while on a vehicle.");
|
||||
else {
|
||||
log("Remount delay set to " + remountDelayTicks + " tick(s). That is roughly " + (remountDelayTicks * 50) + "ms / " + (((double) remountDelayTicks * 50.0) / 1000.0) + " seconds.");
|
||||
if (ticks < 10)
|
||||
logWarn("setting the remount delay to less than 10 (and greater than 0) is not recommended. This can lead to nasty client glitches.");
|
||||
}
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static int RemountTicks() {
|
||||
return remountDelayTicks;
|
||||
}
|
||||
|
||||
public static void setFillAutosaveFrequency(int seconds) {
|
||||
fillAutosaveFrequency = seconds;
|
||||
if (fillAutosaveFrequency == 0)
|
||||
log("World autosave frequency during Fill process set to 0, disabling it. Note that much progress can be lost this way if there is a bug or crash in the world generation process from Bukkit or any world generation plugin you use.");
|
||||
else
|
||||
log("World autosave frequency during Fill process set to " + fillAutosaveFrequency + " seconds (rounded to a multiple of 5). New chunks generated by the Fill process will be forcibly saved to disk this often to prevent loss of progress due to bugs or crashes in the world generation process.");
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static int FillAutosaveFrequency() {
|
||||
return fillAutosaveFrequency;
|
||||
}
|
||||
|
||||
public static void setDynmapBorderEnabled(boolean enable) {
|
||||
dynmapEnable = enable;
|
||||
log("DynMap border display is now " + (enable ? "enabled" : "disabled") + ".");
|
||||
save(true);
|
||||
DynMapFeatures.showAllBorders();
|
||||
}
|
||||
|
||||
public static boolean DynmapBorderEnabled() {
|
||||
return dynmapEnable;
|
||||
}
|
||||
|
||||
public static void setDynmapLayerLabel(String label) {
|
||||
dynmapLayerLabel = label;
|
||||
log("DynMap layer label is now set to: " + label);
|
||||
save(true);
|
||||
DynMapFeatures.showAllBorders();
|
||||
}
|
||||
|
||||
public static String DynmapLayerLabel() {
|
||||
return dynmapLayerLabel;
|
||||
}
|
||||
|
||||
public static void setDynmapMessage(String msg) {
|
||||
dynmapMessage = msg;
|
||||
log("DynMap border label is now set to: " + msg);
|
||||
save(true);
|
||||
DynMapFeatures.showAllBorders();
|
||||
}
|
||||
|
||||
public static String DynmapMessage() {
|
||||
return dynmapMessage;
|
||||
}
|
||||
|
||||
public static boolean DynmapHideByDefault() {
|
||||
return dynmapHideByDefault;
|
||||
}
|
||||
|
||||
public static int DynmapPriority() {
|
||||
return dynmapPriority;
|
||||
}
|
||||
|
||||
public static void setPlayerBypass(UUID player, boolean bypass) {
|
||||
if (bypass)
|
||||
bypassPlayers.add(player);
|
||||
else
|
||||
bypassPlayers.remove(player);
|
||||
save(true);
|
||||
}
|
||||
|
||||
public static boolean isPlayerBypassing(UUID player) {
|
||||
return bypassPlayers.contains(player);
|
||||
}
|
||||
|
||||
public static ArrayList<UUID> getPlayerBypassList() {
|
||||
return new ArrayList<>(bypassPlayers);
|
||||
}
|
||||
|
||||
// for converting bypass UUID list to/from String list, for storage in config
|
||||
private static void importBypassStringList(List<String> strings) {
|
||||
for (String string : strings) {
|
||||
bypassPlayers.add(UUID.fromString(string));
|
||||
}
|
||||
}
|
||||
|
||||
private static ArrayList<String> exportBypassStringList() {
|
||||
ArrayList<String> strings = new ArrayList<String>();
|
||||
for (UUID uuid : bypassPlayers) {
|
||||
strings.add(uuid.toString());
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
public static boolean isBorderTimerRunning() {
|
||||
if (borderTask == -1) return false;
|
||||
return (plugin.getServer().getScheduler().isQueued(borderTask) || plugin.getServer().getScheduler().isCurrentlyRunning(borderTask));
|
||||
}
|
||||
|
||||
public static void StartBorderTimer() {
|
||||
StopBorderTimer(false);
|
||||
|
||||
borderTask = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, new BorderCheckTask(), timerTicks, timerTicks);
|
||||
|
||||
if (borderTask == -1)
|
||||
logWarn("Failed to start timed border-checking task! This will prevent the plugin from working. Try restarting Bukkit.");
|
||||
|
||||
logConfig("Border-checking timed task started.");
|
||||
}
|
||||
|
||||
public static void StopBorderTimer() {
|
||||
StopBorderTimer(true);
|
||||
}
|
||||
|
||||
public static void StopBorderTimer(boolean logIt) {
|
||||
if (borderTask == -1) return;
|
||||
|
||||
plugin.getServer().getScheduler().cancelTask(borderTask);
|
||||
borderTask = -1;
|
||||
if (logIt)
|
||||
logConfig("Border-checking timed task stopped.");
|
||||
}
|
||||
|
||||
public static void StopFillTask(boolean Save) {
|
||||
if (fillTask != null && fillTask.valid())
|
||||
fillTask.cancel(Save);
|
||||
}
|
||||
|
||||
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, boolean forceLoad) {
|
||||
fillTask = new WorldFillTask(plugin.getServer(), null, world, fillDistance, chunksPerRun, tickFrequency, forceLoad);
|
||||
if (fillTask.valid()) {
|
||||
fillTask.continueProgress(x, z, length, total);
|
||||
int task = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, fillTask, 20, tickFrequency);
|
||||
fillTask.setTaskID(task);
|
||||
}
|
||||
}
|
||||
|
||||
// for backwards compatibility
|
||||
public static void RestoreFillTask(String world, int fillDistance, int chunksPerRun, int tickFrequency, int x, int z, int length, int total) {
|
||||
RestoreFillTask(world, fillDistance, chunksPerRun, tickFrequency, x, z, length, total, false);
|
||||
}
|
||||
|
||||
public static void StopTrimTask() {
|
||||
if (trimTask != null && trimTask.valid())
|
||||
trimTask.cancel();
|
||||
}
|
||||
|
||||
public static int AvailableMemory() {
|
||||
return (int) ((rt.maxMemory() - rt.totalMemory() + rt.freeMemory()) / 1048576); // 1024*1024 = 1048576 (bytes in 1 MB)
|
||||
}
|
||||
|
||||
public static boolean AvailableMemoryTooLow() {
|
||||
return AvailableMemory() < fillMemoryTolerance;
|
||||
}
|
||||
|
||||
public static boolean HasPermission(Player player, String request) {
|
||||
return HasPermission(player, request, true);
|
||||
}
|
||||
|
||||
public static boolean HasPermission(Player player, String request, boolean notify) {
|
||||
if (player == null) // console, always permitted
|
||||
return true;
|
||||
|
||||
if (player.hasPermission("worldborder." + request)) // built-in Bukkit superperms
|
||||
return true;
|
||||
|
||||
if (notify)
|
||||
player.sendMessage("You do not have sufficient permissions.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String replaceAmpColors(String message) {
|
||||
return ChatColor.translateAlternateColorCodes('&', message);
|
||||
}
|
||||
|
||||
// adapted from code posted by Sleaker
|
||||
public static String stripAmpColors(String message) {
|
||||
return message.replaceAll("(?i)&([a-fk-or0-9])", "");
|
||||
}
|
||||
|
||||
public static void log(Level lvl, String text) {
|
||||
wbLog.log(lvl, text);
|
||||
}
|
||||
|
||||
public static void log(String text) {
|
||||
log(Level.INFO, text);
|
||||
}
|
||||
|
||||
public static void logWarn(String text) {
|
||||
log(Level.WARNING, text);
|
||||
}
|
||||
|
||||
public static void logConfig(String text) {
|
||||
log(Level.INFO, "[CONFIG] " + text);
|
||||
}
|
||||
|
||||
public static void load(WorldBorder master, boolean logIt) { // load config from file
|
||||
plugin = master;
|
||||
wbLog = plugin.getLogger();
|
||||
|
||||
plugin.reloadConfig();
|
||||
cfg = plugin.getConfig();
|
||||
|
||||
int cfgVersion = cfg.getInt("cfg-version", currentCfgVersion);
|
||||
|
||||
String msg = cfg.getString("message");
|
||||
shapeRound = cfg.getBoolean("round-border", false);
|
||||
DEBUG = cfg.getBoolean("debug-mode", false);
|
||||
whooshEffect = cfg.getBoolean("whoosh-effect", true);
|
||||
portalRedirection = cfg.getBoolean("portal-redirection", true);
|
||||
knockBack = cfg.getDouble("knock-back-dist", 3.0);
|
||||
timerTicks = cfg.getInt("timer-delay-ticks", 5);
|
||||
remountDelayTicks = cfg.getInt("remount-delay-ticks", 0);
|
||||
dynmapEnable = cfg.getBoolean("dynmap-border-enabled", true);
|
||||
dynmapLayerLabel = cfg.getString("dynmap-border-layer-label", "WorldBorder");
|
||||
dynmapMessage = cfg.getString("dynmap-border-message", "The border of the world.");
|
||||
dynmapHideByDefault = cfg.getBoolean("dynmap-border-hideByDefault", false);
|
||||
dynmapPriority = cfg.getInt("dynmap-border-priority", 0);
|
||||
logConfig("Using " + (ShapeName()) + " border, knockback of " + knockBack + " blocks, and timer delay of " + timerTicks + ".");
|
||||
killPlayer = cfg.getBoolean("player-killed-bad-spawn", false);
|
||||
denyEnderpearl = cfg.getBoolean("deny-enderpearl", true);
|
||||
fillAutosaveFrequency = cfg.getInt("fill-autosave-frequency", 30);
|
||||
importBypassStringList(cfg.getStringList("bypass-list-uuids"));
|
||||
fillMemoryTolerance = cfg.getInt("fill-memory-tolerance", 500);
|
||||
preventBlockPlace = cfg.getBoolean("prevent-block-place");
|
||||
preventMobSpawn = cfg.getBoolean("prevent-mob-spawn");
|
||||
noPlayersToggle = cfg.getBoolean("no-players-toggle");
|
||||
|
||||
StartBorderTimer();
|
||||
|
||||
borders.clear();
|
||||
|
||||
// if empty border message, assume no config
|
||||
if (msg == null) { // store defaults
|
||||
logConfig("Configuration not present, creating new file.");
|
||||
msg = "&cYou have reached the edge of this world.";
|
||||
updateMessage(msg);
|
||||
save(false);
|
||||
return;
|
||||
}
|
||||
// if loading older config which didn't support color codes in border message, make sure default red color code is added at start of it
|
||||
else if (cfgVersion < 8 && msg.charAt(0) != '&')
|
||||
updateMessage("&c" + msg);
|
||||
// otherwise just set border message
|
||||
else
|
||||
updateMessage(msg);
|
||||
|
||||
// this option defaulted to false previously, but what it actually does has changed to something that almost everyone should now want by default
|
||||
if (cfgVersion < 10)
|
||||
denyEnderpearl = true;
|
||||
|
||||
// the border bypass list used to be stored as list of names rather than UUIDs; wipe that old list so the data won't be automatically saved back to the config file again
|
||||
if (cfgVersion < 11)
|
||||
cfg.set("bypass-list", null);
|
||||
|
||||
ConfigurationSection worlds = cfg.getConfigurationSection("worlds");
|
||||
if (worlds != null) {
|
||||
Set<String> worldNames = worlds.getKeys(false);
|
||||
|
||||
for (String worldName : worldNames) {
|
||||
ConfigurationSection bord = worlds.getConfigurationSection(worldName);
|
||||
|
||||
// we're swapping "<" to "." at load since periods denote configuration nodes without a working way to change that, so world names with periods wreak havoc and are thus modified for storage
|
||||
if (cfgVersion > 3)
|
||||
worldName = worldName.replace("<", ".");
|
||||
|
||||
// backwards compatibility for config from before elliptical/rectangular borders were supported
|
||||
if (bord.isSet("radius") && !bord.isSet("radiusX")) {
|
||||
int radius = bord.getInt("radius");
|
||||
bord.set("radiusX", radius);
|
||||
bord.set("radiusZ", radius);
|
||||
}
|
||||
|
||||
Boolean overrideShape = (Boolean) bord.get("shape-round");
|
||||
boolean wrap = bord.getBoolean("wrapping", false);
|
||||
BorderData border = new BorderData(bord.getDouble("x", 0), bord.getDouble("z", 0), bord.getInt("radiusX", 0), bord.getInt("radiusZ", 0), overrideShape, wrap);
|
||||
borders.put(worldName, border);
|
||||
logConfig(BorderDescription(worldName));
|
||||
}
|
||||
}
|
||||
|
||||
// if we have an unfinished fill task stored from a previous run, load it up
|
||||
ConfigurationSection storedFillTask = cfg.getConfigurationSection("fillTask");
|
||||
if (storedFillTask != null) {
|
||||
String worldName = storedFillTask.getString("world");
|
||||
int fillDistance = storedFillTask.getInt("fillDistance", 208);
|
||||
int chunksPerRun = storedFillTask.getInt("chunksPerRun", 1);
|
||||
int tickFrequency = storedFillTask.getInt("tickFrequency", 1);
|
||||
int fillX = storedFillTask.getInt("x", 0);
|
||||
int fillZ = storedFillTask.getInt("z", 0);
|
||||
int fillLength = storedFillTask.getInt("length", 0);
|
||||
int fillTotal = storedFillTask.getInt("total", 0);
|
||||
boolean forceLoad = storedFillTask.getBoolean("forceLoad", false);
|
||||
RestoreFillTask(worldName, fillDistance, chunksPerRun, tickFrequency, fillX, fillZ, fillLength, fillTotal, forceLoad);
|
||||
}
|
||||
|
||||
if (logIt)
|
||||
logConfig("Configuration loaded.");
|
||||
|
||||
if (cfgVersion < currentCfgVersion)
|
||||
save(false);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
cfg.set("cfg-version", currentCfgVersion);
|
||||
cfg.set("message", message);
|
||||
cfg.set("round-border", shapeRound);
|
||||
cfg.set("debug-mode", DEBUG);
|
||||
cfg.set("whoosh-effect", whooshEffect);
|
||||
cfg.set("portal-redirection", portalRedirection);
|
||||
cfg.set("knock-back-dist", knockBack);
|
||||
cfg.set("timer-delay-ticks", timerTicks);
|
||||
cfg.set("remount-delay-ticks", remountDelayTicks);
|
||||
cfg.set("dynmap-border-enabled", dynmapEnable);
|
||||
cfg.set("dynmap-border-layer-label", dynmapLayerLabel);
|
||||
cfg.set("dynmap-border-message", dynmapMessage);
|
||||
cfg.set("dynmap-border-hideByDefault", dynmapHideByDefault);
|
||||
cfg.set("dynmap-border-priority", dynmapPriority);
|
||||
cfg.set("player-killed-bad-spawn", killPlayer);
|
||||
cfg.set("deny-enderpearl", denyEnderpearl);
|
||||
cfg.set("fill-autosave-frequency", fillAutosaveFrequency);
|
||||
cfg.set("bypass-list-uuids", exportBypassStringList());
|
||||
cfg.set("fill-memory-tolerance", fillMemoryTolerance);
|
||||
cfg.set("prevent-block-place", preventBlockPlace);
|
||||
cfg.set("prevent-mob-spawn", preventMobSpawn);
|
||||
cfg.set("no-players-toggle", noPlayersToggle);
|
||||
|
||||
cfg.set("worlds", null);
|
||||
for (Entry<String, BorderData> stringBorderDataEntry : borders.entrySet()) {
|
||||
String name = stringBorderDataEntry.getKey().replace(".", "<");
|
||||
BorderData bord = stringBorderDataEntry.getValue();
|
||||
|
||||
cfg.set("worlds." + name + ".x", bord.getX());
|
||||
cfg.set("worlds." + name + ".z", bord.getZ());
|
||||
cfg.set("worlds." + name + ".radiusX", bord.getRadiusX());
|
||||
cfg.set("worlds." + name + ".radiusZ", bord.getRadiusZ());
|
||||
cfg.set("worlds." + name + ".wrapping", bord.getWrapping());
|
||||
|
||||
if (bord.getShape() != null)
|
||||
cfg.set("worlds." + name + ".shape-round", bord.getShape());
|
||||
}
|
||||
|
||||
if (storeFillTask && fillTask != null && fillTask.valid()) {
|
||||
cfg.set("fillTask.world", fillTask.refWorld());
|
||||
cfg.set("fillTask.fillDistance", fillTask.refFillDistance());
|
||||
cfg.set("fillTask.chunksPerRun", fillTask.refChunksPerRun());
|
||||
cfg.set("fillTask.tickFrequency", fillTask.refTickFrequency());
|
||||
cfg.set("fillTask.x", fillTask.refX());
|
||||
cfg.set("fillTask.z", fillTask.refZ());
|
||||
cfg.set("fillTask.length", fillTask.refLength());
|
||||
cfg.set("fillTask.total", fillTask.refTotal());
|
||||
cfg.set("fillTask.forceLoad", fillTask.refForceLoad());
|
||||
} else
|
||||
cfg.set("fillTask", null);
|
||||
|
||||
plugin.saveConfig();
|
||||
|
||||
if (logIt)
|
||||
logConfig("Configuration saved.");
|
||||
}
|
||||
|
||||
public static void setBorder(String worldName, double radiusX, double radiusZ, double x, double z) {
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/wimbli/WorldBorder/CoordXZ.java
Normal file
55
src/main/java/com/wimbli/WorldBorder/CoordXZ.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
|
||||
// simple storage class for chunk x/z values
|
||||
public class CoordXZ {
|
||||
public int x, z;
|
||||
|
||||
public CoordXZ(int x, int z) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
// transform values between block, chunk, and region
|
||||
// bit-shifting is used because it's mucho rapido
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
else if (obj == null || obj.getClass() != this.getClass())
|
||||
return false;
|
||||
|
||||
CoordXZ test = (CoordXZ) obj;
|
||||
return test.x == this.x && test.z == this.z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (this.x << 9) + this.z;
|
||||
}
|
||||
}
|
||||
217
src/main/java/com/wimbli/WorldBorder/DynMapFeatures.java
Normal file
217
src/main/java/com/wimbli/WorldBorder/DynMapFeatures.java
Normal file
@@ -0,0 +1,217 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.dynmap.DynmapAPI;
|
||||
import org.dynmap.markers.AreaMarker;
|
||||
import org.dynmap.markers.CircleMarker;
|
||||
import org.dynmap.markers.MarkerAPI;
|
||||
import org.dynmap.markers.MarkerSet;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
|
||||
public class DynMapFeatures {
|
||||
private static final int lineWeight = 3;
|
||||
private static final double lineOpacity = 1.0;
|
||||
private static final int lineColor = 0xFF0000;
|
||||
private static final Map<String, CircleMarker> roundBorders = new HashMap<String, CircleMarker>();
|
||||
private static final Map<String, AreaMarker> squareBorders = new HashMap<String, AreaMarker>();
|
||||
private static DynmapAPI api;
|
||||
private static MarkerAPI markApi;
|
||||
private static MarkerSet markSet;
|
||||
|
||||
// Whether re-rendering functionality is available
|
||||
public static boolean renderEnabled() {
|
||||
return api != null;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Re-rendering methods, used for updating trimmed chunks to show them as gone
|
||||
* Sadly, not currently working. Might not even be possible to make it work.
|
||||
*/
|
||||
|
||||
// Whether circular border markers are available
|
||||
public static boolean borderEnabled() {
|
||||
return markApi != null;
|
||||
}
|
||||
|
||||
public static void setup() {
|
||||
Plugin test = Bukkit.getServer().getPluginManager().getPlugin("dynmap");
|
||||
if (test == null || !test.isEnabled()) return;
|
||||
|
||||
api = (DynmapAPI) test;
|
||||
|
||||
// make sure DynMap version is new enough to include circular markers
|
||||
try {
|
||||
Class.forName("org.dynmap.markers.CircleMarker");
|
||||
|
||||
// for version 0.35 of DynMap, CircleMarkers had just been introduced and were bugged (center position always 0,0)
|
||||
if (api.getDynmapVersion().startsWith("0.35-"))
|
||||
throw new ClassNotFoundException();
|
||||
} catch (ClassNotFoundException ex) {
|
||||
Config.logConfig("DynMap is available, but border display is currently disabled: you need DynMap v0.36 or newer.");
|
||||
return;
|
||||
} catch (NullPointerException ex) {
|
||||
Config.logConfig("DynMap is present, but an NPE (type 1) was encountered while trying to integrate. Border display disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
markApi = api.getMarkerAPI();
|
||||
if (markApi == null) return;
|
||||
} catch (NullPointerException ex) {
|
||||
Config.logConfig("DynMap is present, but an NPE (type 2) was encountered while trying to integrate. Border display disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
// go ahead and show borders for all worlds
|
||||
showAllBorders();
|
||||
|
||||
Config.logConfig("Successfully hooked into DynMap for the ability to display borders.");
|
||||
}
|
||||
|
||||
public static void renderRegion(String worldName, CoordXZ coord) {
|
||||
if (!renderEnabled()) return;
|
||||
|
||||
World world = Bukkit.getWorld(worldName);
|
||||
int y = (world != null) ? world.getMaxHeight() : 255;
|
||||
int x = CoordXZ.regionToBlock(coord.x);
|
||||
int z = CoordXZ.regionToBlock(coord.z);
|
||||
api.triggerRenderOfVolume(worldName, x, 0, z, x + 511, y, z + 511);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Methods for displaying our borders on DynMap's world maps
|
||||
*/
|
||||
|
||||
public static void renderChunks(String worldName, List<CoordXZ> coords) {
|
||||
if (!renderEnabled()) return;
|
||||
|
||||
World world = Bukkit.getWorld(worldName);
|
||||
int y = (world != null) ? world.getMaxHeight() : 255;
|
||||
|
||||
for (CoordXZ coord : coords) {
|
||||
renderChunk(worldName, coord, y);
|
||||
}
|
||||
}
|
||||
|
||||
public static void renderChunk(String worldName, CoordXZ coord, int maxY) {
|
||||
if (!renderEnabled()) return;
|
||||
|
||||
int x = CoordXZ.chunkToBlock(coord.x);
|
||||
int z = CoordXZ.chunkToBlock(coord.z);
|
||||
api.triggerRenderOfVolume(worldName, x, 0, z, x + 15, maxY, z + 15);
|
||||
}
|
||||
|
||||
public static void showAllBorders() {
|
||||
if (!borderEnabled()) return;
|
||||
|
||||
// in case any borders are already shown
|
||||
removeAllBorders();
|
||||
|
||||
if (!Config.DynmapBorderEnabled()) {
|
||||
// don't want to show the marker set in DynMap if our integration is disabled
|
||||
if (markSet != null)
|
||||
markSet.deleteMarkerSet();
|
||||
markSet = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure the marker set is initialized
|
||||
markSet = markApi.getMarkerSet("worldborder.markerset");
|
||||
if (markSet == null)
|
||||
markSet = markApi.createMarkerSet("worldborder.markerset", Config.DynmapLayerLabel(), null, false);
|
||||
else
|
||||
markSet.setMarkerSetLabel(Config.DynmapLayerLabel());
|
||||
markSet.setLayerPriority(Config.DynmapPriority());
|
||||
markSet.setHideByDefault(Config.DynmapHideByDefault());
|
||||
Map<String, BorderData> borders = Config.getBorders();
|
||||
for (Entry<String, BorderData> stringBorderDataEntry : borders.entrySet()) {
|
||||
String worldName = stringBorderDataEntry.getKey();
|
||||
BorderData border = stringBorderDataEntry.getValue();
|
||||
showBorder(worldName, border);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showBorder(String worldName, BorderData border) {
|
||||
if (!borderEnabled()) return;
|
||||
|
||||
if (!Config.DynmapBorderEnabled()) return;
|
||||
|
||||
if ((border.getShape() == null) ? Config.ShapeRound() : border.getShape())
|
||||
showRoundBorder(worldName, border);
|
||||
else
|
||||
showSquareBorder(worldName, border);
|
||||
}
|
||||
|
||||
private static void showRoundBorder(String worldName, BorderData border) {
|
||||
if (squareBorders.containsKey(worldName))
|
||||
removeBorder(worldName);
|
||||
|
||||
World world = Bukkit.getWorld(worldName);
|
||||
int y = (world != null) ? world.getMaxHeight() : 255;
|
||||
|
||||
CircleMarker marker = roundBorders.get(worldName);
|
||||
if (marker == null) {
|
||||
marker = markSet.createCircleMarker("worldborder_" + worldName, Config.DynmapMessage(), false, worldName, border.getX(), y, border.getZ(), border.getRadiusX(), border.getRadiusZ(), true);
|
||||
marker.setLineStyle(lineWeight, lineOpacity, lineColor);
|
||||
marker.setFillStyle(0.0, 0x000000);
|
||||
roundBorders.put(worldName, marker);
|
||||
} else {
|
||||
marker.setCenter(worldName, border.getX(), y, border.getZ());
|
||||
marker.setRadius(border.getRadiusX(), border.getRadiusZ());
|
||||
}
|
||||
}
|
||||
|
||||
private static void showSquareBorder(String worldName, BorderData border) {
|
||||
if (roundBorders.containsKey(worldName))
|
||||
removeBorder(worldName);
|
||||
|
||||
// corners of the square border
|
||||
double[] xVals = {border.getX() - border.getRadiusX(), border.getX() + border.getRadiusX()};
|
||||
double[] zVals = {border.getZ() - border.getRadiusZ(), border.getZ() + border.getRadiusZ()};
|
||||
|
||||
AreaMarker marker = squareBorders.get(worldName);
|
||||
if (marker == null) {
|
||||
marker = markSet.createAreaMarker("worldborder_" + worldName, Config.DynmapMessage(), false, worldName, xVals, zVals, true);
|
||||
marker.setLineStyle(3, 1.0, 0xFF0000);
|
||||
marker.setFillStyle(0.0, 0x000000);
|
||||
squareBorders.put(worldName, marker);
|
||||
} else {
|
||||
marker.setCornerLocations(xVals, zVals);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeAllBorders() {
|
||||
if (!borderEnabled()) return;
|
||||
|
||||
for (CircleMarker marker : roundBorders.values()) {
|
||||
marker.deleteMarker();
|
||||
}
|
||||
roundBorders.clear();
|
||||
|
||||
for (AreaMarker marker : squareBorders.values()) {
|
||||
marker.deleteMarker();
|
||||
}
|
||||
squareBorders.clear();
|
||||
}
|
||||
|
||||
public static void removeBorder(String worldName) {
|
||||
if (!borderEnabled()) return;
|
||||
|
||||
CircleMarker marker = roundBorders.remove(worldName);
|
||||
if (marker != null)
|
||||
marker.deleteMarker();
|
||||
|
||||
AreaMarker marker2 = squareBorders.remove(worldName);
|
||||
if (marker2 != null)
|
||||
marker2.deleteMarker();
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/wimbli/WorldBorder/MobSpawnListener.java
Normal file
31
src/main/java/com/wimbli/WorldBorder/MobSpawnListener.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
|
||||
|
||||
public class MobSpawnListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void onCreatureSpawn(CreatureSpawnEvent event) {
|
||||
Location loc = event.getEntity().getLocation();
|
||||
if (loc == null) return;
|
||||
|
||||
World world = loc.getWorld();
|
||||
if (world == null) return;
|
||||
BorderData border = Config.Border(world.getName());
|
||||
if (border == null) return;
|
||||
|
||||
if (!border.insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
}
|
||||
184
src/main/java/com/wimbli/WorldBorder/WBCommand.java
Normal file
184
src/main/java/com/wimbli/WorldBorder/WBCommand.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import com.wimbli.WorldBorder.cmd.*;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
||||
public class WBCommand implements CommandExecutor {
|
||||
// ref. list of the commands which can have a world name in front of the command itself (ex. /wb _world_ radius 100)
|
||||
private final Set<String> subCommandsWithWorldNames = new LinkedHashSet<String>();
|
||||
// map of all sub-commands with the command name (string) for quick reference
|
||||
public Map<String, WBCmd> subCommands = new LinkedHashMap<String, WBCmd>();
|
||||
private boolean wasWorldQuotation = false;
|
||||
|
||||
|
||||
// constructor
|
||||
public WBCommand() {
|
||||
addCmd(new CmdHelp()); // 1 example
|
||||
addCmd(new CmdSet()); // 4 examples for player, 3 for console
|
||||
addCmd(new CmdSetcorners()); // 1
|
||||
addCmd(new CmdRadius()); // 1
|
||||
addCmd(new CmdList()); // 1
|
||||
//----- 8 per page of examples
|
||||
addCmd(new CmdShape()); // 2
|
||||
addCmd(new CmdClear()); // 2
|
||||
addCmd(new CmdFill()); // 1
|
||||
addCmd(new CmdTrim()); // 1
|
||||
addCmd(new CmdBypass()); // 1
|
||||
addCmd(new CmdBypasslist()); // 1
|
||||
//-----
|
||||
addCmd(new CmdKnockback()); // 1
|
||||
addCmd(new CmdWrap()); // 1
|
||||
addCmd(new CmdWhoosh()); // 1
|
||||
addCmd(new CmdGetmsg()); // 1
|
||||
addCmd(new CmdSetmsg()); // 1
|
||||
addCmd(new CmdWshape()); // 3
|
||||
//-----
|
||||
addCmd(new CmdPreventPlace()); // 1
|
||||
addCmd(new CmdPreventSpawn()); // 1
|
||||
addCmd(new CmdDelay()); // 1
|
||||
addCmd(new CmdDynmap()); // 1
|
||||
addCmd(new CmdDynmaplabel()); // 1
|
||||
addCmd(new CmdDynmapmsg()); // 1
|
||||
addCmd(new CmdRemount()); // 1
|
||||
addCmd(new CmdFillautosave()); // 1
|
||||
addCmd(new CmdPortal()); // 1
|
||||
//-----
|
||||
addCmd(new CmdDenypearl()); // 1
|
||||
addCmd(new CmdReload()); // 1
|
||||
addCmd(new CmdDebug()); // 1
|
||||
// this is the default command, which shows command example pages; should be last just in case
|
||||
addCmd(new CmdCommands());
|
||||
}
|
||||
|
||||
private void addCmd(WBCmd cmd) {
|
||||
subCommands.put(cmd.name, cmd);
|
||||
if (cmd.hasWorldNameInput)
|
||||
subCommandsWithWorldNames.add(cmd.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] split) {
|
||||
Player player = (sender instanceof Player) ? (Player) sender : null;
|
||||
|
||||
// if world name is passed inside quotation marks, handle that, and get List<String> instead of String[]
|
||||
List<String> params = concatenateQuotedWorldName(split);
|
||||
|
||||
String worldName = null;
|
||||
// is second parameter the command and first parameter a world name? definitely world name if it was in quotation marks
|
||||
if (wasWorldQuotation || (params.size() > 1 && !subCommands.containsKey(params.get(0)) && subCommandsWithWorldNames.contains(params.get(1))))
|
||||
worldName = params.get(0);
|
||||
|
||||
// no command specified? show command examples / help
|
||||
if (params.isEmpty())
|
||||
params.add(0, "commands");
|
||||
|
||||
// determined the command name
|
||||
String cmdName = (worldName == null) ? params.get(0).toLowerCase() : params.get(1).toLowerCase();
|
||||
|
||||
// remove command name and (if there) world name from front of param array
|
||||
params.remove(0);
|
||||
if (worldName != null)
|
||||
params.remove(0);
|
||||
|
||||
// make sure command is recognized, default to showing command examples / help if not; also check for specified page number
|
||||
if (!subCommands.containsKey(cmdName)) {
|
||||
int page = (player == null) ? 0 : 1;
|
||||
try {
|
||||
page = Integer.parseInt(cmdName);
|
||||
} catch (NumberFormatException ignored) {
|
||||
sender.sendMessage(WBCmd.C_ERR + "Command not recognized. Showing command list.");
|
||||
}
|
||||
cmdName = "commands";
|
||||
params.add(0, Integer.toString(page));
|
||||
}
|
||||
|
||||
WBCmd subCommand = subCommands.get(cmdName);
|
||||
|
||||
// check permission
|
||||
if (!Config.HasPermission(player, subCommand.permission))
|
||||
return true;
|
||||
|
||||
// if command requires world name when run by console, make sure that's in place
|
||||
if (player == null && subCommand.hasWorldNameInput && subCommand.consoleRequiresWorldName && worldName == null) {
|
||||
sender.sendMessage(WBCmd.C_ERR + "This command requires a world to be specified if run by the console.");
|
||||
subCommand.sendCmdHelp(sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
// make sure valid number of parameters has been provided
|
||||
if (params.size() < subCommand.minParams || params.size() > subCommand.maxParams) {
|
||||
if (subCommand.maxParams == 0)
|
||||
sender.sendMessage(WBCmd.C_ERR + "This command does not accept any parameters.");
|
||||
else
|
||||
sender.sendMessage(WBCmd.C_ERR + "You have not provided a valid number of parameters.");
|
||||
subCommand.sendCmdHelp(sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
// execute command
|
||||
subCommand.execute(sender, player, params, worldName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// if world name is surrounded by quotation marks, combine it down and flag wasWorldQuotation if it's first param.
|
||||
// also return List<String> instead of input primitive String[]
|
||||
private List<String> concatenateQuotedWorldName(String[] split) {
|
||||
wasWorldQuotation = false;
|
||||
List<String> args = new ArrayList<String>(Arrays.asList(split));
|
||||
|
||||
int startIndex = -1;
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
if (args.get(i).startsWith("\"")) {
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (startIndex == -1)
|
||||
return args;
|
||||
|
||||
if (args.get(startIndex).endsWith("\"")) {
|
||||
args.set(startIndex, args.get(startIndex).substring(1, args.get(startIndex).length() - 1));
|
||||
if (startIndex == 0)
|
||||
wasWorldQuotation = true;
|
||||
} else {
|
||||
List<String> concat = new ArrayList<String>(args);
|
||||
Iterator<String> concatI = concat.iterator();
|
||||
|
||||
// skip past any parameters in front of the one we're starting on
|
||||
for (int i = 1; i < startIndex + 1; i++) {
|
||||
concatI.next();
|
||||
}
|
||||
|
||||
StringBuilder quote = new StringBuilder(concatI.next());
|
||||
while (concatI.hasNext()) {
|
||||
String next = concatI.next();
|
||||
concatI.remove();
|
||||
quote.append(" ");
|
||||
quote.append(next);
|
||||
if (next.endsWith("\"")) {
|
||||
concat.set(startIndex, quote.substring(1, quote.length() - 1));
|
||||
args = concat;
|
||||
if (startIndex == 0)
|
||||
wasWorldQuotation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
public Set<String> getCommandNames() {
|
||||
// using TreeSet to sort alphabetically
|
||||
Set<String> commands = new TreeSet<>(subCommands.keySet());
|
||||
// removing default "commands" command as it's not normally shown or run like other commands
|
||||
commands.remove("commands");
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
108
src/main/java/com/wimbli/WorldBorder/WBListener.java
Normal file
108
src/main/java/com/wimbli/WorldBorder/WBListener.java
Normal file
@@ -0,0 +1,108 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerPortalEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||
|
||||
|
||||
public class WBListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void onPlayerTeleport(PlayerTeleportEvent event) {
|
||||
// if knockback is set to 0, simply return
|
||||
if (Config.KnockBack() == 0.0)
|
||||
return;
|
||||
|
||||
if (Config.Debug())
|
||||
Config.log("Teleport cause: " + event.getCause().toString());
|
||||
|
||||
Location newLoc = BorderCheckTask.checkPlayer(event.getPlayer(), event.getTo(), true, true);
|
||||
if (newLoc != null) {
|
||||
if (event.getCause() == PlayerTeleportEvent.TeleportCause.ENDER_PEARL && Config.getDenyEnderpearl()) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
event.setTo(newLoc);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
public void onPlayerPortal(PlayerPortalEvent event) {
|
||||
// if knockback is set to 0, or portal redirection is disabled, simply return
|
||||
if (Config.KnockBack() == 0.0 || !Config.portalRedirection())
|
||||
return;
|
||||
|
||||
Location newLoc = BorderCheckTask.checkPlayer(event.getPlayer(), event.getTo(), true, false);
|
||||
if (newLoc != null)
|
||||
event.setTo(newLoc);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onChunkLoad(ChunkLoadEvent event) {
|
||||
/* // tested, found to spam pretty rapidly as client repeatedly requests the same chunks since they're not being sent
|
||||
// definitely too spammy at only 16 blocks outside border
|
||||
// potentially useful at standard 208 block padding as it was triggering only occasionally while trying to get out all along edge of round border, though sometimes up to 3 triggers within a second corresponding to 3 adjacent chunks
|
||||
// would of course need to be further worked on to have it only affect chunks outside a border, along with an option somewhere to disable it or even set specified distance outside border for it to take effect; maybe send client chunk composed entirely of air to shut it up
|
||||
|
||||
// method to prevent new chunks from being generated, core method courtesy of code from NoNewChunk plugin (http://dev.bukkit.org/bukkit-plugins/nonewchunk/)
|
||||
if(event.isNewChunk())
|
||||
{
|
||||
Chunk chunk = event.getChunk();
|
||||
chunk.unload(false, false);
|
||||
Config.logWarn("New chunk generation has been prevented at X " + chunk.getX() + ", Z " + chunk.getZ());
|
||||
}
|
||||
*/
|
||||
// make sure our border monitoring task is still running like it should
|
||||
if (Config.isBorderTimerRunning()) return;
|
||||
|
||||
Config.logWarn("Border-checking task was not running! Something on your server apparently killed it. It will now be restarted.");
|
||||
Config.StartBorderTimer();
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if there is a fill task running, and if yes, if it's for the
|
||||
* world that the unload event refers to, set "force loaded" flag off
|
||||
* and track if chunk was somehow on unload prevention list
|
||||
*/
|
||||
@EventHandler
|
||||
public void onChunkUnload(ChunkUnloadEvent e) {
|
||||
if (Config.fillTask == null)
|
||||
return;
|
||||
|
||||
Chunk chunk = e.getChunk();
|
||||
if (e.getWorld() != Config.fillTask.getWorld())
|
||||
return;
|
||||
|
||||
// just to be on the safe side, in case it's still set at this point somehow
|
||||
chunk.setForceLoaded(false);
|
||||
}
|
||||
|
||||
// If player joins and noPlayersToggle is on automatically pause any existing fill task
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent e) {
|
||||
if (!Config.NoPlayersToggle() || Config.fillTask == null || Config.fillTask.isPaused())
|
||||
return;
|
||||
|
||||
Config.fillTask.pause(true);
|
||||
Config.log("Detected player online. World map generation task automatically paused.");
|
||||
}
|
||||
|
||||
// If no players online and noPlayersToggle is on automatically unpause any existing fill task
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent e) {
|
||||
if (!Config.NoPlayersToggle() || Config.fillTask == null || Bukkit.getOnlinePlayers().size() > 1 || !Config.fillTask.isPaused())
|
||||
return;
|
||||
|
||||
Config.fillTask.pause(false);
|
||||
Config.log("No players online. World map generation task automatically unpaused.");
|
||||
}
|
||||
}
|
||||
77
src/main/java/com/wimbli/WorldBorder/WorldBorder.java
Normal file
77
src/main/java/com/wimbli/WorldBorder/WorldBorder.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
|
||||
public class WorldBorder extends JavaPlugin {
|
||||
public static volatile WorldBorder plugin = null;
|
||||
public static volatile WBCommand wbCommand = null;
|
||||
private BlockPlaceListener blockPlaceListener = null;
|
||||
private MobSpawnListener mobSpawnListener = null;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (plugin == null)
|
||||
plugin = this;
|
||||
if (wbCommand == null)
|
||||
wbCommand = new WBCommand();
|
||||
|
||||
// Load (or create new) config file
|
||||
Config.load(this, false);
|
||||
|
||||
// our one real command, though it does also have aliases "wb" and "worldborder"
|
||||
getCommand("wborder").setExecutor(wbCommand);
|
||||
|
||||
// keep an eye on teleports, to redirect them to a spot inside the border if necessary
|
||||
getServer().getPluginManager().registerEvents(new WBListener(), this);
|
||||
|
||||
if (Config.preventBlockPlace())
|
||||
enableBlockPlaceListener(true);
|
||||
|
||||
if (Config.preventMobSpawn())
|
||||
enableMobSpawnListener(true);
|
||||
|
||||
// integrate with DynMap if it's available
|
||||
DynMapFeatures.setup();
|
||||
|
||||
// Well I for one find this info useful, so...
|
||||
Location spawn = getServer().getWorlds().get(0).getSpawnLocation();
|
||||
Config.log("For reference, the main world's spawn location is at X: " + Config.coord.format(spawn.getX()) + " Y: " + Config.coord.format(spawn.getY()) + " Z: " + Config.coord.format(spawn.getZ()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
DynMapFeatures.removeAllBorders();
|
||||
Config.StopBorderTimer();
|
||||
Config.StopFillTask(true);
|
||||
Config.StoreFillTask();
|
||||
}
|
||||
|
||||
// for other plugins to hook into
|
||||
public BorderData getWorldBorder(String worldName) {
|
||||
return Config.Border(worldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Replaced by {@link #getWorldBorder(String worldName)};
|
||||
* this method name starts with an uppercase letter, which it shouldn't
|
||||
*/
|
||||
public BorderData GetWorldBorder(String worldName) {
|
||||
return getWorldBorder(worldName);
|
||||
}
|
||||
|
||||
public void enableBlockPlaceListener(boolean enable) {
|
||||
if (enable)
|
||||
getServer().getPluginManager().registerEvents(this.blockPlaceListener = new BlockPlaceListener(), this);
|
||||
else if (blockPlaceListener != null)
|
||||
blockPlaceListener.unregister();
|
||||
}
|
||||
|
||||
public void enableMobSpawnListener(boolean enable) {
|
||||
if (enable)
|
||||
getServer().getPluginManager().registerEvents(this.mobSpawnListener = new MobSpawnListener(), this);
|
||||
else if (mobSpawnListener != null)
|
||||
mobSpawnListener.unregister();
|
||||
}
|
||||
}
|
||||
198
src/main/java/com/wimbli/WorldBorder/WorldFileData.java
Normal file
198
src/main/java/com/wimbli/WorldBorder/WorldFileData.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
// import io.papermc.lib.PaperLib; // <-- ENTFERNT
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.util.*;
|
||||
|
||||
// image output stuff, for debugging method at bottom of this file
|
||||
|
||||
|
||||
// by the way, this region file handler was created based on the divulged region file format: http://mojang.com/2011/02/16/minecraft-save-file-format-in-beta-1-3/
|
||||
|
||||
public class WorldFileData {
|
||||
private final transient World world;
|
||||
private final transient Map<CoordXZ, List<Boolean>> regionChunkExistence = Collections.synchronizedMap(new HashMap<CoordXZ, List<Boolean>>());
|
||||
private transient File regionFolder = null;
|
||||
private transient File[] regionFiles = null;
|
||||
private transient Player notifyPlayer = null;
|
||||
|
||||
// the constructor is private; use create() method above to create an instance of this class.
|
||||
private WorldFileData(World world, Player notifyPlayer) {
|
||||
this.world = world;
|
||||
this.notifyPlayer = notifyPlayer;
|
||||
}
|
||||
|
||||
// Use this static method to create a new instance of this class. If null is returned, there was a problem so any process relying on this should be cancelled.
|
||||
public static WorldFileData create(World world, Player notifyPlayer) {
|
||||
WorldFileData newData = new WorldFileData(world, notifyPlayer);
|
||||
|
||||
newData.regionFolder = new File(newData.world.getWorldFolder(), "region");
|
||||
if (!newData.regionFolder.exists() || !newData.regionFolder.isDirectory()) {
|
||||
// check for region folder inside a DIM* folder (DIM-1 for nether, DIM1 for end, DIMwhatever for custom world types)
|
||||
File[] possibleDimFolders = newData.world.getWorldFolder().listFiles(new DimFolderFileFilter());
|
||||
assert possibleDimFolders != null;
|
||||
for (File possibleDimFolder : possibleDimFolders) {
|
||||
File possible = new File(newData.world.getWorldFolder(), possibleDimFolder.getName() + File.separator + "region");
|
||||
if (possible.exists() && possible.isDirectory()) {
|
||||
newData.regionFolder = possible;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!newData.regionFolder.exists() || !newData.regionFolder.isDirectory()) {
|
||||
newData.sendMessage("Could not validate folder for world's region files. Looked in " + newData.world.getWorldFolder().getPath() + " for valid DIM* folder with a region folder in it.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Accepted region file formats: MCR is from late beta versions through 1.1, MCA is from 1.2+
|
||||
newData.regionFiles = newData.regionFolder.listFiles(new ExtFileFilter(".MCA"));
|
||||
if (newData.regionFiles == null || newData.regionFiles.length == 0) {
|
||||
newData.regionFiles = newData.regionFolder.listFiles(new ExtFileFilter(".MCR"));
|
||||
if (newData.regionFiles == null || newData.regionFiles.length == 0) {
|
||||
newData.sendMessage("Could not find any region files. Looked in: " + newData.regionFolder.getPath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
// number of region files this world has
|
||||
public int regionFileCount() {
|
||||
return regionFiles.length;
|
||||
}
|
||||
|
||||
// folder where world's region files are located
|
||||
public File regionFolder() {
|
||||
return regionFolder;
|
||||
}
|
||||
|
||||
// return entire list of region files
|
||||
public File[] regionFiles() {
|
||||
return regionFiles.clone();
|
||||
}
|
||||
|
||||
// return a region file by index
|
||||
public File regionFile(int index) {
|
||||
if (regionFiles.length < index)
|
||||
return null;
|
||||
return regionFiles[index];
|
||||
}
|
||||
|
||||
// get the X and Z world coordinates of the region from the filename
|
||||
public CoordXZ regionFileCoordinates(int index) {
|
||||
File regionFile = this.regionFile(index);
|
||||
String[] coords = regionFile.getName().split("\\.");
|
||||
int x, z;
|
||||
try {
|
||||
x = Integer.parseInt(coords[1]);
|
||||
z = Integer.parseInt(coords[2]);
|
||||
return new CoordXZ(x, z);
|
||||
} catch (Exception ex) {
|
||||
sendMessage("Error! Region file found with abnormal name: " + regionFile.getName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Find out if the chunk at the given coordinates exists.
|
||||
public boolean doesChunkExist(int x, int z) {
|
||||
CoordXZ region = new CoordXZ(CoordXZ.chunkToRegion(x), CoordXZ.chunkToRegion(z));
|
||||
List<Boolean> regionChunks = this.getRegionData(region);
|
||||
// Bukkit.getLogger().info("x: "+x+" z: "+z+" offset: "+coordToRegionOffset(x, z));
|
||||
if (regionChunks.get(coordToRegionOffset(x, z))) {
|
||||
return true;
|
||||
}
|
||||
return world.isChunkGenerated(x, z); // <-- KORRIGIERT: Ersetzt PaperLib.isChunkGenerated
|
||||
}
|
||||
|
||||
// Find out if the chunk at the given coordinates has been fully generated.
|
||||
// Minecraft only fully generates a chunk when adjacent chunks are also loaded.
|
||||
public boolean isChunkFullyGenerated(int x, int z) { // if all adjacent chunks exist, it should be a safe enough bet that this one is fully generated
|
||||
// For 1.13+, due to world gen changes, this is now effectively a 3 chunk radius requirement vs a 1 chunk radius
|
||||
for (int xx = x - 3; xx <= x + 3; xx++) {
|
||||
for (int zz = z - 3; zz <= z + 3; zz++) {
|
||||
if (!doesChunkExist(xx, zz))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method to let us know a chunk has been generated, to update our region map.
|
||||
public void chunkExistsNow(int x, int z) {
|
||||
CoordXZ region = new CoordXZ(CoordXZ.chunkToRegion(x), CoordXZ.chunkToRegion(z));
|
||||
List<Boolean> regionChunks = this.getRegionData(region);
|
||||
regionChunks.set(coordToRegionOffset(x, z), true);
|
||||
}
|
||||
|
||||
|
||||
// region is 32 * 32 chunks; chunk pointers are stored in region file at position: x + z*32 (32 * 32 chunks = 1024)
|
||||
// input x and z values can be world-based chunk coordinates or local-to-region chunk coordinates either one
|
||||
private int coordToRegionOffset(int x, int z) {
|
||||
// "%" modulus is used to convert potential world coordinates to definitely be local region coordinates
|
||||
x = x % 32;
|
||||
z = z % 32;
|
||||
// similarly, for local coordinates, we need to wrap negative values around
|
||||
if (x < 0) x += 32;
|
||||
if (z < 0) z += 32;
|
||||
// return offset position for the now definitely local x and z values
|
||||
return (x + (z * 32));
|
||||
}
|
||||
|
||||
private List<Boolean> getRegionData(CoordXZ region) {
|
||||
List<Boolean> data = regionChunkExistence.get(region);
|
||||
if (data != null)
|
||||
return data;
|
||||
|
||||
// data for the specified region isn't loaded yet, so init it as empty and try to find the file and load the data
|
||||
data = new ArrayList<Boolean>(1024);
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
data.add(Boolean.FALSE);
|
||||
}
|
||||
|
||||
regionChunkExistence.put(region, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
// send a message to the server console/log and possibly to an in-game player
|
||||
private void sendMessage(String text) {
|
||||
Config.log("[WorldData] " + text);
|
||||
if (notifyPlayer != null && notifyPlayer.isOnline())
|
||||
notifyPlayer.sendMessage("[WorldData] " + text);
|
||||
}
|
||||
|
||||
// file filter used for region files
|
||||
private static class ExtFileFilter implements FileFilter {
|
||||
String ext;
|
||||
|
||||
public ExtFileFilter(String extension) {
|
||||
this.ext = extension.toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
return (
|
||||
file.exists()
|
||||
&& file.isFile()
|
||||
&& file.getName().toLowerCase().endsWith(ext)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// file filter used for DIM* folders (for nether, End, and custom world types)
|
||||
private static class DimFolderFileFilter implements FileFilter {
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
return (
|
||||
file.exists()
|
||||
&& file.isDirectory()
|
||||
&& file.getName().toLowerCase().startsWith("dim")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
386
src/main/java/com/wimbli/WorldBorder/WorldFillTask.java
Normal file
386
src/main/java/com/wimbli/WorldBorder/WorldFillTask.java
Normal file
@@ -0,0 +1,386 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import com.wimbli.WorldBorder.Events.WorldBorderFillFinishedEvent;
|
||||
import com.wimbli.WorldBorder.Events.WorldBorderFillStartEvent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
// Die asynchronen Klassen werden nicht mehr benötigt
|
||||
// import java.util.HashMap;
|
||||
// import java.util.HashSet;
|
||||
// import java.util.Map;
|
||||
// import java.util.Set;
|
||||
// import java.util.concurrent.CompletableFuture;
|
||||
|
||||
|
||||
public class WorldFillTask implements Runnable {
|
||||
private final transient CoordXZ lastChunk = new CoordXZ(0, 0);
|
||||
// general task-related reference data
|
||||
private transient Server server = null;
|
||||
private transient World world = null;
|
||||
private transient BorderData border = null;
|
||||
private transient WorldFileData worldData = null;
|
||||
private transient boolean readyToGo = false;
|
||||
private transient boolean paused = false;
|
||||
private transient boolean pausedForMemory = false;
|
||||
private transient int taskID = -1;
|
||||
private transient Player notifyPlayer = null;
|
||||
private transient int chunksPerRun = 1;
|
||||
private transient boolean continueNotice = false;
|
||||
private transient boolean forceLoad = false;
|
||||
// these are only stored for saving task to config
|
||||
private transient int fillDistance = 208;
|
||||
private transient int tickFrequency = 1;
|
||||
private transient int refX = 0, lastLegX = 0;
|
||||
private transient int refZ = 0, lastLegZ = 0;
|
||||
private transient int refLength = -1;
|
||||
private transient int refTotal = 0, lastLegTotal = 0;
|
||||
// values for the spiral pattern check which fills out the map to the border
|
||||
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;
|
||||
private transient boolean insideBorder = true;
|
||||
// for reporting progress back to user occasionally
|
||||
private transient long lastReport = Config.Now();
|
||||
private transient long lastAutosave = Config.Now();
|
||||
private transient int reportTarget = 0;
|
||||
private transient int reportTotal = 0;
|
||||
private transient int reportNum = 0;
|
||||
|
||||
// Asynchrone Logik entfernt
|
||||
// private transient Map<CompletableFuture<Void>, CoordXZ> pendingChunks;
|
||||
// private transient Set<UnloadDependency> preventUnload;
|
||||
|
||||
public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency, boolean forceLoad) {
|
||||
this.server = theServer;
|
||||
this.notifyPlayer = player;
|
||||
this.fillDistance = fillDistance;
|
||||
this.tickFrequency = tickFrequency;
|
||||
this.chunksPerRun = chunksPerRun;
|
||||
this.forceLoad = forceLoad;
|
||||
|
||||
this.world = server.getWorld(worldName);
|
||||
if (this.world == null) {
|
||||
if (worldName.isEmpty())
|
||||
sendMessage("You must specify a world!");
|
||||
else
|
||||
sendMessage("World \"" + worldName + "\" not found!");
|
||||
this.stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.border = (Config.Border(worldName) == null) ? null : Config.Border(worldName).copy();
|
||||
if (this.border == null) {
|
||||
sendMessage("No border found for world \"" + worldName + "\"!");
|
||||
this.stop(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// load up a new WorldFileData for the world in question, used to scan region files for which chunks are already fully generated and such
|
||||
worldData = WorldFileData.create(world, notifyPlayer);
|
||||
if (worldData == null) {
|
||||
this.stop(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Asynchrone Logik entfernt
|
||||
// pendingChunks = new HashMap<>();
|
||||
// preventUnload = new HashSet<>();
|
||||
|
||||
this.border.setRadiusX(border.getRadiusX() + fillDistance);
|
||||
this.border.setRadiusZ(border.getRadiusZ() + fillDistance);
|
||||
this.x = CoordXZ.blockToChunk((int) border.getX());
|
||||
this.z = CoordXZ.blockToChunk((int) border.getZ());
|
||||
|
||||
int chunkWidthX = (int) Math.ceil((double) ((border.getRadiusX() + 16) * 2) / 16);
|
||||
int chunkWidthZ = (int) Math.ceil((double) ((border.getRadiusZ() + 16) * 2) / 16);
|
||||
int biggerWidth = Math.max(chunkWidthX, chunkWidthZ);
|
||||
this.reportTarget = (biggerWidth * biggerWidth) + biggerWidth + 1;
|
||||
|
||||
this.readyToGo = true;
|
||||
Bukkit.getServer().getPluginManager().callEvent(new WorldBorderFillStartEvent(this));
|
||||
}
|
||||
|
||||
// for backwards compatibility
|
||||
public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency) {
|
||||
this(theServer, player, worldName, fillDistance, chunksPerRun, tickFrequency, false);
|
||||
}
|
||||
|
||||
public void setTaskID(int ID) {
|
||||
if (ID == -1) this.stop(false);
|
||||
this.taskID = ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (continueNotice) {
|
||||
continueNotice = false;
|
||||
sendMessage("World map generation task automatically continuing.");
|
||||
sendMessage("Reminder: you can cancel at any time with \"wb fill cancel\", or pause/unpause with \"wb fill pause\".");
|
||||
}
|
||||
|
||||
if (pausedForMemory) {
|
||||
if (Config.AvailableMemoryTooLow())
|
||||
return;
|
||||
|
||||
pausedForMemory = false;
|
||||
readyToGo = true;
|
||||
sendMessage("Available memory is sufficient, automatically continuing.");
|
||||
}
|
||||
|
||||
if (server == null || !readyToGo || paused)
|
||||
return;
|
||||
|
||||
readyToGo = false;
|
||||
long loopStartTime = Config.Now();
|
||||
|
||||
// NEUE SYNCHRONE LOGIK
|
||||
// Verarbeite eine feste Anzahl von Chunks pro Tick
|
||||
for (int loop = 0; loop < this.chunksPerRun; loop++) {
|
||||
if (paused || pausedForMemory)
|
||||
return;
|
||||
|
||||
long now = Config.Now();
|
||||
|
||||
if (now > lastReport + 5000)
|
||||
reportProgress();
|
||||
|
||||
if (now > loopStartTime + 45) {
|
||||
readyToGo = true;
|
||||
return;
|
||||
}
|
||||
|
||||
while (!border.insideBorder(CoordXZ.chunkToBlock(x) + 8, CoordXZ.chunkToBlock(z) + 8)) {
|
||||
if (!moveToNext())
|
||||
return;
|
||||
}
|
||||
insideBorder = true;
|
||||
|
||||
if (!forceLoad) {
|
||||
int rLoop = 0;
|
||||
while (worldData.isChunkFullyGenerated(x, z)) {
|
||||
rLoop++;
|
||||
insideBorder = true;
|
||||
if (!moveToNext())
|
||||
return;
|
||||
|
||||
if (rLoop > 255) {
|
||||
readyToGo = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lade den Chunk SYNCHRON und warte, bis er fertig ist.
|
||||
try {
|
||||
Chunk chunk = world.getChunkAt(x, z);
|
||||
worldData.chunkExistsNow(x, z);
|
||||
reportNum++;
|
||||
} catch (Exception e) {
|
||||
sendMessage("Error! Could not load chunk at " + x + ", " + z + ". " + e.getMessage());
|
||||
}
|
||||
|
||||
if (!moveToNext())
|
||||
return;
|
||||
}
|
||||
|
||||
readyToGo = true;
|
||||
}
|
||||
|
||||
// step through chunks in spiral pattern from center; returns false if we're done, otherwise returns true
|
||||
public boolean moveToNext() {
|
||||
if (paused || pausedForMemory)
|
||||
return false;
|
||||
|
||||
reportNum++;
|
||||
|
||||
if (!isNeg && current == 0 && length > 3) {
|
||||
if (!isZLeg) {
|
||||
lastLegX = x;
|
||||
lastLegZ = z;
|
||||
lastLegTotal = reportTotal + reportNum;
|
||||
} else {
|
||||
refX = lastLegX;
|
||||
refZ = lastLegZ;
|
||||
refTotal = lastLegTotal;
|
||||
refLength = length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (current < length)
|
||||
current++;
|
||||
else {
|
||||
current = 0;
|
||||
isZLeg ^= true;
|
||||
if (isZLeg) {
|
||||
isNeg ^= true;
|
||||
length++;
|
||||
}
|
||||
}
|
||||
|
||||
lastChunk.x = x;
|
||||
lastChunk.z = z;
|
||||
|
||||
if (isZLeg)
|
||||
z += (isNeg) ? -1 : 1;
|
||||
else
|
||||
x += (isNeg) ? -1 : 1;
|
||||
|
||||
if (isZLeg && isNeg && current == 0) {
|
||||
if (!insideBorder) {
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
else
|
||||
insideBorder = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// for successful completion
|
||||
public void finish() {
|
||||
this.paused = true;
|
||||
reportProgress();
|
||||
world.save();
|
||||
Bukkit.getServer().getPluginManager().callEvent(new WorldBorderFillFinishedEvent(world, reportTotal));
|
||||
sendMessage("task successfully completed for world \"" + refWorld() + "\"!");
|
||||
this.stop(false);
|
||||
}
|
||||
|
||||
// for cancelling prematurely
|
||||
public void cancel(boolean Save) {
|
||||
this.stop(Save);
|
||||
}
|
||||
|
||||
// we're done, whether finished or cancelled
|
||||
private void stop(boolean Save) {
|
||||
if (!Save) Config.UnStoreFillTask();
|
||||
if (server == null)
|
||||
return;
|
||||
|
||||
readyToGo = false;
|
||||
if (taskID != -1)
|
||||
server.getScheduler().cancelTask(taskID);
|
||||
server = null;
|
||||
|
||||
// Komplexe Unload-Logik entfernt, da der Server dies nun selbst verwaltet
|
||||
}
|
||||
|
||||
// is this task still valid/workable?
|
||||
public boolean valid() {
|
||||
return this.server != null;
|
||||
}
|
||||
|
||||
// handle pausing/unpausing the task
|
||||
public void pause() {
|
||||
if (this.pausedForMemory)
|
||||
pause(false);
|
||||
else
|
||||
pause(!this.paused);
|
||||
}
|
||||
|
||||
public void pause(boolean pause) {
|
||||
if (this.pausedForMemory && !pause)
|
||||
this.pausedForMemory = false;
|
||||
else
|
||||
this.paused = pause;
|
||||
if (this.paused) {
|
||||
Config.StoreFillTask();
|
||||
reportProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return this.paused || this.pausedForMemory;
|
||||
}
|
||||
|
||||
// Methode nicht mehr benötigt
|
||||
// public boolean chunkOnUnloadPreventionList(int x, int z) { ... }
|
||||
|
||||
public World getWorld() {
|
||||
return world;
|
||||
}
|
||||
|
||||
// let the user know how things are coming along
|
||||
private void reportProgress() {
|
||||
lastReport = Config.Now();
|
||||
double perc = getPercentageCompleted();
|
||||
if (perc > 100) perc = 100;
|
||||
sendMessage(reportNum + " more chunks processed (" + (reportTotal + reportNum) + " total, ~" + Config.coord.format(perc) + "%" + ")");
|
||||
reportTotal += reportNum;
|
||||
reportNum = 0;
|
||||
|
||||
if (Config.FillAutosaveFrequency() > 0 && lastAutosave + (Config.FillAutosaveFrequency() * 1000) < lastReport) {
|
||||
lastAutosave = lastReport;
|
||||
sendMessage("Saving the world to disk, just to be on the safe side.");
|
||||
world.save();
|
||||
Config.StoreFillTask();
|
||||
}
|
||||
}
|
||||
|
||||
// send a message to the server console/log and possibly to an in-game player
|
||||
private void sendMessage(String text) {
|
||||
int availMem = Config.AvailableMemory();
|
||||
|
||||
Config.log("[Fill] " + text + " (free mem: " + availMem + " MB)");
|
||||
if (notifyPlayer != null)
|
||||
notifyPlayer.sendMessage("[Fill] " + text);
|
||||
|
||||
if (availMem < 400) {
|
||||
pausedForMemory = true;
|
||||
Config.StoreFillTask();
|
||||
text = "Available memory is very low, task is pausing. A cleanup will be attempted now, and the task will automatically continue if/when sufficient memory is freed up.\n Alternatively, if you restart the server, this task will automatically continue once the server is back up.";
|
||||
Config.log("[Fill] " + text);
|
||||
if (notifyPlayer != null)
|
||||
notifyPlayer.sendMessage("[Fill] " + text);
|
||||
System.gc();
|
||||
Runtime.getRuntime().gc();
|
||||
System.runFinalization();
|
||||
}
|
||||
}
|
||||
|
||||
// stuff for saving / restoring progress
|
||||
public void continueProgress(int x, int z, int length, int totalDone) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.length = length;
|
||||
this.reportTotal = totalDone;
|
||||
this.continueNotice = true;
|
||||
this.refX = x;
|
||||
this.refZ = z;
|
||||
this.refLength = length;
|
||||
this.refTotal = totalDone;
|
||||
}
|
||||
|
||||
public int refX() { return refX; }
|
||||
public int refZ() { return refZ; }
|
||||
public int refLength() { return refLength; }
|
||||
public int refTotal() { return refTotal; }
|
||||
public int refFillDistance() { return fillDistance; }
|
||||
public int refTickFrequency() { return tickFrequency; }
|
||||
public int refChunksPerRun() { return chunksPerRun; }
|
||||
public String refWorld() { return world.getName(); }
|
||||
public boolean refForceLoad() { return forceLoad; }
|
||||
|
||||
public double getPercentageCompleted() {
|
||||
return ((double) (reportTotal + reportNum) / (double) reportTarget) * 100;
|
||||
}
|
||||
|
||||
public int getChunksCompleted() {
|
||||
return reportTotal;
|
||||
}
|
||||
|
||||
public int getChunksTotal() {
|
||||
return reportTarget;
|
||||
}
|
||||
|
||||
// Asynchrone Methode und innere Klasse entfernt
|
||||
// private CompletableFuture<Void> loadChunkAsync(...) { ... }
|
||||
// private class UnloadDependency { ... }
|
||||
}
|
||||
387
src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java
Normal file
387
src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java
Normal file
@@ -0,0 +1,387 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import com.wimbli.WorldBorder.Events.WorldBorderTrimFinishedEvent;
|
||||
import com.wimbli.WorldBorder.Events.WorldBorderTrimStartEvent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class WorldTrimTask implements Runnable {
|
||||
// general task-related reference data
|
||||
private transient Server server = null;
|
||||
private transient World world = null;
|
||||
private transient WorldFileData worldData = null;
|
||||
private transient BorderData border = null;
|
||||
private transient boolean readyToGo = false;
|
||||
private transient boolean paused = false;
|
||||
private transient int taskID = -1;
|
||||
private transient Player notifyPlayer = null;
|
||||
private transient int chunksPerRun = 1;
|
||||
|
||||
// values for what chunk in the current region we're at
|
||||
private transient int currentRegion = -1; // region(file) we're at in regionFiles
|
||||
private transient int regionX = 0; // X location value of the current region
|
||||
private transient int regionZ = 0; // X location value of the current region
|
||||
private transient int currentChunk = 0; // chunk we've reached in the current region (regionChunks)
|
||||
private transient List<CoordXZ> regionChunks = new ArrayList<CoordXZ>(1024);
|
||||
private transient List<CoordXZ> trimChunks = new ArrayList<CoordXZ>(1024);
|
||||
private transient int counter = 0;
|
||||
|
||||
// for reporting progress back to user occasionally
|
||||
private transient long lastReport = Config.Now();
|
||||
private transient int reportTarget = 0;
|
||||
private transient int reportTotal = 0;
|
||||
private transient int reportTrimmedRegions = 0;
|
||||
private transient int reportTrimmedChunks = 0;
|
||||
|
||||
|
||||
public WorldTrimTask(Server theServer, Player player, String worldName, int trimDistance, int chunksPerRun) {
|
||||
this.server = theServer;
|
||||
this.notifyPlayer = player;
|
||||
this.chunksPerRun = chunksPerRun;
|
||||
|
||||
this.world = server.getWorld(worldName);
|
||||
if (this.world == null) {
|
||||
if (worldName.isEmpty())
|
||||
sendMessage("You must specify a world!");
|
||||
else
|
||||
sendMessage("World \"" + worldName + "\" not found!");
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.border = (Config.Border(worldName) == null) ? null : Config.Border(worldName).copy();
|
||||
if (this.border == null) {
|
||||
sendMessage("No border found for world \"" + worldName + "\"!");
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.border.setRadiusX(border.getRadiusX() + trimDistance);
|
||||
this.border.setRadiusZ(border.getRadiusZ() + trimDistance);
|
||||
|
||||
worldData = WorldFileData.create(world, notifyPlayer);
|
||||
if (worldData == null) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// each region file covers up to 1024 chunks; with all operations we might need to do, let's figure 3X that
|
||||
this.reportTarget = worldData.regionFileCount() * 3072;
|
||||
|
||||
// queue up the first file
|
||||
if (!nextFile())
|
||||
return;
|
||||
|
||||
this.readyToGo = true;
|
||||
Bukkit.getServer().getPluginManager().callEvent(new WorldBorderTrimStartEvent(this));
|
||||
}
|
||||
|
||||
public void setTaskID(int ID) {
|
||||
this.taskID = ID;
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
if (server == null || !readyToGo || paused)
|
||||
return;
|
||||
|
||||
// this is set so it only does one iteration at a time, no matter how frequently the timer fires
|
||||
readyToGo = false;
|
||||
// and this is tracked to keep one iteration from dragging on too long and possibly choking the system if the user specified a really high frequency
|
||||
long loopStartTime = Config.Now();
|
||||
|
||||
counter = 0;
|
||||
while (counter <= chunksPerRun) {
|
||||
// in case the task has been paused while we're repeating...
|
||||
if (paused)
|
||||
return;
|
||||
|
||||
long now = Config.Now();
|
||||
|
||||
// every 5 seconds or so, give basic progress report to let user know how it's going
|
||||
if (now > lastReport + 5000)
|
||||
reportProgress();
|
||||
|
||||
// if this iteration has been running for 45ms (almost 1 tick) or more, stop to take a breather; shouldn't normally be possible with Trim, but just in case
|
||||
if (now > loopStartTime + 45) {
|
||||
readyToGo = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (regionChunks.isEmpty())
|
||||
addCornerChunks();
|
||||
else if (currentChunk == 4) { // determine if region is completely _inside_ border based on corner chunks
|
||||
if (trimChunks.isEmpty()) { // it is, so skip it and move on to next file
|
||||
counter += 4;
|
||||
nextFile();
|
||||
continue;
|
||||
}
|
||||
addEdgeChunks();
|
||||
addInnerChunks();
|
||||
} else if (currentChunk == 124 && trimChunks.size() == 124) { // region is completely _outside_ border based on edge chunks, so delete file and move on to next
|
||||
counter += 16;
|
||||
trimChunks = regionChunks;
|
||||
unloadChunks();
|
||||
reportTrimmedRegions++;
|
||||
File regionFile = worldData.regionFile(currentRegion);
|
||||
if (!regionFile.delete()) {
|
||||
sendMessage("Error! Region file which is outside the border could not be deleted: " + regionFile.getName());
|
||||
wipeChunks();
|
||||
} else {
|
||||
// if DynMap is installed, re-render the trimmed region ... disabled since it's not currently working, oh well
|
||||
// DynMapFeatures.renderRegion(world.getName(), new CoordXZ(regionX, regionZ));
|
||||
}
|
||||
|
||||
nextFile();
|
||||
continue;
|
||||
} else if (currentChunk == 1024) { // last chunk of the region has been checked, time to wipe out whichever chunks are outside the border
|
||||
counter += 32;
|
||||
unloadChunks();
|
||||
wipeChunks();
|
||||
nextFile();
|
||||
continue;
|
||||
}
|
||||
|
||||
// check whether chunk is inside the border or not, add it to the "trim" list if not
|
||||
CoordXZ chunk = regionChunks.get(currentChunk);
|
||||
if (!isChunkInsideBorder(chunk))
|
||||
trimChunks.add(chunk);
|
||||
|
||||
currentChunk++;
|
||||
counter++;
|
||||
}
|
||||
|
||||
reportTotal += counter;
|
||||
|
||||
// ready for the next iteration to run
|
||||
readyToGo = true;
|
||||
}
|
||||
|
||||
// Advance to the next region file. Returns true if successful, false if the next file isn't accessible for any reason
|
||||
private boolean nextFile() {
|
||||
reportTotal = currentRegion * 3072;
|
||||
currentRegion++;
|
||||
regionX = regionZ = currentChunk = 0;
|
||||
regionChunks = new ArrayList<CoordXZ>(1024);
|
||||
trimChunks = new ArrayList<CoordXZ>(1024);
|
||||
|
||||
// have we already handled all region files?
|
||||
if (currentRegion >= worldData.regionFileCount()) { // hey, we're done
|
||||
paused = true;
|
||||
readyToGo = false;
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
|
||||
counter += 16;
|
||||
|
||||
// get the X and Z coordinates of the current region
|
||||
CoordXZ coord = worldData.regionFileCoordinates(currentRegion);
|
||||
if (coord == null)
|
||||
return false;
|
||||
|
||||
regionX = coord.x;
|
||||
regionZ = coord.z;
|
||||
return true;
|
||||
}
|
||||
|
||||
// add just the 4 corner chunks of the region; can determine if entire region is _inside_ the border
|
||||
private void addCornerChunks() {
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ)));
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ)));
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ) + 31));
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ) + 31));
|
||||
}
|
||||
|
||||
// add all chunks along the 4 edges of the region (minus the corners); can determine if entire region is _outside_ the border
|
||||
private void addEdgeChunks() {
|
||||
int chunkX = 0, chunkZ;
|
||||
|
||||
for (chunkZ = 1; chunkZ < 31; chunkZ++) {
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + chunkX, CoordXZ.regionToChunk(regionZ) + chunkZ));
|
||||
}
|
||||
chunkX = 31;
|
||||
for (chunkZ = 1; chunkZ < 31; chunkZ++) {
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + chunkX, CoordXZ.regionToChunk(regionZ) + chunkZ));
|
||||
}
|
||||
chunkZ = 0;
|
||||
for (chunkX = 1; chunkX < 31; chunkX++) {
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + chunkX, CoordXZ.regionToChunk(regionZ) + chunkZ));
|
||||
}
|
||||
chunkZ = 31;
|
||||
for (chunkX = 1; chunkX < 31; chunkX++) {
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + chunkX, CoordXZ.regionToChunk(regionZ) + chunkZ));
|
||||
}
|
||||
counter += 4;
|
||||
}
|
||||
|
||||
// add the remaining interior chunks (after corners and edges)
|
||||
private void addInnerChunks() {
|
||||
for (int chunkX = 1; chunkX < 31; chunkX++) {
|
||||
for (int chunkZ = 1; chunkZ < 31; chunkZ++) {
|
||||
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + chunkX, CoordXZ.regionToChunk(regionZ) + chunkZ));
|
||||
}
|
||||
}
|
||||
counter += 32;
|
||||
}
|
||||
|
||||
// make sure chunks set to be trimmed are not currently loaded by the server
|
||||
private void unloadChunks() {
|
||||
for (CoordXZ unload : trimChunks) {
|
||||
if (world.isChunkLoaded(unload.x, unload.z))
|
||||
world.unloadChunk(unload.x, unload.z, false);
|
||||
}
|
||||
counter += trimChunks.size();
|
||||
}
|
||||
|
||||
// edit region file to wipe all chunk pointers for chunks outside the border
|
||||
private void wipeChunks() {
|
||||
File regionFile = worldData.regionFile(currentRegion);
|
||||
if (!regionFile.canWrite()) {
|
||||
if (!regionFile.setWritable(true))
|
||||
throw new RuntimeException();
|
||||
|
||||
if (!regionFile.canWrite()) {
|
||||
sendMessage("Error! region file is locked and can't be trimmed: " + regionFile.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// since our stored chunk positions are based on world, we need to offset those to positions in the region file
|
||||
int offsetX = CoordXZ.regionToChunk(regionX);
|
||||
int offsetZ = CoordXZ.regionToChunk(regionZ);
|
||||
long wipePos = 0;
|
||||
int chunkCount = 0;
|
||||
|
||||
try {
|
||||
RandomAccessFile unChunk = new RandomAccessFile(regionFile, "rwd");
|
||||
for (CoordXZ wipe : trimChunks) {
|
||||
// if the chunk pointer is empty (chunk doesn't technically exist), no need to wipe the already empty pointer
|
||||
if (!worldData.doesChunkExist(wipe.x, wipe.z))
|
||||
continue;
|
||||
|
||||
// wipe this extraneous chunk's pointer... note that this method isn't perfect since the actual chunk data is left orphaned,
|
||||
// but Minecraft will overwrite the orphaned data sector if/when another chunk is created in the region, so it's not so bad
|
||||
wipePos = 4 * ((wipe.x - offsetX) + ((wipe.z - offsetZ) * 32));
|
||||
unChunk.seek(wipePos);
|
||||
unChunk.writeInt(0);
|
||||
chunkCount++;
|
||||
}
|
||||
unChunk.close();
|
||||
|
||||
// if DynMap is installed, re-render the trimmed chunks ... disabled since it's not currently working, oh well
|
||||
// DynMapFeatures.renderChunks(world.getName(), trimChunks);
|
||||
|
||||
reportTrimmedChunks += chunkCount;
|
||||
} catch (FileNotFoundException ex) {
|
||||
sendMessage("Error! Could not open region file to wipe individual chunks: " + regionFile.getName());
|
||||
} catch (IOException ex) {
|
||||
sendMessage("Error! Could not modify region file to wipe individual chunks: " + regionFile.getName());
|
||||
}
|
||||
counter += trimChunks.size();
|
||||
}
|
||||
|
||||
private boolean isChunkInsideBorder(CoordXZ chunk) {
|
||||
return border.insideBorder(CoordXZ.chunkToBlock(chunk.x) + 8, CoordXZ.chunkToBlock(chunk.z) + 8);
|
||||
}
|
||||
|
||||
// for successful completion
|
||||
public void finish() {
|
||||
reportTotal = reportTarget;
|
||||
reportProgress();
|
||||
Bukkit.getServer().getPluginManager().callEvent(new WorldBorderTrimFinishedEvent(world, reportTotal));
|
||||
sendMessage("task successfully completed!");
|
||||
this.stop();
|
||||
}
|
||||
|
||||
// for cancelling prematurely
|
||||
public void cancel() {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
// we're done, whether finished or cancelled
|
||||
private void stop() {
|
||||
if (server == null)
|
||||
return;
|
||||
|
||||
readyToGo = false;
|
||||
if (taskID != -1)
|
||||
server.getScheduler().cancelTask(taskID);
|
||||
server = null;
|
||||
|
||||
sendMessage("NOTICE: it is recommended that you restart your server after a Trim, to be on the safe side.");
|
||||
if (DynMapFeatures.renderEnabled())
|
||||
sendMessage("This especially true with DynMap. You should also run a fullrender in DynMap for the trimmed world after restarting, so trimmed chunks are updated on the map.");
|
||||
}
|
||||
|
||||
// is this task still valid/workable?
|
||||
public boolean valid() {
|
||||
return this.server != null;
|
||||
}
|
||||
|
||||
// handle pausing/unpausing the task
|
||||
public void pause() {
|
||||
pause(!this.paused);
|
||||
}
|
||||
|
||||
public void pause(boolean pause) {
|
||||
this.paused = pause;
|
||||
if (pause)
|
||||
reportProgress();
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return this.paused;
|
||||
}
|
||||
|
||||
// let the user know how things are coming along
|
||||
private void reportProgress() {
|
||||
lastReport = Config.Now();
|
||||
double perc = getPercentageCompleted();
|
||||
sendMessage(reportTrimmedRegions + " entire region(s) and " + reportTrimmedChunks + " individual chunk(s) trimmed so far (" + Config.coord.format(perc) + "% done" + ")");
|
||||
}
|
||||
|
||||
// send a message to the server console/log and possibly to an in-game player
|
||||
private void sendMessage(String text) {
|
||||
Config.log("[Trim] " + text);
|
||||
if (notifyPlayer != null)
|
||||
notifyPlayer.sendMessage("[Trim] " + text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the percentage completed for the trim task.
|
||||
*
|
||||
* @return Percentage
|
||||
*/
|
||||
public double getPercentageCompleted() {
|
||||
return ((double) (reportTotal) / (double) reportTarget) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount of chunks completed for the trim task.
|
||||
*
|
||||
* @return Number of chunks processed.
|
||||
*/
|
||||
public int getChunksCompleted() {
|
||||
return reportTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of chunks that need to be trimmed for the trim task.
|
||||
*
|
||||
* @return Number of chunks that need to be processed.
|
||||
*/
|
||||
public int getChunksTotal() {
|
||||
return reportTarget;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user