├── settings.gradle.kts ├── worldborder_logo.psd ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── java │ └── com │ │ └── wimbli │ │ └── WorldBorder │ │ ├── WorldFileDataType.java │ │ ├── Events │ │ ├── WorldBorderTrimStartEvent.java │ │ ├── WorldBorderFillStartEvent.java │ │ ├── WorldBorderTrimFinishedEvent.java │ │ └── WorldBorderFillFinishedEvent.java │ │ ├── cmd │ │ ├── CmdGetmsg.java │ │ ├── CmdReload.java │ │ ├── CmdList.java │ │ ├── CmdDebug.java │ │ ├── CmdWhoosh.java │ │ ├── CmdDynmap.java │ │ ├── CmdPreventSpawn.java │ │ ├── CmdPreventPlace.java │ │ ├── CmdPortal.java │ │ ├── CmdSetmsg.java │ │ ├── CmdDynmapmsg.java │ │ ├── CmdDelay.java │ │ ├── CmdDenypearl.java │ │ ├── CmdKnockback.java │ │ ├── CmdShape.java │ │ ├── CmdHelp.java │ │ ├── CmdBypasslist.java │ │ ├── CmdWrap.java │ │ ├── CmdSetcorners.java │ │ ├── CmdClear.java │ │ ├── CmdFillautosave.java │ │ ├── CmdRemount.java │ │ ├── CmdWshape.java │ │ ├── CmdCommands.java │ │ ├── CmdRadius.java │ │ ├── CmdBypass.java │ │ ├── CmdSet.java │ │ ├── WBCmd.java │ │ ├── CmdFill.java │ │ └── CmdTrim.java │ │ ├── UUID │ │ ├── UUIDTypeAdapter.java │ │ └── UUIDFetcher.java │ │ ├── BlockPlaceListener.java │ │ ├── MobSpawnListener.java │ │ ├── CoordXZ.java │ │ ├── WorldBorder.java │ │ ├── WBListener.java │ │ ├── BorderCheckTask.java │ │ ├── WBCommand.java │ │ ├── DynMapFeatures.java │ │ ├── WorldFileData.java │ │ ├── WorldTrimTask.java │ │ ├── BorderData.java │ │ └── WorldFillTask.java │ └── resources │ └── plugin.yml ├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── gradlew.bat ├── .gitignore └── gradlew /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "WorldBorder" 2 | -------------------------------------------------------------------------------- /worldborder_logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Puremin0rez/WorldBorder/HEAD/worldborder_logo.psd -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Puremin0rez/WorldBorder/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WorldFileDataType.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | public enum WorldFileDataType 4 | { 5 | ALL, 6 | REGION, 7 | POI, 8 | ENTITIES 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/Events/WorldBorderTrimStartEvent.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.Events; 2 | 3 | import org.bukkit.event.Event; 4 | import org.bukkit.event.HandlerList; 5 | 6 | import com.wimbli.WorldBorder.WorldTrimTask; 7 | 8 | 9 | /** 10 | * Created by Maximvdw on 12.01.2016. 11 | */ 12 | public class WorldBorderTrimStartEvent extends Event 13 | { 14 | private static final HandlerList handlers = new HandlerList(); 15 | private WorldTrimTask trimTask; 16 | 17 | public WorldBorderTrimStartEvent(WorldTrimTask trimTask) 18 | { 19 | this.trimTask = trimTask; 20 | } 21 | 22 | @Override 23 | public HandlerList getHandlers() 24 | { 25 | return handlers; 26 | } 27 | 28 | public static HandlerList getHandlerList() 29 | { 30 | return handlers; 31 | } 32 | 33 | public WorldTrimTask getTrimTask(){ 34 | return this.trimTask; 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/Events/WorldBorderFillStartEvent.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.Events; 2 | 3 | import org.bukkit.event.Event; 4 | import org.bukkit.event.HandlerList; 5 | 6 | import com.wimbli.WorldBorder.WorldFillTask; 7 | 8 | 9 | /** 10 | * Created by Maximvdw on 12.01.2016. 11 | */ 12 | public class WorldBorderFillStartEvent extends Event 13 | { 14 | private static final HandlerList handlers = new HandlerList(); 15 | private WorldFillTask fillTask; 16 | 17 | public WorldBorderFillStartEvent(WorldFillTask worldFillTask) 18 | { 19 | this.fillTask = worldFillTask; 20 | } 21 | 22 | @Override 23 | public HandlerList getHandlers() 24 | { 25 | return handlers; 26 | } 27 | 28 | public static HandlerList getHandlerList() 29 | { 30 | return handlers; 31 | } 32 | 33 | public WorldFillTask getFillTask(){ 34 | return this.fillTask; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdGetmsg.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdGetmsg extends WBCmd 12 | { 13 | public CmdGetmsg() 14 | { 15 | name = permission = "getmsg"; 16 | minParams = maxParams = 0; 17 | 18 | addCmdExample(nameEmphasized() + "- display border message."); 19 | helpText = "This command simply displays the message shown to players knocked back from the border."; 20 | } 21 | 22 | @Override 23 | public void execute(CommandSender sender, Player player, List params, String worldName) 24 | { 25 | sender.sendMessage("Border message is currently set to:"); 26 | sender.sendMessage(Config.MessageRaw()); 27 | sender.sendMessage("Formatted border message:"); 28 | sender.sendMessage(Config.Message()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/Events/WorldBorderTrimFinishedEvent.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.Events; 2 | 3 | import org.bukkit.event.Event; 4 | import org.bukkit.event.HandlerList; 5 | import org.bukkit.World; 6 | 7 | /** 8 | * Created by timafh on 04.09.2015. 9 | */ 10 | public class WorldBorderTrimFinishedEvent extends Event 11 | { 12 | private static final HandlerList handlers = new HandlerList(); 13 | private World world; 14 | private long totalChunks; 15 | 16 | public WorldBorderTrimFinishedEvent(World world, long totalChunks) 17 | { 18 | this.world = world; 19 | this.totalChunks = totalChunks; 20 | } 21 | 22 | @Override 23 | public HandlerList getHandlers() 24 | { 25 | return handlers; 26 | } 27 | 28 | public static HandlerList getHandlerList() 29 | { 30 | return handlers; 31 | } 32 | 33 | public World getWorld() 34 | { 35 | return world; 36 | } 37 | 38 | public long getTotalChunks() 39 | { 40 | return totalChunks; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/Events/WorldBorderFillFinishedEvent.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.Events; 2 | 3 | import org.bukkit.event.Event; 4 | import org.bukkit.event.HandlerList; 5 | import org.bukkit.World; 6 | 7 | /** 8 | * Created by timafh on 04.09.2015. 9 | */ 10 | public class WorldBorderFillFinishedEvent extends Event 11 | { 12 | private static final HandlerList handlers = new HandlerList(); 13 | private World world; 14 | private long totalChunks; 15 | 16 | public WorldBorderFillFinishedEvent(World world, long totalChunks) 17 | { 18 | this.world = world; 19 | this.totalChunks = totalChunks; 20 | } 21 | 22 | @Override 23 | public HandlerList getHandlers() 24 | { 25 | return handlers; 26 | } 27 | 28 | public static HandlerList getHandlerList() 29 | { 30 | return handlers; 31 | } 32 | 33 | public World getWorld() 34 | { 35 | return world; 36 | } 37 | 38 | public long getTotalChunks() 39 | { 40 | return totalChunks; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/UUID/UUIDTypeAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This code from: https://github.com/eitetu/minecraft-server/blob/master/src/main/java/com/eitetu/minecraft/server/util/UUIDTypeAdapter.java 3 | */ 4 | 5 | package com.wimbli.WorldBorder.UUID; 6 | 7 | import java.io.IOException; 8 | import java.util.UUID; 9 | 10 | import com.google.gson.TypeAdapter; 11 | import com.google.gson.stream.JsonReader; 12 | import com.google.gson.stream.JsonWriter; 13 | 14 | 15 | public class UUIDTypeAdapter extends TypeAdapter { 16 | public void write(JsonWriter out, UUID value) throws IOException { 17 | out.value(fromUUID(value)); 18 | } 19 | 20 | public UUID read(JsonReader in) throws IOException { 21 | return fromString(in.nextString()); 22 | } 23 | 24 | public static String fromUUID(UUID value) { 25 | return value.toString().replace("-", ""); 26 | } 27 | 28 | public static UUID fromString(String input) { 29 | return UUID.fromString(input.replaceFirst( 30 | "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/BlockPlaceListener.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import org.bukkit.Location; 4 | import org.bukkit.World; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.EventPriority; 7 | import org.bukkit.event.HandlerList; 8 | import org.bukkit.event.Listener; 9 | import org.bukkit.event.block.BlockPlaceEvent; 10 | 11 | 12 | public class BlockPlaceListener implements Listener 13 | { 14 | @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) 15 | public void onBlockPlace(BlockPlaceEvent event) 16 | { 17 | Location loc = event.getBlockPlaced().getLocation(); 18 | if (loc == null) return; 19 | 20 | World world = loc.getWorld(); 21 | if (world == null) return; 22 | BorderData border = Config.Border(world.getName()); 23 | if (border == null) return; 24 | 25 | if (!border.insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound())) 26 | { 27 | event.setCancelled(true); 28 | } 29 | } 30 | 31 | public void unregister() 32 | { 33 | HandlerList.unregisterAll(this); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/MobSpawnListener.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import org.bukkit.Location; 4 | import org.bukkit.World; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.EventPriority; 7 | import org.bukkit.event.HandlerList; 8 | import org.bukkit.event.Listener; 9 | import org.bukkit.event.entity.CreatureSpawnEvent; 10 | 11 | 12 | public class MobSpawnListener implements Listener 13 | { 14 | @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) 15 | public void onCreatureSpawn(CreatureSpawnEvent event) 16 | { 17 | Location loc = event.getEntity().getLocation(); 18 | if (loc == null) return; 19 | 20 | World world = loc.getWorld(); 21 | if (world == null) return; 22 | BorderData border = Config.Border(world.getName()); 23 | if (border == null) return; 24 | 25 | if (!border.insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound())) 26 | { 27 | event.setCancelled(true); 28 | } 29 | } 30 | 31 | public void unregister() 32 | { 33 | HandlerList.unregisterAll(this); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdReload.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdReload extends WBCmd 12 | { 13 | public CmdReload() 14 | { 15 | name = permission = "reload"; 16 | minParams = maxParams = 0; 17 | 18 | addCmdExample(nameEmphasized() + "- re-load data from config.yml."); 19 | helpText = "If you make manual changes to config.yml while the server is running, you can use this command " + 20 | "to make WorldBorder load the changes without needing to restart the server."; 21 | } 22 | 23 | @Override 24 | public void execute(CommandSender sender, Player player, List params, String worldName) 25 | { 26 | if (player != null) 27 | Config.log("Reloading config file at the command of player \"" + player.getName() + "\"."); 28 | 29 | Config.load(WorldBorder.plugin, true); 30 | 31 | if (player != null) 32 | sender.sendMessage("WorldBorder configuration reloaded."); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdList.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | 6 | import org.bukkit.command.*; 7 | import org.bukkit.entity.Player; 8 | 9 | import com.wimbli.WorldBorder.*; 10 | 11 | 12 | public class CmdList extends WBCmd 13 | { 14 | public CmdList() 15 | { 16 | name = permission = "list"; 17 | minParams = maxParams = 0; 18 | 19 | addCmdExample(nameEmphasized() + "- show border information for all worlds."); 20 | helpText = "This command will list full information for every border you have set including position, " + 21 | "radius, and shape. The default border shape will also be indicated."; 22 | } 23 | 24 | @Override 25 | public void execute(CommandSender sender, Player player, List params, String worldName) 26 | { 27 | sender.sendMessage("Default border shape for all worlds is \"" + Config.ShapeName() + "\"."); 28 | 29 | Set list = Config.BorderDescriptions(); 30 | 31 | if (list.isEmpty()) 32 | { 33 | sender.sendMessage("There are no borders currently set."); 34 | return; 35 | } 36 | 37 | for(String borderDesc : list) 38 | { 39 | sender.sendMessage(borderDesc); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDebug.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdDebug extends WBCmd 12 | { 13 | public CmdDebug() 14 | { 15 | name = permission = "debug"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - turn console debug output on or off."); 19 | helpText = "Default value: off. Debug mode will show some extra debugging data in the server console/log when " + 20 | "players are knocked back from the border or are teleported."; 21 | } 22 | 23 | @Override 24 | public void cmdStatus(CommandSender sender) 25 | { 26 | sender.sendMessage(C_HEAD + "Debug mode is " + enabledColored(Config.Debug()) + C_HEAD + "."); 27 | } 28 | 29 | @Override 30 | public void execute(CommandSender sender, Player player, List params, String worldName) 31 | { 32 | Config.setDebug(strAsBool(params.get(0))); 33 | 34 | if (player != null) 35 | { 36 | Config.log((Config.Debug() ? "Enabled" : "Disabled") + " debug output at the command of player \"" + player.getName() + "\"."); 37 | cmdStatus(sender); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdWhoosh.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdWhoosh extends WBCmd 12 | { 13 | public CmdWhoosh() 14 | { 15 | name = permission = "whoosh"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - turn knockback effect on or off."); 19 | helpText = "Default value: on. This will show a particle effect and play a sound where a player is knocked " + 20 | "back from the border."; 21 | } 22 | 23 | @Override 24 | public void cmdStatus(CommandSender sender) 25 | { 26 | sender.sendMessage(C_HEAD + "\"Whoosh\" knockback effect is " + enabledColored(Config.whooshEffect()) + C_HEAD + "."); 27 | } 28 | 29 | @Override 30 | public void execute(CommandSender sender, Player player, List params, String worldName) 31 | { 32 | Config.setWhooshEffect(strAsBool(params.get(0))); 33 | 34 | if (player != null) 35 | { 36 | Config.log((Config.whooshEffect() ? "Enabled" : "Disabled") + " \"whoosh\" knockback effect at the command of player \"" + player.getName() + "\"."); 37 | cmdStatus(sender); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDynmap.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdDynmap extends WBCmd 12 | { 13 | public CmdDynmap() 14 | { 15 | name = permission = "dynmap"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - turn DynMap border display on or off."); 19 | helpText = "Default value: on. If you are running the DynMap plugin and this setting is enabled, all borders will " + 20 | "be visually shown in DynMap."; 21 | } 22 | 23 | @Override 24 | public void cmdStatus(CommandSender sender) 25 | { 26 | sender.sendMessage(C_HEAD + "DynMap border display is " + enabledColored(Config.DynmapBorderEnabled()) + C_HEAD + "."); 27 | } 28 | 29 | @Override 30 | public void execute(CommandSender sender, Player player, List params, String worldName) 31 | { 32 | Config.setDynmapBorderEnabled(strAsBool(params.get(0))); 33 | 34 | if (player != null) 35 | { 36 | cmdStatus(sender); 37 | Config.log((Config.DynmapBorderEnabled() ? "Enabled" : "Disabled") + " DynMap border display at the command of player \"" + player.getName() + "\"."); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdPreventSpawn.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import org.bukkit.command.CommandSender; 5 | import org.bukkit.entity.Player; 6 | 7 | import java.util.List; 8 | 9 | public class CmdPreventSpawn extends WBCmd { 10 | 11 | public CmdPreventSpawn() { 12 | name = permission = "preventmobspawn"; 13 | minParams = maxParams = 1; 14 | 15 | addCmdExample(nameEmphasized() + " - stop mob spawning past border."); 16 | helpText = "Default value: off. When enabled, this setting will prevent mobs from naturally spawning outside the world's border."; 17 | } 18 | 19 | @Override 20 | public void cmdStatus(CommandSender sender) 21 | { 22 | sender.sendMessage(C_HEAD + "Prevention of mob spawning outside the border is " + enabledColored(Config.preventMobSpawn()) + C_HEAD + "."); 23 | } 24 | 25 | @Override 26 | public void execute(CommandSender sender, Player player, List params, String worldName) 27 | { 28 | Config.setPreventMobSpawn(strAsBool(params.get(0))); 29 | 30 | if (player != null) 31 | { 32 | Config.log((Config.preventMobSpawn() ? "Enabled" : "Disabled") + " preventmobspawn at the command of player \"" + player.getName() + "\"."); 33 | cmdStatus(sender); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build WorldBorder 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | # Only run on PRs if the source branch is on someone else's repo 6 | if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout Repository 11 | uses: actions/checkout@v2 12 | 13 | - name: Setup Java 14 | uses: actions/setup-java@v2 15 | with: 16 | distribution: 'temurin' 17 | java-version: '8' 18 | 19 | - name: Validate Gradle Wrapper 20 | uses: gradle/wrapper-validation-action@v1 21 | 22 | - name: Restore Gradle Cache 23 | uses: actions/cache@v2 24 | with: 25 | path: | 26 | ~/.gradle/caches 27 | ~/.gradle/wrapper 28 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 29 | restore-keys: | 30 | ${{ runner.os }}-gradle- 31 | 32 | - name: Build with Gradle 33 | run: ./gradlew clean build 34 | 35 | - name: Upload Artifact(s) 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: WorldBorder Archive 39 | path: build/libs/WorldBorder.jar 40 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdPreventPlace.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.CommandSender; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.Config; 9 | 10 | public class CmdPreventPlace extends WBCmd { 11 | 12 | public CmdPreventPlace() { 13 | name = permission = "preventblockplace"; 14 | minParams = maxParams = 1; 15 | 16 | addCmdExample(nameEmphasized() + " - stop block placement past border."); 17 | helpText = "Default value: off. When enabled, this setting will prevent players from placing blocks outside the world's border."; 18 | } 19 | 20 | @Override 21 | public void cmdStatus(CommandSender sender) 22 | { 23 | sender.sendMessage(C_HEAD + "Prevention of block placement outside the border is " + enabledColored(Config.preventBlockPlace()) + C_HEAD + "."); 24 | } 25 | 26 | @Override 27 | public void execute(CommandSender sender, Player player, List params, String worldName) 28 | { 29 | Config.setPreventBlockPlace(strAsBool(params.get(0))); 30 | 31 | if (player != null) 32 | { 33 | Config.log((Config.preventBlockPlace() ? "Enabled" : "Disabled") + " preventblockplace at the command of player \"" + player.getName() + "\"."); 34 | cmdStatus(sender); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdPortal.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdPortal extends WBCmd 12 | { 13 | public CmdPortal() 14 | { 15 | name = permission = "portal"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - turn portal redirection on or off."); 19 | helpText = "Default value: on. This feature monitors new portal creation and changes the target new portal " + 20 | "location if it is outside of the border. Try disabling this if you have problems with other plugins " + 21 | "related to portals."; 22 | } 23 | 24 | @Override 25 | public void cmdStatus(CommandSender sender) 26 | { 27 | sender.sendMessage(C_HEAD + "Portal redirection is " + enabledColored(Config.portalRedirection()) + C_HEAD + "."); 28 | } 29 | 30 | @Override 31 | public void execute(CommandSender sender, Player player, List params, String worldName) 32 | { 33 | Config.setPortalRedirection(strAsBool(params.get(0))); 34 | 35 | if (player != null) 36 | { 37 | Config.log((Config.portalRedirection() ? "Enabled" : "Disabled") + " portal redirection at the command of player \"" + player.getName() + "\"."); 38 | cmdStatus(sender); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdSetmsg.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdSetmsg extends WBCmd 12 | { 13 | public CmdSetmsg() 14 | { 15 | name = permission = "setmsg"; 16 | minParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - set border message."); 19 | helpText = "Default value: \"&cYou have reached the edge of this world.\". This command lets you set the message shown to players who are knocked back from the border."; 20 | } 21 | 22 | @Override 23 | public void cmdStatus(CommandSender sender) 24 | { 25 | sender.sendMessage(C_HEAD + "Border message is set to:"); 26 | sender.sendMessage(Config.MessageRaw()); 27 | sender.sendMessage(C_HEAD + "Formatted border message:"); 28 | sender.sendMessage(Config.Message()); 29 | } 30 | 31 | @Override 32 | public void execute(CommandSender sender, Player player, List params, String worldName) 33 | { 34 | StringBuilder message = new StringBuilder(); 35 | boolean first = true; 36 | for (String param : params) 37 | { 38 | if (!first) 39 | message.append(" "); 40 | message.append(param); 41 | first = false; 42 | } 43 | 44 | Config.setMessage(message.toString()); 45 | 46 | cmdStatus(sender); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Brett Flannigan 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDynmapmsg.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdDynmapmsg extends WBCmd 12 | { 13 | public CmdDynmapmsg() 14 | { 15 | name = permission = "dynmapmsg"; 16 | minParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - DynMap border labels will show this."); 19 | helpText = "Default value: \"The border of the world.\". If you are running the DynMap plugin and the " + 20 | commandEmphasized("dynmap") + C_DESC + "command setting is enabled, the borders shown in DynMap will " + 21 | "be labelled with this text."; 22 | } 23 | 24 | @Override 25 | public void cmdStatus(CommandSender sender) 26 | { 27 | sender.sendMessage(C_HEAD + "DynMap border label is set to: " + C_ERR + Config.DynmapMessage()); 28 | } 29 | 30 | @Override 31 | public void execute(CommandSender sender, Player player, List params, String worldName) 32 | { 33 | StringBuilder message = new StringBuilder(); 34 | boolean first = true; 35 | for (String param : params) 36 | { 37 | if (!first) 38 | message.append(" "); 39 | message.append(param); 40 | first = false; 41 | } 42 | 43 | Config.setDynmapMessage(message.toString()); 44 | 45 | if (player != null) 46 | cmdStatus(sender); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDelay.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdDelay extends WBCmd 12 | { 13 | public CmdDelay() 14 | { 15 | name = permission = "delay"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - time between border checks."); 19 | helpText = "Default value: 5. The is in server ticks, of which there are roughly 20 every second, each " + 20 | "tick taking ~50ms. The default value therefore has border checks run about 4 times per second."; 21 | } 22 | 23 | @Override 24 | public void cmdStatus(CommandSender sender) 25 | { 26 | int delay = Config.TimerTicks(); 27 | sender.sendMessage(C_HEAD + "Timer delay is set to " + delay + " tick(s). That is roughly " + (delay * 50) + "ms."); 28 | } 29 | 30 | @Override 31 | public void execute(CommandSender sender, Player player, List params, String worldName) 32 | { 33 | int delay = 0; 34 | try 35 | { 36 | delay = Integer.parseInt(params.get(0)); 37 | if (delay < 1) 38 | throw new NumberFormatException(); 39 | } 40 | catch(NumberFormatException ex) 41 | { 42 | sendErrorAndHelp(sender, "The timer delay must be an integer of 1 or higher."); 43 | return; 44 | } 45 | 46 | Config.setTimerTicks(delay); 47 | 48 | if (player != null) 49 | cmdStatus(sender); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDenypearl.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdDenypearl extends WBCmd 12 | { 13 | public CmdDenypearl() 14 | { 15 | name = permission = "denypearl"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - stop ender pearls past the border."); 19 | helpText = "Default value: on. When enabled, this setting will directly cancel attempts to use an ender pearl to " + 20 | "get past the border rather than just knocking the player back. This should prevent usage of ender " + 21 | "pearls to glitch into areas otherwise inaccessible at the border edge."; 22 | } 23 | 24 | @Override 25 | public void cmdStatus(CommandSender sender) 26 | { 27 | sender.sendMessage(C_HEAD + "Direct cancellation of ender pearls thrown past the border is " + 28 | enabledColored(Config.getDenyEnderpearl()) + C_HEAD + "."); 29 | } 30 | 31 | @Override 32 | public void execute(CommandSender sender, Player player, List params, String worldName) 33 | { 34 | Config.setDenyEnderpearl(strAsBool(params.get(0))); 35 | 36 | if (player != null) 37 | { 38 | Config.log((Config.getDenyEnderpearl() ? "Enabled" : "Disabled") + " direct cancellation of ender pearls thrown past the border at the command of player \"" + player.getName() + "\"."); 39 | cmdStatus(sender); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/CoordXZ.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | 4 | // simple storage class for chunk x/z values 5 | public class CoordXZ 6 | { 7 | public int x, z; 8 | public CoordXZ(int x, int z) 9 | { 10 | this.x = x; 11 | this.z = z; 12 | } 13 | 14 | // transform values between block, chunk, and region 15 | // bit-shifting is used because it's mucho rapido 16 | public static int blockToChunk(int blockVal) 17 | { // 1 chunk is 16x16 blocks 18 | return blockVal >> 4; // ">>4" == "/16" 19 | } 20 | public static int blockToRegion(int blockVal) 21 | { // 1 region is 512x512 blocks 22 | return blockVal >> 9; // ">>9" == "/512" 23 | } 24 | public static int chunkToRegion(int chunkVal) 25 | { // 1 region is 32x32 chunks 26 | return chunkVal >> 5; // ">>5" == "/32" 27 | } 28 | public static int chunkToBlock(int chunkVal) 29 | { 30 | return chunkVal << 4; // "<<4" == "*16" 31 | } 32 | public static int regionToBlock(int regionVal) 33 | { 34 | return regionVal << 9; // "<<9" == "*512" 35 | } 36 | public static int regionToChunk(int regionVal) 37 | { 38 | return regionVal << 5; // "<<5" == "*32" 39 | } 40 | 41 | 42 | @Override 43 | public boolean equals(Object obj) 44 | { 45 | if (this == obj) 46 | return true; 47 | else if (obj == null || obj.getClass() != this.getClass()) 48 | return false; 49 | 50 | CoordXZ test = (CoordXZ)obj; 51 | return test.x == this.x && test.z == this.z; 52 | } 53 | 54 | @Override 55 | public int hashCode() 56 | { 57 | return (this.x << 9) + this.z; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdKnockback.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdKnockback extends WBCmd 12 | { 13 | public CmdKnockback() 14 | { 15 | name = permission = "knockback"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - how far to move the player back."); 19 | helpText = "Default value: 3.0 (blocks). Players who cross the border will be knocked back to this distance inside."; 20 | } 21 | 22 | @Override 23 | public void cmdStatus(CommandSender sender) 24 | { 25 | double kb = Config.KnockBack(); 26 | if (kb < 1) 27 | sender.sendMessage(C_HEAD + "Knockback is set to 0, disabling border enforcement."); 28 | else 29 | sender.sendMessage(C_HEAD + "Knockback is set to " + kb + " blocks inside the border."); 30 | } 31 | 32 | @Override 33 | public void execute(CommandSender sender, Player player, List params, String worldName) 34 | { 35 | double numBlocks = 0.0; 36 | try 37 | { 38 | numBlocks = Double.parseDouble(params.get(0)); 39 | if (numBlocks < 0.0 || (numBlocks > 0.0 && numBlocks < 1.0)) 40 | throw new NumberFormatException(); 41 | } 42 | catch(NumberFormatException ex) 43 | { 44 | sendErrorAndHelp(sender, "The knockback must be a decimal value of at least 1.0, or it can be 0."); 45 | return; 46 | } 47 | 48 | Config.setKnockBack(numBlocks); 49 | 50 | if (player != null) 51 | cmdStatus(sender); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdShape.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdShape extends WBCmd 12 | { 13 | public CmdShape() 14 | { 15 | name = permission = "shape"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - set the default border shape."); 19 | addCmdExample(nameEmphasized() + " - same as above."); 20 | helpText = "Default value: round/elliptic. The default border shape will be used on all worlds which don't " + 21 | "have an individual shape set using the " + commandEmphasized("wshape") + C_DESC + "command. Elliptic " + 22 | "and round work the same, as rectangular and square do. The difference is down to whether the X and Z " + 23 | "radius are the same."; 24 | } 25 | 26 | @Override 27 | public void cmdStatus(CommandSender sender) 28 | { 29 | sender.sendMessage(C_HEAD + "The default border shape for all worlds is currently set to \"" + Config.ShapeName() + "\"."); 30 | } 31 | 32 | @Override 33 | public void execute(CommandSender sender, Player player, List params, String worldName) 34 | { 35 | String shape = params.get(0).toLowerCase(); 36 | if (shape.equals("rectangular") || shape.equals("square")) 37 | Config.setShape(false); 38 | else if (shape.equals("elliptic") || shape.equals("round")) 39 | Config.setShape(true); 40 | else 41 | { 42 | sendErrorAndHelp(sender, "You must specify one of the 4 valid shape names below."); 43 | return; 44 | } 45 | 46 | if (player != null) 47 | cmdStatus(sender); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdHelp.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | 6 | import org.bukkit.command.*; 7 | import org.bukkit.entity.Player; 8 | 9 | import com.wimbli.WorldBorder.*; 10 | 11 | 12 | public class CmdHelp extends WBCmd 13 | { 14 | public CmdHelp() 15 | { 16 | name = permission = "help"; 17 | minParams = 0; 18 | maxParams = 10; 19 | 20 | addCmdExample(nameEmphasized() + "[command] - get help on command usage."); 21 | // helpText = "If [command] is specified, info for that particular command will be provided."; 22 | } 23 | 24 | @Override 25 | public void cmdStatus(CommandSender sender) 26 | { 27 | String commands = WorldBorder.wbCommand.getCommandNames().toString().replace(", ", C_DESC + ", " + C_CMD); 28 | sender.sendMessage(C_HEAD + "Commands: " + C_CMD + commands.substring(1, commands.length() - 1)); 29 | sender.sendMessage("Example, for info on \"set\" command: " + cmd(sender) + nameEmphasized() + C_CMD + "set"); 30 | sender.sendMessage(C_HEAD + "For a full command example list, simply run the root " + cmd(sender) + C_HEAD + "command by itself with nothing specified."); 31 | } 32 | 33 | @Override 34 | public void execute(CommandSender sender, Player player, List params, String worldName) 35 | { 36 | if (params.isEmpty()) 37 | { 38 | sendCmdHelp(sender); 39 | return; 40 | } 41 | 42 | Set commands = WorldBorder.wbCommand.getCommandNames(); 43 | for (String param : params) 44 | { 45 | if (commands.contains(param.toLowerCase())) 46 | { 47 | WorldBorder.wbCommand.subCommands.get(param.toLowerCase()).sendCmdHelp(sender); 48 | return; 49 | } 50 | } 51 | sendErrorAndHelp(sender, "No command recognized."); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.UUID; 7 | 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.command.*; 10 | import org.bukkit.entity.Player; 11 | 12 | import com.wimbli.WorldBorder.*; 13 | import com.wimbli.WorldBorder.UUID.UUIDFetcher; 14 | 15 | 16 | public class CmdBypasslist extends WBCmd 17 | { 18 | public CmdBypasslist() 19 | { 20 | name = permission = "bypasslist"; 21 | minParams = maxParams = 0; 22 | 23 | addCmdExample(nameEmphasized() + "- list players with border bypass enabled."); 24 | helpText = "The bypass list will persist between server restarts, and applies to all worlds. Use the " + 25 | commandEmphasized("bypass") + C_DESC + "command to add or remove players."; 26 | } 27 | 28 | @Override 29 | public void execute(final CommandSender sender, Player player, List params, String worldName) 30 | { 31 | final ArrayList uuids = Config.getPlayerBypassList(); 32 | if (uuids == null || uuids.isEmpty()) 33 | { 34 | sender.sendMessage("Players with border bypass enabled: "); 35 | return; 36 | } 37 | 38 | Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(WorldBorder.plugin, new Runnable() 39 | { 40 | @Override 41 | public void run() 42 | { 43 | try 44 | { 45 | Map names = UUIDFetcher.getNameList(uuids); 46 | String nameString = names.values().toString(); 47 | 48 | sender.sendMessage("Players with border bypass enabled: " + nameString.substring(1, nameString.length() - 1)); 49 | } 50 | catch(Exception ex) 51 | { 52 | sendErrorAndHelp(sender, "Failed to look up names for the UUIDs in the border bypass list. " + ex.getLocalizedMessage()); 53 | return; 54 | } 55 | } 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdWrap.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdWrap extends WBCmd 12 | { 13 | public CmdWrap() 14 | { 15 | name = permission = "wrap"; 16 | minParams = 1; 17 | maxParams = 2; 18 | 19 | addCmdExample(nameEmphasized() + "{world} - can make border crossings wrap."); 20 | helpText = "When border wrapping is enabled for a world, players will be sent around to the opposite edge " + 21 | "of the border when they cross it instead of being knocked back. [world] is optional for players and " + 22 | "defaults to the world the player is in."; 23 | } 24 | 25 | @Override 26 | public void execute(CommandSender sender, Player player, List params, String worldName) 27 | { 28 | if (player == null && params.size() == 1) 29 | { 30 | sendErrorAndHelp(sender, "When running this command from console, you must specify a world."); 31 | return; 32 | } 33 | 34 | boolean wrap = false; 35 | 36 | // world and wrap on/off specified 37 | if (params.size() == 2) 38 | { 39 | worldName = params.get(0); 40 | wrap = strAsBool(params.get(1)); 41 | } 42 | // no world specified, just wrap on/off 43 | else 44 | { 45 | worldName = player.getWorld().getName(); 46 | wrap = strAsBool(params.get(0)); 47 | } 48 | 49 | BorderData border = Config.Border(worldName); 50 | if (border == null) 51 | { 52 | sendErrorAndHelp(sender, "This world (\"" + worldName + "\") does not have a border set."); 53 | return; 54 | } 55 | 56 | border.setWrapping(wrap); 57 | Config.setBorder(worldName, border, false); 58 | 59 | sender.sendMessage("Border for world \"" + worldName + "\" is now set to " + (wrap ? "" : "not ") + "wrap around."); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdSetcorners.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.World; 8 | 9 | import com.wimbli.WorldBorder.*; 10 | 11 | 12 | public class CmdSetcorners extends WBCmd 13 | { 14 | public CmdSetcorners() 15 | { 16 | name = "setcorners"; 17 | permission = "set"; 18 | hasWorldNameInput = true; 19 | minParams = maxParams = 4; 20 | 21 | addCmdExample(nameEmphasizedW() + " - corner coords."); 22 | helpText = "This is an alternate way to set a border, by specifying the X and Z coordinates of two opposite " + 23 | "corners of the border area ((x1, z1) to (x2, z2)). [world] is optional for players and defaults to the " + 24 | "world the player is in."; 25 | } 26 | 27 | @Override 28 | public void execute(CommandSender sender, Player player, List params, String worldName) 29 | { 30 | if (worldName == null) 31 | { 32 | worldName = player.getWorld().getName(); 33 | } 34 | else 35 | { 36 | World worldTest = sender.getServer().getWorld(worldName); 37 | if (worldTest == null) 38 | sender.sendMessage("The world you specified (\"" + worldName + "\") could not be found on the server, but data for it will be stored anyway."); 39 | } 40 | 41 | try 42 | { 43 | double x1 = Double.parseDouble(params.get(0)); 44 | double z1 = Double.parseDouble(params.get(1)); 45 | double x2 = Double.parseDouble(params.get(2)); 46 | double z2 = Double.parseDouble(params.get(3)); 47 | Config.setBorderCorners(worldName, x1, z1, x2, z2); 48 | } 49 | catch(NumberFormatException ex) 50 | { 51 | sendErrorAndHelp(sender, "The x1, z1, x2, and z2 coordinate values must be numerical."); 52 | return; 53 | } 54 | 55 | if(player != null) 56 | sender.sendMessage("Border has been set. " + Config.BorderDescription(worldName)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdClear.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdClear extends WBCmd 12 | { 13 | public CmdClear() 14 | { 15 | name = permission = "clear"; 16 | hasWorldNameInput = true; 17 | consoleRequiresWorldName = false; 18 | minParams = 0; 19 | maxParams = 1; 20 | 21 | addCmdExample(nameEmphasizedW() + "- remove border for this world."); 22 | addCmdExample(nameEmphasized() + "^all - remove border for all worlds."); 23 | helpText = "If run by an in-game player and [world] or \"all\" isn't specified, the world you are currently " + 24 | "in is used."; 25 | } 26 | 27 | @Override 28 | public void execute(CommandSender sender, Player player, List params, String worldName) 29 | { 30 | // handle "clear all" command separately 31 | if (params.size() == 1 && params.get(0).equalsIgnoreCase("all")) 32 | { 33 | if (worldName != null) 34 | { 35 | sendErrorAndHelp(sender, "You should not specify a world with \"clear all\"."); 36 | return; 37 | } 38 | 39 | Config.removeAllBorders(); 40 | 41 | if (player != null) 42 | sender.sendMessage("All borders have been cleared for all worlds."); 43 | return; 44 | } 45 | 46 | if (worldName == null) 47 | { 48 | if (player == null) 49 | { 50 | sendErrorAndHelp(sender, "You must specify a world name from console if not using \"clear all\"."); 51 | return; 52 | } 53 | worldName = player.getWorld().getName(); 54 | } 55 | 56 | BorderData border = Config.Border(worldName); 57 | if (border == null) 58 | { 59 | sendErrorAndHelp(sender, "This world (\"" + worldName + "\") does not have a border set."); 60 | return; 61 | } 62 | 63 | Config.removeBorder(worldName); 64 | 65 | if (player != null) 66 | sender.sendMessage("Border cleared for world \"" + worldName + "\"."); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdFillautosave.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdFillautosave extends WBCmd 12 | { 13 | public CmdFillautosave() 14 | { 15 | name = permission = "fillautosave"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - world save interval for Fill."); 19 | helpText = "Default value: 30 seconds."; 20 | } 21 | 22 | @Override 23 | public void cmdStatus(CommandSender sender) 24 | { 25 | int seconds = Config.FillAutosaveFrequency(); 26 | if (seconds == 0) 27 | { 28 | sender.sendMessage(C_HEAD + "World autosave frequency during Fill process is set to 0, disabling it."); 29 | sender.sendMessage(C_HEAD + "Note that much progress can be lost this way if there is a bug or crash in " + 30 | "the world generation process from Bukkit or any world generation plugin you use."); 31 | } 32 | else 33 | { 34 | sender.sendMessage(C_HEAD + "World autosave frequency during Fill process is set to " + seconds + " seconds (rounded to a multiple of 5)."); 35 | sender.sendMessage(C_HEAD + "New chunks generated by the Fill process will be forcibly saved to disk " + 36 | "this often to prevent loss of progress due to bugs or crashes in the world generation process."); 37 | } 38 | } 39 | 40 | @Override 41 | public void execute(CommandSender sender, Player player, List params, String worldName) 42 | { 43 | int seconds = 0; 44 | try 45 | { 46 | seconds = Integer.parseInt(params.get(0)); 47 | if (seconds < 0) 48 | throw new NumberFormatException(); 49 | } 50 | catch(NumberFormatException ex) 51 | { 52 | sendErrorAndHelp(sender, "The world autosave frequency must be an integer of 0 or higher. Setting to 0 will disable autosaving of the world during the Fill process."); 53 | return; 54 | } 55 | 56 | Config.setFillAutosaveFrequency(seconds); 57 | 58 | if (player != null) 59 | cmdStatus(sender); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdRemount.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdRemount extends WBCmd 12 | { 13 | public CmdRemount() 14 | { 15 | name = permission = "remount"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - player remount delay after knockback."); 19 | helpText = "Default value: 0 (disabled). If set higher than 0, WorldBorder will attempt to re-mount players who " + 20 | "are knocked back from the border while riding something after this many server ticks. This setting can " + 21 | "cause really nasty glitches if enabled and set too low due to CraftBukkit teleportation problems."; 22 | } 23 | 24 | @Override 25 | public void cmdStatus(CommandSender sender) 26 | { 27 | int delay = Config.RemountTicks(); 28 | if (delay == 0) 29 | sender.sendMessage(C_HEAD + "Remount delay set to 0. Players will be left dismounted when knocked back from the border while on a vehicle."); 30 | else 31 | { 32 | sender.sendMessage(C_HEAD + "Remount delay set to " + delay + " tick(s). That is roughly " + (delay * 50) + "ms / " + (((double)delay * 50.0) / 1000.0) + " seconds. Setting to 0 would disable remounting."); 33 | if (delay < 10) 34 | sender.sendMessage(C_ERR + "WARNING:" + C_DESC + " setting this to less than 10 (and greater than 0) is not recommended. This can lead to nasty client glitches."); 35 | } 36 | } 37 | 38 | @Override 39 | public void execute(CommandSender sender, Player player, List params, String worldName) 40 | { 41 | int delay = 0; 42 | try 43 | { 44 | delay = Integer.parseInt(params.get(0)); 45 | if (delay < 0) 46 | throw new NumberFormatException(); 47 | } 48 | catch(NumberFormatException ex) 49 | { 50 | sendErrorAndHelp(sender, "The remount delay must be an integer of 0 or higher. Setting to 0 will disable remounting."); 51 | return; 52 | } 53 | 54 | Config.setRemountTicks(delay); 55 | 56 | if (player != null) 57 | cmdStatus(sender); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdWshape.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdWshape extends WBCmd 12 | { 13 | public CmdWshape() 14 | { 15 | name = permission = "wshape"; 16 | minParams = 1; 17 | maxParams = 2; 18 | 19 | addCmdExample(nameEmphasized() + "{world} - shape"); 20 | addCmdExample(C_DESC + " override for a single world.", true, true, false); 21 | addCmdExample(nameEmphasized() + "{world} - same as above."); 22 | helpText = "This will override the default border shape for a single world. The value \"default\" implies " + 23 | "a world is just using the default border shape. See the " + commandEmphasized("shape") + C_DESC + 24 | "command for more info and to set the default border shape."; 25 | } 26 | 27 | @Override 28 | public void execute(CommandSender sender, Player player, List params, String worldName) 29 | { 30 | if (player == null && params.size() == 1) 31 | { 32 | sendErrorAndHelp(sender, "When running this command from console, you must specify a world."); 33 | return; 34 | } 35 | 36 | String shapeName = ""; 37 | 38 | // world and shape specified 39 | if (params.size() == 2) 40 | { 41 | worldName = params.get(0); 42 | shapeName = params.get(1).toLowerCase(); 43 | } 44 | // no world specified, just shape 45 | else 46 | { 47 | worldName = player.getWorld().getName(); 48 | shapeName = params.get(0).toLowerCase(); 49 | } 50 | 51 | BorderData border = Config.Border(worldName); 52 | if (border == null) 53 | { 54 | sendErrorAndHelp(sender, "This world (\"" + worldName + "\") does not have a border set."); 55 | return; 56 | } 57 | 58 | Boolean shape = null; 59 | if (shapeName.equals("rectangular") || shapeName.equals("square")) 60 | shape = false; 61 | else if (shapeName.equals("elliptic") || shapeName.equals("round")) 62 | shape = true; 63 | 64 | border.setShape(shape); 65 | Config.setBorder(worldName, border, false); 66 | 67 | sender.sendMessage("Border shape for world \"" + worldName + "\" is now set to \"" + Config.ShapeName(shape) + "\"."); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WorldBorder (Pure's Fork) 2 | 3 | **Compatible with Minecraft 1.13+ (Tested on Minecraft 1.19)** 4 | 5 | This is a continuation / maintained version of the original plugin created by BrettFlan. 6 | 7 | The goal of this project is to maintain the original projects fully working operation and add new features to improve upon the ideas 8 | and philosophies of the original project. 9 | 10 | ## What's different? 11 | 12 | This is a list of everything that has been altered from the original from a users perspective: 13 | * The world generation fill speed has been significantly increased (at the cost of more memory usage) 14 | * Improvements have been made to better preserving fill progress between restarts / crashes 15 | * Fixes involving height issues and teleports for border checking tasks 16 | * Auto resume for the world generation fill task will now work properly with worlds loaded by [Multiverse-Core](https://www.spigotmc.org/resources/multiverse-core.390/) & [Hyperverse](https://www.spigotmc.org/resources/hyperverse-w-i-p.77550/) 17 | * An incompatibility between Java 8 and Java 9+ was resolved 18 | * Ability to bypass the worldborder via permission `worldborder.allowbypass` as an alternative to the bypass list 19 | * The world trimming feature now supports entity (1.17+) and POI (1.14+) removal 20 | 21 | This project is a direct drop in replacement for the original. You can upgrade without any loss or worries. 22 | 23 | ## How do I obtain it? 24 | 25 | You can download stable releases via Github Releases, [located here.](https://github.com/Puremin0rez/WorldBorder/releases) 26 | 27 | You can download development builds via Github Actions, [located here.](https://github.com/Puremin0rez/WorldBorder/actions?query=branch%3Amaster+is%3Asuccess) (Github Account Required) 28 | 29 | You can compile it by running the following command in the project directory: 30 | 31 | ``` 32 | ./gradlew clean build 33 | ``` 34 | 35 | (The jar file will be located in `/build/libs/`) 36 | 37 | ## Can I use your code? 38 | 39 | The original project, and therefore this project, is licensed as [BSD 2-Clause "Simplified" License](https://github.com/Puremin0rez/WorldBorder/blob/master/LICENSE) 40 | 41 | ## Acknowledgements 42 | 43 | * [BrettFlan](https://github.com/Brettflan) for creating the true and tested [WorldBorder](https://github.com/Brettflan/WorldBorder) project that server admins have relied on for years. 44 | * [Contributors](https://github.com/Puremin0rez/WorldBorder/graphs/contributors) for helping improve the project. 45 | * You, for reading this and checking out the project. 46 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdCommands.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdCommands extends WBCmd 12 | { 13 | private static int pageSize = 8; // examples to list per page; 10 lines available, 1 for header, 1 for footer 14 | 15 | public CmdCommands() 16 | { 17 | name = "commands"; 18 | permission = "help"; 19 | hasWorldNameInput = false; 20 | } 21 | 22 | @Override 23 | public void execute(CommandSender sender, Player player, List params, String worldName) 24 | { 25 | // determine which page we're viewing 26 | int page = (player == null) ? 0 : 1; 27 | if (!params.isEmpty()) 28 | { 29 | try 30 | { 31 | page = Integer.parseInt(params.get(0)); 32 | } 33 | catch(NumberFormatException ignored) {} 34 | } 35 | 36 | // see whether we're showing examples to player or to console, and determine number of pages available 37 | List examples = (player == null) ? cmdExamplesConsole : cmdExamplesPlayer; 38 | int pageCount = (int) Math.ceil(examples.size() / (double) pageSize); 39 | 40 | // if specified page number is negative or higher than we have available, default back to first page 41 | if (page < 0 || page > pageCount) 42 | page = (player == null) ? 0 : 1; 43 | 44 | // send command example header 45 | sender.sendMessage( C_HEAD + WorldBorder.plugin.getDescription().getFullName() + " - key: " + 46 | commandEmphasized("command") + C_REQ + " " + C_OPT + "[optional]" ); 47 | 48 | if (page > 0) 49 | { 50 | // send examples for this page 51 | int first = ((page - 1) * pageSize); 52 | int count = Math.min(pageSize, examples.size() - first); 53 | for(int i = first; i < first + count; i++) 54 | { 55 | sender.sendMessage(examples.get(i)); 56 | } 57 | 58 | // send page footer, if relevant; manual spacing to get right side lined up near edge is crude, but sufficient 59 | String footer = C_HEAD + " (Page " + page + "/" + pageCount + ") " + cmd(sender); 60 | if (page < pageCount) 61 | sender.sendMessage(footer + Integer.toString(page + 1) + C_DESC + " - view next page of commands."); 62 | else if (page > 1) 63 | sender.sendMessage(footer + C_DESC + "- view first page of commands."); 64 | } 65 | else 66 | { 67 | // if page "0" is specified, send all examples; done by default for console but can be specified by player 68 | for (String example : examples) 69 | { 70 | sender.sendMessage(example); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WorldBorder.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import org.bukkit.Location; 4 | import org.bukkit.plugin.java.JavaPlugin; 5 | 6 | 7 | public class WorldBorder extends JavaPlugin 8 | { 9 | public static volatile WorldBorder plugin = null; 10 | public static volatile WBCommand wbCommand = null; 11 | private BlockPlaceListener blockPlaceListener = null; 12 | private MobSpawnListener mobSpawnListener = null; 13 | 14 | @Override 15 | public void onEnable() 16 | { 17 | if (plugin == null) 18 | plugin = this; 19 | if (wbCommand == null) 20 | wbCommand = new WBCommand(); 21 | 22 | // Load (or create new) config file 23 | Config.load(this, false); 24 | 25 | // our one real command, though it does also have aliases "wb" and "worldborder" 26 | getCommand("wborder").setExecutor(wbCommand); 27 | 28 | // keep an eye on teleports, to redirect them to a spot inside the border if necessary 29 | getServer().getPluginManager().registerEvents(new WBListener(), this); 30 | 31 | if (Config.preventBlockPlace()) 32 | enableBlockPlaceListener(true); 33 | 34 | if (Config.preventMobSpawn()) 35 | enableMobSpawnListener(true); 36 | 37 | // integrate with DynMap if it's available 38 | DynMapFeatures.setup(); 39 | 40 | // Well I for one find this info useful, so... 41 | Location spawn = getServer().getWorlds().get(0).getSpawnLocation(); 42 | 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())); 43 | } 44 | 45 | @Override 46 | public void onDisable() 47 | { 48 | DynMapFeatures.removeAllBorders(); 49 | Config.StopBorderTimer(); 50 | Config.StoreFillTask(); 51 | Config.StopFillTask(true); 52 | } 53 | 54 | // for other plugins to hook into 55 | public BorderData getWorldBorder(String worldName) 56 | { 57 | return Config.Border(worldName); 58 | } 59 | 60 | /** 61 | * @deprecated Replaced by {@link #getWorldBorder(String worldName)}; 62 | * this method name starts with an uppercase letter, which it shouldn't 63 | */ 64 | public BorderData GetWorldBorder(String worldName) 65 | { 66 | return getWorldBorder(worldName); 67 | } 68 | 69 | public void enableBlockPlaceListener(boolean enable) 70 | { 71 | if (enable) 72 | getServer().getPluginManager().registerEvents(this.blockPlaceListener = new BlockPlaceListener(), this); 73 | else if (blockPlaceListener != null) 74 | blockPlaceListener.unregister(); 75 | } 76 | 77 | public void enableMobSpawnListener(boolean enable) 78 | { 79 | if (enable) 80 | getServer().getPluginManager().registerEvents(this.mobSpawnListener = new MobSpawnListener(), this); 81 | else if (mobSpawnListener != null) 82 | mobSpawnListener.unregister(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdRadius.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.command.*; 6 | import org.bukkit.entity.Player; 7 | 8 | import com.wimbli.WorldBorder.*; 9 | 10 | 11 | public class CmdRadius extends WBCmd 12 | { 13 | public CmdRadius() 14 | { 15 | name = permission = "radius"; 16 | hasWorldNameInput = true; 17 | minParams = 1; 18 | maxParams = 2; 19 | 20 | addCmdExample(nameEmphasizedW() + " [radiusZ] - change radius."); 21 | helpText = "Using this command you can adjust the radius of an existing border. If [radiusZ] is not " + 22 | "specified, the radiusX value will be used for both. You can also optionally specify + or - at the start " + 23 | "of and [radiusZ] to increase or decrease the existing radius rather than setting a new value."; 24 | } 25 | 26 | @Override 27 | public void execute(CommandSender sender, Player player, List params, String worldName) 28 | { 29 | if (worldName == null) 30 | worldName = player.getWorld().getName(); 31 | 32 | BorderData border = Config.Border(worldName); 33 | if (border == null) 34 | { 35 | sendErrorAndHelp(sender, "This world (\"" + worldName + "\") must first have a border set normally."); 36 | return; 37 | } 38 | 39 | double x = border.getX(); 40 | double z = border.getZ(); 41 | int radiusX; 42 | int radiusZ; 43 | try 44 | { 45 | if (params.get(0).startsWith("+")) 46 | { 47 | // Add to the current radius 48 | radiusX = border.getRadiusX(); 49 | radiusX += Integer.parseInt(params.get(0).substring(1)); 50 | } 51 | else if(params.get(0).startsWith("-")) 52 | { 53 | // Subtract from the current radius 54 | radiusX = border.getRadiusX(); 55 | radiusX -= Integer.parseInt(params.get(0).substring(1)); 56 | } 57 | else 58 | radiusX = Integer.parseInt(params.get(0)); 59 | 60 | if (params.size() == 2) 61 | { 62 | if (params.get(1).startsWith("+")) 63 | { 64 | // Add to the current radius 65 | radiusZ = border.getRadiusZ(); 66 | radiusZ += Integer.parseInt(params.get(1).substring(1)); 67 | } 68 | else if(params.get(1).startsWith("-")) 69 | { 70 | // Subtract from the current radius 71 | radiusZ = border.getRadiusZ(); 72 | radiusZ -= Integer.parseInt(params.get(1).substring(1)); 73 | } 74 | else 75 | radiusZ = Integer.parseInt(params.get(1)); 76 | } 77 | else 78 | radiusZ = radiusX; 79 | } 80 | catch(NumberFormatException ex) 81 | { 82 | sendErrorAndHelp(sender, "The radius value(s) must be integers."); 83 | return; 84 | } 85 | 86 | Config.setBorder(worldName, radiusX, radiusZ, x, z); 87 | 88 | if (player != null) 89 | sender.sendMessage("Radius has been set. " + Config.BorderDescription(worldName)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.command.*; 8 | import org.bukkit.entity.Player; 9 | 10 | import com.wimbli.WorldBorder.*; 11 | import com.wimbli.WorldBorder.UUID.UUIDFetcher; 12 | 13 | 14 | public class CmdBypass extends WBCmd 15 | { 16 | public CmdBypass() 17 | { 18 | name = permission = "bypass"; 19 | minParams = 0; 20 | maxParams = 2; 21 | 22 | addCmdExample(nameEmphasized() + "{player} [on|off] - let player go beyond border."); 23 | helpText = "If [player] isn't specified, command sender is used. If [on|off] isn't specified, the value will " + 24 | "be toggled. Once bypass is enabled, the player will not be stopped by any borders until bypass is " + 25 | "disabled for them again. Use the " + commandEmphasized("bypasslist") + C_DESC + "command to list all " + 26 | "players with bypass enabled."; 27 | } 28 | 29 | @Override 30 | public void cmdStatus(CommandSender sender) 31 | { 32 | if (!(sender instanceof Player)) 33 | return; 34 | 35 | boolean bypass = Config.isPlayerBypassing(((Player)sender).getUniqueId()); 36 | sender.sendMessage(C_HEAD + "Border bypass is currently " + enabledColored(bypass) + C_HEAD + " for you."); 37 | } 38 | 39 | @Override 40 | public void execute(final CommandSender sender, final Player player, final List params, String worldName) 41 | { 42 | if (player == null && params.isEmpty()) 43 | { 44 | sendErrorAndHelp(sender, "When running this command from console, you must specify a player."); 45 | return; 46 | } 47 | 48 | Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(WorldBorder.plugin, new Runnable() 49 | { 50 | @Override 51 | public void run() 52 | { 53 | final String sPlayer = (params.isEmpty()) ? player.getName() : params.get(0); 54 | UUID uPlayer = (params.isEmpty()) ? player.getUniqueId() : null; 55 | 56 | if (uPlayer == null) 57 | { 58 | Player p = Bukkit.getPlayer(sPlayer); 59 | if (p != null) 60 | { 61 | uPlayer = p.getUniqueId(); 62 | } 63 | else 64 | { 65 | // only do UUID lookup using Mojang server if specified player isn't online 66 | try 67 | { 68 | uPlayer = UUIDFetcher.getUUID(sPlayer); 69 | } 70 | catch(Exception ex) 71 | { 72 | sendErrorAndHelp(sender, "Failed to look up UUID for the player name you specified. " + ex.getLocalizedMessage()); 73 | return; 74 | } 75 | } 76 | } 77 | if (uPlayer == null) 78 | { 79 | sendErrorAndHelp(sender, "Failed to look up UUID for the player name you specified; null value returned."); 80 | return; 81 | } 82 | 83 | boolean bypassing = !Config.isPlayerBypassing(uPlayer); 84 | if (params.size() > 1) 85 | bypassing = strAsBool(params.get(1)); 86 | 87 | Config.setPlayerBypass(uPlayer, bypassing); 88 | 89 | Player target = Bukkit.getPlayer(sPlayer); 90 | if (target != null && target.isOnline()) 91 | target.sendMessage("Border bypass is now " + enabledColored(bypassing) + "."); 92 | 93 | Config.log("Border bypass for player \"" + sPlayer + "\" is " + (bypassing ? "enabled" : "disabled") + 94 | (player != null ? " at the command of player \"" + player.getName() + "\"" : "") + "."); 95 | if (player != null && player != target) 96 | sender.sendMessage("Border bypass for player \"" + sPlayer + "\" is " + enabledColored(bypassing) + "."); 97 | } 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WBListener.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import org.bukkit.Chunk; 4 | import org.bukkit.event.EventHandler; 5 | import org.bukkit.event.EventPriority; 6 | import org.bukkit.event.Listener; 7 | import org.bukkit.event.player.PlayerTeleportEvent; 8 | import org.bukkit.event.player.PlayerPortalEvent; 9 | import org.bukkit.event.world.ChunkLoadEvent; 10 | import org.bukkit.event.world.ChunkUnloadEvent; 11 | import org.bukkit.Location; 12 | 13 | 14 | public class WBListener implements Listener 15 | { 16 | @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) 17 | public void onPlayerTeleport(PlayerTeleportEvent event) 18 | { 19 | // if knockback is set to 0, simply return 20 | if (Config.KnockBack() == 0.0) 21 | return; 22 | 23 | if (Config.Debug()) 24 | Config.log("Teleport cause: " + event.getCause().toString()); 25 | 26 | Location newLoc = BorderCheckTask.checkPlayer(event.getPlayer(), event.getTo(), true, true); 27 | if (newLoc != null) 28 | { 29 | if(event.getCause() == PlayerTeleportEvent.TeleportCause.ENDER_PEARL && Config.getDenyEnderpearl()) 30 | { 31 | event.setCancelled(true); 32 | return; 33 | } 34 | 35 | event.setTo(newLoc); 36 | } 37 | } 38 | 39 | @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) 40 | public void onPlayerPortal(PlayerPortalEvent event) 41 | { 42 | // if knockback is set to 0, or portal redirection is disabled, simply return 43 | if (Config.KnockBack() == 0.0 || !Config.portalRedirection()) 44 | return; 45 | 46 | Location newLoc = BorderCheckTask.checkPlayer(event.getPlayer(), event.getTo(), true, false); 47 | if (newLoc != null) 48 | event.setTo(newLoc); 49 | } 50 | 51 | @EventHandler(priority = EventPriority.MONITOR) 52 | public void onChunkLoad(ChunkLoadEvent event) 53 | { 54 | /* // tested, found to spam pretty rapidly as client repeatedly requests the same chunks since they're not being sent 55 | // definitely too spammy at only 16 blocks outside border 56 | // 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 57 | // 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 58 | 59 | // method to prevent new chunks from being generated, core method courtesy of code from NoNewChunk plugin (http://dev.bukkit.org/bukkit-plugins/nonewchunk/) 60 | if(event.isNewChunk()) 61 | { 62 | Chunk chunk = event.getChunk(); 63 | chunk.unload(false, false); 64 | Config.logWarn("New chunk generation has been prevented at X " + chunk.getX() + ", Z " + chunk.getZ()); 65 | } 66 | */ 67 | // make sure our border monitoring task is still running like it should 68 | if (Config.isBorderTimerRunning()) return; 69 | 70 | Config.logWarn("Border-checking task was not running! Something on your server apparently killed it. It will now be restarted."); 71 | Config.StartBorderTimer(); 72 | } 73 | 74 | /* 75 | * Check if there is a fill task running, and if yes, if it's for the 76 | * world that the unload event refers to, set "force loaded" flag off 77 | * and track if chunk was somehow on unload prevention list 78 | */ 79 | @EventHandler 80 | public void onChunkUnload(ChunkUnloadEvent e) 81 | { 82 | if (Config.fillTask == null) 83 | return; 84 | 85 | Chunk chunk = e.getChunk(); 86 | if (e.getWorld() != Config.fillTask.getWorld()) 87 | return; 88 | 89 | // just to be on the safe side, in case it's still set at this point somehow 90 | chunk.setForceLoaded(false); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This code mostly taken from https://gist.github.com/Jofkos/d0c469528b032d820f42 3 | */ 4 | 5 | package com.wimbli.WorldBorder.UUID; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.InputStreamReader; 9 | import java.net.HttpURLConnection; 10 | import java.net.URL; 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.UUID; 15 | import java.util.concurrent.ExecutorService; 16 | import java.util.concurrent.Executors; 17 | import java.util.function.Consumer; 18 | 19 | import com.google.gson.Gson; 20 | import com.google.gson.GsonBuilder; 21 | 22 | 23 | public class UUIDFetcher { 24 | 25 | /** 26 | * Date when name changes were introduced 27 | * @see UUIDFetcher#getUUIDAt(String, long) 28 | */ 29 | public static final long FEBRUARY_2015 = 1422748800000L; 30 | 31 | 32 | private static Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); 33 | 34 | private static final String UUID_URL = "https://api.mojang.com/users/profiles/minecraft/%s?at=%d"; 35 | private static final String NAME_URL = "https://api.mojang.com/user/profiles/%s/names"; 36 | 37 | private static Map uuidCache = new HashMap(); 38 | private static Map nameCache = new HashMap(); 39 | 40 | private static ExecutorService pool = Executors.newCachedThreadPool(); 41 | 42 | private String name; 43 | private UUID id; 44 | 45 | /** 46 | * Fetches the uuid asynchronously and passes it to the consumer 47 | * 48 | * @param name The name 49 | * @param action Do what you want to do with the uuid her 50 | */ 51 | public static void getUUID(String name, Consumer action) { 52 | pool.execute(() -> action.accept(getUUID(name))); 53 | } 54 | 55 | /** 56 | * Fetches the uuid synchronously and returns it 57 | * 58 | * @param name The name 59 | * @return The uuid 60 | */ 61 | public static UUID getUUID(String name) { 62 | return getUUIDAt(name, System.currentTimeMillis()); 63 | } 64 | 65 | /** 66 | * Fetches the uuid synchronously for a specified name and time and passes the result to the consumer 67 | * 68 | * @param name The name 69 | * @param timestamp Time when the player had this name in milliseconds 70 | * @param action Do what you want to do with the uuid her 71 | */ 72 | public static void getUUIDAt(String name, long timestamp, Consumer action) { 73 | pool.execute(() -> action.accept(getUUIDAt(name, timestamp))); 74 | } 75 | 76 | /** 77 | * Fetches the uuid synchronously for a specified name and time 78 | * 79 | * @param name The name 80 | * @param timestamp Time when the player had this name in milliseconds 81 | * @see UUIDFetcher#FEBRUARY_2015 82 | */ 83 | public static UUID getUUIDAt(String name, long timestamp) { 84 | name = name.toLowerCase(); 85 | if (uuidCache.containsKey(name)) { 86 | return uuidCache.get(name); 87 | } 88 | try { 89 | HttpURLConnection connection = (HttpURLConnection) new URL(String.format(UUID_URL, name, timestamp/1000)).openConnection(); 90 | connection.setReadTimeout(5000); 91 | UUIDFetcher data = gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), UUIDFetcher.class); 92 | 93 | uuidCache.put(name, data.id); 94 | nameCache.put(data.id, data.name); 95 | 96 | return data.id; 97 | } catch (Exception e) { 98 | e.printStackTrace(); 99 | } 100 | 101 | return null; 102 | } 103 | 104 | /** 105 | * Fetches the name asynchronously and passes it to the consumer 106 | * 107 | * @param uuid The uuid 108 | * @param action Do what you want to do with the name her 109 | */ 110 | public static void getName(UUID uuid, Consumer action) { 111 | pool.execute(() -> action.accept(getName(uuid))); 112 | } 113 | 114 | /** 115 | * Fetches the name synchronously and returns it 116 | * 117 | * @param uuid The uuid 118 | * @return The name 119 | */ 120 | public static String getName(UUID uuid) { 121 | if (nameCache.containsKey(uuid)) { 122 | return nameCache.get(uuid); 123 | } 124 | try { 125 | HttpURLConnection connection = (HttpURLConnection) new URL(String.format(NAME_URL, UUIDTypeAdapter.fromUUID(uuid))).openConnection(); 126 | connection.setReadTimeout(5000); 127 | UUIDFetcher[] nameHistory = gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), UUIDFetcher[].class); 128 | UUIDFetcher currentNameData = nameHistory[nameHistory.length - 1]; 129 | 130 | uuidCache.put(currentNameData.name.toLowerCase(), uuid); 131 | nameCache.put(uuid, currentNameData.name); 132 | 133 | return currentNameData.name; 134 | } catch (Exception e) { 135 | e.printStackTrace(); 136 | } 137 | 138 | return null; 139 | } 140 | 141 | public static Map getNameList(ArrayList uuids) { 142 | Map uuidStringMap = new HashMap<>(); 143 | for (UUID uuid: uuids) 144 | { 145 | uuidStringMap.put(uuid, getName(uuid)); 146 | } 147 | return uuidStringMap; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdSet.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.command.*; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.Location; 9 | import org.bukkit.World; 10 | 11 | import com.wimbli.WorldBorder.*; 12 | 13 | 14 | public class CmdSet extends WBCmd 15 | { 16 | public CmdSet() 17 | { 18 | name = permission = "set"; 19 | hasWorldNameInput = true; 20 | consoleRequiresWorldName = false; 21 | minParams = 1; 22 | maxParams = 4; 23 | 24 | addCmdExample(nameEmphasizedW() + " [radiusZ] - use x/z coords."); 25 | addCmdExample(nameEmphasizedW() + " [radiusZ] ^spawn - use spawn point."); 26 | addCmdExample(nameEmphasized() + " [radiusZ] - set border, centered on you.", true, false, true); 27 | addCmdExample(nameEmphasized() + " [radiusZ] ^player - center on player."); 28 | helpText = "Set a border for a world, with several options for defining the center location. [world] is " + 29 | "optional for players and defaults to the world the player is in. If [radiusZ] is not specified, the " + 30 | "radiusX value will be used for both. The and coordinates can be decimal values (ex. 1.234)."; 31 | } 32 | 33 | @Override 34 | public void execute(CommandSender sender, Player player, List params, String worldName) 35 | { 36 | // passsing a single parameter (radiusX) is only acceptable from player 37 | if ((params.size() == 1) && player == null) 38 | { 39 | sendErrorAndHelp(sender, "You have not provided a sufficient number of parameters."); 40 | return; 41 | } 42 | 43 | // "set" command from player or console, world specified 44 | if (worldName != null) 45 | { 46 | if (params.size() == 2 && ! params.get(params.size() - 1).equalsIgnoreCase("spawn")) 47 | { // command can only be this short if "spawn" is specified rather than x + z or player name 48 | sendErrorAndHelp(sender, "You have not provided a sufficient number of arguments."); 49 | return; 50 | } 51 | 52 | World world = sender.getServer().getWorld(worldName); 53 | if (world == null) 54 | { 55 | if (params.get(params.size() - 1).equalsIgnoreCase("spawn")) 56 | { 57 | sendErrorAndHelp(sender, "The world you specified (\"" + worldName + "\") could not be found on the server, so the spawn point cannot be determined."); 58 | return; 59 | } 60 | sender.sendMessage("The world you specified (\"" + worldName + "\") could not be found on the server, but data for it will be stored anyway."); 61 | } 62 | } 63 | // "set" command from player using current world since it isn't specified, or allowed from console only if player name is specified 64 | else 65 | { 66 | if (player == null) 67 | { 68 | if (! params.get(params.size() - 2).equalsIgnoreCase("player")) 69 | { // command can only be called by console without world specified if player is specified instead 70 | sendErrorAndHelp(sender, "You must specify a world name from console if not specifying a player name."); 71 | return; 72 | } 73 | player = Bukkit.getPlayer(params.get(params.size() - 1)); 74 | if (player == null || ! player.isOnline()) 75 | { 76 | sendErrorAndHelp(sender, "The player you specified (\"" + params.get(params.size() - 1) + "\") does not appear to be online."); 77 | return; 78 | } 79 | } 80 | worldName = player.getWorld().getName(); 81 | } 82 | 83 | int radiusX, radiusZ; 84 | double x, z; 85 | int radiusCount = params.size(); 86 | 87 | try 88 | { 89 | if (params.get(params.size() - 1).equalsIgnoreCase("spawn")) 90 | { // "spawn" specified for x/z coordinates 91 | Location loc = sender.getServer().getWorld(worldName).getSpawnLocation(); 92 | x = loc.getX(); 93 | z = loc.getZ(); 94 | radiusCount -= 1; 95 | } 96 | else if (params.size() > 2 && params.get(params.size() - 2).equalsIgnoreCase("player")) 97 | { // player name specified for x/z coordinates 98 | Player playerT = Bukkit.getPlayer(params.get(params.size() - 1)); 99 | if (playerT == null || ! playerT.isOnline()) 100 | { 101 | sendErrorAndHelp(sender, "The player you specified (\"" + params.get(params.size() - 1) + "\") does not appear to be online."); 102 | return; 103 | } 104 | worldName = playerT.getWorld().getName(); 105 | x = playerT.getLocation().getX(); 106 | z = playerT.getLocation().getZ(); 107 | radiusCount -= 2; 108 | } 109 | else 110 | { 111 | if (player == null || radiusCount > 2) 112 | { // x and z specified 113 | x = Double.parseDouble(params.get(params.size() - 2)); 114 | z = Double.parseDouble(params.get(params.size() - 1)); 115 | radiusCount -= 2; 116 | } 117 | else 118 | { // using coordinates of command sender (player) 119 | x = player.getLocation().getX(); 120 | z = player.getLocation().getZ(); 121 | } 122 | } 123 | 124 | radiusX = Integer.parseInt(params.get(0)); 125 | if (radiusCount < 2) 126 | radiusZ = radiusX; 127 | else 128 | radiusZ = Integer.parseInt(params.get(1)); 129 | 130 | if (radiusX < Config.KnockBack() || radiusZ < Config.KnockBack()) 131 | { 132 | sendErrorAndHelp(sender, "Radius value(s) must be more than the knockback distance."); 133 | return; 134 | } 135 | } 136 | catch(NumberFormatException ex) 137 | { 138 | sendErrorAndHelp(sender, "Radius value(s) must be integers and x and z values must be numerical."); 139 | return; 140 | } 141 | 142 | Config.setBorder(worldName, radiusX, radiusZ, x, z); 143 | sender.sendMessage("Border has been set. " + Config.BorderDescription(worldName)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/WBCmd.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.bukkit.ChatColor; 7 | import org.bukkit.command.*; 8 | import org.bukkit.entity.Player; 9 | 10 | 11 | public abstract class WBCmd 12 | { 13 | /* 14 | * Primary variables, should be set as needed in constructors for the subclassed commands 15 | */ 16 | 17 | // command name, command permission; normally the same thing 18 | public String name = ""; 19 | public String permission = null; 20 | 21 | // whether command can accept a world name before itself 22 | public boolean hasWorldNameInput = false; 23 | public boolean consoleRequiresWorldName = true; 24 | 25 | // minimum and maximum number of accepted parameters 26 | public int minParams = 0; 27 | public int maxParams = 9999; 28 | 29 | // help/explanation text to be shown after command example(s) for this command 30 | public String helpText = null; 31 | 32 | /* 33 | * The guts of the command run in here; needs to be overriden in the subclassed commands 34 | */ 35 | public abstract void execute(CommandSender sender, Player player, List params, String worldName); 36 | 37 | /* 38 | * This is an optional override, used to provide some extra command status info, like the currently set value 39 | */ 40 | public void cmdStatus(CommandSender sender) {} 41 | 42 | 43 | /* 44 | * Helper variables and methods 45 | */ 46 | 47 | // color values for strings 48 | public final static String C_CMD = ChatColor.AQUA.toString(); // main commands 49 | public final static String C_DESC = ChatColor.WHITE.toString(); // command descriptions 50 | public final static String C_ERR = ChatColor.RED.toString(); // errors / notices 51 | public final static String C_HEAD = ChatColor.YELLOW.toString(); // command listing header 52 | public final static String C_OPT = ChatColor.DARK_GREEN.toString(); // optional values 53 | public final static String C_REQ = ChatColor.GREEN.toString(); // required values 54 | 55 | // colorized root command, for console and for player 56 | public final static String CMD_C = C_CMD + "wb "; 57 | public final static String CMD_P = C_CMD + "/wb "; 58 | 59 | // list of command examples for this command to be displayed as usage reference, separate between players and console 60 | // ... these generally should be set indirectly using addCmdExample() within the constructor for each command class 61 | public List cmdExamplePlayer = new ArrayList(); 62 | public List cmdExampleConsole = new ArrayList(); 63 | 64 | // much like the above, but used for displaying command list from root /wb command, listing all commands 65 | public final static List cmdExamplesConsole = new ArrayList(48); // 48 command capacity, 6 full pages 66 | public final static List cmdExamplesPlayer = new ArrayList(48); // still, could need to increase later 67 | 68 | 69 | // add command examples for use the default "/wb" command list and for internal usage reference, formatted and colorized 70 | public void addCmdExample(String example) 71 | { 72 | addCmdExample(example, true, true, true); 73 | } 74 | public void addCmdExample(String example, boolean forPlayer, boolean forConsole, boolean prefix) 75 | { 76 | // go ahead and colorize required "<>" and optional "[]" parameters, extra command words, and description 77 | example = example.replace("<", C_REQ+"<").replace("[", C_OPT+"[").replace("^", C_CMD).replace("- ", C_DESC+"- "); 78 | 79 | // all "{}" are replaced by "[]" (optional) for player, "<>" (required) for console 80 | if (forPlayer) 81 | { 82 | String exampleP = (prefix ? CMD_P : "") + example.replace("{", C_OPT + "[").replace("}", "]"); 83 | cmdExamplePlayer.add(exampleP); 84 | cmdExamplesPlayer.add(exampleP); 85 | } 86 | if (forConsole) 87 | { 88 | String exampleC = (prefix ? CMD_C : "") + example.replace("{", C_REQ + "<").replace("}", ">"); 89 | cmdExampleConsole.add(exampleC); 90 | cmdExamplesConsole.add(exampleC); 91 | } 92 | } 93 | 94 | // return root command formatted for player or console, based on sender 95 | public String cmd(CommandSender sender) 96 | { 97 | return (sender instanceof Player) ? CMD_P : CMD_C; 98 | } 99 | 100 | // formatted and colorized text, intended for marking command name 101 | public String commandEmphasized(String text) 102 | { 103 | return C_CMD + ChatColor.UNDERLINE + text + ChatColor.RESET + " "; 104 | } 105 | 106 | // returns green "enabled" or red "disabled" text 107 | public String enabledColored(boolean enabled) 108 | { 109 | return enabled ? C_REQ+"enabled" : C_ERR+"disabled"; 110 | } 111 | 112 | // formatted and colorized command name, optionally prefixed with "[world]" (for player) / "" (for console) 113 | public String nameEmphasized() 114 | { 115 | return commandEmphasized(name); 116 | } 117 | public String nameEmphasizedW() 118 | { 119 | return "{world} " + nameEmphasized(); 120 | } 121 | 122 | // send command example message(s) and other helpful info 123 | public void sendCmdHelp(CommandSender sender) 124 | { 125 | for (String example : ((sender instanceof Player) ? cmdExamplePlayer : cmdExampleConsole)) 126 | { 127 | sender.sendMessage(example); 128 | } 129 | cmdStatus(sender); 130 | if (helpText != null && !helpText.isEmpty()) 131 | sender.sendMessage(C_DESC + helpText); 132 | } 133 | 134 | // send error message followed by command example message(s) 135 | public void sendErrorAndHelp(CommandSender sender, String error) 136 | { 137 | sender.sendMessage(C_ERR + error); 138 | sendCmdHelp(sender); 139 | } 140 | 141 | // interpret string as boolean value (yes/no, true/false, on/off, +/-, 1/0) 142 | public boolean strAsBool(String str) 143 | { 144 | str = str.toLowerCase(); 145 | return str.startsWith("y") || str.startsWith("t") || str.startsWith("on") || str.startsWith("+") || str.startsWith("1"); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,java,gradle,intellij+all,eclipse,visualstudiocode,netbeans 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,java,gradle,intellij+all,eclipse,visualstudiocode,netbeans 4 | 5 | ### Eclipse ### 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # PyDev specific (Python IDE for Eclipse) 25 | *.pydevproject 26 | 27 | # CDT-specific (C/C++ Development Tooling) 28 | .cproject 29 | 30 | # CDT- autotools 31 | .autotools 32 | 33 | # Java annotation processor (APT) 34 | .factorypath 35 | 36 | # PDT-specific (PHP Development Tools) 37 | .buildpath 38 | 39 | # sbteclipse plugin 40 | .target 41 | 42 | # Tern plugin 43 | .tern-project 44 | 45 | # TeXlipse plugin 46 | .texlipse 47 | 48 | # STS (Spring Tool Suite) 49 | .springBeans 50 | 51 | # Code Recommenders 52 | .recommenders/ 53 | 54 | # Annotation Processing 55 | .apt_generated/ 56 | .apt_generated_test/ 57 | 58 | # Scala IDE specific (Scala & Java development for Eclipse) 59 | .cache-main 60 | .scala_dependencies 61 | .worksheet 62 | 63 | # Uncomment this line if you wish to ignore the project description file. 64 | # Typically, this file would be tracked if it contains build/dependency configurations: 65 | #.project 66 | 67 | ### Eclipse Patch ### 68 | # Spring Boot Tooling 69 | .sts4-cache/ 70 | 71 | ### Intellij+all ### 72 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 73 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 74 | 75 | # User-specific stuff 76 | .idea/**/workspace.xml 77 | .idea/**/tasks.xml 78 | .idea/**/usage.statistics.xml 79 | .idea/**/dictionaries 80 | .idea/**/shelf 81 | 82 | # AWS User-specific 83 | .idea/**/aws.xml 84 | 85 | # Generated files 86 | .idea/**/contentModel.xml 87 | 88 | # Sensitive or high-churn files 89 | .idea/**/dataSources/ 90 | .idea/**/dataSources.ids 91 | .idea/**/dataSources.local.xml 92 | .idea/**/sqlDataSources.xml 93 | .idea/**/dynamic.xml 94 | .idea/**/uiDesigner.xml 95 | .idea/**/dbnavigator.xml 96 | 97 | # Gradle 98 | .idea/**/gradle.xml 99 | .idea/**/libraries 100 | 101 | # Gradle and Maven with auto-import 102 | # When using Gradle or Maven with auto-import, you should exclude module files, 103 | # since they will be recreated, and may cause churn. Uncomment if using 104 | # auto-import. 105 | # .idea/artifacts 106 | # .idea/compiler.xml 107 | # .idea/jarRepositories.xml 108 | # .idea/modules.xml 109 | # .idea/*.iml 110 | # .idea/modules 111 | # *.iml 112 | # *.ipr 113 | 114 | # CMake 115 | cmake-build-*/ 116 | 117 | # Mongo Explorer plugin 118 | .idea/**/mongoSettings.xml 119 | 120 | # File-based project format 121 | *.iws 122 | 123 | # IntelliJ 124 | out/ 125 | 126 | # mpeltonen/sbt-idea plugin 127 | .idea_modules/ 128 | 129 | # JIRA plugin 130 | atlassian-ide-plugin.xml 131 | 132 | # Cursive Clojure plugin 133 | .idea/replstate.xml 134 | 135 | # Crashlytics plugin (for Android Studio and IntelliJ) 136 | com_crashlytics_export_strings.xml 137 | crashlytics.properties 138 | crashlytics-build.properties 139 | fabric.properties 140 | 141 | # Editor-based Rest Client 142 | .idea/httpRequests 143 | 144 | # Android studio 3.1+ serialized cache file 145 | .idea/caches/build_file_checksums.ser 146 | 147 | ### Intellij+all Patch ### 148 | # Ignores the whole .idea folder and all .iml files 149 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 150 | 151 | .idea/ 152 | 153 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 154 | 155 | *.iml 156 | modules.xml 157 | .idea/misc.xml 158 | *.ipr 159 | 160 | # Sonarlint plugin 161 | .idea/sonarlint 162 | 163 | ### Java ### 164 | # Compiled class file 165 | *.class 166 | 167 | # Log file 168 | *.log 169 | 170 | # BlueJ files 171 | *.ctxt 172 | 173 | # Mobile Tools for Java (J2ME) 174 | .mtj.tmp/ 175 | 176 | # Package Files # 177 | *.jar 178 | *.war 179 | *.nar 180 | *.ear 181 | *.zip 182 | *.tar.gz 183 | *.rar 184 | 185 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 186 | hs_err_pid* 187 | 188 | ### Linux ### 189 | *~ 190 | 191 | # temporary files which can be created if a process still has a handle open of a deleted file 192 | .fuse_hidden* 193 | 194 | # KDE directory preferences 195 | .directory 196 | 197 | # Linux trash folder which might appear on any partition or disk 198 | .Trash-* 199 | 200 | # .nfs files are created when an open file is removed but is still being accessed 201 | .nfs* 202 | 203 | ### macOS ### 204 | # General 205 | .DS_Store 206 | .AppleDouble 207 | .LSOverride 208 | 209 | # Icon must end with two \r 210 | Icon 211 | 212 | 213 | # Thumbnails 214 | ._* 215 | 216 | # Files that might appear in the root of a volume 217 | .DocumentRevisions-V100 218 | .fseventsd 219 | .Spotlight-V100 220 | .TemporaryItems 221 | .Trashes 222 | .VolumeIcon.icns 223 | .com.apple.timemachine.donotpresent 224 | 225 | # Directories potentially created on remote AFP share 226 | .AppleDB 227 | .AppleDesktop 228 | Network Trash Folder 229 | Temporary Items 230 | .apdisk 231 | 232 | ### NetBeans ### 233 | **/nbproject/private/ 234 | **/nbproject/Makefile-*.mk 235 | **/nbproject/Package-*.bash 236 | build/ 237 | nbbuild/ 238 | dist/ 239 | nbdist/ 240 | .nb-gradle/ 241 | 242 | ### VisualStudioCode ### 243 | .vscode/* 244 | !.vscode/settings.json 245 | !.vscode/tasks.json 246 | !.vscode/launch.json 247 | !.vscode/extensions.json 248 | *.code-workspace 249 | 250 | # Local History for Visual Studio Code 251 | .history/ 252 | 253 | ### VisualStudioCode Patch ### 254 | # Ignore all local history of files 255 | .history 256 | .ionide 257 | 258 | ### Windows ### 259 | # Windows thumbnail cache files 260 | Thumbs.db 261 | Thumbs.db:encryptable 262 | ehthumbs.db 263 | ehthumbs_vista.db 264 | 265 | # Dump file 266 | *.stackdump 267 | 268 | # Folder config file 269 | [Dd]esktop.ini 270 | 271 | # Recycle Bin used on file shares 272 | $RECYCLE.BIN/ 273 | 274 | # Windows Installer files 275 | *.cab 276 | *.msi 277 | *.msix 278 | *.msm 279 | *.msp 280 | 281 | # Windows shortcuts 282 | *.lnk 283 | 284 | ### Gradle ### 285 | .gradle 286 | 287 | # Ignore Gradle GUI config 288 | gradle-app.setting 289 | 290 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 291 | !gradle-wrapper.jar 292 | 293 | # Cache of project 294 | .gradletasknamecache 295 | 296 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 297 | # gradle/wrapper/gradle-wrapper.properties 298 | 299 | ### Gradle Patch ### 300 | **/build/ 301 | 302 | # End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,java,gradle,intellij+all,eclipse,visualstudiocode,netbeans 303 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdFill.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.command.*; 7 | import org.bukkit.entity.Player; 8 | 9 | import com.wimbli.WorldBorder.*; 10 | 11 | 12 | public class CmdFill extends WBCmd 13 | { 14 | public CmdFill() 15 | { 16 | name = permission = "fill"; 17 | hasWorldNameInput = true; 18 | consoleRequiresWorldName = false; 19 | minParams = 0; 20 | maxParams = 3; 21 | 22 | addCmdExample(nameEmphasizedW() + "[freq] [pad] [force] - fill world to border."); 23 | helpText = "This command will generate missing world chunks inside your border. [freq] is the frequency " + 24 | "of chunks per second that will be checked (default 20). [pad] is the number of blocks padding added " + 25 | "beyond the border itself (default 208, to cover player visual range). [force] can be specified as true " + 26 | "to force all chunks to be loaded even if they seem to be fully generated (default false)."; 27 | } 28 | 29 | @Override 30 | public void execute(CommandSender sender, Player player, List params, String worldName) 31 | { 32 | boolean confirm = false; 33 | // check for "cancel", "pause", or "confirm" 34 | if (params.size() >= 1) 35 | { 36 | String check = params.get(0).toLowerCase(); 37 | 38 | if (check.equals("cancel") || check.equals("stop")) 39 | { 40 | if (!makeSureFillIsRunning(sender)) 41 | return; 42 | sender.sendMessage(C_HEAD + "Cancelling the world map generation task."); 43 | fillDefaults(); 44 | Config.StopFillTask(false); 45 | return; 46 | } 47 | else if (check.equals("pause")) 48 | { 49 | if (!makeSureFillIsRunning(sender)) 50 | return; 51 | Config.fillTask.pause(); 52 | sender.sendMessage(C_HEAD + "The world map generation task is now " + (Config.fillTask.isPaused() ? "" : "un") + "paused."); 53 | return; 54 | } 55 | 56 | confirm = check.equals("confirm"); 57 | } 58 | 59 | // if not just confirming, make sure a world name is available 60 | if (worldName == null && !confirm) 61 | { 62 | if (player != null) 63 | worldName = player.getWorld().getName(); 64 | else 65 | { 66 | sendErrorAndHelp(sender, "You must specify a world!"); 67 | return; 68 | } 69 | } 70 | 71 | // colorized "/wb fill " 72 | String cmd = cmd(sender) + nameEmphasized() + C_CMD; 73 | 74 | // make sure Fill isn't already running 75 | if (Config.fillTask != null && Config.fillTask.valid()) 76 | { 77 | sender.sendMessage(C_ERR + "The world map generation task is already running."); 78 | sender.sendMessage(C_DESC + "You can cancel at any time with " + cmd + "cancel" + C_DESC + ", or pause/unpause with " + cmd + "pause" + C_DESC + "."); 79 | return; 80 | } 81 | 82 | // set frequency and/or padding if those were specified 83 | try 84 | { 85 | if (params.size() >= 1 && !confirm) 86 | fillFrequency = Math.abs(Integer.parseInt(params.get(0))); 87 | if (params.size() >= 2 && !confirm) 88 | fillPadding = Math.abs(Integer.parseInt(params.get(1))); 89 | } 90 | catch(NumberFormatException ex) 91 | { 92 | sendErrorAndHelp(sender, "The frequency and padding values must be integers."); 93 | fillDefaults(); 94 | return; 95 | } 96 | if (fillFrequency <= 0) 97 | { 98 | sendErrorAndHelp(sender, "The frequency value must be greater than zero."); 99 | fillDefaults(); 100 | return; 101 | } 102 | 103 | // see if the command specifies to load even chunks which should already be fully generated 104 | if (params.size() == 3) 105 | fillForceLoad = strAsBool(params.get(2)); 106 | 107 | // set world if it was specified 108 | if (worldName != null) 109 | fillWorld = worldName; 110 | 111 | if (confirm) 112 | { // command confirmed, go ahead with it 113 | if (fillWorld.isEmpty()) 114 | { 115 | sendErrorAndHelp(sender, "You must first use this command successfully without confirming."); 116 | return; 117 | } 118 | 119 | if (player != null) 120 | Config.log("Filling out world to border at the command of player \"" + player.getName() + "\"."); 121 | 122 | int ticks = 1, repeats = 1; 123 | if (fillFrequency > 20) 124 | repeats = fillFrequency / 20; 125 | else 126 | ticks = 20 / fillFrequency; 127 | 128 | /* */ Config.log("world: " + fillWorld + " padding: " + fillPadding + " repeats: " + repeats + " ticks: " + ticks); 129 | Config.fillTask = new WorldFillTask(Bukkit.getServer(), player, fillWorld, fillPadding, repeats, ticks, fillForceLoad); 130 | if (Config.fillTask.valid()) 131 | { 132 | int task = Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(WorldBorder.plugin, Config.fillTask, ticks, ticks); 133 | Config.fillTask.setTaskID(task); 134 | sender.sendMessage("WorldBorder map generation task for world \"" + fillWorld + "\" started."); 135 | } 136 | else 137 | sender.sendMessage(C_ERR + "The world map generation task failed to start."); 138 | 139 | fillDefaults(); 140 | } 141 | else 142 | { 143 | if (fillWorld.isEmpty()) 144 | { 145 | sendErrorAndHelp(sender, "You must first specify a valid world."); 146 | return; 147 | } 148 | 149 | sender.sendMessage(C_HEAD + "World generation task is ready for world \"" + fillWorld + "\", attempting to process up to " + fillFrequency + " chunks per second (default 20). The map will be padded out " + fillPadding + " blocks beyond the border (default " + defaultPadding + "). Parts of the world which are already fully generated will be " + (fillForceLoad ? "loaded anyway." : "skipped.")); 150 | sender.sendMessage(C_HEAD + "This process can take a very long time depending on the world's border size. Also, depending on the chunk processing rate, players will likely experience severe lag for the duration."); 151 | sender.sendMessage(C_DESC + "You should now use " + cmd + "confirm" + C_DESC + " to start the process."); 152 | sender.sendMessage(C_DESC + "You can cancel at any time with " + cmd + "cancel" + C_DESC + ", or pause/unpause with " + cmd + "pause" + C_DESC + "."); 153 | } 154 | } 155 | 156 | 157 | /* with "view-distance=10" in server.properties on a fast VM test server and "Render Distance: Far" in client, 158 | * hitting border during testing was loading 11+ chunks beyond the border in a couple of directions (10 chunks in 159 | * the other two directions). This could be worse on a more loaded or worse server, so: 160 | */ 161 | private final int defaultPadding = CoordXZ.chunkToBlock(13); 162 | 163 | private String fillWorld = ""; 164 | private int fillFrequency = 20; 165 | private int fillPadding = defaultPadding; 166 | private boolean fillForceLoad = false; 167 | 168 | private void fillDefaults() 169 | { 170 | fillWorld = ""; 171 | fillFrequency = 20; 172 | fillPadding = defaultPadding; 173 | fillForceLoad = false; 174 | } 175 | 176 | private boolean makeSureFillIsRunning(CommandSender sender) 177 | { 178 | if (Config.fillTask != null && Config.fillTask.valid()) 179 | return true; 180 | sendErrorAndHelp(sender, "The world map generation task is not currently running."); 181 | return false; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.LinkedHashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import com.google.common.collect.ImmutableList; 10 | 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.entity.Entity; 13 | import org.bukkit.entity.LivingEntity; 14 | import org.bukkit.entity.Player; 15 | import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; 16 | import org.bukkit.Location; 17 | import org.bukkit.util.Vector; 18 | import org.bukkit.World; 19 | 20 | 21 | public class BorderCheckTask implements Runnable 22 | { 23 | @Override 24 | public void run() 25 | { 26 | // if knockback is set to 0, simply return 27 | if (Config.KnockBack() == 0.0) 28 | return; 29 | 30 | Collection players = ImmutableList.copyOf(Bukkit.getServer().getOnlinePlayers()); 31 | 32 | for (Player player : players) 33 | { 34 | checkPlayer(player, null, false, true); 35 | } 36 | } 37 | 38 | // 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. 39 | private static Set handlingPlayers = Collections.synchronizedSet(new LinkedHashSet()); 40 | 41 | // 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 42 | public static Location checkPlayer(Player player, Location targetLoc, boolean returnLocationOnly, boolean notify) 43 | { 44 | if (player == null || !player.isOnline()) return null; 45 | 46 | Location loc = (targetLoc == null) ? player.getLocation().clone() : targetLoc; 47 | if (loc == null) return null; 48 | 49 | World world = loc.getWorld(); 50 | if (world == null) return null; 51 | BorderData border = Config.Border(world.getName()); 52 | if (border == null) return null; 53 | 54 | if (border.insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound())) 55 | return null; 56 | 57 | // if player is in bypass list (from bypass command), allow them beyond border; also ignore players currently being handled already 58 | if (Config.isPlayerBypassing(player.getUniqueId()) || player.hasPermission("worldborder.allowbypass") || handlingPlayers.contains(player.getName().toLowerCase())) 59 | return null; 60 | 61 | // 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 62 | handlingPlayers.add(player.getName().toLowerCase()); 63 | 64 | Location newLoc = newLocation(player, loc, border, notify); 65 | boolean handlingVehicle = false; 66 | 67 | /* 68 | * since we need to forcibly eject players who are inside vehicles, that fires a teleport event (go figure) and 69 | * so would effectively double trigger for us, so we need to handle it here to prevent sending two messages and 70 | * two log entries etc. 71 | * after players are ejected we can wait a few ticks (long enough for their client to receive new entity location) 72 | * and then set them as passenger of the vehicle again 73 | */ 74 | if (player.isInsideVehicle()) 75 | { 76 | Entity ride = player.getVehicle(); 77 | player.leaveVehicle(); 78 | if (ride != null) 79 | { // vehicles need to be offset vertically and have velocity stopped 80 | double vertOffset = (ride instanceof LivingEntity) ? 0 : ride.getLocation().getY() - loc.getY(); 81 | Location rideLoc = newLoc.clone(); 82 | rideLoc.setY(newLoc.getY() + vertOffset); 83 | if (Config.Debug()) 84 | Config.logWarn("Player was riding a \"" + ride.toString() + "\"."); 85 | 86 | ride.setVelocity(new Vector(0, 0, 0)); 87 | ride.teleport(rideLoc, TeleportCause.PLUGIN); 88 | 89 | if (Config.RemountTicks() > 0) 90 | { 91 | setPassengerDelayed(ride, player, player.getName(), Config.RemountTicks()); 92 | handlingVehicle = true; 93 | } 94 | } 95 | } 96 | 97 | // check if player has something (a pet, maybe?) riding them; only possible through odd plugins. 98 | // it can prevent all teleportation of the player completely, so it's very much not good and needs handling 99 | List passengers = player.getPassengers(); 100 | if (!passengers.isEmpty()) 101 | { 102 | player.eject(); 103 | for (Entity rider : passengers) 104 | { 105 | rider.teleport(newLoc, TeleportCause.PLUGIN); 106 | if (Config.Debug()) 107 | Config.logWarn("Player had a passenger riding on them: " + rider.getType()); 108 | } 109 | player.sendMessage("Your passenger" + ((passengers.size() > 1) ? "s have" : " has") + " been ejected."); 110 | } 111 | 112 | // give some particle and sound effects where the player was beyond the border, if "whoosh effect" is enabled 113 | Config.showWhooshEffect(loc); 114 | 115 | if (!returnLocationOnly) 116 | player.teleport(newLoc, TeleportCause.PLUGIN); 117 | 118 | if (!handlingVehicle) 119 | handlingPlayers.remove(player.getName().toLowerCase()); 120 | 121 | if (returnLocationOnly) 122 | return newLoc; 123 | 124 | return null; 125 | } 126 | public static Location checkPlayer(Player player, Location targetLoc, boolean returnLocationOnly) 127 | { 128 | return checkPlayer(player, targetLoc, returnLocationOnly, true); 129 | } 130 | 131 | private static Location newLocation(Player player, Location loc, BorderData border, boolean notify) 132 | { 133 | if (Config.Debug()) 134 | { 135 | Config.logWarn((notify ? "Border crossing" : "Check was run") + " in \"" + loc.getWorld().getName() + "\". Border " + border.toString()); 136 | Config.logWarn("Player position X: " + Config.coord.format(loc.getX()) + " Y: " + Config.coord.format(loc.getY()) + " Z: " + Config.coord.format(loc.getZ())); 137 | } 138 | 139 | Location newLoc = border.correctedPosition(loc, Config.ShapeRound(), player.isFlying()); 140 | 141 | // it's remotely possible (such as in the Nether) a suitable location isn't available, in which case... 142 | if (newLoc == null) 143 | { 144 | if (Config.Debug()) 145 | Config.logWarn("Target new location unviable, using spawn or killing player."); 146 | if (Config.getIfPlayerKill()) 147 | { 148 | player.setHealth(0.0D); 149 | return null; 150 | } 151 | newLoc = player.getWorld().getSpawnLocation(); 152 | } 153 | 154 | if (Config.Debug()) 155 | 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())); 156 | 157 | if (notify) 158 | player.sendMessage(Config.Message()); 159 | 160 | return newLoc; 161 | } 162 | 163 | private static void setPassengerDelayed(final Entity vehicle, final Player player, final String playerName, long delay) 164 | { 165 | Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(WorldBorder.plugin, new Runnable() 166 | { 167 | @Override 168 | public void run() 169 | { 170 | handlingPlayers.remove(playerName.toLowerCase()); 171 | if (vehicle == null || player == null) 172 | return; 173 | 174 | vehicle.addPassenger(player); 175 | } 176 | }, delay); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WBCommand.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Iterator; 6 | import java.util.LinkedHashMap; 7 | import java.util.LinkedHashSet; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Set; 11 | import java.util.TreeSet; 12 | 13 | import org.bukkit.command.*; 14 | import org.bukkit.entity.Player; 15 | 16 | import com.wimbli.WorldBorder.cmd.*; 17 | 18 | 19 | public class WBCommand implements CommandExecutor 20 | { 21 | // map of all sub-commands with the command name (string) for quick reference 22 | public Map subCommands = new LinkedHashMap(); 23 | // ref. list of the commands which can have a world name in front of the command itself (ex. /wb _world_ radius 100) 24 | private Set subCommandsWithWorldNames = new LinkedHashSet(); 25 | 26 | // constructor 27 | public WBCommand () 28 | { 29 | addCmd(new CmdHelp()); // 1 example 30 | addCmd(new CmdSet()); // 4 examples for player, 3 for console 31 | addCmd(new CmdSetcorners()); // 1 32 | addCmd(new CmdRadius()); // 1 33 | addCmd(new CmdList()); // 1 34 | //----- 8 per page of examples 35 | addCmd(new CmdShape()); // 2 36 | addCmd(new CmdClear()); // 2 37 | addCmd(new CmdFill()); // 1 38 | addCmd(new CmdTrim()); // 1 39 | addCmd(new CmdBypass()); // 1 40 | addCmd(new CmdBypasslist()); // 1 41 | //----- 42 | addCmd(new CmdKnockback()); // 1 43 | addCmd(new CmdWrap()); // 1 44 | addCmd(new CmdWhoosh()); // 1 45 | addCmd(new CmdGetmsg()); // 1 46 | addCmd(new CmdSetmsg()); // 1 47 | addCmd(new CmdWshape()); // 3 48 | //----- 49 | addCmd(new CmdPreventPlace()); // 1 50 | addCmd(new CmdPreventSpawn()); // 1 51 | addCmd(new CmdDelay()); // 1 52 | addCmd(new CmdDynmap()); // 1 53 | addCmd(new CmdDynmapmsg()); // 1 54 | addCmd(new CmdRemount()); // 1 55 | addCmd(new CmdFillautosave()); // 1 56 | addCmd(new CmdPortal()); // 1 57 | //----- 58 | addCmd(new CmdDenypearl()); // 1 59 | addCmd(new CmdReload()); // 1 60 | addCmd(new CmdDebug()); // 1 61 | 62 | // this is the default command, which shows command example pages; should be last just in case 63 | addCmd(new CmdCommands()); 64 | } 65 | 66 | 67 | private void addCmd(WBCmd cmd) 68 | { 69 | subCommands.put(cmd.name, cmd); 70 | if (cmd.hasWorldNameInput) 71 | subCommandsWithWorldNames.add(cmd.name); 72 | } 73 | 74 | @Override 75 | public boolean onCommand(CommandSender sender, Command command, String label, String[] split) 76 | { 77 | Player player = (sender instanceof Player) ? (Player)sender : null; 78 | 79 | // if world name is passed inside quotation marks, handle that, and get List instead of String[] 80 | List params = concatenateQuotedWorldName(split); 81 | 82 | String worldName = null; 83 | // is second parameter the command and first parameter a world name? definitely world name if it was in quotation marks 84 | if (wasWorldQuotation || (params.size() > 1 && !subCommands.containsKey(params.get(0)) && subCommandsWithWorldNames.contains(params.get(1)))) 85 | worldName = params.get(0); 86 | 87 | // no command specified? show command examples / help 88 | if (params.isEmpty()) 89 | params.add(0, "commands"); 90 | 91 | // determined the command name 92 | String cmdName = (worldName == null) ? params.get(0).toLowerCase() : params.get(1).toLowerCase(); 93 | 94 | // remove command name and (if there) world name from front of param array 95 | params.remove(0); 96 | if (worldName != null) 97 | params.remove(0); 98 | 99 | // make sure command is recognized, default to showing command examples / help if not; also check for specified page number 100 | if (!subCommands.containsKey(cmdName)) 101 | { 102 | int page = (player == null) ? 0 : 1; 103 | try 104 | { 105 | page = Integer.parseInt(cmdName); 106 | } 107 | catch(NumberFormatException ignored) 108 | { 109 | sender.sendMessage(WBCmd.C_ERR + "Command not recognized. Showing command list."); 110 | } 111 | cmdName = "commands"; 112 | params.add(0, Integer.toString(page)); 113 | } 114 | 115 | WBCmd subCommand = subCommands.get(cmdName); 116 | 117 | // check permission 118 | if (!Config.HasPermission(player, subCommand.permission)) 119 | return true; 120 | 121 | // if command requires world name when run by console, make sure that's in place 122 | if (player == null && subCommand.hasWorldNameInput && subCommand.consoleRequiresWorldName && worldName == null) 123 | { 124 | sender.sendMessage(WBCmd.C_ERR + "This command requires a world to be specified if run by the console."); 125 | subCommand.sendCmdHelp(sender); 126 | return true; 127 | } 128 | 129 | // make sure valid number of parameters has been provided 130 | if (params.size() < subCommand.minParams || params.size() > subCommand.maxParams) 131 | { 132 | if (subCommand.maxParams == 0) 133 | sender.sendMessage(WBCmd.C_ERR + "This command does not accept any parameters."); 134 | else 135 | sender.sendMessage(WBCmd.C_ERR + "You have not provided a valid number of parameters."); 136 | subCommand.sendCmdHelp(sender); 137 | return true; 138 | } 139 | 140 | // execute command 141 | subCommand.execute(sender, player, params, worldName); 142 | 143 | return true; 144 | } 145 | 146 | 147 | private boolean wasWorldQuotation = false; 148 | 149 | // if world name is surrounded by quotation marks, combine it down and flag wasWorldQuotation if it's first param. 150 | // also return List instead of input primitive String[] 151 | private List concatenateQuotedWorldName(String[] split) 152 | { 153 | wasWorldQuotation = false; 154 | List args = new ArrayList(Arrays.asList(split)); 155 | 156 | int startIndex = -1; 157 | for (int i = 0; i < args.size(); i++) 158 | { 159 | if (args.get(i).startsWith("\"")) 160 | { 161 | startIndex = i; 162 | break; 163 | } 164 | } 165 | if (startIndex == -1) 166 | return args; 167 | 168 | if (args.get(startIndex).endsWith("\"")) 169 | { 170 | args.set(startIndex, args.get(startIndex).substring(1, args.get(startIndex).length() - 1)); 171 | if (startIndex == 0) 172 | wasWorldQuotation = true; 173 | } 174 | else 175 | { 176 | List concat = new ArrayList(args); 177 | Iterator concatI = concat.iterator(); 178 | 179 | // skip past any parameters in front of the one we're starting on 180 | for (int i = 1; i < startIndex + 1; i++) 181 | { 182 | concatI.next(); 183 | } 184 | 185 | StringBuilder quote = new StringBuilder(concatI.next()); 186 | while (concatI.hasNext()) 187 | { 188 | String next = concatI.next(); 189 | concatI.remove(); 190 | quote.append(" "); 191 | quote.append(next); 192 | if (next.endsWith("\"")) 193 | { 194 | concat.set(startIndex, quote.substring(1, quote.length() - 1)); 195 | args = concat; 196 | if (startIndex == 0) 197 | wasWorldQuotation = true; 198 | break; 199 | } 200 | } 201 | } 202 | return args; 203 | } 204 | 205 | public Set getCommandNames() 206 | { 207 | // using TreeSet to sort alphabetically 208 | Set commands = new TreeSet<>(subCommands.keySet()); 209 | // removing default "commands" command as it's not normally shown or run like other commands 210 | commands.remove("commands"); 211 | return commands; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: "${name}" 2 | authors: ["Brettflan", "Puremin0rez"] 3 | description: Efficient, feature-rich plugin for limiting and generating your worlds. 4 | version: "${version}" 5 | api-version: "1.13" 6 | main: "${group}.${name}" 7 | softdepend: 8 | - dynmap 9 | - Multiverse-Core 10 | - Hyperverse 11 | commands: 12 | wborder: 13 | description: Primary command for WorldBorder. 14 | aliases: [wb] 15 | usage: | 16 | / - list available commands (show help). 17 | / help [command] - get help on command usage. 18 | / [world] set [radiusZ] - set world border. 19 | / [world] set [radiusZ] spawn - use spawn point. 20 | / set [radiusZ] - set world border, centered on you. 21 | / set [radiusZ] player - center on player. 22 | / [world] setcorners - set border from corners. 23 | / [world] radius [radiusZ] - change border's radius. 24 | / list - show border information for all worlds. 25 | / shape - set the default border shape. 26 | / shape - same as above, backwards compatible. 27 | / [world] clear - remove border for this world. 28 | / clear all - remove border for all worlds. 29 | / [world] fill [freq] [pad] [force] - generate world to border. 30 | / [world] trim [freq] [pad] - trim world outside of border. 31 | / bypass [player] [on/off] - let player go beyond border. 32 | / bypasslist - list players with border bypass enabled. 33 | / knockback - how far to move the player back. 34 | / wrap [world] - can make border crossings wrap around. 35 | / whoosh - turn knockback effect on or off. 36 | / getmsg - display border message. 37 | / setmsg - set border message. 38 | / wshape [world] - override shape. 39 | / wshape [world] - same as above values. 40 | / delay - time between border checks. 41 | / dynmap - turn DynMap border display on or off. 42 | / dynmapmsg - DynMap border labels will show this. 43 | / remount - delay before remounting after knockback. 44 | / fillautosave - world save interval for Fill process. 45 | / portal - turn portal redirection on or off. 46 | / denypearl - stop ender pearls thrown past the border. 47 | / preventblockplace - stop block placement past border. 48 | / preventmobspawn - stop mob spawning past border. 49 | / reload - re-load data from config.yml. 50 | / debug - turn debug mode on or off. 51 | permissions: 52 | worldborder.*: 53 | description: Grants all WorldBorder permissions 54 | children: 55 | worldborder.allowbypass: false 56 | worldborder.bypass: true 57 | worldborder.bypasslist: true 58 | worldborder.clear: true 59 | worldborder.debug: true 60 | worldborder.delay: true 61 | worldborder.denypearl: true 62 | worldborder.dynmap: true 63 | worldborder.dynmapmsg: true 64 | worldborder.fill: true 65 | worldborder.fillautosave: true 66 | worldborder.getmsg: true 67 | worldborder.help: true 68 | worldborder.knockback: true 69 | worldborder.list: true 70 | worldborder.portal: true 71 | worldborder.preventblockplace: true 72 | worldborder.preventmobspawn: true 73 | worldborder.radius: true 74 | worldborder.reload: true 75 | worldborder.remount: true 76 | worldborder.set: true 77 | worldborder.setmsg: true 78 | worldborder.shape: true 79 | worldborder.trim: true 80 | worldborder.whoosh: true 81 | worldborder.wrap: true 82 | worldborder.wshape: true 83 | worldborder.allowbypass: 84 | description: Can allow a player to bypass the world border 85 | default: false 86 | worldborder.bypass: 87 | description: Can enable bypass mode to go beyond the border 88 | default: op 89 | worldborder.bypasslist: 90 | description: Can get list of players with border bypass enabled 91 | default: op 92 | worldborder.clear: 93 | description: Can remove any border 94 | default: op 95 | worldborder.debug: 96 | description: Can enable/disable debug output to console 97 | default: op 98 | worldborder.delay: 99 | description: Can set the frequency at which the plugin checks for border crossings 100 | default: op 101 | worldborder.denypearl: 102 | description: Can enable/disable direct cancellation of ender pearls thrown past border 103 | default: op 104 | worldborder.dynmap: 105 | description: Can enable/disable DynMap border display integration 106 | default: op 107 | worldborder.dynmapmsg: 108 | description: Can set the label text for borders shown in DynMap 109 | default: op 110 | worldborder.fill: 111 | description: Can fill in (generate) any missing map chunks out to the border 112 | default: op 113 | worldborder.fillautosave: 114 | description: Can set the world save interval for the Fill process 115 | default: op 116 | worldborder.getmsg: 117 | description: Can view the border crossing message 118 | default: op 119 | worldborder.help: 120 | description: Can view the command reference help pages 121 | default: op 122 | worldborder.knockback: 123 | description: Can set the knockback distance for border crossings 124 | default: op 125 | worldborder.list: 126 | description: Can view a list of all borders 127 | default: op 128 | worldborder.portal: 129 | description: Can enable/disable portal redirection to be inside border 130 | default: op 131 | worldborder.preventblockplace: 132 | description: Can prevent placement of blocks outside the border 133 | default: op 134 | worldborder.preventmobspawn: 135 | description: Can prevent spawning of mobs outside the border 136 | default: op 137 | worldborder.radius: 138 | description: Can set the radius of an existing border 139 | default: op 140 | worldborder.reload: 141 | description: Can force the plugin to reload from the config file 142 | default: op 143 | worldborder.remount: 144 | description: Can set the delay before remounting a player to their vehicle after knockback 145 | default: op 146 | worldborder.set: 147 | description: Can set borders for any world 148 | default: op 149 | worldborder.setmsg: 150 | description: Can set the border crossing message 151 | default: op 152 | worldborder.shape: 153 | description: Can set the default shape (round or square) for all borders 154 | default: op 155 | worldborder.trim: 156 | description: Can trim (remove) any excess map chunks outside of the border 157 | default: op 158 | worldborder.whoosh: 159 | description: Can enable/disable "whoosh" knockback effect 160 | default: op 161 | worldborder.wrap: 162 | description: Can set border crossings to wrap around to the other side of the world 163 | default: op 164 | worldborder.wshape: 165 | description: Can set an overriding border shape for a single world 166 | default: op 167 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdTrim.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import java.util.List; 4 | 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.command.*; 7 | import org.bukkit.entity.Player; 8 | 9 | import com.wimbli.WorldBorder.*; 10 | 11 | 12 | public class CmdTrim extends WBCmd 13 | { 14 | public CmdTrim() 15 | { 16 | name = permission = "trim"; 17 | hasWorldNameInput = true; 18 | consoleRequiresWorldName = false; 19 | minParams = 0; 20 | maxParams = 3; 21 | 22 | addCmdExample(nameEmphasizedW() + "[freq] [pad] [type] - trim world outside of border."); 23 | helpText = "This command will remove chunks which are outside the world's border. [freq] is the frequency " + 24 | "of chunks per second that will be checked (default 5000). [pad] is the number of blocks padding kept " + 25 | "beyond the border itself (default 208, to cover player visual range). [type] is the type of data to" + 26 | "scan ('all', 'region', 'poi' or 'entities', default 'all')"; 27 | } 28 | 29 | @Override 30 | public void execute(CommandSender sender, Player player, List params, String worldName) 31 | { 32 | boolean confirm = false; 33 | // check for "cancel", "pause", or "confirm" 34 | if (params.size() >= 1) 35 | { 36 | String check = params.get(0).toLowerCase(); 37 | 38 | if (check.equals("cancel") || check.equals("stop")) 39 | { 40 | if (!makeSureTrimIsRunning(sender)) 41 | return; 42 | sender.sendMessage(C_HEAD + "Cancelling the world map trimming task."); 43 | trimDefaults(); 44 | Config.StopTrimTask(); 45 | return; 46 | } 47 | else if (check.equals("pause")) 48 | { 49 | if (!makeSureTrimIsRunning(sender)) 50 | return; 51 | Config.trimTask.pause(); 52 | sender.sendMessage(C_HEAD + "The world map trimming task is now " + (Config.trimTask.isPaused() ? "" : "un") + "paused."); 53 | return; 54 | } 55 | 56 | confirm = check.equals("confirm"); 57 | } 58 | 59 | // if not just confirming, make sure a world name is available 60 | if (worldName == null && !confirm) 61 | { 62 | if (player != null) 63 | worldName = player.getWorld().getName(); 64 | else 65 | { 66 | sendErrorAndHelp(sender, "You must specify a world!"); 67 | return; 68 | } 69 | } 70 | 71 | // colorized "/wb trim " 72 | String cmd = cmd(sender) + nameEmphasized() + C_CMD; 73 | 74 | // make sure Trim isn't already running 75 | if (Config.trimTask != null && Config.trimTask.valid()) 76 | { 77 | sender.sendMessage(C_ERR + "The world map trimming task is already running."); 78 | sender.sendMessage(C_DESC + "You can cancel at any time with " + cmd + "cancel" + C_DESC + ", or pause/unpause with " + cmd + "pause" + C_DESC + "."); 79 | return; 80 | } 81 | 82 | // set frequency and/or padding if those were specified 83 | try 84 | { 85 | if (params.size() >= 1 && !confirm) 86 | trimFrequency = Math.abs(Integer.parseInt(params.get(0))); 87 | if (params.size() >= 2 && !confirm) 88 | trimPadding = Math.abs(Integer.parseInt(params.get(1))); 89 | } 90 | catch(NumberFormatException ex) 91 | { 92 | sendErrorAndHelp(sender, "The frequency and padding values must be integers."); 93 | trimDefaults(); 94 | return; 95 | } 96 | if (trimFrequency <= 0) 97 | { 98 | sendErrorAndHelp(sender, "The frequency value must be greater than zero."); 99 | trimDefaults(); 100 | return; 101 | } 102 | 103 | // set type (region, poi or entities) 104 | if (params.size() >= 3 && !confirm) 105 | { 106 | if (params.get(2).equals("region")) 107 | trimType = WorldFileDataType.REGION; 108 | else if (params.get(2).equals("poi")) 109 | trimType = WorldFileDataType.POI; 110 | else if (params.get(2).equals("entities")) 111 | trimType = WorldFileDataType.ENTITIES; 112 | else if (params.get(2).equals("all")) 113 | trimType = WorldFileDataType.ALL; 114 | else 115 | { 116 | sendErrorAndHelp(sender, "The type value must be 'all', 'region', 'poi' or 'entities'." + params.get(2)); 117 | trimDefaults(); 118 | return; 119 | } 120 | } 121 | 122 | // set world if it was specified 123 | if (worldName != null) 124 | trimWorld = worldName; 125 | 126 | String printType = "regions, POIs and entities"; 127 | if (trimType == WorldFileDataType.REGION) printType = "regions"; 128 | if (trimType == WorldFileDataType.POI) printType = "POIs"; 129 | if (trimType == WorldFileDataType.ENTITIES) printType = "entities"; 130 | 131 | if (confirm) 132 | { // command confirmed, go ahead with it 133 | if (trimWorld.isEmpty()) 134 | { 135 | sendErrorAndHelp(sender, "You must first use this command successfully without confirming."); 136 | return; 137 | } 138 | 139 | if (player != null) 140 | Config.log("Trimming world beyond border at the command of player \"" + player.getName() + "\"."); 141 | 142 | int ticks = 1, repeats = 1; 143 | if (trimFrequency > 20) 144 | repeats = trimFrequency / 20; 145 | else 146 | ticks = 20 / trimFrequency; 147 | 148 | Config.trimTask = new WorldTrimTask(Bukkit.getServer(), player, trimWorld, trimPadding, repeats, trimType); 149 | if (Config.trimTask.valid()) 150 | { 151 | int task = Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(WorldBorder.plugin, Config.trimTask, ticks, ticks); 152 | Config.trimTask.setTaskID(task); 153 | sender.sendMessage("WorldBorder " + printType + " trimming task for world \"" + trimWorld + "\" started."); 154 | } 155 | else 156 | sender.sendMessage(C_ERR + "The world map trimming task failed to start."); 157 | 158 | trimDefaults(); 159 | } 160 | else 161 | { 162 | if (trimWorld.isEmpty()) 163 | { 164 | sendErrorAndHelp(sender, "You must first specify a valid world."); 165 | return; 166 | } 167 | 168 | sender.sendMessage(C_HEAD + "World trimming task is ready for world \"" + trimWorld + "\", attempting to process up to " + trimFrequency + " chunks per second (default 20). The map will be trimmed past " + trimPadding + " blocks beyond the border (default " + defaultPadding + "). Files of " + printType + " will be trimmed (default all)."); 169 | sender.sendMessage(C_HEAD + "This process can take a very long time depending on the world's overall size. Also, depending on the chunk processing rate, players may experience lag for the duration."); 170 | sender.sendMessage(C_DESC + "You should now use " + cmd + "confirm" + C_DESC + " to start the process."); 171 | sender.sendMessage(C_DESC + "You can cancel at any time with " + cmd + "cancel" + C_DESC + ", or pause/unpause with " + cmd + "pause" + C_DESC + "."); 172 | } 173 | } 174 | 175 | 176 | /* with "view-distance=10" in server.properties on a fast VM test server and "Render Distance: Far" in client, 177 | * hitting border during testing was loading 11+ chunks beyond the border in a couple of directions (10 chunks in 178 | * the other two directions). This could be worse on a more loaded or worse server, so: 179 | */ 180 | private final int defaultPadding = CoordXZ.chunkToBlock(13); 181 | 182 | private String trimWorld = ""; 183 | private int trimFrequency = 5000; 184 | private int trimPadding = defaultPadding; 185 | private WorldFileDataType trimType = WorldFileDataType.ALL; 186 | 187 | private void trimDefaults() 188 | { 189 | trimWorld = ""; 190 | trimFrequency = 5000; 191 | trimPadding = defaultPadding; 192 | trimType = WorldFileDataType.ALL; 193 | } 194 | 195 | private boolean makeSureTrimIsRunning(CommandSender sender) 196 | { 197 | if (Config.trimTask != null && Config.trimTask.valid()) 198 | return true; 199 | sendErrorAndHelp(sender, "The world map trimming task is not currently running."); 200 | return false; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/DynMapFeatures.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.plugin.Plugin; 10 | import org.bukkit.World; 11 | 12 | import org.dynmap.DynmapAPI; 13 | import org.dynmap.markers.AreaMarker; 14 | import org.dynmap.markers.CircleMarker; 15 | import org.dynmap.markers.MarkerAPI; 16 | import org.dynmap.markers.MarkerSet; 17 | 18 | 19 | public class DynMapFeatures 20 | { 21 | private static DynmapAPI api; 22 | private static MarkerAPI markApi; 23 | private static MarkerSet markSet; 24 | private static int lineWeight = 3; 25 | private static double lineOpacity = 1.0; 26 | private static int lineColor = 0xFF0000; 27 | 28 | // Whether re-rendering functionality is available 29 | public static boolean renderEnabled() 30 | { 31 | return api != null; 32 | } 33 | 34 | // Whether circular border markers are available 35 | public static boolean borderEnabled() 36 | { 37 | return markApi != null; 38 | } 39 | 40 | public static void setup() 41 | { 42 | Plugin test = Bukkit.getServer().getPluginManager().getPlugin("dynmap"); 43 | if (test == null || !test.isEnabled()) return; 44 | 45 | api = (DynmapAPI)test; 46 | 47 | // make sure DynMap version is new enough to include circular markers 48 | try 49 | { 50 | Class.forName("org.dynmap.markers.CircleMarker"); 51 | 52 | // for version 0.35 of DynMap, CircleMarkers had just been introduced and were bugged (center position always 0,0) 53 | if (api.getDynmapVersion().startsWith("0.35-")) 54 | throw new ClassNotFoundException(); 55 | } 56 | catch (ClassNotFoundException ex) 57 | { 58 | Config.logConfig("DynMap is available, but border display is currently disabled: you need DynMap v0.36 or newer."); 59 | return; 60 | } 61 | catch (NullPointerException ex) 62 | { 63 | Config.logConfig("DynMap is present, but an NPE (type 1) was encountered while trying to integrate. Border display disabled."); 64 | return; 65 | } 66 | 67 | try 68 | { 69 | markApi = api.getMarkerAPI(); 70 | if (markApi == null) return; 71 | } 72 | catch (NullPointerException ex) 73 | { 74 | Config.logConfig("DynMap is present, but an NPE (type 2) was encountered while trying to integrate. Border display disabled."); 75 | return; 76 | } 77 | 78 | // go ahead and show borders for all worlds 79 | showAllBorders(); 80 | 81 | Config.logConfig("Successfully hooked into DynMap for the ability to display borders."); 82 | } 83 | 84 | 85 | /* 86 | * Re-rendering methods, used for updating trimmed chunks to show them as gone 87 | * Sadly, not currently working. Might not even be possible to make it work. 88 | */ 89 | 90 | public static void renderRegion(String worldName, CoordXZ coord) 91 | { 92 | if (!renderEnabled()) return; 93 | 94 | World world = Bukkit.getWorld(worldName); 95 | int y = (world != null) ? world.getMaxHeight() : 255; 96 | int x = CoordXZ.regionToBlock(coord.x); 97 | int z = CoordXZ.regionToBlock(coord.z); 98 | api.triggerRenderOfVolume(worldName, x, 0, z, x+511, y, z+511); 99 | } 100 | 101 | public static void renderChunks(String worldName, List coords) 102 | { 103 | if (!renderEnabled()) return; 104 | 105 | World world = Bukkit.getWorld(worldName); 106 | int y = (world != null) ? world.getMaxHeight() : 255; 107 | 108 | for (CoordXZ coord : coords) 109 | { 110 | renderChunk(worldName, coord, y); 111 | } 112 | } 113 | 114 | public static void renderChunk(String worldName, CoordXZ coord, int maxY) 115 | { 116 | if (!renderEnabled()) return; 117 | 118 | int x = CoordXZ.chunkToBlock(coord.x); 119 | int z = CoordXZ.chunkToBlock(coord.z); 120 | api.triggerRenderOfVolume(worldName, x, 0, z, x+15, maxY, z+15); 121 | } 122 | 123 | 124 | /* 125 | * Methods for displaying our borders on DynMap's world maps 126 | */ 127 | 128 | private static Map roundBorders = new HashMap(); 129 | private static Map squareBorders = new HashMap(); 130 | 131 | public static void showAllBorders() 132 | { 133 | if (!borderEnabled()) return; 134 | 135 | // in case any borders are already shown 136 | removeAllBorders(); 137 | 138 | if (!Config.DynmapBorderEnabled()) 139 | { 140 | // don't want to show the marker set in DynMap if our integration is disabled 141 | if (markSet != null) 142 | markSet.deleteMarkerSet(); 143 | markSet = null; 144 | return; 145 | } 146 | 147 | // make sure the marker set is initialized 148 | markSet = markApi.getMarkerSet("worldborder.markerset"); 149 | if(markSet == null) 150 | markSet = markApi.createMarkerSet("worldborder.markerset", "WorldBorder", null, false); 151 | else 152 | markSet.setMarkerSetLabel("WorldBorder"); 153 | markSet.setLayerPriority(Config.DynmapPriority()); 154 | markSet.setHideByDefault(Config.DynmapHideByDefault()); 155 | Map borders = Config.getBorders(); 156 | for(Entry stringBorderDataEntry : borders.entrySet()) 157 | { 158 | String worldName = stringBorderDataEntry.getKey(); 159 | BorderData border = stringBorderDataEntry.getValue(); 160 | showBorder(worldName, border); 161 | } 162 | } 163 | 164 | public static void showBorder(String worldName, BorderData border) 165 | { 166 | if (!borderEnabled()) return; 167 | 168 | if (!Config.DynmapBorderEnabled()) return; 169 | 170 | if ((border.getShape() == null) ? Config.ShapeRound() : border.getShape()) 171 | showRoundBorder(worldName, border); 172 | else 173 | showSquareBorder(worldName, border); 174 | } 175 | 176 | private static void showRoundBorder(String worldName, BorderData border) 177 | { 178 | if (squareBorders.containsKey(worldName)) 179 | removeBorder(worldName); 180 | 181 | World world = Bukkit.getWorld(worldName); 182 | int y = (world != null) ? world.getMaxHeight() : 255; 183 | 184 | CircleMarker marker = roundBorders.get(worldName); 185 | if (marker == null) 186 | { 187 | marker = markSet.createCircleMarker("worldborder_"+worldName, Config.DynmapMessage(), false, worldName, border.getX(), y, border.getZ(), border.getRadiusX(), border.getRadiusZ(), true); 188 | marker.setLineStyle(lineWeight, lineOpacity, lineColor); 189 | marker.setFillStyle(0.0, 0x000000); 190 | roundBorders.put(worldName, marker); 191 | } 192 | else 193 | { 194 | marker.setCenter(worldName, border.getX(), y, border.getZ()); 195 | marker.setRadius(border.getRadiusX(), border.getRadiusZ()); 196 | } 197 | } 198 | 199 | private static void showSquareBorder(String worldName, BorderData border) 200 | { 201 | if (roundBorders.containsKey(worldName)) 202 | removeBorder(worldName); 203 | 204 | // corners of the square border 205 | double[] xVals = {border.getX() - border.getRadiusX(), border.getX() + border.getRadiusX()}; 206 | double[] zVals = {border.getZ() - border.getRadiusZ(), border.getZ() + border.getRadiusZ()}; 207 | 208 | AreaMarker marker = squareBorders.get(worldName); 209 | if (marker == null) 210 | { 211 | marker = markSet.createAreaMarker("worldborder_"+worldName, Config.DynmapMessage(), false, worldName, xVals, zVals, true); 212 | marker.setLineStyle(3, 1.0, 0xFF0000); 213 | marker.setFillStyle(0.0, 0x000000); 214 | squareBorders.put(worldName, marker); 215 | } 216 | else 217 | { 218 | marker.setCornerLocations(xVals, zVals); 219 | } 220 | } 221 | 222 | public static void removeAllBorders() 223 | { 224 | if (!borderEnabled()) return; 225 | 226 | for(CircleMarker marker : roundBorders.values()) 227 | { 228 | marker.deleteMarker(); 229 | } 230 | roundBorders.clear(); 231 | 232 | for(AreaMarker marker : squareBorders.values()) 233 | { 234 | marker.deleteMarker(); 235 | } 236 | squareBorders.clear(); 237 | } 238 | 239 | public static void removeBorder(String worldName) 240 | { 241 | if (!borderEnabled()) return; 242 | 243 | CircleMarker marker = roundBorders.remove(worldName); 244 | if (marker != null) 245 | marker.deleteMarker(); 246 | 247 | AreaMarker marker2 = squareBorders.remove(worldName); 248 | if (marker2 != null) 249 | marker2.deleteMarker(); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WorldFileData.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import java.io.*; 4 | import java.util.ArrayList; 5 | import java.nio.ByteBuffer; 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.nio.IntBuffer; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.World; 14 | 15 | // image output stuff, for debugging method at bottom of this file 16 | import java.awt.*; 17 | import java.awt.image.*; 18 | import javax.imageio.*; 19 | 20 | 21 | // 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/ 22 | 23 | public class WorldFileData 24 | { 25 | private transient World world; 26 | private transient File regionFolder = null; 27 | private transient File[] regionFiles = null; 28 | private transient Player notifyPlayer = null; 29 | private transient Map> regionChunkExistence = Collections.synchronizedMap(new HashMap>()); 30 | 31 | public static WorldFileData create(World world, Player notifyPlayer) 32 | { 33 | return create(world, notifyPlayer, WorldFileDataType.REGION, false); 34 | } 35 | 36 | // 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. 37 | // Defaults to "REGION" if "type" is "WorldFileDataType.ALL" 38 | public static WorldFileData create(World world, Player notifyPlayer, WorldFileDataType type, boolean silent) 39 | { 40 | WorldFileData newData = new WorldFileData(world, notifyPlayer); 41 | 42 | String subFolder = "region"; 43 | if (type == WorldFileDataType.REGION) subFolder = "region"; 44 | else if (type == WorldFileDataType.POI) subFolder = "poi"; 45 | else if (type == WorldFileDataType.ENTITIES) subFolder = "entities"; 46 | 47 | newData.regionFolder = new File(newData.world.getWorldFolder(), subFolder); 48 | if (!newData.regionFolder.exists() || !newData.regionFolder.isDirectory()) 49 | { 50 | // check for region folder inside a DIM* folder (DIM-1 for nether, DIM1 for end, DIMwhatever for custom world types) 51 | File[] possibleDimFolders = newData.world.getWorldFolder().listFiles(new DimFolderFileFilter()); 52 | for (File possibleDimFolder : possibleDimFolders) 53 | { 54 | File possible = new File(newData.world.getWorldFolder(), possibleDimFolder.getName() + File.separator + subFolder); 55 | if (possible.exists() && possible.isDirectory()) 56 | { 57 | newData.regionFolder = possible; 58 | break; 59 | } 60 | } 61 | if (!newData.regionFolder.exists() || !newData.regionFolder.isDirectory()) 62 | { 63 | if (!silent) 64 | newData.sendMessage("Could not validate folder for world's "+subFolder+" files. Looked in "+newData.world.getWorldFolder().getPath()+" for valid DIM* folder with a region folder in it."); 65 | return null; 66 | } 67 | } 68 | 69 | // Accepted region file formats: MCR is from late beta versions through 1.1, MCA is from 1.2+ 70 | newData.regionFiles = newData.regionFolder.listFiles(new ExtFileFilter(".MCA")); 71 | if (newData.regionFiles == null || newData.regionFiles.length == 0) 72 | { 73 | newData.regionFiles = newData.regionFolder.listFiles(new ExtFileFilter(".MCR")); 74 | if (newData.regionFiles == null || newData.regionFiles.length == 0) 75 | { 76 | if (!silent) 77 | newData.sendMessage("Could not find any "+subFolder+" files. Looked in: "+newData.regionFolder.getPath()); 78 | return null; 79 | } 80 | } 81 | 82 | return newData; 83 | } 84 | 85 | // the constructor is private; use create() method above to create an instance of this class. 86 | private WorldFileData(World world, Player notifyPlayer) 87 | { 88 | this.world = world; 89 | this.notifyPlayer = notifyPlayer; 90 | } 91 | 92 | 93 | // number of region files this world has 94 | public int regionFileCount() 95 | { 96 | return regionFiles.length; 97 | } 98 | 99 | // folder where world's region files are located 100 | public File regionFolder() 101 | { 102 | return regionFolder; 103 | } 104 | 105 | // return entire list of region files 106 | public File[] regionFiles() 107 | { 108 | return regionFiles.clone(); 109 | } 110 | 111 | // return a region file by index 112 | public File regionFile(int index) 113 | { 114 | if (regionFiles.length < index) 115 | return null; 116 | return regionFiles[index]; 117 | } 118 | 119 | // get the X and Z world coordinates of the region from the filename 120 | public CoordXZ regionFileCoordinates(int index) 121 | { 122 | File regionFile = this.regionFile(index); 123 | String[] coords = regionFile.getName().split("\\."); 124 | int x, z; 125 | try 126 | { 127 | x = Integer.parseInt(coords[1]); 128 | z = Integer.parseInt(coords[2]); 129 | return new CoordXZ (x, z); 130 | } 131 | catch(Exception ex) 132 | { 133 | sendMessage("Error! Region file found with abnormal name: "+regionFile.getName()); 134 | return null; 135 | } 136 | } 137 | 138 | 139 | // Find out if the chunk at the given coordinates exists. 140 | public boolean doesChunkExist(int x, int z) 141 | { 142 | CoordXZ region = new CoordXZ(CoordXZ.chunkToRegion(x), CoordXZ.chunkToRegion(z)); 143 | List regionChunks = this.getRegionData(region); 144 | // Bukkit.getLogger().info("x: "+x+" z: "+z+" offset: "+coordToRegionOffset(x, z)); 145 | return regionChunks.get(coordToRegionOffset(x, z)); 146 | } 147 | 148 | // Find out if the chunk at the given coordinates has been fully generated. 149 | // Minecraft only fully generates a chunk when adjacent chunks are also loaded. 150 | public boolean isChunkFullyGenerated(int x, int z) 151 | { // if all adjacent chunks exist, it should be a safe enough bet that this one is fully generated 152 | // For 1.13+, due to world gen changes, this is now effectively a 3 chunk radius requirement vs a 1 chunk radius 153 | for (int xx = x-3; xx <= x+3; xx++) 154 | { 155 | for (int zz = z-3; zz <= z+3; zz++) 156 | { 157 | if (!doesChunkExist(xx, zz)) 158 | return false; 159 | } 160 | } 161 | return true; 162 | } 163 | 164 | // Method to let us know a chunk has been generated, to update our region map. 165 | public void chunkExistsNow(int x, int z) 166 | { 167 | CoordXZ region = new CoordXZ(CoordXZ.chunkToRegion(x), CoordXZ.chunkToRegion(z)); 168 | List regionChunks = this.getRegionData(region); 169 | regionChunks.set(coordToRegionOffset(x, z), true); 170 | } 171 | 172 | 173 | 174 | // region is 32 * 32 chunks; chunk pointers are stored in region file at position: x + z*32 (32 * 32 chunks = 1024) 175 | // input x and z values can be world-based chunk coordinates or local-to-region chunk coordinates either one 176 | private int coordToRegionOffset(int x, int z) 177 | { 178 | // "%" modulus is used to convert potential world coordinates to definitely be local region coordinates 179 | x = x % 32; 180 | z = z % 32; 181 | // similarly, for local coordinates, we need to wrap negative values around 182 | if (x < 0) x += 32; 183 | if (z < 0) z += 32; 184 | // return offset position for the now definitely local x and z values 185 | return (x + (z * 32)); 186 | } 187 | 188 | private List getRegionData(CoordXZ region) 189 | { 190 | List data = regionChunkExistence.get(region); 191 | if (data != null) 192 | return data; 193 | 194 | // data for the specified region isn't loaded yet, so init it as empty and try to find the file and load the data 195 | data = new ArrayList(1024); 196 | for (int i = 0; i < 1024; i++) 197 | { 198 | data.add(Boolean.FALSE); 199 | } 200 | 201 | for (int i = 0; i < regionFiles.length; i++) 202 | { 203 | CoordXZ coord = regionFileCoordinates(i); 204 | // is this region file the one we're looking for? 205 | if ( ! coord.equals(region)) 206 | continue; 207 | 208 | try (final RandomAccessFile regionData = new RandomAccessFile(this.regionFile(i), "r")) 209 | { 210 | final byte[] header = new byte[8192]; 211 | regionData.readFully(header); 212 | IntBuffer headerAsInts = ByteBuffer.wrap(header).asIntBuffer(); 213 | 214 | // first 4096 bytes of region file consists of 4-byte int pointers to chunk data in the file (32*32 chunks = 1024; 1024 chunks * 4 bytes each = 4096) 215 | for (int j = 0; j < 1024; j++) 216 | { 217 | // if chunk pointer data is 0, chunk doesn't exist yet; otherwise, it does 218 | if (headerAsInts.get() != 0) 219 | data.set(j, true); 220 | } 221 | // Read timestamps 222 | for (int j = 0; j < 1024; j++) 223 | { 224 | // if timestamp is zero, it is protochunk (ignore it) 225 | if ((headerAsInts.get() == 0) && data.get(j)) 226 | data.set(j, false); 227 | } 228 | } 229 | catch (FileNotFoundException ex) 230 | { 231 | sendMessage("Error! Could not open region file to find generated chunks: "+this.regionFile(i).getName()); 232 | } 233 | catch (IOException ex) 234 | { 235 | sendMessage("Error! Could not read region file to find generated chunks: "+this.regionFile(i).getName()); 236 | } 237 | } 238 | regionChunkExistence.put(region, data); 239 | // testImage(region, data); 240 | return data; 241 | } 242 | 243 | // send a message to the server console/log and possibly to an in-game player 244 | private void sendMessage(String text) 245 | { 246 | Config.log("[WorldData] " + text); 247 | if (notifyPlayer != null && notifyPlayer.isOnline()) 248 | notifyPlayer.sendMessage("[WorldData] " + text); 249 | } 250 | 251 | // file filter used for region files 252 | private static class ExtFileFilter implements FileFilter 253 | { 254 | String ext; 255 | public ExtFileFilter(String extension) 256 | { 257 | this.ext = extension.toLowerCase(); 258 | } 259 | 260 | @Override 261 | public boolean accept(File file) 262 | { 263 | return ( 264 | file.exists() 265 | && file.isFile() 266 | && file.getName().toLowerCase().endsWith(ext) 267 | ); 268 | } 269 | } 270 | 271 | // file filter used for DIM* folders (for nether, End, and custom world types) 272 | private static class DimFolderFileFilter implements FileFilter 273 | { 274 | @Override 275 | public boolean accept(File file) 276 | { 277 | return ( 278 | file.exists() 279 | && file.isDirectory() 280 | && file.getName().toLowerCase().startsWith("dim") 281 | ); 282 | } 283 | } 284 | 285 | 286 | // crude chunk map PNG image output, for debugging 287 | private void testImage(CoordXZ region, List data) { 288 | int width = 32; 289 | int height = 32; 290 | BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 291 | Graphics2D g2 = bi.createGraphics(); 292 | int current = 0; 293 | g2.setColor(Color.BLACK); 294 | 295 | for (int x = 0; x < 32; x++) 296 | { 297 | for (int z = 0; z < 32; z++) 298 | { 299 | if (data.get(current).booleanValue()) 300 | g2.fillRect(x,z, x+1, z+1); 301 | current++; 302 | } 303 | } 304 | 305 | File f = new File("region_"+region.x+"_"+region.z+"_.png"); 306 | Config.log(f.getAbsolutePath()); 307 | try { 308 | // png is an image format (like gif or jpg) 309 | ImageIO.write(bi, "png", f); 310 | } catch (IOException ex) { 311 | Config.log("[SEVERE]" + ex.getLocalizedMessage()); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.Server; 13 | import org.bukkit.World; 14 | 15 | import com.wimbli.WorldBorder.Events.WorldBorderTrimFinishedEvent; 16 | import com.wimbli.WorldBorder.Events.WorldBorderTrimStartEvent; 17 | 18 | 19 | public class WorldTrimTask implements Runnable 20 | { 21 | // general task-related reference data 22 | private transient Server server = null; 23 | private transient World world = null; 24 | private transient WorldFileData worldData = null; 25 | private transient BorderData border = null; 26 | private transient boolean readyToGo = false; 27 | private transient boolean paused = false; 28 | private transient int taskID = -1; 29 | private transient Player notifyPlayer = null; 30 | private transient int chunksPerRun = 1; 31 | private transient WorldFileDataType typeToTrim = null; 32 | 33 | // values for what chunk in the current region we're at 34 | private transient WorldFileDataType currentType = WorldFileDataType.REGION; 35 | private transient int currentRegion = -1; // region(file) we're at in regionFiles 36 | private transient int regionX = 0; // X location value of the current region 37 | private transient int regionZ = 0; // X location value of the current region 38 | private transient int currentChunk = 0; // chunk we've reached in the current region (regionChunks) 39 | private transient List regionChunks = new ArrayList(1024); 40 | private transient List trimChunks = new ArrayList(1024); 41 | private transient int counter = 0; 42 | 43 | // for reporting progress back to user occasionally 44 | private transient long lastReport = Config.Now(); 45 | private transient int reportTarget = 0; 46 | private transient int reportTotal = 0; 47 | private transient int reportTrimmedRegions = 0; 48 | private transient int reportTrimmedChunks = 0; 49 | 50 | 51 | public WorldTrimTask(Server theServer, Player player, String worldName, int trimDistance, int chunksPerRun) 52 | { 53 | this(theServer, player, worldName, trimDistance, chunksPerRun, WorldFileDataType.ALL); 54 | } 55 | 56 | public WorldTrimTask(Server theServer, Player player, String worldName, int trimDistance, int chunksPerRun, WorldFileDataType type) 57 | { 58 | this.server = theServer; 59 | this.notifyPlayer = player; 60 | this.chunksPerRun = chunksPerRun; 61 | this.typeToTrim = type; 62 | if (type != WorldFileDataType.ALL) this.currentType = type; 63 | else this.currentType = WorldFileDataType.REGION; 64 | 65 | this.world = server.getWorld(worldName); 66 | if (this.world == null) 67 | { 68 | if (worldName.isEmpty()) 69 | sendMessage("You must specify a world!"); 70 | else 71 | sendMessage("World \"" + worldName + "\" not found!"); 72 | this.stop(); 73 | return; 74 | } 75 | 76 | this.border = (Config.Border(worldName) == null) ? null : Config.Border(worldName).copy(); 77 | if (this.border == null) 78 | { 79 | sendMessage("No border found for world \"" + worldName + "\"!"); 80 | this.stop(); 81 | return; 82 | } 83 | 84 | this.border.setRadiusX(border.getRadiusX() + trimDistance); 85 | this.border.setRadiusZ(border.getRadiusZ() + trimDistance); 86 | 87 | worldData = WorldFileData.create(world, notifyPlayer, currentType, false); 88 | if (worldData == null) 89 | { 90 | this.stop(); 91 | return; 92 | } 93 | 94 | // each region file covers up to 1024 chunks; with all operations we might need to do, let's figure 3X that 95 | this.reportTarget = worldData.regionFileCount() * 3072; 96 | 97 | // queue up the first file 98 | if (!nextFile()) 99 | return; 100 | 101 | this.readyToGo = true; 102 | Bukkit.getServer().getPluginManager().callEvent(new WorldBorderTrimStartEvent(this)); 103 | } 104 | 105 | public void setTaskID(int ID) 106 | { 107 | this.taskID = ID; 108 | } 109 | 110 | 111 | public void run() 112 | { 113 | if (server == null || !readyToGo || paused) 114 | return; 115 | 116 | // this is set so it only does one iteration at a time, no matter how frequently the timer fires 117 | readyToGo = false; 118 | // 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 119 | long loopStartTime = Config.Now(); 120 | 121 | counter = 0; 122 | while (counter <= chunksPerRun) 123 | { 124 | // in case the task has been paused while we're repeating... 125 | if (paused) 126 | return; 127 | 128 | long now = Config.Now(); 129 | 130 | // every 5 seconds or so, give basic progress report to let user know how it's going 131 | if (now > lastReport + 5000) 132 | reportProgress(); 133 | 134 | // 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 135 | if (now > loopStartTime + 45) 136 | { 137 | readyToGo = true; 138 | return; 139 | } 140 | 141 | if (regionChunks.isEmpty()) 142 | addCornerChunks(); 143 | else if (currentChunk == 4) 144 | { // determine if region is completely _inside_ border based on corner chunks 145 | if (trimChunks.isEmpty()) 146 | { // it is, so skip it and move on to next file 147 | counter += 4; 148 | nextFile(); 149 | continue; 150 | } 151 | addEdgeChunks(); 152 | addInnerChunks(); 153 | } 154 | else if (currentChunk == 124 && trimChunks.size() == 124) 155 | { // region is completely _outside_ border based on edge chunks, so delete file and move on to next 156 | counter += 16; 157 | trimChunks = regionChunks; 158 | unloadChunks(); 159 | reportTrimmedRegions++; 160 | File regionFile = worldData.regionFile(currentRegion); 161 | if (!regionFile.delete()) 162 | { 163 | sendMessage("Error! Region file which is outside the border could not be deleted: "+regionFile.getName()); 164 | wipeChunks(); 165 | } 166 | else 167 | { 168 | // if DynMap is installed, re-render the trimmed region ... disabled since it's not currently working, oh well 169 | // DynMapFeatures.renderRegion(world.getName(), new CoordXZ(regionX, regionZ)); 170 | } 171 | 172 | nextFile(); 173 | continue; 174 | } 175 | else if (currentChunk == 1024) 176 | { // last chunk of the region has been checked, time to wipe out whichever chunks are outside the border 177 | counter += 32; 178 | unloadChunks(); 179 | wipeChunks(); 180 | nextFile(); 181 | continue; 182 | } 183 | 184 | // check whether chunk is inside the border or not, add it to the "trim" list if not 185 | CoordXZ chunk = regionChunks.get(currentChunk); 186 | if (!isChunkInsideBorder(chunk)) 187 | trimChunks.add(chunk); 188 | 189 | currentChunk++; 190 | counter++; 191 | } 192 | 193 | reportTotal += counter; 194 | 195 | // ready for the next iteration to run 196 | readyToGo = true; 197 | } 198 | 199 | // Advance to the next region file. Returns true if successful, false if the next file isn't accessible for any reason 200 | private boolean nextFile() 201 | { 202 | reportTotal = currentRegion * 3072; 203 | currentRegion++; 204 | regionX = regionZ = currentChunk = 0; 205 | regionChunks = new ArrayList(1024); 206 | trimChunks = new ArrayList(1024); 207 | 208 | // have we already handled all region files? 209 | if (currentRegion >= worldData.regionFileCount()) 210 | { // hey, we're done 211 | paused = true; 212 | readyToGo = false; 213 | finish(); 214 | return false; 215 | } 216 | 217 | counter += 16; 218 | 219 | // get the X and Z coordinates of the current region 220 | CoordXZ coord = worldData.regionFileCoordinates(currentRegion); 221 | if (coord == null) 222 | return false; 223 | 224 | regionX = coord.x; 225 | regionZ = coord.z; 226 | return true; 227 | } 228 | 229 | // add just the 4 corner chunks of the region; can determine if entire region is _inside_ the border 230 | private void addCornerChunks() 231 | { 232 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ))); 233 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ))); 234 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ) + 31)); 235 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ) + 31)); 236 | } 237 | 238 | // add all chunks along the 4 edges of the region (minus the corners); can determine if entire region is _outside_ the border 239 | private void addEdgeChunks() 240 | { 241 | int chunkX = 0, chunkZ; 242 | 243 | for (chunkZ = 1; chunkZ < 31; chunkZ++) 244 | { 245 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 246 | } 247 | chunkX = 31; 248 | for (chunkZ = 1; chunkZ < 31; chunkZ++) 249 | { 250 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 251 | } 252 | chunkZ = 0; 253 | for (chunkX = 1; chunkX < 31; chunkX++) 254 | { 255 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 256 | } 257 | chunkZ = 31; 258 | for (chunkX = 1; chunkX < 31; chunkX++) 259 | { 260 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 261 | } 262 | counter += 4; 263 | } 264 | 265 | // add the remaining interior chunks (after corners and edges) 266 | private void addInnerChunks() 267 | { 268 | for (int chunkX = 1; chunkX < 31; chunkX++) 269 | { 270 | for (int chunkZ = 1; chunkZ < 31; chunkZ++) 271 | { 272 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 273 | } 274 | } 275 | counter += 32; 276 | } 277 | 278 | // make sure chunks set to be trimmed are not currently loaded by the server 279 | private void unloadChunks() 280 | { 281 | for (CoordXZ unload : trimChunks) 282 | { 283 | if (world.isChunkLoaded(unload.x, unload.z)) 284 | world.unloadChunk(unload.x, unload.z, false); 285 | } 286 | counter += trimChunks.size(); 287 | } 288 | 289 | // edit region file to wipe all chunk pointers for chunks outside the border 290 | private void wipeChunks() 291 | { 292 | File regionFile = worldData.regionFile(currentRegion); 293 | if (!regionFile.canWrite()) 294 | { 295 | if (!regionFile.setWritable(true)) 296 | throw new RuntimeException(); 297 | 298 | if (!regionFile.canWrite()) 299 | { 300 | sendMessage("Error! region file is locked and can't be trimmed: "+regionFile.getName()); 301 | return; 302 | } 303 | } 304 | 305 | // since our stored chunk positions are based on world, we need to offset those to positions in the region file 306 | int offsetX = CoordXZ.regionToChunk(regionX); 307 | int offsetZ = CoordXZ.regionToChunk(regionZ); 308 | long wipePos = 0; 309 | int chunkCount = 0; 310 | 311 | try 312 | { 313 | RandomAccessFile unChunk = new RandomAccessFile(regionFile, "rwd"); 314 | for (CoordXZ wipe : trimChunks) 315 | { 316 | // if the chunk pointer is empty (chunk doesn't technically exist), no need to wipe the already empty pointer 317 | if (!worldData.doesChunkExist(wipe.x, wipe.z)) 318 | continue; 319 | 320 | // wipe this extraneous chunk's pointer... note that this method isn't perfect since the actual chunk data is left orphaned, 321 | // but Minecraft will overwrite the orphaned data sector if/when another chunk is created in the region, so it's not so bad 322 | wipePos = 4 * ((wipe.x - offsetX) + ((wipe.z - offsetZ) * 32)); 323 | unChunk.seek(wipePos); 324 | unChunk.writeInt(0); 325 | chunkCount++; 326 | } 327 | unChunk.close(); 328 | 329 | // if DynMap is installed, re-render the trimmed chunks ... disabled since it's not currently working, oh well 330 | // DynMapFeatures.renderChunks(world.getName(), trimChunks); 331 | 332 | reportTrimmedChunks += chunkCount; 333 | } 334 | catch (FileNotFoundException ex) 335 | { 336 | sendMessage("Error! Could not open region file to wipe individual chunks: "+regionFile.getName()); 337 | } 338 | catch (IOException ex) 339 | { 340 | sendMessage("Error! Could not modify region file to wipe individual chunks: "+regionFile.getName()); 341 | } 342 | counter += trimChunks.size(); 343 | } 344 | 345 | private boolean isChunkInsideBorder(CoordXZ chunk) 346 | { 347 | return border.insideBorder(CoordXZ.chunkToBlock(chunk.x) + 8, CoordXZ.chunkToBlock(chunk.z) + 8); 348 | } 349 | 350 | // for successful completion 351 | public void finish() 352 | { 353 | reportTotal = reportTarget; 354 | reportProgress(); 355 | 356 | boolean resetAndRestart = false; 357 | 358 | // If trim all types : region -> poi 359 | if (!resetAndRestart && typeToTrim == WorldFileDataType.ALL && currentType == WorldFileDataType.REGION ) 360 | { 361 | currentType = WorldFileDataType.POI; 362 | worldData = WorldFileData.create(world, notifyPlayer, currentType, true); 363 | if (worldData != null) resetAndRestart = true; 364 | } 365 | 366 | // If trim all types : poi -> entities 367 | if (!resetAndRestart && typeToTrim == WorldFileDataType.ALL && currentType == WorldFileDataType.POI ) 368 | { 369 | currentType = WorldFileDataType.ENTITIES; 370 | worldData = WorldFileData.create(world, notifyPlayer, currentType, true); 371 | if (worldData != null) resetAndRestart = true; 372 | } 373 | 374 | if (resetAndRestart) 375 | { 376 | currentRegion = -1; 377 | reportTarget = worldData.regionFileCount() * 3072; 378 | reportTotal = 0; 379 | reportTrimmedRegions = 0; 380 | reportTrimmedChunks = 0; 381 | counter = 0; 382 | nextFile(); 383 | 384 | paused = false; 385 | readyToGo = true; 386 | return; 387 | } 388 | 389 | Bukkit.getServer().getPluginManager().callEvent(new WorldBorderTrimFinishedEvent(world, reportTotal)); 390 | sendMessage("Task successfully completed!"); 391 | sendMessage("NOTICE: it is recommended that you restart your server after a Trim, to be on the safe side."); 392 | if (DynMapFeatures.renderEnabled()) 393 | 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."); 394 | this.stop(); 395 | } 396 | 397 | // for cancelling prematurely 398 | public void cancel() 399 | { 400 | this.stop(); 401 | } 402 | 403 | // we're done, whether finished or cancelled 404 | private void stop() 405 | { 406 | if (server == null) 407 | return; 408 | 409 | readyToGo = false; 410 | if (taskID != -1) 411 | server.getScheduler().cancelTask(taskID); 412 | server = null; 413 | } 414 | 415 | // is this task still valid/workable? 416 | public boolean valid() 417 | { 418 | return this.server != null; 419 | } 420 | 421 | // handle pausing/unpausing the task 422 | public void pause() 423 | { 424 | pause(!this.paused); 425 | } 426 | public void pause(boolean pause) 427 | { 428 | this.paused = pause; 429 | if (pause) 430 | reportProgress(); 431 | } 432 | public boolean isPaused() 433 | { 434 | return this.paused; 435 | } 436 | 437 | // let the user know how things are coming along 438 | private void reportProgress() 439 | { 440 | lastReport = Config.Now(); 441 | double perc = getPercentageCompleted(); 442 | String type = "Regions"; 443 | if (currentType == WorldFileDataType.POI) type = "POIs"; 444 | if (currentType == WorldFileDataType.ENTITIES) type = "Entities"; 445 | sendMessage("[" + type + "] " + reportTrimmedRegions + " entire region(s) and " + reportTrimmedChunks + " individual chunk(s) trimmed so far (" + Config.coord.format(perc) + "% done" + ")"); 446 | } 447 | 448 | // send a message to the server console/log and possibly to an in-game player 449 | private void sendMessage(String text) 450 | { 451 | Config.log("[Trim] " + text); 452 | if (notifyPlayer != null) 453 | notifyPlayer.sendMessage("[Trim] " + text); 454 | } 455 | 456 | /** 457 | * Get the percentage completed for the trim task. 458 | * 459 | * @return Percentage 460 | */ 461 | public double getPercentageCompleted() { 462 | return ((double) (reportTotal) / (double) reportTarget) * 100; 463 | } 464 | 465 | /** 466 | * Amount of chunks completed for the trim task. 467 | * 468 | * @return Number of chunks processed. 469 | */ 470 | public int getChunksCompleted() { 471 | return reportTotal; 472 | } 473 | 474 | /** 475 | * Total amount of chunks that need to be trimmed for the trim task. 476 | * 477 | * @return Number of chunks that need to be processed. 478 | */ 479 | public int getChunksTotal() { 480 | return reportTarget; 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/BorderData.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import java.util.EnumSet; 4 | 5 | import org.bukkit.Chunk; 6 | import org.bukkit.Location; 7 | import org.bukkit.Material; 8 | import org.bukkit.World; 9 | 10 | 11 | public class BorderData 12 | { 13 | // the main data interacted with 14 | private double x = 0; 15 | private double z = 0; 16 | private int radiusX = 0; 17 | private int radiusZ = 0; 18 | private Boolean shapeRound = null; 19 | private boolean wrapping = false; 20 | 21 | // some extra data kept handy for faster border checks 22 | private double maxX; 23 | private double minX; 24 | private double maxZ; 25 | private double minZ; 26 | private double radiusXSquared; 27 | private double radiusZSquared; 28 | private double DefiniteRectangleX; 29 | private double DefiniteRectangleZ; 30 | private double radiusSquaredQuotient; 31 | 32 | public BorderData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound, boolean wrap) 33 | { 34 | setData(x, z, radiusX, radiusZ, shapeRound, wrap); 35 | } 36 | public BorderData(double x, double z, int radiusX, int radiusZ) 37 | { 38 | setData(x, z, radiusX, radiusZ, null); 39 | } 40 | public BorderData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound) 41 | { 42 | setData(x, z, radiusX, radiusZ, shapeRound); 43 | } 44 | public BorderData(double x, double z, int radius) 45 | { 46 | setData(x, z, radius, null); 47 | } 48 | public BorderData(double x, double z, int radius, Boolean shapeRound) 49 | { 50 | setData(x, z, radius, shapeRound); 51 | } 52 | 53 | public final void setData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound, boolean wrap) 54 | { 55 | this.x = x; 56 | this.z = z; 57 | this.shapeRound = shapeRound; 58 | this.wrapping = wrap; 59 | this.setRadiusX(radiusX); 60 | this.setRadiusZ(radiusZ); 61 | } 62 | public final void setData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound) 63 | { 64 | setData(x, z, radiusX, radiusZ, shapeRound, false); 65 | } 66 | public final void setData(double x, double z, int radius, Boolean shapeRound) 67 | { 68 | setData(x, z, radius, radius, shapeRound, false); 69 | } 70 | 71 | public BorderData copy() 72 | { 73 | return new BorderData(x, z, radiusX, radiusZ, shapeRound, wrapping); 74 | } 75 | 76 | public double getX() 77 | { 78 | return x; 79 | } 80 | public void setX(double x) 81 | { 82 | this.x = x; 83 | this.maxX = x + radiusX; 84 | this.minX = x - radiusX; 85 | } 86 | public double getZ() 87 | { 88 | return z; 89 | } 90 | public void setZ(double z) 91 | { 92 | this.z = z; 93 | this.maxZ = z + radiusZ; 94 | this.minZ = z - radiusZ; 95 | } 96 | public int getRadiusX() 97 | { 98 | return radiusX; 99 | } 100 | public int getRadiusZ() 101 | { 102 | return radiusZ; 103 | } 104 | public void setRadiusX(int radiusX) 105 | { 106 | this.radiusX = radiusX; 107 | this.maxX = x + radiusX; 108 | this.minX = x - radiusX; 109 | this.radiusXSquared = (double)radiusX * (double)radiusX; 110 | this.radiusSquaredQuotient = this.radiusXSquared / this.radiusZSquared; 111 | this.DefiniteRectangleX = Math.sqrt(.5 * this.radiusXSquared); 112 | } 113 | public void setRadiusZ(int radiusZ) 114 | { 115 | this.radiusZ = radiusZ; 116 | this.maxZ = z + radiusZ; 117 | this.minZ = z - radiusZ; 118 | this.radiusZSquared = (double)radiusZ * (double)radiusZ; 119 | this.radiusSquaredQuotient = this.radiusXSquared / this.radiusZSquared; 120 | this.DefiniteRectangleZ = Math.sqrt(.5 * this.radiusZSquared); 121 | } 122 | 123 | 124 | // backwards-compatible methods from before elliptical/rectangular shapes were supported 125 | /** 126 | * @deprecated Replaced by {@link #getRadiusX()} and {@link #getRadiusZ()}; 127 | * this method now returns an average of those two values and is thus imprecise 128 | */ 129 | public int getRadius() 130 | { 131 | return (radiusX + radiusZ) / 2; // average radius; not great, but probably best for backwards compatibility 132 | } 133 | public void setRadius(int radius) 134 | { 135 | setRadiusX(radius); 136 | setRadiusZ(radius); 137 | } 138 | 139 | 140 | public Boolean getShape() 141 | { 142 | return shapeRound; 143 | } 144 | public void setShape(Boolean shapeRound) 145 | { 146 | this.shapeRound = shapeRound; 147 | } 148 | 149 | 150 | public boolean getWrapping() 151 | { 152 | return wrapping; 153 | } 154 | public void setWrapping(boolean wrap) 155 | { 156 | this.wrapping = wrap; 157 | } 158 | 159 | 160 | @Override 161 | public String toString() 162 | { 163 | 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)") : ""); 164 | } 165 | 166 | // This algorithm of course needs to be fast, since it will be run very frequently 167 | public boolean insideBorder(double xLoc, double zLoc, boolean round) 168 | { 169 | // if this border has a shape override set, use it 170 | if (shapeRound != null) 171 | round = shapeRound.booleanValue(); 172 | 173 | // square border 174 | if (!round) 175 | return !(xLoc < minX || xLoc > maxX || zLoc < minZ || zLoc > maxZ); 176 | 177 | // round border 178 | else 179 | { 180 | // elegant round border checking algorithm is from rBorder by Reil with almost no changes, all credit to him for it 181 | double X = Math.abs(x - xLoc); 182 | double Z = Math.abs(z - zLoc); 183 | 184 | if (X < DefiniteRectangleX && Z < DefiniteRectangleZ) 185 | return true; // Definitely inside 186 | else if (X >= radiusX || Z >= radiusZ) 187 | return false; // Definitely outside 188 | else if (X * X + Z * Z * radiusSquaredQuotient < radiusXSquared) 189 | return true; // After further calculation, inside 190 | else 191 | return false; // Apparently outside, then 192 | } 193 | } 194 | public boolean insideBorder(double xLoc, double zLoc) 195 | { 196 | return insideBorder(xLoc, zLoc, Config.ShapeRound()); 197 | } 198 | public boolean insideBorder(Location loc) 199 | { 200 | return insideBorder(loc.getX(), loc.getZ(), Config.ShapeRound()); 201 | } 202 | public boolean insideBorder(CoordXZ coord, boolean round) 203 | { 204 | return insideBorder(coord.x, coord.z, round); 205 | } 206 | public boolean insideBorder(CoordXZ coord) 207 | { 208 | return insideBorder(coord.x, coord.z, Config.ShapeRound()); 209 | } 210 | 211 | public Location correctedPosition(Location loc, boolean round, boolean flying) 212 | { 213 | // if this border has a shape override set, use it 214 | if (shapeRound != null) 215 | round = shapeRound.booleanValue(); 216 | 217 | double xLoc = loc.getX(); 218 | double zLoc = loc.getZ(); 219 | double yLoc = loc.getY(); 220 | 221 | // square border 222 | if (!round) 223 | { 224 | if (wrapping) 225 | { 226 | if (xLoc <= minX) 227 | xLoc = maxX - Config.KnockBack(); 228 | else if (xLoc >= maxX) 229 | xLoc = minX + Config.KnockBack(); 230 | if (zLoc <= minZ) 231 | zLoc = maxZ - Config.KnockBack(); 232 | else if (zLoc >= maxZ) 233 | zLoc = minZ + Config.KnockBack(); 234 | } 235 | else 236 | { 237 | if (xLoc <= minX) 238 | xLoc = minX + Config.KnockBack(); 239 | else if (xLoc >= maxX) 240 | xLoc = maxX - Config.KnockBack(); 241 | if (zLoc <= minZ) 242 | zLoc = minZ + Config.KnockBack(); 243 | else if (zLoc >= maxZ) 244 | zLoc = maxZ - Config.KnockBack(); 245 | } 246 | } 247 | 248 | // round border 249 | else 250 | { 251 | // algorithm originally from: http://stackoverflow.com/questions/300871/best-way-to-find-a-point-on-a-circle-closest-to-a-given-point 252 | // modified by Lang Lukas to support elliptical border shape 253 | 254 | //Transform the ellipse to a circle with radius 1 (we need to transform the point the same way) 255 | double dX = xLoc - x; 256 | double dZ = zLoc - z; 257 | double dU = Math.sqrt(dX *dX + dZ * dZ); //distance of the untransformed point from the center 258 | double dT = Math.sqrt(dX *dX / radiusXSquared + dZ * dZ / radiusZSquared); //distance of the transformed point from the center 259 | double f = (1 / dT - Config.KnockBack() / dU); //"correction" factor for the distances 260 | if (wrapping) 261 | { 262 | xLoc = x - dX * f; 263 | zLoc = z - dZ * f; 264 | } else { 265 | xLoc = x + dX * f; 266 | zLoc = z + dZ * f; 267 | } 268 | } 269 | 270 | int ixLoc = Location.locToBlock(xLoc); 271 | int izLoc = Location.locToBlock(zLoc); 272 | 273 | // Make sure the chunk we're checking in is actually loaded 274 | Chunk tChunk = loc.getWorld().getChunkAt(CoordXZ.blockToChunk(ixLoc), CoordXZ.blockToChunk(izLoc)); 275 | if (!tChunk.isLoaded()) 276 | tChunk.load(); 277 | 278 | yLoc = getSafeY(loc.getWorld(), ixLoc, Location.locToBlock(yLoc), izLoc, flying); 279 | if (yLoc == -1) 280 | return null; 281 | 282 | return new Location(loc.getWorld(), Math.floor(xLoc) + 0.5, yLoc, Math.floor(zLoc) + 0.5, loc.getYaw(), loc.getPitch()); 283 | } 284 | public Location correctedPosition(Location loc, boolean round) 285 | { 286 | return correctedPosition(loc, round, false); 287 | } 288 | public Location correctedPosition(Location loc) 289 | { 290 | return correctedPosition(loc, Config.ShapeRound(), false); 291 | } 292 | 293 | //these material IDs are acceptable for places to teleport player; breathable blocks and water 294 | public static final EnumSet safeOpenBlocks = EnumSet.noneOf(Material.class); 295 | static 296 | { 297 | safeOpenBlocks.add(Material.AIR); 298 | safeOpenBlocks.add(Material.CAVE_AIR); 299 | safeOpenBlocks.add(Material.OAK_SAPLING); 300 | safeOpenBlocks.add(Material.SPRUCE_SAPLING); 301 | safeOpenBlocks.add(Material.BIRCH_SAPLING); 302 | safeOpenBlocks.add(Material.JUNGLE_SAPLING); 303 | safeOpenBlocks.add(Material.ACACIA_SAPLING); 304 | safeOpenBlocks.add(Material.DARK_OAK_SAPLING); 305 | safeOpenBlocks.add(Material.WATER); 306 | safeOpenBlocks.add(Material.RAIL); 307 | safeOpenBlocks.add(Material.POWERED_RAIL); 308 | safeOpenBlocks.add(Material.DETECTOR_RAIL); 309 | safeOpenBlocks.add(Material.ACTIVATOR_RAIL); 310 | safeOpenBlocks.add(Material.COBWEB); 311 | safeOpenBlocks.add(Material.GRASS); 312 | safeOpenBlocks.add(Material.FERN); 313 | safeOpenBlocks.add(Material.DEAD_BUSH); 314 | safeOpenBlocks.add(Material.DANDELION); 315 | safeOpenBlocks.add(Material.POPPY); 316 | safeOpenBlocks.add(Material.BLUE_ORCHID); 317 | safeOpenBlocks.add(Material.ALLIUM); 318 | safeOpenBlocks.add(Material.AZURE_BLUET); 319 | safeOpenBlocks.add(Material.RED_TULIP); 320 | safeOpenBlocks.add(Material.ORANGE_TULIP); 321 | safeOpenBlocks.add(Material.WHITE_TULIP); 322 | safeOpenBlocks.add(Material.PINK_TULIP); 323 | safeOpenBlocks.add(Material.OXEYE_DAISY); 324 | safeOpenBlocks.add(Material.BROWN_MUSHROOM); 325 | safeOpenBlocks.add(Material.RED_MUSHROOM); 326 | safeOpenBlocks.add(Material.TORCH); 327 | safeOpenBlocks.add(Material.WALL_TORCH); 328 | safeOpenBlocks.add(Material.REDSTONE_WIRE); 329 | safeOpenBlocks.add(Material.WHEAT); 330 | safeOpenBlocks.add(Material.LADDER); 331 | safeOpenBlocks.add(Material.LEVER); 332 | safeOpenBlocks.add(Material.LIGHT_WEIGHTED_PRESSURE_PLATE); 333 | safeOpenBlocks.add(Material.HEAVY_WEIGHTED_PRESSURE_PLATE); 334 | safeOpenBlocks.add(Material.STONE_PRESSURE_PLATE); 335 | safeOpenBlocks.add(Material.OAK_PRESSURE_PLATE); 336 | safeOpenBlocks.add(Material.SPRUCE_PRESSURE_PLATE); 337 | safeOpenBlocks.add(Material.BIRCH_PRESSURE_PLATE); 338 | safeOpenBlocks.add(Material.JUNGLE_PRESSURE_PLATE); 339 | safeOpenBlocks.add(Material.ACACIA_PRESSURE_PLATE); 340 | safeOpenBlocks.add(Material.DARK_OAK_PRESSURE_PLATE); 341 | safeOpenBlocks.add(Material.REDSTONE_TORCH); 342 | safeOpenBlocks.add(Material.REDSTONE_WALL_TORCH); 343 | safeOpenBlocks.add(Material.STONE_BUTTON); 344 | safeOpenBlocks.add(Material.SNOW); 345 | safeOpenBlocks.add(Material.SUGAR_CANE); 346 | safeOpenBlocks.add(Material.REPEATER); 347 | safeOpenBlocks.add(Material.COMPARATOR); 348 | safeOpenBlocks.add(Material.OAK_TRAPDOOR); 349 | safeOpenBlocks.add(Material.SPRUCE_TRAPDOOR); 350 | safeOpenBlocks.add(Material.BIRCH_TRAPDOOR); 351 | safeOpenBlocks.add(Material.JUNGLE_TRAPDOOR); 352 | safeOpenBlocks.add(Material.ACACIA_TRAPDOOR); 353 | safeOpenBlocks.add(Material.DARK_OAK_TRAPDOOR); 354 | safeOpenBlocks.add(Material.MELON_STEM); 355 | safeOpenBlocks.add(Material.ATTACHED_MELON_STEM); 356 | safeOpenBlocks.add(Material.PUMPKIN_STEM); 357 | safeOpenBlocks.add(Material.ATTACHED_PUMPKIN_STEM); 358 | safeOpenBlocks.add(Material.VINE); 359 | safeOpenBlocks.add(Material.NETHER_WART); 360 | safeOpenBlocks.add(Material.TRIPWIRE); 361 | safeOpenBlocks.add(Material.TRIPWIRE_HOOK); 362 | safeOpenBlocks.add(Material.CARROTS); 363 | safeOpenBlocks.add(Material.POTATOES); 364 | safeOpenBlocks.add(Material.OAK_BUTTON); 365 | safeOpenBlocks.add(Material.SPRUCE_BUTTON); 366 | safeOpenBlocks.add(Material.BIRCH_BUTTON); 367 | safeOpenBlocks.add(Material.JUNGLE_BUTTON); 368 | safeOpenBlocks.add(Material.ACACIA_BUTTON); 369 | safeOpenBlocks.add(Material.DARK_OAK_BUTTON); 370 | safeOpenBlocks.add(Material.SUNFLOWER); 371 | safeOpenBlocks.add(Material.LILAC); 372 | safeOpenBlocks.add(Material.ROSE_BUSH); 373 | safeOpenBlocks.add(Material.PEONY); 374 | safeOpenBlocks.add(Material.TALL_GRASS); 375 | safeOpenBlocks.add(Material.LARGE_FERN); 376 | safeOpenBlocks.add(Material.BEETROOTS); 377 | try 378 | { // signs in 1.14 can be different wood types 379 | safeOpenBlocks.add(Material.ACACIA_SIGN); 380 | safeOpenBlocks.add(Material.ACACIA_WALL_SIGN); 381 | safeOpenBlocks.add(Material.BIRCH_SIGN); 382 | safeOpenBlocks.add(Material.BIRCH_WALL_SIGN); 383 | safeOpenBlocks.add(Material.DARK_OAK_SIGN); 384 | safeOpenBlocks.add(Material.DARK_OAK_WALL_SIGN); 385 | safeOpenBlocks.add(Material.JUNGLE_SIGN); 386 | safeOpenBlocks.add(Material.JUNGLE_WALL_SIGN); 387 | safeOpenBlocks.add(Material.OAK_SIGN); 388 | safeOpenBlocks.add(Material.OAK_WALL_SIGN); 389 | safeOpenBlocks.add(Material.SPRUCE_SIGN); 390 | safeOpenBlocks.add(Material.SPRUCE_WALL_SIGN); 391 | } 392 | catch (NoSuchFieldError ex) {} 393 | } 394 | 395 | //these material IDs are ones we don't want to drop the player onto, like cactus or lava or fire or activated Ender portal 396 | public static final EnumSet painfulBlocks = EnumSet.noneOf(Material.class); 397 | static 398 | { 399 | painfulBlocks.add(Material.LAVA); 400 | painfulBlocks.add(Material.FIRE); 401 | painfulBlocks.add(Material.CACTUS); 402 | painfulBlocks.add(Material.END_PORTAL); 403 | painfulBlocks.add(Material.MAGMA_BLOCK); 404 | } 405 | 406 | // check if a particular spot consists of 2 breathable blocks over something relatively solid 407 | private boolean isSafeSpot(World world, int X, int Y, int Z, boolean flying) 408 | { 409 | boolean safe = 410 | // target block open and safe or is above maximum Y coordinate 411 | (Y == world.getMaxHeight() 412 | || (safeOpenBlocks.contains(world.getBlockAt(X, Y, Z).getType()) 413 | // above target block open and safe or is above maximum Y coordinate 414 | && (Y + 1 == world.getMaxHeight() 415 | || safeOpenBlocks.contains(world.getBlockAt(X, Y + 1, Z).getType())))); 416 | if (!safe || flying) 417 | return safe; 418 | 419 | Material below = world.getBlockAt(X, Y - 1, Z).getType(); 420 | return (safe 421 | && (!safeOpenBlocks.contains(below) || below == Material.WATER) // below target block not open/breathable (so presumably solid), or is water 422 | && !painfulBlocks.contains(below) // below target block not painful 423 | ); 424 | } 425 | 426 | private static final int limBot = 0; 427 | 428 | // find closest safe Y position from the starting position 429 | private double getSafeY(World world, int X, int Y, int Z, boolean flying) 430 | { 431 | // 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; we don't bother checking if Y = 126 or 127 are safe because they never will be unless there's a hole in the bedrock 432 | final boolean isNether = world.getEnvironment() == World.Environment.NETHER; 433 | int limTop = isNether ? 125 : world.getMaxHeight(); 434 | // add 1 because getHighestBlockYAt() will give us the Y coordinate of a solid block, and we want the air block above it 435 | final int highestBlockBoundary = Math.min(world.getHighestBlockYAt(X, Z) + 1, limTop); 436 | 437 | // 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 438 | if (flying && Y > limTop && !isNether) 439 | return (double) Y; 440 | 441 | // make sure Y values are within the boundaries of the world. 442 | if (Y > limTop) 443 | { 444 | if (isNether) 445 | Y = limTop; // because of the roof, the nether can not rely on highestBlockBoundary, so limTop has to be used 446 | else 447 | { 448 | if (flying) 449 | Y = limTop; 450 | else 451 | Y = highestBlockBoundary; // there will never be a save block to stand on for Y values > highestBlockBoundary 452 | } 453 | } 454 | if (Y < limBot) 455 | Y = limBot; 456 | 457 | // for non Nether worlds we don't need to check upwards to the world-limit, it is enough to check up to and including the highestBlockBoundary, unless player is flying 458 | if (!isNether && !flying) 459 | limTop = highestBlockBoundary; 460 | // Expanding Y search method adapted from Acru's code in the Nether plugin 461 | 462 | // Note that we want to include limTop in the search - in the extreme case, world.getMaxHeight() should be included since the player can stand on top of the highest block 463 | for(int y1 = Y, y2 = Y; (y1 > limBot) || (y2 <= limTop); y1--, y2++){ 464 | // Look below. 465 | if(y1 > limBot) 466 | { 467 | if (isSafeSpot(world, X, y1, Z, flying)) 468 | return (double)y1; 469 | } 470 | 471 | // Look above. 472 | if(y2 <= limTop && y2 != y1) 473 | { 474 | if (isSafeSpot(world, X, y2, Z, flying)) 475 | return (double)y2; 476 | } 477 | } 478 | 479 | return -1.0; // no safe Y location?!?!? Must be a rare spot in a Nether world or something 480 | } 481 | 482 | 483 | @Override 484 | public boolean equals(Object obj) 485 | { 486 | if (this == obj) 487 | return true; 488 | else if (obj == null || obj.getClass() != this.getClass()) 489 | return false; 490 | 491 | BorderData test = (BorderData)obj; 492 | return test.x == this.x && test.z == this.z && test.radiusX == this.radiusX && test.radiusZ == this.radiusZ; 493 | } 494 | 495 | @Override 496 | public int hashCode() 497 | { 498 | return (((int)(this.x * 10) << 4) + (int)this.z + (this.radiusX << 2) + (this.radiusZ << 3)); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WorldFillTask.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.HashMap; 5 | import java.util.HashSet; 6 | import java.util.Map; 7 | import java.util.Set; 8 | 9 | import org.bukkit.Bukkit; 10 | import org.bukkit.Chunk; 11 | import org.bukkit.entity.Player; 12 | import org.bukkit.Server; 13 | import org.bukkit.World; 14 | 15 | import io.papermc.lib.PaperLib; 16 | 17 | import com.wimbli.WorldBorder.Events.WorldBorderFillFinishedEvent; 18 | import com.wimbli.WorldBorder.Events.WorldBorderFillStartEvent; 19 | 20 | 21 | public class WorldFillTask implements Runnable 22 | { 23 | // general task-related reference data 24 | private transient Server server = null; 25 | private transient World world = null; 26 | private transient BorderData border = null; 27 | private transient WorldFileData worldData = null; 28 | private transient boolean readyToGo = false; 29 | private transient boolean paused = false; 30 | private transient boolean pausedForMemory = false; 31 | private transient int taskID = -1; 32 | private transient Player notifyPlayer = null; 33 | private transient int chunksPerRun = 1; 34 | private transient boolean continueNotice = false; 35 | private transient boolean forceLoad = false; 36 | 37 | // these are only stored for saving task to config 38 | private transient int fillDistance = 208; 39 | private transient int tickFrequency = 1; 40 | private transient int refX = 0, lastLegX = 0; 41 | private transient int refZ = 0, lastLegZ = 0; 42 | private transient int refLength = -1; 43 | private transient int refTotal = 0, lastLegTotal = 0; 44 | 45 | // values for the spiral pattern check which fills out the map to the border 46 | private transient int x = 0; 47 | private transient int z = 0; 48 | private transient boolean isZLeg = false; 49 | private transient boolean isNeg = false; 50 | private transient int length = -1; 51 | private transient int current = 0; 52 | private transient boolean insideBorder = true; 53 | private transient CoordXZ lastChunk = new CoordXZ(0, 0); 54 | 55 | // for reporting progress back to user occasionally 56 | private transient long lastReport = Config.Now(); 57 | private transient long lastAutosave = Config.Now(); 58 | private transient int reportTarget = 0; 59 | private transient int reportTotal = 0; 60 | private transient int reportNum = 0; 61 | 62 | // A map that holds to-be-loaded chunks, and their coordinates 63 | private transient Map, CoordXZ> pendingChunks; 64 | 65 | // and a set of "Chunk a needed for Chunk b" dependencies, which 66 | // unfortunately can't be a Map as a chunk might be needed for 67 | // several others. 68 | private transient Set preventUnload; 69 | 70 | private class UnloadDependency 71 | { 72 | int neededX, neededZ; 73 | int forX, forZ; 74 | 75 | UnloadDependency(int neededX, int neededZ, int forX, int forZ) 76 | { 77 | this.neededX=neededX; 78 | this.neededZ=neededZ; 79 | this.forX=forX; 80 | this.forZ=forZ; 81 | } 82 | 83 | @Override 84 | public boolean equals(Object other) 85 | { 86 | if (other == null || !(other instanceof UnloadDependency)) 87 | return false; 88 | 89 | return this.neededX == ((UnloadDependency) other).neededX 90 | && this.neededZ == ((UnloadDependency) other).neededZ 91 | && this.forX == ((UnloadDependency) other).forX 92 | && this.forZ == ((UnloadDependency) other).forZ; 93 | } 94 | 95 | @Override 96 | public int hashCode() 97 | { 98 | int hash = 7; 99 | hash = 79 * hash + this.neededX; 100 | hash = 79 * hash + this.neededZ; 101 | hash = 79 * hash + this.forX; 102 | hash = 79 * hash + this.forZ; 103 | return hash; 104 | } 105 | } 106 | 107 | public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency, boolean forceLoad) 108 | { 109 | this.server = theServer; 110 | this.notifyPlayer = player; 111 | this.fillDistance = fillDistance; 112 | this.tickFrequency = tickFrequency; 113 | this.chunksPerRun = chunksPerRun; 114 | this.forceLoad = forceLoad; 115 | 116 | this.world = server.getWorld(worldName); 117 | if (this.world == null) 118 | { 119 | if (worldName.isEmpty()) 120 | sendMessage("You must specify a world!"); 121 | else 122 | sendMessage("World \"" + worldName + "\" not found!"); 123 | //In case world is not loaded yet, do not delete saved progress 124 | this.stop(true); 125 | return; 126 | } 127 | 128 | this.border = (Config.Border(worldName) == null) ? null : Config.Border(worldName).copy(); 129 | if (this.border == null) 130 | { 131 | sendMessage("No border found for world \"" + worldName + "\"!"); 132 | this.stop(false); 133 | return; 134 | } 135 | 136 | // load up a new WorldFileData for the world in question, used to scan region files for which chunks are already fully generated and such 137 | worldData = WorldFileData.create(world, notifyPlayer); 138 | if (worldData == null) 139 | { 140 | this.stop(false); 141 | return; 142 | } 143 | 144 | pendingChunks = new HashMap<>(); 145 | preventUnload = new HashSet<>(); 146 | 147 | this.border.setRadiusX(border.getRadiusX() + fillDistance); 148 | this.border.setRadiusZ(border.getRadiusZ() + fillDistance); 149 | this.x = CoordXZ.blockToChunk((int)border.getX()); 150 | this.z = CoordXZ.blockToChunk((int)border.getZ()); 151 | 152 | int chunkWidthX = (int) Math.ceil((double)((border.getRadiusX() + 16) * 2) / 16); 153 | int chunkWidthZ = (int) Math.ceil((double)((border.getRadiusZ() + 16) * 2) / 16); 154 | int biggerWidth = (chunkWidthX > chunkWidthZ) ? chunkWidthX : chunkWidthZ; //We need to calculate the reportTarget with the bigger width, since the spiral will only stop if it has a size of biggerWidth x biggerWidth 155 | this.reportTarget = (biggerWidth * biggerWidth) + biggerWidth + 1; 156 | 157 | //This would be another way to calculate reportTarget, it assumes that we don't need time to check if the chunk is outside and then skip it (it calculates the area of the rectangle/ellipse) 158 | //this.reportTarget = (this.border.getShape()) ? ((int) Math.ceil(chunkWidthX * chunkWidthZ / 4 * Math.PI + 2 * chunkWidthX)) : (chunkWidthX * chunkWidthZ); 159 | // Area of the ellipse just to be safe area of the rectangle 160 | 161 | this.readyToGo = true; 162 | Bukkit.getServer().getPluginManager().callEvent(new WorldBorderFillStartEvent(this)); 163 | } 164 | 165 | // for backwards compatibility 166 | public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency) 167 | { 168 | this(theServer, player, worldName, fillDistance, chunksPerRun, tickFrequency, false); 169 | } 170 | 171 | public void setTaskID(int ID) 172 | { 173 | if (ID == -1) this.stop(false); 174 | this.taskID = ID; 175 | } 176 | 177 | @Override 178 | public void run() 179 | { 180 | if (continueNotice) 181 | { // notify user that task has continued automatically 182 | continueNotice = false; 183 | sendMessage("World map generation task automatically continuing."); 184 | sendMessage("Reminder: you can cancel at any time with \"wb fill cancel\", or pause/unpause with \"wb fill pause\"."); 185 | } 186 | 187 | if (pausedForMemory) 188 | { // if available memory gets too low, we automatically pause, so handle that 189 | if (Config.AvailableMemoryTooLow()) 190 | return; 191 | 192 | pausedForMemory = false; 193 | readyToGo = true; 194 | sendMessage("Available memory is sufficient, automatically continuing."); 195 | } 196 | 197 | if (server == null || !readyToGo || paused) 198 | return; 199 | 200 | // this is set so it only does one iteration at a time, no matter how frequently the timer fires 201 | readyToGo = false; 202 | // 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 203 | long loopStartTime = Config.Now(); 204 | 205 | // Process async results from last time. We don't make a difference 206 | // whether they were really async, or sync. 207 | 208 | // First, Check which chunk generations have been finished. 209 | // Mark those chunks as existing and unloadable, and remove 210 | // them from the pending set. 211 | int chunksProcessedLastTick = 0; 212 | Map, CoordXZ> newPendingChunks = new HashMap<>(); 213 | Set chunksToUnload = new HashSet<>(); 214 | for (CompletableFuture cf : pendingChunks.keySet()) 215 | { 216 | if (cf.isDone()) 217 | { 218 | ++chunksProcessedLastTick; 219 | // If cf.get() returned the chunk reliably, pendingChunks could 220 | // be a set and we wouldn't have to map CFs to coords ... 221 | CoordXZ xz = pendingChunks.get(cf); 222 | worldData.chunkExistsNow(xz.x, xz.z); 223 | chunksToUnload.add(xz); 224 | } 225 | else 226 | newPendingChunks.put(cf, pendingChunks.get(cf)); 227 | } 228 | pendingChunks = newPendingChunks; 229 | 230 | // Next, check which chunks had been loaded because a to-be-generated 231 | // chunk needed them, and don't have to remain in memory any more. 232 | Set newPreventUnload = new HashSet<>(); 233 | for (UnloadDependency dependency : preventUnload) 234 | { 235 | if (worldData.doesChunkExist(dependency.forX, dependency.forZ)) 236 | chunksToUnload.add(new CoordXZ(dependency.neededX, dependency.neededZ)); 237 | else 238 | newPreventUnload.add(dependency); 239 | } 240 | preventUnload = newPreventUnload; 241 | 242 | // Unload all chunks that aren't needed anymore. NB a chunk could have 243 | // been needed for two different others, or been generated and needed 244 | // for one other chunk, so it might be in the unload set wrongly. 245 | // The ChunkUnloadListener checks this anyway, but it doesn't hurt to 246 | // save a few µs by not even requesting the unload. 247 | 248 | for (CoordXZ unload : chunksToUnload) 249 | { 250 | if (!chunkOnUnloadPreventionList(unload.x, unload.z)) 251 | { 252 | world.setChunkForceLoaded(unload.x, unload.z, false); 253 | // this causes severe TPS loss by forcibly unloading chunks - instead, let server unload them naturally 254 | //world.unloadChunkRequest(unload.x, unload.z); 255 | } 256 | } 257 | 258 | // Put some damper on chunksPerRun. We don't want the queue to be too 259 | // full; only fill it to a bit more than what we can 260 | // process per tick. This ensures the background task can run at 261 | // full speed and we recover from a temporary drop in generation rate, 262 | // but doesn't push user-induced chunk generations behind a very 263 | // long queue of fill-generations. 264 | 265 | int chunksToProcess = chunksPerRun; 266 | if (chunksProcessedLastTick > 0 || pendingChunks.size() > 0) 267 | { 268 | // Note we generally queue 3 chunks, so real numbers are 1/3 of chunksProcessedLastTick and pendingchunks.size 269 | int chunksExpectedToGetProcessed = (chunksProcessedLastTick - pendingChunks.size()) / 3 + 3; 270 | if (chunksExpectedToGetProcessed < chunksToProcess) 271 | chunksToProcess = chunksExpectedToGetProcessed; 272 | } 273 | 274 | for (int loop = 0; loop < chunksToProcess; loop++) 275 | { 276 | // in case the task has been paused while we're repeating... 277 | if (paused || pausedForMemory) 278 | return; 279 | 280 | long now = Config.Now(); 281 | 282 | // every 5 seconds or so, give basic progress report to let user know how it's going 283 | if (now > lastReport + 5000) 284 | reportProgress(); 285 | 286 | // if this iteration has been running for 45ms (almost 1 tick) or more, stop to take a breather 287 | if (now > loopStartTime + 45) 288 | { 289 | readyToGo = true; 290 | return; 291 | } 292 | 293 | // if we've made it at least partly outside the border, skip past any such chunks 294 | while (!border.insideBorder(CoordXZ.chunkToBlock(x) + 8, CoordXZ.chunkToBlock(z) + 8)) 295 | { 296 | if (!moveToNext()) 297 | return; 298 | } 299 | insideBorder = true; 300 | 301 | if (!forceLoad) 302 | { 303 | // skip past any chunks which are confirmed as fully generated using our super-special isChunkFullyGenerated routine 304 | int rLoop = 0; 305 | while (worldData.isChunkFullyGenerated(x, z)) 306 | { 307 | rLoop++; 308 | insideBorder = true; 309 | if (!moveToNext()) 310 | return; 311 | 312 | if (rLoop > 255) 313 | { // only skim through max 256 chunks (~8 region files) at a time here, to allow process to take a break if needed 314 | readyToGo = true; 315 | return; 316 | } 317 | } 318 | } 319 | 320 | pendingChunks.put(getPaperLibChunk(world, x, z, true), new CoordXZ(x, z)); 321 | 322 | // There need to be enough nearby chunks loaded to make the server populate a chunk with trees, snow, etc. 323 | // So, we keep the last few chunks loaded, and need to also temporarily load an extra inside chunk (neighbor closest to center of map) 324 | int popX = !isZLeg ? x : (x + (isNeg ? -1 : 1)); 325 | int popZ = isZLeg ? z : (z + (!isNeg ? -1 : 1)); 326 | 327 | pendingChunks.put(getPaperLibChunk(world, popX, popZ, false), new CoordXZ(popX, popZ)); 328 | preventUnload.add(new UnloadDependency(popX, popZ, x, z)); 329 | 330 | // make sure the previous chunk in our spiral is loaded as well (might have already existed and been skipped over) 331 | pendingChunks.put(getPaperLibChunk(world, lastChunk.x, lastChunk.z, false), new CoordXZ(lastChunk.x, lastChunk.z)); // <-- new CoordXZ as lastChunk isn't immutable 332 | preventUnload.add(new UnloadDependency(lastChunk.x, lastChunk.z, x, z)); 333 | 334 | // move on to next chunk 335 | if (!moveToNext()) 336 | return; 337 | } 338 | // ready for the next iteration to run 339 | readyToGo = true; 340 | } 341 | 342 | // step through chunks in spiral pattern from center; returns false if we're done, otherwise returns true 343 | public boolean moveToNext() 344 | { 345 | if (paused || pausedForMemory) 346 | return false; 347 | 348 | reportNum++; 349 | 350 | // keep track of progress in case we need to save to config for restoring progress after server restart 351 | if (!isNeg && current == 0 && length > 3) 352 | { 353 | if (!isZLeg) 354 | { 355 | lastLegX = x; 356 | lastLegZ = z; 357 | lastLegTotal = reportTotal + reportNum; 358 | } 359 | else 360 | { 361 | refX = lastLegX; 362 | refZ = lastLegZ; 363 | refTotal = lastLegTotal; 364 | refLength = length - 1; 365 | } 366 | } 367 | 368 | // make sure of the direction we're moving (X or Z? negative or positive?) 369 | if (current < length) 370 | current++; 371 | else 372 | { // one leg/side of the spiral down... 373 | current = 0; 374 | isZLeg ^= true; 375 | if (isZLeg) 376 | { // every second leg (between X and Z legs, negative or positive), length increases 377 | isNeg ^= true; 378 | length++; 379 | } 380 | } 381 | 382 | // keep track of the last chunk we were at 383 | lastChunk.x = x; 384 | lastChunk.z = z; 385 | 386 | // move one chunk further in the appropriate direction 387 | if (isZLeg) 388 | z += (isNeg) ? -1 : 1; 389 | else 390 | x += (isNeg) ? -1 : 1; 391 | 392 | // if we've been around one full loop (4 legs)... 393 | if (isZLeg && isNeg && current == 0) 394 | { // see if we've been outside the border for the whole loop 395 | if (!insideBorder) 396 | { // and finish if so 397 | finish(); 398 | return false; 399 | } // otherwise, reset the "inside border" flag 400 | else 401 | insideBorder = false; 402 | } 403 | return true; 404 | 405 | /* reference diagram used, should move in this pattern: 406 | * 8 [>][>][>][>][>] etc. 407 | * [^][6][>][>][>][>][>][6] 408 | * [^][^][4][>][>][>][4][v] 409 | * [^][^][^][2][>][2][v][v] 410 | * [^][^][^][^][0][v][v][v] 411 | * [^][^][^][1][1][v][v][v] 412 | * [^][^][3][<][<][3][v][v] 413 | * [^][5][<][<][<][<][5][v] 414 | * [7][<][<][<][<][<][<][7] 415 | */ 416 | } 417 | 418 | // for successful completion 419 | public void finish() 420 | { 421 | this.paused = true; 422 | reportProgress(); 423 | world.save(); 424 | Bukkit.getServer().getPluginManager().callEvent(new WorldBorderFillFinishedEvent(world, reportTotal)); 425 | sendMessage("task successfully completed for world \"" + refWorld() + "\"!"); 426 | this.stop(false); 427 | } 428 | 429 | // for cancelling prematurely 430 | public void cancel(boolean SaveFill) 431 | { 432 | this.stop(SaveFill); 433 | } 434 | 435 | // we're done, whether finished or cancelled 436 | private void stop(boolean SaveFill) 437 | { 438 | //If being called by onDisable(), don't delete fill progress 439 | if (!SaveFill) 440 | Config.UnStoreFillTask(); 441 | 442 | if (server == null) 443 | return; 444 | 445 | readyToGo = false; 446 | if (taskID != -1) 447 | server.getScheduler().cancelTask(taskID); 448 | server = null; 449 | 450 | // go ahead and unload any chunks we still have loaded 451 | // Set preventUnload to empty first so the ChunkUnloadEvent Listener 452 | // doesn't get in our way 453 | if (preventUnload != null) 454 | { 455 | Set tempPreventUnload = preventUnload; 456 | preventUnload = null; 457 | for (UnloadDependency entry: tempPreventUnload) 458 | { 459 | world.setChunkForceLoaded(entry.neededX, entry.neededZ, false); 460 | world.unloadChunkRequest(entry.neededX, entry.neededZ); 461 | } 462 | } 463 | } 464 | 465 | // is this task still valid/workable? 466 | public boolean valid() 467 | { 468 | return this.server != null; 469 | } 470 | 471 | // handle pausing/unpausing the task 472 | public void pause() 473 | { 474 | if(this.pausedForMemory) 475 | pause(false); 476 | else 477 | pause(!this.paused); 478 | } 479 | public void pause(boolean pause) 480 | { 481 | if (this.pausedForMemory && !pause) 482 | this.pausedForMemory = false; 483 | else 484 | this.paused = pause; 485 | if (this.paused) 486 | { 487 | Config.StoreFillTask(); 488 | reportProgress(); 489 | } 490 | 491 | } 492 | public boolean isPaused() 493 | { 494 | return this.paused || this.pausedForMemory; 495 | } 496 | 497 | public boolean chunkOnUnloadPreventionList(int x, int z) 498 | { 499 | if (preventUnload != null) 500 | { 501 | for (UnloadDependency entry: preventUnload) 502 | { 503 | if (entry.neededX == x && entry.neededZ == z) 504 | return true; 505 | } 506 | } 507 | return false; 508 | } 509 | 510 | public World getWorld() 511 | { 512 | return world; 513 | } 514 | 515 | // let the user know how things are coming along 516 | private void reportProgress() 517 | { 518 | lastReport = Config.Now(); 519 | double perc = getPercentageCompleted(); 520 | if (perc > 100) perc = 100; 521 | sendMessage(reportNum + " more chunks processed (" + (reportTotal + reportNum) + " total, ~" + Config.coord.format(perc) + "%" + ")"); 522 | reportTotal += reportNum; 523 | reportNum = 0; 524 | 525 | // go ahead and save world to disk every 30 seconds or so by default, just in case; can take a couple of seconds or more, so we don't want to run it too often 526 | if (Config.FillAutosaveFrequency() > 0 && lastAutosave + (Config.FillAutosaveFrequency() * 1000) < lastReport) 527 | { 528 | lastAutosave = lastReport; 529 | sendMessage("Saving the world to disk, just to be on the safe side."); 530 | world.save(); 531 | //In case of hard-crashes 532 | Config.StoreFillTask(); 533 | } 534 | } 535 | 536 | // send a message to the server console/log and possibly to an in-game player 537 | private void sendMessage(String text) 538 | { 539 | // Due to chunk generation eating up memory and Java being too slow about GC, we need to track memory availability 540 | int availMem = Config.AvailableMemory(); 541 | 542 | Config.log("[Fill] " + text + " (free mem: " + availMem + " MB)"); 543 | if (notifyPlayer != null) 544 | notifyPlayer.sendMessage("[Fill] " + text); 545 | 546 | if (availMem < 200) 547 | { // running low on memory, auto-pause 548 | pausedForMemory = true; 549 | Config.StoreFillTask(); 550 | 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."; 551 | Config.log("[Fill] " + text); 552 | if (notifyPlayer != null) 553 | notifyPlayer.sendMessage("[Fill] " + text); 554 | // prod Java with a request to go ahead and do GC to clean unloaded chunks from memory; this seems to work wonders almost immediately 555 | // yes, explicit calls to System.gc() are normally bad, but in this case it otherwise can take a long long long time for Java to recover memory 556 | System.gc(); 557 | } 558 | } 559 | 560 | // stuff for saving / restoring progress 561 | public void continueProgress(int x, int z, int length, int totalDone) 562 | { 563 | this.x = x; 564 | this.z = z; 565 | this.length = length; 566 | this.reportTotal = totalDone; 567 | this.continueNotice = true; 568 | //Prevents saving zeroes on first StoreFillTask after restoring 569 | this.refX = x; 570 | this.refZ = z; 571 | this.refLength = length; 572 | this.refTotal = totalDone; 573 | } 574 | public int refX() 575 | { 576 | return refX; 577 | } 578 | public int refZ() 579 | { 580 | return refZ; 581 | } 582 | public int refLength() 583 | { 584 | return refLength; 585 | } 586 | public int refTotal() 587 | { 588 | return refTotal; 589 | } 590 | public int refFillDistance() 591 | { 592 | return fillDistance; 593 | } 594 | public int refTickFrequency() 595 | { 596 | return tickFrequency; 597 | } 598 | public int refChunksPerRun() 599 | { 600 | return chunksPerRun; 601 | } 602 | public String refWorld() 603 | { 604 | return world.getName(); 605 | } 606 | public boolean refForceLoad() 607 | { 608 | return forceLoad; 609 | } 610 | 611 | /** 612 | * Get the percentage completed for the fill task. 613 | * 614 | * @return Percentage 615 | */ 616 | public double getPercentageCompleted() 617 | { 618 | return ((double) (reportTotal + reportNum) / (double) reportTarget) * 100; 619 | } 620 | 621 | /** 622 | * Amount of chunks completed for the fill task. 623 | * 624 | * @return Number of chunks processed. 625 | */ 626 | public int getChunksCompleted() 627 | { 628 | return reportTotal; 629 | } 630 | 631 | /** 632 | * Total amount of chunks that need to be generated for the fill task. 633 | * 634 | * @return Number of chunks that need to be processed. 635 | */ 636 | public int getChunksTotal() 637 | { 638 | return reportTarget; 639 | } 640 | 641 | private CompletableFuture getPaperLibChunk(World world, int x, int z, boolean gen) 642 | { 643 | return PaperLib.getChunkAtAsync(world, x, z, gen).thenAccept( (Chunk chunk) -> 644 | { 645 | if (chunk != null) 646 | { 647 | // toggle "force loaded" flag on for chunk to prevent it from being unloaded while we need it 648 | world.setChunkForceLoaded(x, z, true); 649 | 650 | // alternatively for 1.14.4+ 651 | //world.addPluginChunkTicket(x, z, pluginInstance); 652 | } 653 | }); 654 | } 655 | } 656 | --------------------------------------------------------------------------------