├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── libs └── Dynmap-3.0-beta-3-forge-1.12.2.jar ├── gradle.properties ├── .gitignore ├── .gitattributes ├── src ├── main │ ├── resources │ │ └── mcmod.info │ └── java │ │ └── com │ │ └── wimbli │ │ └── WorldBorder │ │ ├── cmd │ │ ├── CmdGetmsg.java │ │ ├── CmdReload.java │ │ ├── CmdList.java │ │ ├── CmdWhoosh.java │ │ ├── CmdPreventSpawn.java │ │ ├── CmdPreventPlace.java │ │ ├── CmdDynmap.java │ │ ├── CmdSetmsg.java │ │ ├── CmdRemount.java │ │ ├── CmdDynmapmsg.java │ │ ├── CmdBypasslist.java │ │ ├── CmdDelay.java │ │ ├── CmdKnockback.java │ │ ├── CmdDenypearl.java │ │ ├── CmdShape.java │ │ ├── CmdHelp.java │ │ ├── CmdSetcorners.java │ │ ├── CmdWrap.java │ │ ├── CmdFillautosave.java │ │ ├── CmdClear.java │ │ ├── CmdWshape.java │ │ ├── CmdCommands.java │ │ ├── CmdBypass.java │ │ ├── CmdRadius.java │ │ ├── WBCmd.java │ │ ├── CmdSet.java │ │ ├── CmdTrim.java │ │ └── CmdFill.java │ │ ├── task │ │ ├── ChunkUtil.java │ │ ├── BorderCheckTask.java │ │ ├── WorldTrimTask.java │ │ └── WorldFillTask.java │ │ ├── forge │ │ ├── Log.java │ │ ├── Profiles.java │ │ ├── Particles.java │ │ ├── Util.java │ │ ├── Location.java │ │ ├── Worlds.java │ │ └── Configuration.java │ │ ├── listener │ │ ├── EnderPearlListener.java │ │ ├── BlockPlaceListener.java │ │ └── MobSpawnListener.java │ │ ├── CoordXZ.java │ │ ├── WorldBorder.java │ │ ├── BorderCheck.java │ │ ├── WorldFileData.java │ │ ├── DynMapFeatures.java │ │ ├── WBCommand.java │ │ └── BorderData.java └── test │ └── java │ └── com │ └── wimbli │ └── WorldBorder │ └── forge │ └── UtilTest.java ├── .editorconfig ├── LICENSE.md ├── gradlew.bat ├── README.md └── gradlew /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abused/World-Border/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /libs/Dynmap-3.0-beta-3-forge-1.12.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abused/World-Border/HEAD/libs/Dynmap-3.0-beta-3-forge-1.12.2.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Sets default memory used for gradle commands. Can be overridden by user or command line properties. 2 | # This is required to provide enough memory for the Minecraft decompilation process. 3 | org.gradle.jvmargs=-Xmx3G 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .settings 5 | .metadata 6 | .classpath 7 | .project 8 | 9 | # idea 10 | out 11 | *.ipr 12 | *.iws 13 | *.iml 14 | .idea 15 | 16 | # gradle 17 | build 18 | .gradle 19 | 20 | # other 21 | eclipse 22 | run 23 | debug 24 | output -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Sep 14 12:28:28 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-bin.zip 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /src/main/resources/mcmod.info: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "modid": "WorldBorder", 4 | "name": "WorldBorder", 5 | "description": "Efficient, feature-rich mod for limiting the size of your worlds", 6 | "version": "${version}", 7 | "mcversion": "${mcversion}", 8 | "url": "", 9 | "updateUrl": "", 10 | "authorList": ["Brettflan", "RoyCurtis", "abused_master"], 11 | "credits": "", 12 | "logoFile": "", 13 | "screenshots": [], 14 | "dependencies": [] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor config file for WorldBorder-Forge 2 | # If this config prevents you from contributing in an easy manner, ignore it 3 | # See http://EditorConfig.org for more information 4 | 5 | root = true 6 | 7 | [*] 8 | # Spacing 9 | indent_style = space 10 | indent_size = 4 11 | tab_width = 4 12 | 13 | # Formatting 14 | end_of_line = lf 15 | charset = utf-8 16 | 17 | # Whitespace 18 | trim_trailing_whitespace = true 19 | insert_final_newline = false 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdGetmsg.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 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(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 24 | { 25 | Util.chat(sender, "Border message is currently set to:"); 26 | Util.chat(sender, Config.getMessageRaw()); 27 | Util.chat(sender, "Formatted border message:"); 28 | Util.chat(sender, Config.getMessage()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/task/ChunkUtil.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.task; 2 | 3 | import net.minecraft.util.math.BlockPos; 4 | import net.minecraft.util.math.ChunkPos; 5 | import net.minecraft.world.World; 6 | import net.minecraft.world.WorldServer; 7 | 8 | public class ChunkUtil { 9 | 10 | public static void unloadChunksIfNotNearSpawn(WorldServer world, int par1, int par2) 11 | { 12 | //Attempt to unload the chunk if the player can't respawn here, otherwise always unload 13 | if(world.provider.canRespawnHere()) 14 | { 15 | BlockPos var3 = world.getSpawnPoint(); 16 | int var4 = par1 * 16 + 8 - var3.getX(); 17 | int var5 = par2 * 16 + 8 - var3.getZ(); 18 | short var6 = 128; 19 | if(var4 < -var6 || var4 > var6 || var5 < -var6 || var5 > var6) 20 | { 21 | world.getChunkProvider().queueUnload(world.getChunkFromChunkCoords(par1, par2)); 22 | } 23 | } else 24 | { 25 | world.getChunkProvider().queueUnload(world.getChunkFromChunkCoords(par1, par2)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/wimbli/WorldBorder/forge/UtilTest.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class UtilTest 8 | { 9 | @Test 10 | public void testReplaceAmpColors() throws Exception 11 | { 12 | String resultA = Util.replaceAmpColors("&4Red &2Green &&9 Blue"); 13 | String resultB = Util.replaceAmpColors("§a&B§c&d§e&F"); 14 | String resultC = Util.replaceAmpColors("more & more"); 15 | 16 | assertEquals("§4Red §2Green &§9 Blue", resultA); 17 | assertEquals("§a§B§c§d§e§F", resultB); 18 | assertEquals("more & more", resultC); 19 | } 20 | 21 | @Test 22 | public void testRemoveFormatting() throws Exception 23 | { 24 | String resultA = Util.removeFormatting("§4Red §2Green &§9 Blue"); 25 | String resultB = Util.removeFormatting("§a§B§c§d§e§F"); 26 | String resultC = Util.removeFormatting("§ 10000 in the bank"); 27 | 28 | assertEquals("Red Green & Blue", resultA); 29 | assertEquals("", resultB); 30 | assertEquals("§ 10000 in the bank", resultC); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdReload.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.List; 10 | 11 | 12 | public class CmdReload extends WBCmd 13 | { 14 | public CmdReload() 15 | { 16 | name = permission = "reload"; 17 | minParams = maxParams = 0; 18 | 19 | addCmdExample(nameEmphasized() + "- re-load data from config.yml."); 20 | helpText = "If you make manual changes to config.yml while the server is running, you can use this command " + 21 | "to make WorldBorder load the changes without needing to restart the server."; 22 | } 23 | 24 | @Override 25 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 26 | { 27 | if (player != null) 28 | Log.info("Reloading config file at the command of player \"" + player.getDisplayName() + "\"."); 29 | 30 | Config.load(true); 31 | 32 | if (player != null) 33 | Util.chat(sender, "WorldBorder configuration reloaded."); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/forge/Log.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import com.wimbli.WorldBorder.WorldBorder; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | 7 | /** 8 | * Static utility class for WorldBorder logging. 9 | * 10 | * To enable debug logging for WorldBorder: 11 | * 1. Save https://gist.github.com/RoyCurtis/517dd9d6a0619c44e970 to debug directory 12 | * 2. Add `-Dlog4j.configurationFile=log4j.xml` to VM options when running server 13 | */ 14 | public class Log 15 | { 16 | private static final Logger LOG = LogManager.getFormatterLogger(WorldBorder.MODID); 17 | 18 | // 19 | public static void trace(String msg, Object... parts) 20 | { 21 | LOG.trace(msg, parts); 22 | } 23 | 24 | public static void debug(String msg, Object... parts) 25 | { 26 | LOG.debug(msg, parts); 27 | } 28 | 29 | public static void info(String msg, Object... parts) 30 | { 31 | LOG.info(msg, parts); 32 | } 33 | 34 | public static void warn(String msg, Object... parts) 35 | { 36 | LOG.warn(msg, parts); 37 | } 38 | 39 | public static void error(String msg, Object... parts) 40 | { 41 | LOG.error(msg, parts); 42 | } 43 | // 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdList.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 9 | import java.util.Set; 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(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 26 | { 27 | Util.chat(sender, "Default border shape for all worlds is \"" + Config.getShapeName() + "\"."); 28 | 29 | Set list = Config.BorderDescriptions(); 30 | 31 | if (list.isEmpty()) 32 | { 33 | Util.chat(sender, "There are no borders currently set."); 34 | return; 35 | } 36 | 37 | for(String borderDesc : list) 38 | Util.chat(sender, borderDesc); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/forge/Profiles.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import com.wimbli.WorldBorder.WorldBorder; 5 | import net.minecraft.server.management.PlayerProfileCache; 6 | 7 | import java.util.UUID; 8 | 9 | /** Static utility class for player profile shortcuts */ 10 | public class Profiles 11 | { 12 | public static String[] fetchNames(UUID[] uuids) 13 | { 14 | PlayerProfileCache cache = WorldBorder.SERVER.getPlayerProfileCache(); 15 | String[] names = new String[uuids.length]; 16 | 17 | // Makes sure server reads from cache first 18 | cache.load(); 19 | 20 | for (int i = 0; i < uuids.length; i++) 21 | { 22 | GameProfile profile = cache.getProfileByUUID(uuids[i]); 23 | 24 | names[i] = (profile != null) 25 | ? profile.getName() 26 | : ""; 27 | } 28 | 29 | return names; 30 | } 31 | 32 | public static UUID fetchUUID(String name) 33 | { 34 | GameProfile profile = WorldBorder.SERVER 35 | .getPlayerProfileCache() 36 | .getGameProfileForUsername(name); 37 | 38 | if (profile == null) 39 | throw new RuntimeException(name + " is not a valid user"); 40 | else 41 | return profile.getId(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/listener/EnderPearlListener.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.listener; 2 | 3 | import com.wimbli.WorldBorder.BorderCheck; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.forge.Location; 6 | import com.wimbli.WorldBorder.forge.Log; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | import net.minecraftforge.event.entity.living.EnderTeleportEvent; 9 | import net.minecraftforge.fml.common.eventhandler.EventPriority; 10 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 11 | 12 | public class EnderPearlListener 13 | { 14 | @SubscribeEvent(priority = EventPriority.LOWEST) 15 | public void onPlayerPearl(EnderTeleportEvent event) 16 | { 17 | if ( !(event.getEntityLiving() instanceof EntityPlayerMP) ) 18 | return; 19 | 20 | if ( Config.getKnockBack() == 0.0 || !Config.getDenyEnderpearl() ) 21 | return; 22 | 23 | EntityPlayerMP player = (EntityPlayerMP) event.getEntityLiving(); 24 | Log.trace( "Caught pearl teleport event by %s", player.getDisplayName() ); 25 | 26 | Location target = new Location(event, player); 27 | Location newLoc = BorderCheck.checkPlayer(player, target, true, true); 28 | 29 | if (newLoc != null) 30 | { 31 | event.setCanceled(true); 32 | event.setTargetX(newLoc.posX); 33 | event.setTargetY(newLoc.posY); 34 | event.setTargetZ(newLoc.posZ); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdWhoosh.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.List; 10 | 11 | 12 | public class CmdWhoosh extends WBCmd 13 | { 14 | public CmdWhoosh() 15 | { 16 | name = permission = "whoosh"; 17 | minParams = maxParams = 1; 18 | 19 | addCmdExample(nameEmphasized() + " - turn knockback effect on or off."); 20 | helpText = "Default value: on. This will show a particle effect and play a sound where a player is knocked " + 21 | "back from the border."; 22 | } 23 | 24 | @Override 25 | public void cmdStatus(ICommandSender sender) 26 | { 27 | Util.chat(sender, C_HEAD + "\"Whoosh\" knockback effect is " + enabledColored(Config.doWhooshEffect()) + C_HEAD + "."); 28 | } 29 | 30 | @Override 31 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 32 | { 33 | Config.setWhooshEffect(strAsBool(params.get(0))); 34 | 35 | if (player != null) 36 | { 37 | Log.info((Config.doWhooshEffect() ? "Enabled" : "Disabled") + " \"whoosh\" knockback effect at the command of player \"" + player.getDisplayName() + "\"."); 38 | cmdStatus(sender); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdPreventSpawn.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.List; 10 | 11 | public class CmdPreventSpawn extends WBCmd 12 | { 13 | 14 | public CmdPreventSpawn() { 15 | name = permission = "preventmobspawn"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - stop mob spawning past border."); 19 | helpText = "Default value: off. When enabled, this setting will prevent mobs from naturally spawning outside the world's border."; 20 | } 21 | 22 | @Override 23 | public void cmdStatus(ICommandSender sender) 24 | { 25 | Util.chat(sender, C_HEAD + "Prevention of mob spawning outside the border is " + enabledColored(Config.preventMobSpawn()) + C_HEAD + "."); 26 | } 27 | 28 | @Override 29 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 30 | { 31 | Config.setPreventMobSpawn(strAsBool(params.get(0))); 32 | 33 | if (player != null) 34 | { 35 | Log.info((Config.preventMobSpawn() ? "Enabled" : "Disabled") + " preventmobspawn at the command of player \"" + player.getDisplayName() + "\"."); 36 | cmdStatus(sender); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/listener/BlockPlaceListener.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.listener; 2 | 3 | import com.wimbli.WorldBorder.BorderData; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.forge.Worlds; 6 | import net.minecraft.world.World; 7 | import net.minecraftforge.event.world.BlockEvent; 8 | import net.minecraftforge.fml.common.eventhandler.EventPriority; 9 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 10 | 11 | public class BlockPlaceListener 12 | { 13 | @SubscribeEvent(priority = EventPriority.HIGHEST) 14 | public void onBlockPlace(BlockEvent.PlaceEvent event) 15 | { 16 | if ( isInsideBorder(event.getWorld(), event.getPos().getX(), event.getPos().getZ()) ) 17 | return; 18 | 19 | event.setResult(BlockEvent.Result.DENY); 20 | event.setCanceled(true); 21 | } 22 | 23 | @SubscribeEvent(priority = EventPriority.HIGHEST) 24 | public void onMultiBlockPlace(BlockEvent.MultiPlaceEvent event) 25 | { 26 | if ( isInsideBorder(event.getWorld(), event.getPos().getX(), event.getPos().getZ()) ) 27 | return; 28 | 29 | event.setResult(BlockEvent.Result.DENY); 30 | event.setCanceled(true); 31 | } 32 | 33 | private boolean isInsideBorder(World world, int x, int z) 34 | { 35 | BorderData border = Config.Border(Worlds.getWorldName(world)); 36 | 37 | return border == null 38 | || border.insideBorder( x, z, Config.getShapeRound() ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdPreventPlace.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.List; 10 | 11 | public class CmdPreventPlace extends WBCmd 12 | { 13 | 14 | public CmdPreventPlace() { 15 | name = permission = "preventblockplace"; 16 | minParams = maxParams = 1; 17 | 18 | addCmdExample(nameEmphasized() + " - stop block placement past border."); 19 | helpText = "Default value: off. When enabled, this setting will prevent players from placing blocks outside the world's border."; 20 | } 21 | 22 | @Override 23 | public void cmdStatus(ICommandSender sender) 24 | { 25 | Util.chat(sender, C_HEAD + "Prevention of block placement outside the border is " + enabledColored(Config.preventBlockPlace()) + C_HEAD + "."); 26 | } 27 | 28 | @Override 29 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 30 | { 31 | Config.setPreventBlockPlace(strAsBool(params.get(0))); 32 | 33 | if (player != null) 34 | { 35 | Log.info((Config.preventBlockPlace() ? "Enabled" : "Disabled") + " preventblockplace at the command of player \"" + player.getDisplayName() + "\"."); 36 | cmdStatus(sender); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDynmap.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.List; 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(ICommandSender sender) 25 | { 26 | Util.chat(sender, C_HEAD + "DynMap border display is " + enabledColored(Config.isDynmapBorderEnabled()) + C_HEAD + "."); 27 | } 28 | 29 | @Override 30 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 31 | { 32 | Config.setDynmapBorderEnabled(strAsBool(params.get(0))); 33 | 34 | if (player != null) 35 | { 36 | cmdStatus(sender); 37 | Log.info( 38 | (Config.isDynmapBorderEnabled() ? "Enabled" : "Disabled") 39 | + " DynMap border display at the command of player \"" 40 | + player.getDisplayName() + "\"." 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdSetmsg.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 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(ICommandSender sender) 24 | { 25 | Util.chat(sender, C_HEAD + "Border message is set to:"); 26 | Util.chat(sender, Config.getMessageRaw()); 27 | Util.chat(sender, C_HEAD + "Formatted border message:"); 28 | Util.chat(sender, Config.getMessage()); 29 | } 30 | 31 | @Override 32 | public void execute(ICommandSender sender, EntityPlayerMP 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 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdRemount.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.List; 10 | 11 | 12 | public class CmdRemount extends WBCmd 13 | { 14 | public CmdRemount() 15 | { 16 | name = permission = "remount"; 17 | minParams = maxParams = 1; 18 | 19 | addCmdExample(nameEmphasized() + " - turn remount after knockback on or off."); 20 | helpText = "Default value: on. Players who are knocked back from a border are ejected from their vehicle " + 21 | "by vanilla design. With this enabled, they will be remounted on their vehicle."; 22 | } 23 | 24 | @Override 25 | public void cmdStatus(ICommandSender sender) 26 | { 27 | Util.chat(sender, C_HEAD + "Remount after knockback is " + enabledColored(Config.getRemount()) + C_HEAD + "."); 28 | } 29 | 30 | @Override 31 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 32 | { 33 | Config.setRemount(strAsBool(params.get(0))); 34 | 35 | if (player != null) 36 | { 37 | cmdStatus(sender); 38 | Log.info( 39 | (Config.getRemount() ? "Enabled" : "Disabled") 40 | + " remount after knockback at the command of player \"" 41 | + player.getDisplayName() + "\"." 42 | ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/listener/MobSpawnListener.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.listener; 2 | 3 | import com.wimbli.WorldBorder.BorderData; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.forge.Worlds; 6 | import net.minecraft.world.World; 7 | import net.minecraftforge.event.entity.living.LivingSpawnEvent; 8 | import net.minecraftforge.fml.common.eventhandler.EventPriority; 9 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 10 | 11 | // TODO: This requires extensive profiling to ensure least server impact 12 | public class MobSpawnListener 13 | { 14 | @SubscribeEvent(priority = EventPriority.HIGHEST) 15 | public void onCreatureSpawn(LivingSpawnEvent.CheckSpawn event) 16 | { 17 | if ( isInsideBorder(event) ) 18 | return; 19 | 20 | // CheckSpawn uses event result instead of cancellation 21 | event.setResult(LivingSpawnEvent.Result.DENY); 22 | } 23 | 24 | @SubscribeEvent(priority = EventPriority.HIGHEST) 25 | public void onSpecialSpawn(LivingSpawnEvent.SpecialSpawn event) 26 | { 27 | if ( isInsideBorder(event) ) 28 | return; 29 | 30 | // SpecialSpawn uses event cancellation instead of result 31 | event.setCanceled(true); 32 | } 33 | 34 | private boolean isInsideBorder(LivingSpawnEvent event) 35 | { 36 | World world = event.getEntity().world; 37 | BorderData border = Config.Border( Worlds.getWorldName(world) ); 38 | 39 | return border == null 40 | || border.insideBorder( event.getEntity().posX, event.getEntity().posZ, Config.getShapeRound() ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDynmapmsg.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 9 | 10 | public class CmdDynmapmsg extends WBCmd 11 | { 12 | public CmdDynmapmsg() 13 | { 14 | name = permission = "dynmapmsg"; 15 | minParams = 1; 16 | 17 | addCmdExample(nameEmphasized() + " - DynMap border labels will show this."); 18 | helpText = "Default value: \"The border of the world.\". If you are running the DynMap plugin and the " + 19 | commandEmphasized("dynmap") + C_DESC + "command setting is enabled, the borders shown in DynMap will " + 20 | "be labelled with this text."; 21 | } 22 | 23 | @Override 24 | public void cmdStatus(ICommandSender sender) 25 | { 26 | Util.chat(sender, C_HEAD + "DynMap border label is set to: " + C_ERR + Config.getDynmapMessage()); 27 | } 28 | 29 | @Override 30 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 31 | { 32 | StringBuilder message = new StringBuilder(); 33 | boolean first = true; 34 | for (String param : params) 35 | { 36 | if (!first) 37 | message.append(" "); 38 | message.append(param); 39 | first = false; 40 | } 41 | 42 | Config.setDynmapMessage(message.toString()); 43 | 44 | if (player != null) 45 | cmdStatus(sender); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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/CmdBypasslist.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Profiles; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | public class CmdBypasslist extends WBCmd 14 | { 15 | public CmdBypasslist() 16 | { 17 | name = permission = "bypasslist"; 18 | minParams = maxParams = 0; 19 | 20 | addCmdExample(nameEmphasized() + "- list players with border bypass enabled."); 21 | helpText = "The bypass list will persist between server restarts, and applies to all worlds. Use the " + 22 | commandEmphasized("bypass") + C_DESC + "command to add or remove players."; 23 | } 24 | 25 | @Override 26 | public void execute(final ICommandSender sender, EntityPlayerMP player, List params, String worldName) 27 | { 28 | final UUID[] uuids = Config.getPlayerBypassList(); 29 | if (uuids.length == 0) 30 | { 31 | Util.chat(sender, "Players with border bypass enabled: "); 32 | return; 33 | } 34 | 35 | try 36 | { 37 | String[] names = Profiles.fetchNames(uuids); 38 | String list = Arrays.toString(names); 39 | 40 | Util.chat(sender, "Players with border bypass enabled: " + list); 41 | } 42 | catch (Exception ex) 43 | { 44 | sendErrorAndHelp(sender, "Failed to look up names for the UUIDs in the border bypass list: " + ex.getMessage()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdDelay.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 9 | 10 | public class CmdDelay extends WBCmd 11 | { 12 | public CmdDelay() 13 | { 14 | name = permission = "delay"; 15 | minParams = maxParams = 1; 16 | 17 | addCmdExample(nameEmphasized() + " - time between border checks."); 18 | helpText = "Default value: 5. The is in server ticks, of which there are roughly 20 every second, each " + 19 | "tick taking ~50ms. The default value therefore has border checks run about 4 times per second."; 20 | } 21 | 22 | @Override 23 | public void cmdStatus(ICommandSender sender) 24 | { 25 | int delay = Config.getTimerTicks(); 26 | Util.chat(sender, C_HEAD + "Timer delay is set to " + delay + " tick(s). That is roughly " + (delay * 50) + "ms."); 27 | } 28 | 29 | @Override 30 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 31 | { 32 | int delay = 0; 33 | try 34 | { 35 | delay = Integer.parseInt(params.get(0)); 36 | if (delay < 1) 37 | throw new NumberFormatException(); 38 | } 39 | catch(NumberFormatException ex) 40 | { 41 | sendErrorAndHelp(sender, "The timer delay must be an integer of 1 or higher."); 42 | return; 43 | } 44 | 45 | Config.setTimerTicks(delay); 46 | 47 | if (player != null) 48 | cmdStatus(sender); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdKnockback.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 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(ICommandSender sender) 24 | { 25 | double kb = Config.getKnockBack(); 26 | if (kb < 1) 27 | Util.chat(sender, C_HEAD + "Knockback is set to 0, disabling border enforcement."); 28 | else 29 | Util.chat(sender, C_HEAD + "Knockback is set to " + kb + " blocks inside the border."); 30 | } 31 | 32 | @Override 33 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 34 | { 35 | float numBlocks = 0.0F; 36 | try 37 | { 38 | numBlocks = Float.parseFloat(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/CmdDenypearl.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | 9 | import java.util.List; 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(ICommandSender sender) 26 | { 27 | Util.chat(sender, 28 | C_HEAD + "Direct cancellation of ender pearls thrown past the border is " + 29 | enabledColored( Config.getDenyEnderpearl() ) + C_HEAD + "." 30 | ); 31 | } 32 | 33 | @Override 34 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 35 | { 36 | Config.setDenyEnderpearl( strAsBool( params.get(0) ) ); 37 | 38 | if (player != null) 39 | { 40 | Log.info( 41 | (Config.getDenyEnderpearl() ? "Enabled" : "Disabled") 42 | + " direct cancellation of ender pearls thrown past the border at the command of player \"" 43 | + player.getDisplayName() + "\"." 44 | ); 45 | cmdStatus(sender); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/forge/Particles.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import net.minecraft.entity.player.EntityPlayer; 4 | import net.minecraft.entity.player.EntityPlayerMP; 5 | import net.minecraft.init.SoundEvents; 6 | import net.minecraft.network.play.server.SPacketParticles; 7 | import net.minecraft.util.EnumParticleTypes; 8 | import net.minecraft.util.SoundCategory; 9 | import net.minecraft.world.WorldServer; 10 | 11 | /** Handles creation and sending of particle effect packets for server-side emission */ 12 | public class Particles 13 | { 14 | public static void showWhooshEffect(EntityPlayerMP player) 15 | { 16 | WorldServer world = player.getServerWorld(); 17 | Particles.emitEnder(world, player.posX, player.posY, player.posZ); 18 | Particles.emitSmoke(world, player.posX, player.posY, player.posZ); 19 | world.playSound(player, player.getPosition(), SoundEvents.ITEM_FIRECHARGE_USE, SoundCategory.BLOCKS, 1.0F, 1.0f); 20 | } 21 | 22 | private static void emitSmoke(WorldServer world, double x, double y, double z) 23 | { 24 | SPacketParticles packet = new SPacketParticles(EnumParticleTypes.SMOKE_LARGE, false, (float) x, (float) y, (float) z, 0f, 0.5f, 0f, 0.0f, 10); 25 | 26 | dispatch(world, packet); 27 | } 28 | 29 | private static void emitEnder(WorldServer world, double x, double y, double z) 30 | { 31 | SPacketParticles packet = new SPacketParticles(EnumParticleTypes.PORTAL, false, (float) x, (float) y, (float) z, 0.5f, 0.5f, 0.5f, 1.0f, 50); 32 | dispatch(world, packet); 33 | } 34 | 35 | private static void dispatch(WorldServer world, SPacketParticles packet) 36 | { 37 | //fix later, not needed right now 38 | //for (Object o : world.playerEntities) 39 | // ((EntityPlayerMP) o).mcServer.getEntityWorld().sendPacketToServer(packet); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/task/BorderCheckTask.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.task; 2 | 3 | import com.wimbli.WorldBorder.BorderCheck; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.WorldBorder; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | import net.minecraftforge.fml.common.FMLCommonHandler; 8 | import net.minecraftforge.fml.common.eventhandler.EventPriority; 9 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 10 | import net.minecraftforge.fml.common.gameevent.TickEvent; 11 | 12 | /** 13 | * Tick handler that regularly checks for players that have wandered beyond any 14 | * configured borders. Handles knocking back of outside players. 15 | */ 16 | public class BorderCheckTask 17 | { 18 | private boolean running = false; 19 | 20 | public boolean isRunning() 21 | { 22 | return running; 23 | } 24 | 25 | /** Sets whether this task is running by (un)registering it as a tick handler */ 26 | public void setRunning(boolean state) 27 | { 28 | if (state) 29 | FMLCommonHandler.instance().bus().register(this); 30 | else 31 | FMLCommonHandler.instance().bus().unregister(this); 32 | 33 | running = state; 34 | } 35 | 36 | /** Uses lowest event priority to run after everything else has */ 37 | @SubscribeEvent(priority = EventPriority.LOWEST) 38 | public void onServerTick(TickEvent.ServerTickEvent event) 39 | { 40 | // Only run at end of tick to catch players that just moved past border 41 | if (event.phase == TickEvent.Phase.START) 42 | return; 43 | 44 | if ( WorldBorder.SERVER.getTickCounter() % Config.getTimerTicks() != 0 ) 45 | return; 46 | 47 | for (Object o : WorldBorder.SERVER.getPlayerList().getPlayers()) 48 | BorderCheck.checkPlayer( (EntityPlayerMP) o, null, false, true ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdShape.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 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(ICommandSender sender) 28 | { 29 | Util.chat(sender, C_HEAD + "The default border shape for all worlds is currently set to \"" + Config.getShapeName() + "\"."); 30 | } 31 | 32 | @Override 33 | public void execute(ICommandSender sender, EntityPlayerMP 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 com.wimbli.WorldBorder.WorldBorder; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 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(ICommandSender sender) 26 | { 27 | String commands = WorldBorder.COMMAND.getCommandNames().toString().replace(", ", C_DESC + ", " + C_CMD); 28 | Util.chat(sender, C_HEAD + "Commands: " + C_CMD + commands.substring(1, commands.length() - 1)); 29 | Util.chat(sender, "Example, for info on \"set\" command: " + cmd(sender) + nameEmphasized() + C_CMD + "set"); 30 | Util.chat(sender, 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(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 35 | { 36 | if (params.isEmpty()) 37 | { 38 | sendCmdHelp(sender); 39 | return; 40 | } 41 | 42 | ArrayList commands = WorldBorder.COMMAND.getCommandNames(); 43 | for (String param : params) 44 | if (commands.contains(param.toLowerCase())) 45 | { 46 | WorldBorder.COMMAND.subCommands.get(param.toLowerCase()).sendCmdHelp(sender); 47 | return; 48 | } 49 | 50 | sendErrorAndHelp(sender, "No command recognized."); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/forge/Util.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import net.minecraft.command.ICommandSender; 4 | import net.minecraft.server.dedicated.DedicatedServer; 5 | import net.minecraft.util.text.TextComponentString; 6 | import net.minecraft.util.text.translation.I18n; 7 | 8 | /** Static utility class for shortcut methods to help transition from Bukkit to Forge */ 9 | public class Util 10 | { 11 | /** 12 | * Attempts to a translate a given string/key using the local language, and then 13 | * using the fallback language 14 | * @param msg String or language key to translate 15 | * @return Translated or same string 16 | */ 17 | public static String translate(String msg) 18 | { 19 | return I18n.canTranslate(msg) 20 | ? I18n.translateToLocal(msg) 21 | : I18n.translateToFallback(msg); 22 | } 23 | 24 | /** 25 | * Sends an automatically translated and formatted message to a command sender 26 | * @param sender Target to send message to 27 | * @param msg String or language key to broadcast 28 | */ 29 | public static void chat(ICommandSender sender, String msg, Object... parts) 30 | { 31 | String translated = translate(msg); 32 | 33 | // Consoles require ANSI coloring for formatting 34 | if (sender instanceof DedicatedServer) 35 | Log.info( removeFormatting(translated), parts ); 36 | else 37 | { 38 | translated = String.format(translated, parts); 39 | sender.sendMessage( new TextComponentString(translated) ); 40 | } 41 | } 42 | 43 | /** Replaces Bukkit-convention amp format tokens with vanilla ones */ 44 | public static String replaceAmpColors(String message) 45 | { 46 | return message.replaceAll("(?i)&([a-fk-or0-9])", "§$1"); 47 | } 48 | 49 | /** Strips vanilla formatting from a string */ 50 | public static String removeFormatting(String message) 51 | { 52 | return message.replaceAll("(?i)§[a-fk-or0-9]", ""); 53 | } 54 | 55 | /** Shortcut for java.lang.System.currentTimeMillis */ 56 | public static long now() 57 | { 58 | return System.currentTimeMillis(); 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdSetcorners.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import com.wimbli.WorldBorder.forge.Worlds; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | import net.minecraft.world.WorldServer; 9 | 10 | import java.util.List; 11 | 12 | 13 | public class CmdSetcorners extends WBCmd 14 | { 15 | public CmdSetcorners() 16 | { 17 | name = "setcorners"; 18 | permission = "set"; 19 | hasWorldNameInput = true; 20 | minParams = maxParams = 4; 21 | 22 | addCmdExample(nameEmphasizedW() + " - corner coords."); 23 | helpText = "This is an alternate way to set a border, by specifying the X and Z coordinates of two opposite " + 24 | "corners of the border area ((x1, z1) to (x2, z2)). [world] is optional for players and defaults to the " + 25 | "world the player is in."; 26 | } 27 | 28 | @Override 29 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 30 | { 31 | if (worldName == null) 32 | worldName = Worlds.getWorldName(player.world); 33 | else 34 | { 35 | WorldServer worldTest = Worlds.getWorld(worldName); 36 | if (worldTest == null) 37 | Util.chat(sender, "The world you specified (\"" + worldName + "\") could not be found on the server, but data for it will be stored anyway."); 38 | } 39 | 40 | try 41 | { 42 | double x1 = Double.parseDouble(params.get(0)); 43 | double z1 = Double.parseDouble(params.get(1)); 44 | double x2 = Double.parseDouble(params.get(2)); 45 | double z2 = Double.parseDouble(params.get(3)); 46 | Config.setBorderCorners(worldName, x1, z1, x2, z2); 47 | } 48 | catch(NumberFormatException ex) 49 | { 50 | sendErrorAndHelp(sender, "The x1, z1, x2, and z2 coordinate values must be numerical."); 51 | return; 52 | } 53 | 54 | if(player != null) 55 | Util.chat(sender, "Border has been set. " + Config.BorderDescription(worldName)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdWrap.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.BorderData; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import com.wimbli.WorldBorder.forge.Worlds; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.entity.player.EntityPlayerMP; 9 | 10 | import java.util.List; 11 | 12 | 13 | public class CmdWrap extends WBCmd 14 | { 15 | public CmdWrap() 16 | { 17 | name = permission = "wrap"; 18 | minParams = 1; 19 | maxParams = 2; 20 | 21 | addCmdExample(nameEmphasized() + "{world} - can make border crossings wrap."); 22 | helpText = "When border wrapping is enabled for a world, players will be sent around to the opposite edge " + 23 | "of the border when they cross it instead of being knocked back. [world] is optional for players and " + 24 | "defaults to the world the player is in."; 25 | } 26 | 27 | @Override 28 | public void execute(ICommandSender sender, EntityPlayerMP 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 | boolean wrap = false; 37 | 38 | // world and wrap on/off specified 39 | if (params.size() == 2) 40 | { 41 | worldName = params.get(0); 42 | wrap = strAsBool(params.get(1)); 43 | } 44 | // no world specified, just wrap on/off 45 | else 46 | { 47 | worldName = Worlds.getWorldName(player.world); 48 | wrap = strAsBool(params.get(0)); 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 | border.setWrapping(wrap); 59 | Config.setBorder(worldName, border, false); 60 | 61 | Util.chat(sender, "Border for world \"" + worldName + "\" is now set to " + (wrap ? "" : "not ") + "wrap around."); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdFillautosave.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 9 | 10 | public class CmdFillautosave extends WBCmd 11 | { 12 | public CmdFillautosave() 13 | { 14 | name = permission = "fillautosave"; 15 | minParams = maxParams = 1; 16 | 17 | addCmdExample(nameEmphasized() + " - world save interval for Fill."); 18 | helpText = "Default value: 30 seconds."; 19 | } 20 | 21 | @Override 22 | public void cmdStatus(ICommandSender sender) 23 | { 24 | int seconds = Config.getFillAutosaveFrequency(); 25 | if (seconds == 0) 26 | { 27 | Util.chat(sender, C_HEAD + "World autosave frequency during Fill process is set to 0, disabling it."); 28 | Util.chat(sender, C_HEAD + "Note that much progress can be lost this way if there is a bug or crash in " + 29 | "the world generation process from Bukkit or any world generation plugin you use."); 30 | } 31 | else 32 | { 33 | Util.chat(sender, C_HEAD + "World autosave frequency during Fill process is set to " + seconds + " seconds (rounded to a multiple of 5)."); 34 | Util.chat(sender, C_HEAD + "New chunks generated by the Fill process will be forcibly saved to disk " + 35 | "this often to prevent loss of progress due to bugs or crashes in the world generation process."); 36 | } 37 | } 38 | 39 | @Override 40 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 41 | { 42 | int seconds = 0; 43 | try 44 | { 45 | seconds = Integer.parseInt(params.get(0)); 46 | if (seconds < 0) 47 | throw new NumberFormatException(); 48 | } 49 | catch(NumberFormatException ex) 50 | { 51 | 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."); 52 | return; 53 | } 54 | 55 | Config.setFillAutosaveFrequency(seconds); 56 | 57 | if (player != null) 58 | cmdStatus(sender); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdClear.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.BorderData; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import com.wimbli.WorldBorder.forge.Worlds; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.entity.player.EntityPlayerMP; 9 | 10 | import java.util.List; 11 | 12 | public class CmdClear extends WBCmd 13 | { 14 | public CmdClear() 15 | { 16 | name = permission = "clear"; 17 | hasWorldNameInput = true; 18 | consoleRequiresWorldName = false; 19 | minParams = 0; 20 | maxParams = 1; 21 | 22 | addCmdExample(nameEmphasizedW() + "- remove border for this world."); 23 | addCmdExample(nameEmphasized() + "^all - remove border for all worlds."); 24 | helpText = "If run by an in-game player and [world] or \"all\" isn't specified, the world you are currently " + 25 | "in is used."; 26 | } 27 | 28 | @Override 29 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 30 | { 31 | // handle "clear all" command separately 32 | if (params.size() == 1 && params.get(0).equalsIgnoreCase("all")) 33 | { 34 | if (worldName != null) 35 | { 36 | sendErrorAndHelp(sender, "You should not specify a world with \"clear all\"."); 37 | return; 38 | } 39 | 40 | Config.removeAllBorders(); 41 | 42 | if (player != null) 43 | Util.chat(sender, "All borders have been cleared for all worlds."); 44 | return; 45 | } 46 | 47 | if (worldName == null) 48 | { 49 | if (player == null) 50 | { 51 | sendErrorAndHelp(sender, "You must specify a world name from console if not using \"clear all\"."); 52 | return; 53 | } 54 | worldName = Worlds.getWorldName(player.world); 55 | } 56 | 57 | BorderData border = Config.Border(worldName); 58 | if (border == null) 59 | { 60 | sendErrorAndHelp(sender, "This world (\"" + worldName + "\") does not have a border set."); 61 | return; 62 | } 63 | 64 | Config.removeBorder(worldName); 65 | 66 | if (player != null) 67 | Util.chat(sender, "Border cleared for world \"" + worldName + "\"."); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdWshape.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.BorderData; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import com.wimbli.WorldBorder.forge.Worlds; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.entity.player.EntityPlayerMP; 9 | 10 | import java.util.List; 11 | 12 | 13 | public class CmdWshape extends WBCmd 14 | { 15 | public CmdWshape() 16 | { 17 | name = permission = "wshape"; 18 | minParams = 1; 19 | maxParams = 2; 20 | 21 | addCmdExample(nameEmphasized() + "{world} - shape override for a single world."); 22 | addCmdExample(nameEmphasized() + "{world} - same as above."); 23 | helpText = "This will override the default border shape for a single world. The value \"default\" implies " + 24 | "a world is just using the default border shape. See the " + commandEmphasized("shape") + C_DESC + 25 | "command for more info and to set the default border shape."; 26 | } 27 | 28 | @Override 29 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 30 | { 31 | if (player == null && params.size() == 1) 32 | { 33 | sendErrorAndHelp(sender, "When running this command from console, you must specify a world."); 34 | return; 35 | } 36 | 37 | String shapeName; 38 | 39 | // world and shape specified 40 | if (params.size() == 2) 41 | { 42 | worldName = params.get(0); 43 | shapeName = params.get(1).toLowerCase(); 44 | } 45 | // no world specified, just shape 46 | else 47 | { 48 | worldName = Worlds.getWorldName(player.world); 49 | shapeName = params.get(0).toLowerCase(); 50 | } 51 | 52 | BorderData border = Config.Border(worldName); 53 | if (border == null) 54 | { 55 | sendErrorAndHelp(sender, "This world (\"" + worldName + "\") does not have a border set."); 56 | return; 57 | } 58 | 59 | Boolean shape = null; 60 | if (shapeName.equals("rectangular") || shapeName.equals("square")) 61 | shape = false; 62 | else if (shapeName.equals("elliptic") || shapeName.equals("round")) 63 | shape = true; 64 | 65 | border.setShape(shape); 66 | Config.setBorder(worldName, border, false); 67 | 68 | Util.chat(sender, "Border shape for world \"" + worldName + "\" is now set to \"" + Config.getShapeName(shape) + "\"."); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/forge/Location.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import net.minecraft.entity.player.EntityPlayer; 4 | import net.minecraft.entity.player.EntityPlayerMP; 5 | import net.minecraft.util.math.BlockPos; 6 | import net.minecraft.world.WorldServer; 7 | import net.minecraftforge.event.entity.living.EnderTeleportEvent; 8 | 9 | /** 10 | * Represents a position, pitch and yaw of a specific world. Modelled after Bukkit's API 11 | * structure of Location, but does not use any of its implementation 12 | */ 13 | public class Location 14 | { 15 | public WorldServer world = null; 16 | 17 | public double posX = 0; 18 | public double posY = 0; 19 | public double posZ = 0; 20 | public float pitch = 0.0f; 21 | public float yaw = 0.0f; 22 | 23 | /** 24 | * Creates a Location based on the target position of a player and a fired 25 | * {@link EnderTeleportEvent} 26 | */ 27 | public Location(EnderTeleportEvent event, EntityPlayerMP player) 28 | { 29 | world = (WorldServer) player.world; 30 | posX = event.getTargetX(); 31 | posY = event.getTargetY(); 32 | posZ = event.getTargetZ(); 33 | pitch = player.rotationPitch; 34 | yaw = player.rotationYaw; 35 | } 36 | 37 | /** Creates a Location based on the latest (target) position of a player */ 38 | public Location(EntityPlayer player) 39 | { 40 | world = (WorldServer) player.world; 41 | posX = player.posX; 42 | posY = player.posY; 43 | posZ = player.posZ; 44 | pitch = player.rotationPitch; 45 | yaw = player.rotationYaw; 46 | } 47 | 48 | /** Clones an existing Location */ 49 | public Location(Location loc) 50 | { 51 | world = loc.world; 52 | posX = loc.posX; 53 | posY = loc.posY; 54 | posZ = loc.posZ; 55 | pitch = loc.pitch; 56 | yaw = loc.yaw; 57 | } 58 | 59 | /** Creates a location from a world's spawn point */ 60 | public Location(WorldServer world) 61 | { 62 | BlockPos spawn = world.getSpawnPoint(); 63 | 64 | this.world = world; 65 | this.posX = spawn.getX(); 66 | this.posY = spawn.getY(); 67 | this.posZ = spawn.getZ(); 68 | this.pitch = 0; 69 | this.yaw = 0; 70 | } 71 | 72 | /** Creates a new Location with all data provided */ 73 | public Location(WorldServer world, double x, double y, double z, float yaw, float pitch) 74 | { 75 | this.world = world; 76 | this.posX = x; 77 | this.posY = y; 78 | this.posZ = z; 79 | this.pitch = pitch; 80 | this.yaw = yaw; 81 | } 82 | 83 | /** 84 | * TODO: Find faster algorithm than native 85 | */ 86 | public static int locToBlock(double loc) 87 | { 88 | return (int) Math.floor(loc); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdCommands.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.WorldBorder; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import net.minecraft.command.ICommandSender; 6 | import net.minecraft.entity.player.EntityPlayerMP; 7 | 8 | import java.util.List; 9 | 10 | public class CmdCommands extends WBCmd 11 | { 12 | private static int pageSize = 8; // examples to list per page; 10 lines available, 1 for header, 1 for footer 13 | 14 | public CmdCommands() 15 | { 16 | name = "commands"; 17 | permission = "help"; 18 | hasWorldNameInput = false; 19 | } 20 | 21 | @Override 22 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 23 | { 24 | // determine which page we're viewing 25 | int page = (player == null) ? 0 : 1; 26 | if (!params.isEmpty()) 27 | try 28 | { 29 | page = Integer.parseInt(params.get(0)); 30 | } 31 | catch(NumberFormatException ignored) {} 32 | 33 | // see whether we're showing examples to player or to console, and determine number of pages available 34 | List examples = (player == null) ? cmdExamplesConsole : cmdExamplesPlayer; 35 | int pageCount = (int) Math.ceil(examples.size() / (double) pageSize); 36 | 37 | // if specified page number is negative or higher than we have available, default back to first page 38 | if (page < 0 || page > pageCount) 39 | page = (player == null) ? 0 : 1; 40 | 41 | // send command example header 42 | Util.chat(sender, C_HEAD + WorldBorder.MODID + " - key: " + 43 | commandEmphasized("command") + C_REQ + " " + C_OPT + "[optional]"); 44 | 45 | if (page > 0) 46 | { 47 | // send examples for this page 48 | int first = (page - 1) * pageSize; 49 | int count = Math.min(pageSize, examples.size() - first); 50 | 51 | for(int i = first; i < first + count; i++) 52 | Util.chat(sender, examples.get(i)); 53 | 54 | // send page footer, if relevant; manual spacing to get right side lined up near edge is crude, but sufficient 55 | String footer = C_HEAD + " (Page " + page + "/" + pageCount + ") " + cmd(sender); 56 | if (page < pageCount) 57 | Util.chat(sender, footer + Integer.toString(page + 1) + C_DESC + " - view next page of commands."); 58 | else if (page > 1) 59 | Util.chat(sender, footer + C_DESC + "- view first page of commands."); 60 | } 61 | else 62 | // if page "0" is specified, send all examples; done by default for console but can be specified by player 63 | for (String example : examples) 64 | Util.chat(sender, example); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/forge/Worlds.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import com.wimbli.WorldBorder.WorldBorder; 4 | import net.minecraft.block.Block; 5 | import net.minecraft.util.math.BlockPos; 6 | import net.minecraft.world.MinecraftException; 7 | import net.minecraft.world.World; 8 | import net.minecraft.world.WorldServer; 9 | import net.minecraftforge.common.DimensionManager; 10 | 11 | /** Static utility class for managing and dealing with Forge worlds */ 12 | public class Worlds 13 | { 14 | /** 15 | * Generates a canonical name from a given world. 16 | * 17 | * Instead of using internal folder names, this uses the Dynmap method of using the 18 | * dimension number with the 'DIM' prefix. This improves compatibility with Dynmap 19 | * and makes world handling provider-agnostic. 20 | */ 21 | public static String getWorldName(World world) 22 | { 23 | // Dimension 0 will always use the name configured in server.properties 24 | return (world.provider.getDimension() == 0) 25 | ? world.getWorldInfo().getWorldName() 26 | : "DIM" + world.provider.getDimension(); 27 | } 28 | 29 | /** 30 | * Performs a case-sensitive search for a loaded world by a given name. 31 | * 32 | * First, it tries to match the name with dimension 0 (overworld), then it tries to 33 | * match from the world's save folder name (e.g. DIM_MYST10) and then finally the 34 | * Dynmap compatible identifier (e.g. DIM10) 35 | * 36 | * @param name Name of world to find 37 | * @return World if found, else null 38 | */ 39 | public static WorldServer getWorld(String name) 40 | { 41 | if ( name == null || name.isEmpty() ) 42 | throw new IllegalArgumentException("World name cannot be empty or null"); 43 | 44 | for ( WorldServer world : DimensionManager.getWorlds() ) 45 | { 46 | String dimName = "DIM" + world.provider.getDimension(); 47 | String saveFolder = world.provider.getSaveFolder(); 48 | 49 | if (world.provider.getDimension() == 0) 50 | { // Special case for dimension 0 (overworld) 51 | if ( WorldBorder.SERVER.getFolderName().equals(name) ) 52 | return world; 53 | } 54 | else if ( saveFolder.equals(name) || dimName.equals(name) ) 55 | return world; 56 | } 57 | 58 | return null; 59 | } 60 | 61 | /** Safely saves a given world to disk */ 62 | public static void saveWorld(WorldServer world) 63 | { 64 | try 65 | { 66 | Boolean saveFlag = world.disableLevelSaving; 67 | world.disableLevelSaving = true; 68 | world.saveAllChunks(true, null); 69 | world.disableLevelSaving = saveFlag; 70 | } 71 | catch (MinecraftException e) 72 | { 73 | e.printStackTrace(); 74 | } 75 | } 76 | 77 | /** Gets the ID of the block type of the given block position in a world */ 78 | public static int getBlockID(World world, int x, int y, int z) 79 | { 80 | return Block.getIdFromBlock(world.getBlockState(new BlockPos(x, y, z)).getBlock()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/forge/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.forge; 2 | 3 | import java.io.File; 4 | import java.util.Set; 5 | 6 | /** 7 | * More convenient and compatible version of the Forge configuration class 8 | */ 9 | public class Configuration extends net.minecraftforge.common.config.Configuration 10 | { 11 | /** Creates convenient config with case-sensitive categories */ 12 | public Configuration(File file) 13 | { 14 | super(file, true); 15 | } 16 | 17 | /** Shortcut for getting a string from a key and category */ 18 | public String getString(String category, String key, String defValue) 19 | { 20 | return getString(key, category, defValue, ""); 21 | } 22 | 23 | /** Shortcut for getting a boolean from a key and category */ 24 | public boolean getBoolean(String category, String key, boolean defValue) 25 | { 26 | return getBoolean(key, category, defValue, ""); 27 | } 28 | 29 | /** Shortcut for getting a float from a key and category */ 30 | public float getFloat(String category, String key, float defValue) 31 | { 32 | return getFloat(key, category, defValue, 0, Float.MAX_VALUE, ""); 33 | } 34 | 35 | /** Shortcut for getting an int from a key and category */ 36 | public int getInt(String category, String key, int defValue) 37 | { 38 | return getInt(key, category, defValue, Integer.MIN_VALUE, Integer.MAX_VALUE, ""); 39 | } 40 | 41 | /** Shortcut for getting a string array from a key and category */ 42 | public String[] getStringList(String category, String key) 43 | { 44 | return getStringList(key, category, new String[0], ""); 45 | } 46 | 47 | /** Shortcut for setting a boolean to a key and category */ 48 | public void set(String category, String key, boolean value) 49 | { 50 | get(category, key, value).set(value); 51 | } 52 | 53 | /** Shortcut for setting a string to a key and category */ 54 | public void set(String category, String key, String value) 55 | { 56 | get(category, key, value).set(value); 57 | } 58 | 59 | /** Shortcut for setting an integer to a key and category */ 60 | public void set(String category, String key, int value) 61 | { 62 | get(category, key, value).set(value); 63 | } 64 | 65 | /** Shortcut for setting a double to a key and category */ 66 | public void set(String category, String key, double value) 67 | { 68 | get(category, key, value).set(value); 69 | } 70 | 71 | /** Shortcut for setting a float to a key and category */ 72 | public void set(String category, String key, float value) 73 | { 74 | get(category, key, value).set(value); 75 | } 76 | 77 | /** Shortcut for setting a string array to a key and category */ 78 | public void set(String category, String key, String[] values) 79 | { 80 | get(category, key, values).set(values); 81 | } 82 | 83 | /** Removes a category by given name */ 84 | public void removeCategory(String category) 85 | { 86 | removeCategory(getCategory(category)); 87 | } 88 | 89 | /** Clears this configuration of all its data */ 90 | public void clear() 91 | { 92 | Set categories = getCategoryNames(); 93 | 94 | for(String category : categories) 95 | removeCategory( getCategory(category) ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.WorldBorder; 5 | import com.wimbli.WorldBorder.forge.Log; 6 | import com.wimbli.WorldBorder.forge.Profiles; 7 | import com.wimbli.WorldBorder.forge.Util; 8 | import net.minecraft.command.ICommandSender; 9 | import net.minecraft.entity.player.EntityPlayerMP; 10 | 11 | import java.util.List; 12 | import java.util.UUID; 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(ICommandSender sender) 31 | { 32 | if (!(sender instanceof EntityPlayerMP)) 33 | return; 34 | 35 | boolean bypass = Config.isPlayerBypassing(((EntityPlayerMP)sender).getUniqueID()); 36 | Util.chat(sender, C_HEAD + "Border bypass is currently " + enabledColored(bypass) + C_HEAD + " for you."); 37 | } 38 | 39 | @Override 40 | public void execute(final ICommandSender sender, final EntityPlayerMP 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 | UUID uPlayer; 49 | String sPlayer = params.isEmpty() 50 | ? player.getDisplayName().toString() 51 | : params.get(0); 52 | 53 | try 54 | { 55 | uPlayer = Profiles.fetchUUID(sPlayer); 56 | } 57 | catch (Exception ex) 58 | { 59 | sendErrorAndHelp( 60 | sender, 61 | "Failed to look up UUID for the player name you specified: " + ex.getLocalizedMessage() 62 | ); 63 | return; 64 | } 65 | 66 | boolean bypassing = !Config.isPlayerBypassing(uPlayer); 67 | if (params.size() > 1) 68 | bypassing = strAsBool(params.get(1)); 69 | 70 | Config.setPlayerBypass(uPlayer, bypassing); 71 | 72 | // If target is online, notify them 73 | EntityPlayerMP target = WorldBorder.SERVER.getPlayerList().getPlayerByUsername(sPlayer); 74 | if (target != null) 75 | Util.chat(target, "Border bypass is now " + enabledColored(bypassing) + " for you."); 76 | 77 | if (player != target) 78 | Util.chat(sender, "Border bypass for player \"" + sPlayer + "\" is " + enabledColored(bypassing) + "."); 79 | 80 | Log.info( 81 | "Border bypass for player \"" + sPlayer + "\" is " 82 | + (bypassing ? "enabled" : "disabled") 83 | + (player == null 84 | ? "." 85 | : " at the command of \"" + player.getDisplayName() + "\".") 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WorldBorder-Forge is an unofficial and unendorsed port of [Brett Flannigan](https://github.com/Brettflan)'s [WorldBorder](https://github.com/Brettflan/WorldBorder) Bukkit plugin to Forge 1.10.2. This version has been updated and being maintained by abused_master for minecraft versions 1.10.2+. It is a server-only mod that allows the easy management of world sizes. Almost all the features of the Bukkit plugin are available in this version, including but not limited to: 2 | 3 | * Per-dimension border control by radius or by corner points 4 | * Round or square borders, with per-dimension overrides 5 | * Knocking back players from borders 6 | * Wrap-around teleportation 7 | * Deny enderpearl, mob spawning and block placements outside borders 8 | * Generation of all chunks within a border with padding, fill rate and auto-save 9 | * Trimming of all chunks and region files outside a border with trim rate 10 | 11 | # Requirements 12 | 13 | * Minecraft Forge server for 1.10.2, at least [1.10.2-12.18.3.2511](files.minecraftforge.net) 14 | 15 | # Mod support 16 | 17 | * Integrates optionally with [Dynmap-Forge](http://minecraft.curseforge.com/mc-mods/59433-dynmapforge) for the automatic display of borders 18 | 19 | # Installation 20 | 21 | 1. Download the [latest release JAR](minecraft.curseforge.com/projects/worldborder-forge) or clone this repository & build a JAR file 22 | 2. Place the JAR file in the `mods/` directory of the server 23 | 3. Run/restart the server 24 | 4. Open `config/WorldBorder/main.cfg` and modify configuration to desired values 25 | 5. Execute `/wb reload` to reload changes 26 | 27 | # Differences 28 | 29 | This fork is based off version 1.8.4 of the WorldBorder Bukkit plugin. A lot of the codebase has been refactored and reformatted to get it to work best as a Forge mod. As such, it has differences in function and is most likely buggier. Some are intentional, others need further work. These include but are not limited to: 30 | 31 | * Use of .cfg files than .yml files, making configuration backwards incompatiable 32 | * [Unreliable handling of teleport events](https://github.com/Gamealition/WorldBorder-Forge/issues/1) 33 | * Unreliable handling of unloaded dimensions 34 | * Uses "DIM##" instead of friendly names for dimensions 35 | * No support for portal redirection 36 | * Incomplete/untested API 37 | * Single-threaded design; no concurrency-safe collections or patterns are used 38 | * Periodic tasks (border check, fill, trim) use tick handlers instead of timers 39 | * No debug mode, in favor of Log4J debug levels 40 | * All WB commands only work for OPs of level 2 or more (vanilla default is 4) 41 | 42 | # Building 43 | 44 | ## Requirements 45 | 46 | * [Gradle installation with gradle binary in PATH](http://www.gradle.org/installation). Unlike the source package provided by Forge, this repository does not include a gradle wrapper or distribution. 47 | 48 | ## Usage 49 | Simply execute `gradle setupCIWorkspace` in the root directory of this repository. Then execute `gradle build`. If subsequent builds cause problems, do `gradle clean`. 50 | 51 | # Debugging 52 | 53 | WorldBorder-Forge makes use of `DEBUG` and `TRACE` logging levels for debugging. To enable these messages, append this line to the server's JVM arguments: 54 | 55 | > `-Dlog4j.configurationFile=log4j.xml` 56 | 57 | Then in the root directory of the server, create the file `log4j.xml` with these contents: 58 | 59 | ```xml 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ``` 77 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdRadius.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.BorderData; 4 | import com.wimbli.WorldBorder.Config; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import com.wimbli.WorldBorder.forge.Worlds; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.entity.player.EntityPlayerMP; 9 | 10 | import java.util.List; 11 | 12 | 13 | public class CmdRadius extends WBCmd 14 | { 15 | public CmdRadius() 16 | { 17 | name = permission = "radius"; 18 | hasWorldNameInput = true; 19 | minParams = 1; 20 | maxParams = 2; 21 | 22 | addCmdExample(nameEmphasizedW() + " [radiusZ] - change radius."); 23 | helpText = "Using this command you can adjust the radius of an existing border. If [radiusZ] is not " + 24 | "specified, the radiusX value will be used for both. You can also optionally specify + or - at the start " + 25 | "of and [radiusZ] to increase or decrease the existing radius rather than setting a new value."; 26 | } 27 | 28 | @Override 29 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 30 | { 31 | if (worldName == null) 32 | worldName = Worlds.getWorldName(player.world); 33 | 34 | BorderData border = Config.Border(worldName); 35 | if (border == null) 36 | { 37 | sendErrorAndHelp(sender, "This world (\"" + worldName + "\") must first have a border set normally."); 38 | return; 39 | } 40 | 41 | double x = border.getX(); 42 | double z = border.getZ(); 43 | int radiusX; 44 | int radiusZ; 45 | try 46 | { 47 | if ( params.get(0).startsWith("+") ) 48 | { 49 | // Add to the current radius 50 | radiusX = border.getRadiusX(); 51 | radiusX += Integer.parseInt(params.get(0).substring(1)); 52 | } 53 | else if ( params.get(0).startsWith("-") ) 54 | { 55 | // Subtract from the current radius 56 | radiusX = border.getRadiusX(); 57 | radiusX -= Integer.parseInt(params.get(0).substring(1)); 58 | } 59 | else 60 | radiusX = Integer.parseInt(params.get(0)); 61 | 62 | if (params.size() == 2) 63 | { 64 | if ( params.get(1).startsWith("+") ) 65 | { 66 | // Add to the current radius 67 | radiusZ = border.getRadiusZ(); 68 | radiusZ += Integer.parseInt(params.get(1).substring(1)); 69 | } 70 | else if ( params.get(1).startsWith("-") ) 71 | { 72 | // Subtract from the current radius 73 | radiusZ = border.getRadiusZ(); 74 | radiusZ -= Integer.parseInt(params.get(1).substring(1)); 75 | } 76 | else 77 | radiusZ = Integer.parseInt(params.get(1)); 78 | } 79 | else 80 | radiusZ = radiusX; 81 | } 82 | catch(NumberFormatException ex) 83 | { 84 | sendErrorAndHelp(sender, "The radius value(s) must be integers."); 85 | return; 86 | } 87 | 88 | double minimum = Config.getKnockBack(); 89 | 90 | if (radiusX < minimum || radiusZ < minimum) 91 | { 92 | sendErrorAndHelp(sender, "The resulting radius must be more than the knockback."); 93 | return; 94 | } 95 | 96 | Config.setBorder(worldName, radiusX, radiusZ, x, z); 97 | 98 | if (player != null) 99 | Util.chat(sender, "Radius has been set. " + Config.BorderDescription(worldName)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WorldBorder.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import com.wimbli.WorldBorder.forge.Log; 4 | import com.wimbli.WorldBorder.listener.BlockPlaceListener; 5 | import com.wimbli.WorldBorder.listener.EnderPearlListener; 6 | import com.wimbli.WorldBorder.listener.MobSpawnListener; 7 | import net.minecraft.server.MinecraftServer; 8 | import net.minecraftforge.common.MinecraftForge; 9 | import net.minecraftforge.fml.common.Mod; 10 | import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; 11 | import net.minecraftforge.fml.common.event.FMLServerStartedEvent; 12 | import net.minecraftforge.fml.common.event.FMLServerStartingEvent; 13 | import net.minecraftforge.fml.common.event.FMLServerStoppingEvent; 14 | import net.minecraftforge.fml.relauncher.Side; 15 | import net.minecraftforge.fml.relauncher.SideOnly; 16 | 17 | /** 18 | * Main class and mod definition of WorldBorder-Forge. Holds static references to its 19 | * singleton instance and management objects. Should only ever be created by Forge. 20 | */ 21 | @Mod( 22 | modid = WorldBorder.MODID, 23 | name = WorldBorder.MODID, 24 | version = WorldBorder.VERSION, 25 | serverSideOnly = true, 26 | 27 | acceptableRemoteVersions = "*", 28 | acceptableSaveVersions = "" 29 | ) 30 | public class WorldBorder 31 | { 32 | /** Frozen at 1.0.0 to prevent misleading world save error */ 33 | public static final String VERSION = "1.0.0"; 34 | //will change in 1.11.2+ 35 | public static final String MODID = "worldborder"; 36 | 37 | /** Singleton instance of WorldBorder, created by Forge */ 38 | public static WorldBorder INSTANCE = null; 39 | /** Shortcut reference to vanilla server instance */ 40 | public static MinecraftServer SERVER = null; 41 | /** Singleton instance of WorldBorder's command handler */ 42 | public static WBCommand COMMAND = null; 43 | 44 | private BlockPlaceListener blockPlaceListener = null; 45 | private MobSpawnListener mobSpawnListener = null; 46 | private EnderPearlListener enderPearlListener = null; 47 | 48 | /** 49 | * Given WorldBorder's dependency on dedicated server classes and is designed for 50 | * use in multiplayer environments, we don't load anything on the client 51 | */ 52 | @Mod.EventHandler 53 | @SideOnly(Side.CLIENT) 54 | public void clientPreInit(FMLPreInitializationEvent event) 55 | { 56 | Log.error("This mod is intended only for use on servers"); 57 | Log.error("Please consider removing this mod from your installation"); 58 | } 59 | 60 | @Mod.EventHandler 61 | @SideOnly(Side.SERVER) 62 | public void serverPreInit(FMLPreInitializationEvent event) 63 | { 64 | Config.setupConfigDir(event.getModConfigurationDirectory()); 65 | } 66 | 67 | @Mod.EventHandler 68 | @SideOnly(Side.SERVER) 69 | public void serverStart(FMLServerStartingEvent event) 70 | { 71 | if (INSTANCE == null) INSTANCE = this; 72 | if (SERVER == null) SERVER = event.getServer(); 73 | if (COMMAND == null) COMMAND = new WBCommand(); 74 | 75 | // Load (or create new) config files 76 | Config.load(false); 77 | 78 | // our one real command, though it does also have aliases "wb" and "worldborder" 79 | event.registerServerCommand(COMMAND); 80 | 81 | if ( Config.preventBlockPlace() ) 82 | enableBlockPlaceListener(true); 83 | 84 | if ( Config.preventMobSpawn() ) 85 | enableMobSpawnListener(true); 86 | 87 | if ( Config.getDenyEnderpearl() ) 88 | enableEnderPearlListener(true); 89 | 90 | DynMapFeatures.registerListener(); 91 | } 92 | 93 | @Mod.EventHandler 94 | @SideOnly(Side.SERVER) 95 | public void serverPostStart(FMLServerStartedEvent event) 96 | { 97 | WBCommand.checkRegistrations(SERVER); 98 | } 99 | 100 | @Mod.EventHandler 101 | @SideOnly(Side.SERVER) 102 | public void serverStop(FMLServerStoppingEvent event) 103 | { 104 | DynMapFeatures.removeAllBorders(); 105 | Config.storeFillTask(); 106 | } 107 | 108 | // for other plugins to hook into 109 | // TODO: use IMC for this? 110 | @SideOnly(Side.SERVER) 111 | public BorderData getWorldBorder(String worldName) 112 | { 113 | return Config.Border(worldName); 114 | } 115 | 116 | @SideOnly(Side.SERVER) 117 | public void enableBlockPlaceListener(boolean enable) 118 | { 119 | if (enable) 120 | MinecraftForge.EVENT_BUS.register(this.blockPlaceListener = new BlockPlaceListener()); 121 | else if (blockPlaceListener != null) 122 | MinecraftForge.EVENT_BUS.unregister(this.blockPlaceListener); 123 | } 124 | 125 | @SideOnly(Side.SERVER) 126 | public void enableMobSpawnListener(boolean enable) 127 | { 128 | if (enable) 129 | MinecraftForge.EVENT_BUS.register( this.mobSpawnListener = new MobSpawnListener() ); 130 | else if (mobSpawnListener != null) 131 | MinecraftForge.EVENT_BUS.unregister(this.mobSpawnListener); 132 | } 133 | 134 | @SideOnly(Side.SERVER) 135 | public void enableEnderPearlListener(boolean enable) 136 | { 137 | if (enable) 138 | MinecraftForge.EVENT_BUS.register( this.enderPearlListener = new EnderPearlListener() ); 139 | else if (enderPearlListener != null) 140 | MinecraftForge.EVENT_BUS.unregister(this.enderPearlListener); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/WBCmd.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.forge.Util; 4 | import net.minecraft.command.ICommandSender; 5 | import net.minecraft.entity.player.EntityPlayerMP; 6 | import net.minecraft.util.text.TextFormatting; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | 12 | public abstract class WBCmd 13 | { 14 | /* 15 | * Primary variables, should be set as needed in constructors for the subclassed commands 16 | */ 17 | 18 | // command name, command permission; normally the same thing 19 | public String name = ""; 20 | public String permission = null; 21 | 22 | // whether command can accept a world name before itself 23 | public boolean hasWorldNameInput = false; 24 | public boolean consoleRequiresWorldName = true; 25 | 26 | // minimum and maximum number of accepted parameters 27 | public int minParams = 0; 28 | public int maxParams = 9999; 29 | 30 | // help/explanation text to be shown after command example(s) for this command 31 | public String helpText = null; 32 | 33 | /* 34 | * The guts of the command run in here; needs to be overriden in the subclassed commands 35 | */ 36 | public abstract void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName); 37 | 38 | /* 39 | * This is an optional override, used to provide some extra command status info, like the currently set value 40 | */ 41 | public void cmdStatus(ICommandSender sender) {} 42 | 43 | 44 | /* 45 | * Helper variables and methods 46 | */ 47 | 48 | // color values for strings 49 | public final static String C_CMD = TextFormatting.AQUA.toString(); // main commands 50 | public final static String C_DESC = TextFormatting.WHITE.toString(); // command descriptions 51 | public final static String C_ERR = TextFormatting.RED.toString(); // errors / notices 52 | public final static String C_HEAD = TextFormatting.YELLOW.toString(); // command listing header 53 | public final static String C_OPT = TextFormatting.DARK_GREEN.toString(); // optional values 54 | public final static String C_REQ = TextFormatting.GREEN.toString(); // required values 55 | 56 | // colorized root command, for console and for player 57 | public final static String CMD_C = C_CMD + "wb "; 58 | public final static String CMD_P = C_CMD + "/wb "; 59 | 60 | // list of command examples for this command to be displayed as usage reference, separate between players and console 61 | // ... these generally should be set indirectly using addCmdExample() within the constructor for each command class 62 | public List cmdExamplePlayer = new ArrayList(); 63 | public List cmdExampleConsole = new ArrayList(); 64 | 65 | // much like the above, but used for displaying command list from root /wb command, listing all commands 66 | public final static List cmdExamplesConsole = new ArrayList(48); // 48 command capacity, 6 full pages 67 | public final static List cmdExamplesPlayer = new ArrayList(48); // still, could need to increase later 68 | 69 | 70 | // add command examples for use the default "/wb" command list and for internal usage reference, formatted and colorized 71 | public void addCmdExample(String example) 72 | { 73 | addCmdExample(example, true, true, true); 74 | } 75 | public void addCmdExample(String example, boolean forPlayer, boolean forConsole, boolean prefix) 76 | { 77 | // go ahead and colorize required "<>" and optional "[]" parameters, extra command words, and description 78 | example = example.replace("<", C_REQ+"<").replace("[", C_OPT+"[").replace("^", C_CMD).replace("- ", C_DESC+"- "); 79 | 80 | // all "{}" are replaced by "[]" (optional) for player, "<>" (required) for console 81 | if (forPlayer) 82 | { 83 | String exampleP = (prefix ? CMD_P : "") + example.replace("{", C_OPT + "[").replace("}", "]"); 84 | cmdExamplePlayer.add(exampleP); 85 | cmdExamplesPlayer.add(exampleP); 86 | } 87 | if (forConsole) 88 | { 89 | String exampleC = (prefix ? CMD_C : "") + example.replace("{", C_REQ + "<").replace("}", ">"); 90 | cmdExampleConsole.add(exampleC); 91 | cmdExamplesConsole.add(exampleC); 92 | } 93 | } 94 | 95 | // return root command formatted for player or console, based on sender 96 | public String cmd(ICommandSender sender) 97 | { 98 | return (sender instanceof EntityPlayerMP) ? CMD_P : CMD_C; 99 | } 100 | 101 | // formatted and colorized text, intended for marking command name 102 | public String commandEmphasized(String text) 103 | { 104 | return C_CMD + TextFormatting.UNDERLINE + text + TextFormatting.RESET + " "; 105 | } 106 | 107 | // returns green "enabled" or red "disabled" text 108 | public String enabledColored(boolean enabled) 109 | { 110 | return enabled ? C_REQ+"enabled" : C_ERR+"disabled"; 111 | } 112 | 113 | // formatted and colorized command name, optionally prefixed with "[world]" (for player) / "" (for console) 114 | public String nameEmphasized() 115 | { 116 | return commandEmphasized(name); 117 | } 118 | public String nameEmphasizedW() 119 | { 120 | return "{world} " + nameEmphasized(); 121 | } 122 | 123 | // send command example message(s) and other helpful info 124 | public void sendCmdHelp(ICommandSender sender) 125 | { 126 | for (String example : ((sender instanceof EntityPlayerMP) ? cmdExamplePlayer : cmdExampleConsole)) 127 | { 128 | Util.chat(sender, example); 129 | } 130 | cmdStatus(sender); 131 | if (helpText != null && !helpText.isEmpty()) 132 | Util.chat(sender, C_DESC + helpText); 133 | } 134 | 135 | // send error message followed by command example message(s) 136 | public void sendErrorAndHelp(ICommandSender sender, String error) 137 | { 138 | Util.chat(sender, C_ERR + error); 139 | sendCmdHelp(sender); 140 | } 141 | 142 | // interpret string as boolean value (yes/no, true/false, on/off, +/-, 1/0) 143 | public boolean strAsBool(String str) 144 | { 145 | str = str.toLowerCase(); 146 | return str.startsWith("y") || str.startsWith("t") || str.startsWith("on") || str.startsWith("+") || str.startsWith("1"); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/BorderCheck.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import com.wimbli.WorldBorder.forge.*; 4 | import net.minecraft.entity.Entity; 5 | import net.minecraft.entity.EntityLiving; 6 | import net.minecraft.entity.item.EntityBoat; 7 | import net.minecraft.entity.player.EntityPlayerMP; 8 | import net.minecraft.world.WorldServer; 9 | 10 | /** 11 | * Static utility class that holds logic for border and player checking 12 | */ 13 | public class BorderCheck 14 | { 15 | // set targetLoc only if not current player location 16 | // set returnLocationOnly to true to have new Location returned if they need to be moved to one, instead of directly handling it 17 | public static Location checkPlayer(EntityPlayerMP player, Location targetLoc, boolean returnLocationOnly, boolean notify) 18 | { 19 | if (player == null) return null; 20 | 21 | Location loc = (targetLoc == null) ? new Location(player) : targetLoc; 22 | 23 | WorldServer world = loc.world; 24 | if (world == null) return null; 25 | BorderData border = Config.Border(Worlds.getWorldName(world)); 26 | if (border == null) return null; 27 | 28 | if (border.insideBorder(loc.posX, loc.posZ, Config.getShapeRound())) return null; 29 | 30 | // if player is in bypass list (from bypass command), allow them beyond border; also ignore players currently being handled already 31 | if ( Config.isPlayerBypassing( player.getUniqueID() ) ) 32 | return null; 33 | 34 | Location newLoc = newLocation(player, loc, border, notify); 35 | 36 | /* 37 | * since we need to forcibly eject players who are inside vehicles, that fires a teleport event (go figure) and 38 | * so would effectively double trigger for us, so we need to handle it here to prevent sending two messages and 39 | * two log entries etc. 40 | * after players are ejected we can wait a few ticks (long enough for their client to receive new entity location) 41 | * and then set them as passenger of the vehicle again 42 | */ 43 | if (player.isRiding()) 44 | { 45 | Entity ride = player.getRidingEntity(); 46 | player.dismountRidingEntity(); 47 | if (ride != null) 48 | { // vehicles need to be offset vertically and have velocity stopped 49 | double vertOffset = (ride instanceof EntityLiving) ? 0 : ride.posY - loc.posY; 50 | Location rideLoc = new Location(newLoc); 51 | rideLoc.posY = newLoc.posY + vertOffset; 52 | 53 | Log.trace("Player was riding a \"" + ride.toString() + "\"."); 54 | 55 | if (ride instanceof EntityBoat) 56 | { // boats currently glitch on client when teleported, so crappy workaround is to remove it and spawn a new one 57 | ride.setDead(); 58 | ride = new EntityBoat(world, rideLoc.posX, rideLoc.posY, rideLoc.posZ); 59 | world.spawnEntity(ride); 60 | } 61 | else 62 | ride.setPositionAndRotation(rideLoc.posX, rideLoc.posY, rideLoc.posZ, rideLoc.pitch, rideLoc.yaw); 63 | 64 | if ( Config.getRemount() ) 65 | player.startRiding(ride); 66 | } 67 | } 68 | 69 | // check if player has something (a pet, maybe?) riding them; only possible through odd plugins. 70 | // it can prevent all teleportation of the player completely, so it's very much not good and needs handling 71 | if (player.getPassengers() != null) 72 | { 73 | player.removePassengers(); 74 | for(Entity rider : player.getPassengers()) { 75 | //spam 76 | //Util.chat(player, "Your passenger has been ejected."); 77 | rider.setPositionAndRotation(newLoc.posX, newLoc.posY, newLoc.posZ, newLoc.pitch, newLoc.yaw); 78 | 79 | Log.trace( 80 | "%s had %s riding on them", 81 | player.getDisplayName(), 82 | rider.getCommandSenderEntity().getName() 83 | ); 84 | } 85 | } 86 | 87 | // give some particle and sound effects where the player was beyond the border, if "whoosh effect" is enabled 88 | if (Config.doWhooshEffect()) Particles.showWhooshEffect(player); 89 | 90 | if (!returnLocationOnly) player.setPositionAndUpdate(newLoc.posX, newLoc.posY, newLoc.posZ); 91 | 92 | if (returnLocationOnly) return newLoc; 93 | 94 | return null; 95 | } 96 | 97 | private static Location newLocation(EntityPlayerMP player, Location loc, BorderData border, boolean notify) 98 | { 99 | Log.trace( 100 | "%s @ world '%s'. Border: %s", 101 | (notify ? "Border crossing" : "Check was run"), 102 | Worlds.getWorldName(loc.world), border 103 | ); 104 | 105 | Log.trace("Player @ X: %.2f Y: %.2f Z: %.2f", loc.posX, loc.posY, loc.posZ); 106 | 107 | Location newLoc = border.correctedPosition(loc, Config.getShapeRound(), player.capabilities.isFlying); 108 | 109 | // it's remotely possible (such as in the Nether) a suitable location isn't available, in which case... 110 | if (newLoc == null) 111 | { 112 | Log.debug("Target new location unviable, trying again with border center."); 113 | 114 | double safeY = border.getSafeY( 115 | loc.world, (int) border.getX(), 64, (int) border.getZ(), 116 | player.capabilities.isFlying 117 | ); 118 | 119 | if (safeY != 1) 120 | { 121 | newLoc = new Location(loc); 122 | newLoc.posX = Math.floor( border.getX() ) + 0.5; 123 | newLoc.posY = safeY; 124 | newLoc.posZ = Math.floor( border.getZ() ) + 0.5; 125 | } 126 | } 127 | 128 | if (newLoc == null) 129 | { 130 | Log.debug("Target new location still unviable, using spawn or killing player."); 131 | if ( Config.doPlayerKill() ) 132 | { 133 | player.setHealth(0.0F); 134 | return null; 135 | } 136 | 137 | newLoc = new Location( (WorldServer) player.world ); 138 | } 139 | 140 | Log.trace( 141 | "New position @ X: %.2f Y: %.2f Z: %.2f", 142 | newLoc.posX, newLoc.posY, newLoc.posZ 143 | ); 144 | 145 | if (notify) 146 | Util.chat( player, Config.getMessage() ); 147 | 148 | return newLoc; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdSet.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.WorldBorder; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import com.wimbli.WorldBorder.forge.Worlds; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.entity.player.EntityPlayerMP; 9 | import net.minecraft.util.math.BlockPos; 10 | import net.minecraft.world.WorldServer; 11 | 12 | import java.util.List; 13 | 14 | 15 | public class CmdSet extends WBCmd 16 | { 17 | public CmdSet() 18 | { 19 | name = permission = "set"; 20 | hasWorldNameInput = true; 21 | consoleRequiresWorldName = false; 22 | minParams = 1; 23 | maxParams = 4; 24 | 25 | addCmdExample(nameEmphasizedW() + " [radiusZ] - use x/z coords."); 26 | addCmdExample(nameEmphasizedW() + " [radiusZ] ^spawn - use spawn point."); 27 | addCmdExample(nameEmphasized() + " [radiusZ] - set border, centered on you.", true, false, true); 28 | addCmdExample(nameEmphasized() + " [radiusZ] ^player - center on player."); 29 | helpText = "Set a border for a world, with several options for defining the center location. [world] is " + 30 | "optional for players and defaults to the world the player is in. If [radiusZ] is not specified, the " + 31 | "radiusX value will be used for both. The and coordinates can be decimal values (ex. 1.234)."; 32 | } 33 | 34 | @Override 35 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 36 | { 37 | // passsing a single parameter (radiusX) is only acceptable from player 38 | if ((params.size() == 1) && player == null) 39 | { 40 | sendErrorAndHelp(sender, "You have not provided a sufficient number of parameters."); 41 | return; 42 | } 43 | 44 | WorldServer world = null; 45 | 46 | // "set" command from player or console, world specified 47 | if (worldName != null) 48 | { 49 | if (params.size() == 2 && ! params.get(params.size() - 1).equalsIgnoreCase("spawn")) 50 | { // command can only be this short if "spawn" is specified rather than x + z or player name 51 | sendErrorAndHelp(sender, "You have not provided a sufficient number of arguments."); 52 | return; 53 | } 54 | 55 | world = Worlds.getWorld(worldName); 56 | if (world == null) 57 | { 58 | if (params.get(params.size() - 1).equalsIgnoreCase("spawn")) 59 | { 60 | sendErrorAndHelp(sender, "The world you specified (\"" + worldName + "\") could not be found on the server, so the spawn point cannot be determined."); 61 | return; 62 | } 63 | Util.chat(sender, "The world you specified (\"" + worldName + "\") could not be found on the server, but data for it will be stored anyway."); 64 | } 65 | } 66 | // "set" command from player using current world since it isn't specified, or allowed from console only if player name is specified 67 | else 68 | { 69 | if (player == null) 70 | { 71 | if (! params.get(params.size() - 2).equalsIgnoreCase("player")) 72 | { // command can only be called by console without world specified if player is specified instead 73 | sendErrorAndHelp(sender, "You must specify a world name from console if not specifying a player name."); 74 | return; 75 | } 76 | player = WorldBorder.SERVER.getPlayerList().getPlayerByUsername( params.get(params.size() - 1) ); 77 | if (player == null) 78 | { 79 | sendErrorAndHelp(sender, "The player you specified (\"" + params.get(params.size() - 1) + "\") does not appear to be online."); 80 | return; 81 | } 82 | } 83 | worldName = Worlds.getWorldName(player.world); 84 | } 85 | 86 | int radiusX, radiusZ; 87 | double x, z; 88 | int radiusCount = params.size(); 89 | 90 | try 91 | { 92 | if (params.get(params.size() - 1).equalsIgnoreCase("spawn")) 93 | { // "spawn" specified for x/z coordinates 94 | assert world != null; 95 | BlockPos loc = world.getSpawnPoint(); 96 | x = loc.getX(); 97 | z = loc.getZ(); 98 | radiusCount -= 1; 99 | } 100 | else if (params.size() > 2 && params.get(params.size() - 2).equalsIgnoreCase("player")) 101 | { // player name specified for x/z coordinates 102 | EntityPlayerMP playerT = WorldBorder.SERVER.getPlayerList().getPlayerByUsername(params.get(params.size() - 1)); 103 | if (playerT == null) 104 | { 105 | sendErrorAndHelp(sender, "The player you specified (\"" + params.get(params.size() - 1) + "\") does not appear to be online."); 106 | return; 107 | } 108 | worldName = Worlds.getWorldName(playerT.world); 109 | x = playerT.posX; 110 | z = playerT.posZ; 111 | radiusCount -= 2; 112 | } 113 | else 114 | { 115 | if (player == null || radiusCount > 2) 116 | { // x and z specified 117 | x = Double.parseDouble(params.get(params.size() - 2)); 118 | z = Double.parseDouble(params.get(params.size() - 1)); 119 | radiusCount -= 2; 120 | } 121 | else 122 | { // using coordinates of command sender (player) 123 | x = player.posX; 124 | z = player.posZ; 125 | } 126 | } 127 | 128 | radiusX = Integer.parseInt(params.get(0)); 129 | if (radiusCount < 2) 130 | radiusZ = radiusX; 131 | else 132 | radiusZ = Integer.parseInt(params.get(1)); 133 | 134 | if (radiusX < Config.getKnockBack() || radiusZ < Config.getKnockBack()) 135 | { 136 | sendErrorAndHelp(sender, "Radius value(s) must be more than the knockback distance."); 137 | return; 138 | } 139 | } 140 | catch(NumberFormatException ex) 141 | { 142 | sendErrorAndHelp(sender, "Radius value(s) must be integers and x and z values must be numerical."); 143 | return; 144 | } 145 | 146 | assert worldName != null; 147 | Config.setBorder(worldName, radiusX, radiusZ, x, z); 148 | Util.chat(sender, "Border has been set. " + Config.BorderDescription(worldName)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdTrim.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.CoordXZ; 5 | import com.wimbli.WorldBorder.forge.Log; 6 | import com.wimbli.WorldBorder.forge.Util; 7 | import com.wimbli.WorldBorder.forge.Worlds; 8 | import com.wimbli.WorldBorder.task.WorldTrimTask; 9 | import net.minecraft.command.ICommandSender; 10 | import net.minecraft.entity.player.EntityPlayerMP; 11 | 12 | import java.util.List; 13 | 14 | 15 | public class CmdTrim extends WBCmd 16 | { 17 | public CmdTrim() 18 | { 19 | name = permission = "trim"; 20 | hasWorldNameInput = true; 21 | // false because we want to handle `wb trim confirm/cancel` in console 22 | consoleRequiresWorldName = false; 23 | minParams = 0; 24 | maxParams = 2; 25 | 26 | addCmdExample(nameEmphasizedW() + "[freq] [pad] - trim world outside of border."); 27 | helpText = "This command will remove chunks which are outside the world's border. [freq] is the frequency " + 28 | "of chunks per second that will be checked (default 5000). [pad] is the number of blocks padding kept " + 29 | "beyond the border itself (default 208, to cover player visual range)."; 30 | } 31 | 32 | @Override 33 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 34 | { 35 | boolean confirm = false; 36 | // check for "cancel", "pause", or "confirm" 37 | if (params.size() >= 1) 38 | { 39 | String check = params.get(0).toLowerCase(); 40 | 41 | if (check.equals("cancel") || check.equals("stop")) 42 | { 43 | if (!makeSureTrimIsRunning(sender)) 44 | return; 45 | Util.chat(sender, C_HEAD + "Cancelling the world map trimming task."); 46 | trimDefaults(); 47 | WorldTrimTask.getInstance().stop(); 48 | 49 | return; 50 | } 51 | else if (check.equals("pause")) 52 | { 53 | if (!makeSureTrimIsRunning(sender)) 54 | return; 55 | 56 | WorldTrimTask.getInstance().pause(); 57 | Util.chat( 58 | sender, C_HEAD + "The world map trimming task is now " 59 | + (WorldTrimTask.getInstance().isPaused() ? "" : "un") + "paused." 60 | ); 61 | 62 | return; 63 | } 64 | 65 | confirm = check.equals("confirm"); 66 | } 67 | 68 | // if not just confirming, make sure a world name is available 69 | if (worldName == null && !confirm) 70 | { 71 | if (player != null) 72 | worldName = Worlds.getWorldName(player.world); 73 | else 74 | { 75 | sendErrorAndHelp(sender, "You must specify a world!"); 76 | return; 77 | } 78 | } 79 | 80 | // colorized "/wb trim " 81 | String cmd = cmd(sender) + nameEmphasized() + C_CMD; 82 | 83 | // make sure Trim isn't already running 84 | if (WorldTrimTask.getInstance() != null) 85 | { 86 | Util.chat(sender, C_ERR + "The world map trimming task is already running."); 87 | Util.chat(sender, C_DESC + "You can cancel at any time with " + cmd + "cancel" + C_DESC + ", or pause/unpause with " + cmd + "pause" + C_DESC + "."); 88 | return; 89 | } 90 | 91 | // set frequency and/or padding if those were specified 92 | try 93 | { 94 | if (params.size() >= 1 && !confirm) 95 | trimFrequency = Math.abs(Integer.parseInt(params.get(0))); 96 | if (params.size() >= 2 && !confirm) 97 | trimPadding = Math.abs(Integer.parseInt(params.get(1))); 98 | } 99 | catch(NumberFormatException ex) 100 | { 101 | sendErrorAndHelp(sender, "The frequency and padding values must be integers."); 102 | trimDefaults(); 103 | return; 104 | } 105 | if (trimFrequency <= 0) 106 | { 107 | sendErrorAndHelp(sender, "The frequency value must be greater than zero."); 108 | trimDefaults(); 109 | return; 110 | } 111 | 112 | // set world if it was specified 113 | if (worldName != null) 114 | trimWorld = worldName; 115 | 116 | if (confirm) 117 | { // command confirmed, go ahead with it 118 | if (trimWorld.isEmpty()) 119 | { 120 | sendErrorAndHelp(sender, "You must first use this command successfully without confirming."); 121 | return; 122 | } 123 | 124 | if (player != null) 125 | Log.info("Trimming world beyond border at the command of player \"" + player.getDisplayName() + "\"."); 126 | 127 | int ticks = 1, repeats = 1; 128 | if (trimFrequency > 20) 129 | repeats = trimFrequency / 20; 130 | else 131 | ticks = 20 / trimFrequency; 132 | 133 | try 134 | { 135 | WorldTrimTask task = WorldTrimTask.create(player, trimWorld, trimPadding, repeats, ticks); 136 | task.start(); 137 | Util.chat(sender, "WorldBorder map trimming task for world \"" + trimWorld + "\" started."); 138 | } 139 | catch (Exception e) 140 | { 141 | Util.chat(sender, C_ERR + "The world map trimming task failed to start."); 142 | Util.chat(sender, C_ERR + e.getMessage()); 143 | } 144 | 145 | trimDefaults(); 146 | } 147 | else 148 | { 149 | if (trimWorld.isEmpty() || Worlds.getWorld(trimWorld) == null) 150 | { 151 | sendErrorAndHelp(sender, "You must first specify a valid world."); 152 | return; 153 | } 154 | 155 | if (Config.Border(trimWorld) == null) 156 | { 157 | sendErrorAndHelp(sender, "That world does not have a border."); 158 | return; 159 | } 160 | 161 | Util.chat(sender, C_HEAD + "World trimming task is ready for world \"" + trimWorld + "\", attempting to process up to " + trimFrequency + " chunks per second (default 5000). The map will be trimmed past " + trimPadding + " blocks beyond the border (default " + defaultPadding + ")."); 162 | Util.chat(sender, 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."); 163 | Util.chat(sender, C_DESC + "You should now use " + cmd + "confirm" + C_DESC + " to start the process."); 164 | Util.chat(sender, C_DESC + "You can cancel at any time with " + cmd + "cancel" + C_DESC + ", or pause/unpause with " + cmd + "pause" + C_DESC + "."); 165 | } 166 | } 167 | 168 | 169 | /* with "view-distance=10" in server.properties on a fast VM test server and "Render Distance: Far" in client, 170 | * hitting border during testing was loading 11+ chunks beyond the border in a couple of directions (10 chunks in 171 | * the other two directions). This could be worse on a more loaded or worse server, so: 172 | */ 173 | private final int defaultPadding = CoordXZ.chunkToBlock(13); 174 | 175 | private String trimWorld = ""; 176 | private int trimFrequency = 5000; 177 | private int trimPadding = defaultPadding; 178 | 179 | private void trimDefaults() 180 | { 181 | trimWorld = ""; 182 | trimFrequency = 5000; 183 | trimPadding = defaultPadding; 184 | } 185 | 186 | private boolean makeSureTrimIsRunning(ICommandSender sender) 187 | { 188 | if (WorldTrimTask.getInstance() != null) 189 | return true; 190 | 191 | sendErrorAndHelp(sender, "The world map trimming task is not currently running."); 192 | return false; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WorldFileData.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import com.wimbli.WorldBorder.forge.Log; 4 | import com.wimbli.WorldBorder.forge.Util; 5 | import com.wimbli.WorldBorder.forge.Worlds; 6 | import net.minecraft.command.ICommandSender; 7 | import net.minecraft.world.World; 8 | import net.minecraftforge.common.DimensionManager; 9 | 10 | import java.io.*; 11 | import java.util.*; 12 | 13 | /** 14 | * Represents the file structure of a world. 15 | * 16 | * This region file handler was created based on the divulged region file format: 17 | * http://mojang.com/2011/02/16/minecraft-save-file-format-in-beta-1-3/ 18 | */ 19 | public class WorldFileData 20 | { 21 | private static final FileFilter MCA_FILTER = new ExtFileFilter(".MCA"); 22 | 23 | private Map> regionChunkExistence = new HashMap<>(); 24 | 25 | private File[] regionFiles = null; 26 | private ICommandSender requester = null; 27 | 28 | /** Creates a region file handler for a given world and requester */ 29 | public WorldFileData(World world, ICommandSender requester) 30 | { 31 | this.requester = requester; 32 | 33 | // TODO: Move this to "getRootDir" of new Worlds class 34 | File rootDir = (world.provider.getDimension() == 0) 35 | ? DimensionManager.getCurrentSaveRootDirectory() 36 | : new File( 37 | DimensionManager.getCurrentSaveRootDirectory(), 38 | world.provider.getSaveFolder() 39 | ); 40 | 41 | File regionFolder = new File(rootDir, "region"); 42 | if ( !regionFolder.exists() || !regionFolder.isDirectory() ) 43 | throw new RuntimeException( 44 | "Could not validate folder for world's region files. Tried to use " 45 | + regionFolder.getPath() + " as valid region folder." 46 | ); 47 | 48 | this.regionFiles = regionFolder.listFiles(MCA_FILTER); 49 | if (this.regionFiles == null || this.regionFiles.length == 0) 50 | throw new RuntimeException( 51 | "Could not find any region files. Looked in: " + regionFolder.getPath() 52 | ); 53 | 54 | Log.debug( 55 | "Using path '%s' for world '%s'", 56 | regionFolder.getAbsolutePath(), 57 | Worlds.getWorldName(world) 58 | ); 59 | } 60 | 61 | /** Shortcut for number of region files this world has */ 62 | public int regionFileCount() 63 | { 64 | return regionFiles.length; 65 | } 66 | 67 | /** Gets a region file by index, or null if out of bounds */ 68 | public File regionFile(int index) 69 | { 70 | if (regionFiles.length < index) 71 | return null; 72 | 73 | return regionFiles[index]; 74 | } 75 | 76 | /** Gets the X and Z world coordinates of the region from the filename */ 77 | public CoordXZ regionFileCoordinates(int index) 78 | { 79 | File regionFile = this.regionFile(index); 80 | String[] coords = regionFile.getName().split("\\."); 81 | int x, z; 82 | try 83 | { 84 | x = Integer.parseInt(coords[1]); 85 | z = Integer.parseInt(coords[2]); 86 | return new CoordXZ (x, z); 87 | } 88 | catch(Exception ex) 89 | { 90 | sendMessage("Error! Region file found with abnormal name: "+regionFile.getName()); 91 | return null; 92 | } 93 | } 94 | 95 | /** Find out if the chunk at the given coordinates exists */ 96 | public boolean doesChunkExist(int x, int z) 97 | { 98 | CoordXZ region = new CoordXZ( 99 | CoordXZ.chunkToRegion(x), 100 | CoordXZ.chunkToRegion(z) 101 | ); 102 | 103 | return this.getRegionData(region).get( coordToRegionOffset(x, z) ); 104 | } 105 | 106 | /** 107 | * Checks if the chunk at the given coordinates has been fully generated. 108 | * Minecraft only fully generates a chunk when adjacent chunks are also loaded. 109 | */ 110 | public boolean isChunkFullyGenerated(int x, int z) 111 | { // if all adjacent chunks exist, it should be a safe enough bet that this one is fully generated 112 | return 113 | ! ( 114 | ! doesChunkExist(x, z) 115 | || ! doesChunkExist(x+1, z) 116 | || ! doesChunkExist(x-1, z) 117 | || ! doesChunkExist(x, z+1) 118 | || ! doesChunkExist(x, z-1) 119 | ); 120 | } 121 | 122 | /** Callback for when a chunk is generate, to update our region map */ 123 | public void chunkExistsNow(int x, int z) 124 | { 125 | CoordXZ region = new CoordXZ( 126 | CoordXZ.chunkToRegion(x), 127 | CoordXZ.chunkToRegion(z) 128 | ); 129 | 130 | this.getRegionData(region).set(coordToRegionOffset(x, z), true); 131 | } 132 | 133 | /** 134 | * Calculates region offset of the given coordinates. 135 | * 136 | * Region is 32 * 32 chunks; chunk pointers are stored in region file at position: 137 | * x + z * 32 (32 * 32 chunks = 1024) 138 | * Input x and z values can be world-based chunk coordinates or local-to-region 139 | * chunk coordinates, either one 140 | */ 141 | private int coordToRegionOffset(int x, int z) 142 | { 143 | // "%" modulus is used to convert potential world coordinates to definitely be local region coordinates 144 | x = x % 32; 145 | z = z % 32; 146 | // similarly, for local coordinates, we need to wrap negative values around 147 | if (x < 0) x += 32; 148 | if (z < 0) z += 32; 149 | // return offset position for the now definitely local x and z values 150 | return x + (z * 32); 151 | } 152 | 153 | private List getRegionData(CoordXZ region) 154 | { 155 | List data = regionChunkExistence.get(region); 156 | if (data != null) 157 | return data; 158 | 159 | // data for the specified region isn't loaded yet, so init it as empty and try to 160 | // find the file and load the data 161 | data = new ArrayList(1024); 162 | for (int i = 0; i < 1024; i++) 163 | data.add(Boolean.FALSE); 164 | 165 | for (int i = 0; i < regionFiles.length; i++) 166 | { 167 | CoordXZ coord = regionFileCoordinates(i); 168 | // is this region file the one we're looking for? 169 | if ( !coord.equals(region) ) 170 | continue; 171 | 172 | try (RandomAccessFile regionData = new RandomAccessFile(this.regionFile(i), "r")) 173 | { 174 | Log.trace( "Trying to read region file '%s'", regionFile(i) ); 175 | 176 | // first 4096 bytes of region file consists of 4-byte int pointers to chunk data in the file 177 | // (32*32 chunks = 1024; 1024 chunks * 4 bytes each = 4096) 178 | // if chunk pointer data is 0, chunk doesn't exist yet; otherwise, it does 179 | for (int j = 0; j < 1024; j++) 180 | if (regionData.readInt() != 0) 181 | data.set(j, true); 182 | } 183 | catch (FileNotFoundException ex) 184 | { 185 | sendMessage("Error! Could not open region file to find generated chunks: " + this.regionFile(i).getName()); 186 | } 187 | catch (IOException ex) 188 | { 189 | sendMessage("Error! Could not read region file to find generated chunks: " + this.regionFile(i).getName()); 190 | } 191 | } 192 | regionChunkExistence.put(region, data); 193 | return data; 194 | } 195 | 196 | /** Send a message to the server console/log and possibly to an in-game player */ 197 | private void sendMessage(String text) 198 | { 199 | Log.info("[WorldData] " + text); 200 | if (requester != null) 201 | Util.chat(requester, "[WorldData] " + text); 202 | } 203 | 204 | /** File filter used for region files */ 205 | private static class ExtFileFilter implements FileFilter 206 | { 207 | String ext; 208 | public ExtFileFilter(String extension) 209 | { 210 | this.ext = extension.toLowerCase(); 211 | } 212 | 213 | @Override 214 | public boolean accept(File file) 215 | { 216 | return ( 217 | file.exists() 218 | && file.isFile() 219 | && file.getName().toLowerCase().endsWith(ext) 220 | ); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/cmd/CmdFill.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.cmd; 2 | 3 | import com.wimbli.WorldBorder.Config; 4 | import com.wimbli.WorldBorder.CoordXZ; 5 | import com.wimbli.WorldBorder.forge.Log; 6 | import com.wimbli.WorldBorder.forge.Util; 7 | import com.wimbli.WorldBorder.forge.Worlds; 8 | import com.wimbli.WorldBorder.task.WorldFillTask; 9 | import net.minecraft.command.ICommandSender; 10 | import net.minecraft.entity.player.EntityPlayerMP; 11 | 12 | import java.util.List; 13 | 14 | public class CmdFill extends WBCmd 15 | { 16 | public CmdFill() 17 | { 18 | name = permission = "fill"; 19 | hasWorldNameInput = true; 20 | // false because we want to handle `wb fill confirm/cancel` in console 21 | consoleRequiresWorldName = false; 22 | minParams = 0; 23 | maxParams = 3; 24 | 25 | addCmdExample(nameEmphasizedW() + "[freq] [pad] [force] - fill world to border."); 26 | helpText = "This command will generate missing world chunks inside your border. [freq] is the frequency " + 27 | "of chunks per second that will be checked (default 20). [pad] is the number of blocks padding added " + 28 | "beyond the border itself (default 208, to cover player visual range). [force] can be specified as true " + 29 | "to force all chunks to be loaded even if they seem to be fully generated (default false)."; 30 | } 31 | 32 | @Override 33 | public void execute(ICommandSender sender, EntityPlayerMP player, List params, String worldName) 34 | { 35 | boolean confirm = false; 36 | // check for "cancel", "pause", or "confirm" 37 | if (params.size() >= 1) 38 | { 39 | String check = params.get(0).toLowerCase(); 40 | 41 | if (check.equals("cancel") || check.equals("stop")) 42 | { 43 | if (!makeSureFillIsRunning(sender)) 44 | return; 45 | 46 | Util.chat(sender, C_HEAD + "Cancelling the world map generation task."); 47 | fillDefaults(); 48 | WorldFillTask.getInstance().stop(); 49 | 50 | return; 51 | } 52 | else if (check.equals("pause")) 53 | { 54 | if (!makeSureFillIsRunning(sender)) 55 | return; 56 | 57 | WorldFillTask.getInstance().pause(); 58 | Util.chat( 59 | sender, C_HEAD + "The world map generation task is now " 60 | + (WorldFillTask.getInstance().isPaused() ? "" : "un") + "paused." 61 | ); 62 | 63 | return; 64 | } 65 | 66 | confirm = check.equals("confirm"); 67 | } 68 | 69 | // if not just confirming, make sure a world name is available 70 | if (worldName == null && !confirm) 71 | { 72 | if (player != null) 73 | worldName = Worlds.getWorldName(player.world); 74 | else 75 | { 76 | sendErrorAndHelp(sender, "You must specify a world!"); 77 | return; 78 | } 79 | } 80 | 81 | // colorized "/wb fill " 82 | String cmd = cmd(sender) + nameEmphasized() + C_CMD; 83 | 84 | // make sure Fill isn't already running 85 | if (WorldFillTask.getInstance() != null) 86 | { 87 | Util.chat(sender, C_ERR + "The world map generation task is already running."); 88 | Util.chat(sender, C_DESC + "You can cancel at any time with " + cmd + "cancel" + C_DESC + ", or pause/unpause with " + cmd + "pause" + C_DESC + "."); 89 | return; 90 | } 91 | 92 | // set frequency and/or padding if those were specified 93 | try 94 | { 95 | if (params.size() >= 1 && !confirm) 96 | fillFrequency = Math.abs( Integer.parseInt( params.get(0) ) ); 97 | if (params.size() >= 2 && !confirm) 98 | fillPadding = Math.abs( Integer.parseInt( params.get(1) ) ); 99 | } 100 | catch(NumberFormatException ex) 101 | { 102 | sendErrorAndHelp(sender, "The frequency and padding values must be integers."); 103 | fillDefaults(); 104 | return; 105 | } 106 | if (fillFrequency <= 0) 107 | { 108 | sendErrorAndHelp(sender, "The frequency value must be greater than zero."); 109 | fillDefaults(); 110 | return; 111 | } 112 | 113 | // see if the command specifies to load even chunks which should already be fully generated 114 | if (params.size() == 3) 115 | fillForceLoad = strAsBool(params.get(2)); 116 | 117 | // set world if it was specified 118 | if (worldName != null) 119 | fillWorld = worldName; 120 | 121 | if (confirm) 122 | { // command confirmed, go ahead with it 123 | if ( fillWorld.isEmpty() ) 124 | { 125 | sendErrorAndHelp(sender, "You must first use this command successfully without confirming."); 126 | return; 127 | } 128 | 129 | if (player != null) 130 | Log.info("Filling out world to border at the command of player \"" + player.getDisplayName() + "\"."); 131 | 132 | int ticks = 1, repeats = 1; 133 | if (fillFrequency > 20) 134 | repeats = fillFrequency / 20; 135 | else 136 | ticks = 20 / fillFrequency; 137 | 138 | Log.info("world: " + fillWorld + " padding: " + fillPadding + " repeats: " + repeats + " ticks: " + ticks); 139 | 140 | try 141 | { 142 | WorldFillTask task = WorldFillTask.create(player, fillWorld, fillForceLoad, fillPadding, repeats, ticks); 143 | task.start(); 144 | Util.chat(sender, "WorldBorder map generation task for world \"" + fillWorld + "\" started."); 145 | } 146 | catch (Exception e) 147 | { 148 | Util.chat(sender, C_ERR + "The world map generation task failed to start:"); 149 | Util.chat(sender, C_ERR + e.getMessage()); 150 | } 151 | 152 | fillDefaults(); 153 | } 154 | else 155 | { 156 | if (fillWorld.isEmpty() || Worlds.getWorld(fillWorld) == null) 157 | { 158 | sendErrorAndHelp(sender, "You must first specify a valid world."); 159 | return; 160 | } 161 | 162 | if (Config.Border(fillWorld) == null) 163 | { 164 | sendErrorAndHelp(sender, "That world does not have a border."); 165 | return; 166 | } 167 | 168 | Util.chat(sender, 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.")); 169 | Util.chat(sender, 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."); 170 | Util.chat(sender, C_DESC + "You should now use " + cmd + "confirm" + C_DESC + " to start the process."); 171 | Util.chat(sender, 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 | /* with "view-distance=10" in server.properties on a fast VM test server and "Render Distance: Far" in client, 176 | * hitting border during testing was loading 11+ chunks beyond the border in a couple of directions (10 chunks in 177 | * the other two directions). This could be worse on a more loaded or worse server, so: 178 | */ 179 | private final int defaultPadding = CoordXZ.chunkToBlock(13); 180 | 181 | private String fillWorld = ""; 182 | private int fillFrequency = 20; 183 | private int fillPadding = defaultPadding; 184 | private boolean fillForceLoad = false; 185 | 186 | private void fillDefaults() 187 | { 188 | fillWorld = ""; 189 | fillFrequency = 20; 190 | fillPadding = defaultPadding; 191 | fillForceLoad = false; 192 | } 193 | 194 | private boolean makeSureFillIsRunning(ICommandSender sender) 195 | { 196 | if (WorldFillTask.getInstance() != null) 197 | return true; 198 | 199 | sendErrorAndHelp(sender, "The world map generation 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 com.wimbli.WorldBorder.forge.Log; 4 | import com.wimbli.WorldBorder.forge.Worlds; 5 | import net.minecraft.world.World; 6 | import net.minecraftforge.fml.common.Loader; 7 | import org.dynmap.DynmapCommonAPI; 8 | import org.dynmap.DynmapCommonAPIListener; 9 | import org.dynmap.markers.AreaMarker; 10 | import org.dynmap.markers.CircleMarker; 11 | import org.dynmap.markers.MarkerAPI; 12 | import org.dynmap.markers.MarkerSet; 13 | 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Map.Entry; 18 | 19 | /** 20 | * Static class for integrating with the Dynmap API. All methods fail safely and silently 21 | * if the API is not available. 22 | */ 23 | public class DynMapFeatures 24 | { 25 | /** 26 | * This Gateway inner class is used for holding variables that use Dynmap API types, 27 | * so that a NoClassDefFound exception is not thrown if the API is missing. 28 | * 29 | * TODO: This is very hacky; is there a better way? 30 | */ 31 | private static class Gateway 32 | { 33 | private static DynmapCommonAPI api; 34 | private static MarkerAPI markApi; 35 | private static MarkerSet markSet; 36 | 37 | private static Map roundBorders = new HashMap<>(); 38 | private static Map squareBorders = new HashMap<>(); 39 | 40 | private static DynmapCommonAPIListener listener = new DynmapCommonAPIListener() 41 | { 42 | @Override 43 | public void apiEnabled(DynmapCommonAPI dynmapCommonAPI) 44 | { 45 | // FORGE: Old dynmap version check removed; 0.35 is obsolete by now 46 | api = dynmapCommonAPI; 47 | markApi = api.getMarkerAPI(); 48 | 49 | showAllBorders(); 50 | Log.info("Successfully hooked into Dynmap for the ability to display borders"); 51 | } 52 | }; 53 | 54 | public static void register() 55 | { 56 | DynmapCommonAPIListener.register(listener); 57 | } 58 | } 59 | 60 | private static final int LINE_WEIGHT = 3; 61 | private static final double LINE_OPACITY = 1.0; 62 | private static final int LINE_COLOR = 0xFF0000; 63 | 64 | private static boolean enabled = false; 65 | 66 | public static void registerListener() 67 | { 68 | enabled = Loader.isModLoaded("Dynmap"); 69 | 70 | if (enabled) 71 | Gateway.register(); 72 | else 73 | Log.debug("Dynmap is not available; integration disabled"); 74 | } 75 | 76 | /* 77 | * Re-rendering methods, used for updating trimmed chunks to show them as gone 78 | * TODO: Check if these are now working 79 | */ 80 | 81 | public static void renderRegion(World world, CoordXZ coord) 82 | { 83 | if (!enabled) return; 84 | 85 | int y = (world != null) ? world.getHeight() : 255; 86 | int x = CoordXZ.regionToBlock(coord.x); 87 | int z = CoordXZ.regionToBlock(coord.z); 88 | Gateway.api.triggerRenderOfVolume(Worlds.getWorldName(world), x, 0, z, x + 511, y, z + 511); 89 | } 90 | 91 | public static void renderChunks(World world, List coords) 92 | { 93 | if (!enabled) return; 94 | 95 | int y = (world != null) ? world.getHeight() : 255; 96 | 97 | for (CoordXZ coord : coords) 98 | renderChunk(Worlds.getWorldName(world), coord, y); 99 | } 100 | 101 | public static void renderChunk(String worldName, CoordXZ coord, int maxY) 102 | { 103 | if (!enabled) return; 104 | 105 | int x = CoordXZ.chunkToBlock(coord.x); 106 | int z = CoordXZ.chunkToBlock(coord.z); 107 | Gateway.api.triggerRenderOfVolume(worldName, x, 0, z, x + 15, maxY, z + 15); 108 | } 109 | 110 | /* 111 | * Methods for displaying our borders on DynMap's world maps 112 | */ 113 | 114 | public static void showAllBorders() 115 | { 116 | if (!enabled) return; 117 | 118 | // in case any borders are already shown 119 | removeAllBorders(); 120 | 121 | if (!Config.isDynmapBorderEnabled()) 122 | { 123 | // don't want to show the marker set in DynMap if our integration is disabled 124 | if (Gateway.markSet != null) 125 | Gateway.markSet.deleteMarkerSet(); 126 | Gateway.markSet = null; 127 | return; 128 | } 129 | 130 | // make sure the marker set is initialized 131 | Gateway.markSet = Gateway.markApi.getMarkerSet("worldborder.markerset"); 132 | if(Gateway.markSet == null) 133 | Gateway.markSet = Gateway.markApi.createMarkerSet("worldborder.markerset", "WorldBorder", null, false); 134 | else 135 | Gateway.markSet.setMarkerSetLabel("WorldBorder"); 136 | 137 | Map borders = Config.getBorders(); 138 | for( Entry stringBorderDataEntry : borders.entrySet() ) 139 | { 140 | String worldName = stringBorderDataEntry.getKey(); 141 | BorderData border = stringBorderDataEntry.getValue(); 142 | 143 | showBorder(worldName, border); 144 | } 145 | } 146 | 147 | public static void showBorder(String worldName, BorderData border) 148 | { 149 | if (!enabled) return; 150 | 151 | if (!Config.isDynmapBorderEnabled()) return; 152 | 153 | if ((border.getShape() == null) ? Config.getShapeRound() : border.getShape()) 154 | showRoundBorder(worldName, border); 155 | else 156 | showSquareBorder(worldName, border); 157 | } 158 | 159 | private static void showRoundBorder(String worldName, BorderData border) 160 | { 161 | if ( Gateway.squareBorders.containsKey(worldName) ) 162 | removeBorder(worldName); 163 | 164 | CircleMarker marker = Gateway.roundBorders.get(worldName); 165 | if (marker == null) 166 | { 167 | marker = Gateway.markSet.createCircleMarker( 168 | "worldborder_" + worldName, 169 | Config.getDynmapMessage(), 170 | false, worldName, 171 | border.getX(), 64.0, border.getZ(), 172 | border.getRadiusX(), border.getRadiusZ(), 173 | true 174 | ); 175 | 176 | marker.setLineStyle(LINE_WEIGHT, LINE_OPACITY, LINE_COLOR); 177 | marker.setFillStyle(0.0, 0x000000); 178 | Gateway.roundBorders.put(worldName, marker); 179 | } 180 | else 181 | { 182 | marker.setCenter(worldName, border.getX(), 64.0, border.getZ()); 183 | marker.setRadius(border.getRadiusX(), border.getRadiusZ()); 184 | } 185 | } 186 | 187 | private static void showSquareBorder(String worldName, BorderData border) 188 | { 189 | if ( Gateway.roundBorders.containsKey(worldName) ) 190 | removeBorder(worldName); 191 | 192 | // corners of the square border 193 | double[] xVals = {border.getX() - border.getRadiusX(), border.getX() + border.getRadiusX()}; 194 | double[] zVals = {border.getZ() - border.getRadiusZ(), border.getZ() + border.getRadiusZ()}; 195 | 196 | AreaMarker marker = Gateway.squareBorders.get(worldName); 197 | if (marker == null) 198 | { 199 | marker = Gateway.markSet.createAreaMarker( 200 | "worldborder_" + worldName, 201 | Config.getDynmapMessage(), 202 | false, worldName, xVals, zVals, true 203 | ); 204 | 205 | marker.setLineStyle(LINE_WEIGHT, LINE_OPACITY, LINE_COLOR); 206 | marker.setFillStyle(0.0, 0x000000); 207 | Gateway.squareBorders.put(worldName, marker); 208 | } 209 | else 210 | marker.setCornerLocations(xVals, zVals); 211 | } 212 | 213 | public static void removeAllBorders() 214 | { 215 | if (!enabled) return; 216 | 217 | for ( CircleMarker marker : Gateway.roundBorders.values() ) 218 | marker.deleteMarker(); 219 | Gateway.roundBorders.clear(); 220 | 221 | for ( AreaMarker marker : Gateway.squareBorders.values() ) 222 | marker.deleteMarker(); 223 | Gateway.squareBorders.clear(); 224 | } 225 | 226 | public static void removeBorder(String worldName) 227 | { 228 | if (!enabled) return; 229 | 230 | CircleMarker marker = Gateway.roundBorders.remove(worldName); 231 | if (marker != null) 232 | marker.deleteMarker(); 233 | 234 | AreaMarker marker2 = Gateway.squareBorders.remove(worldName); 235 | if (marker2 != null) 236 | marker2.deleteMarker(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/WBCommand.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.mojang.authlib.GameProfile; 5 | import com.wimbli.WorldBorder.cmd.*; 6 | import com.wimbli.WorldBorder.forge.Log; 7 | import com.wimbli.WorldBorder.forge.Util; 8 | import net.minecraft.command.CommandBase; 9 | import net.minecraft.command.CommandException; 10 | import net.minecraft.command.ICommand; 11 | import net.minecraft.command.ICommandSender; 12 | import net.minecraft.entity.player.EntityPlayerMP; 13 | import net.minecraft.server.MinecraftServer; 14 | import net.minecraft.server.dedicated.DedicatedServer; 15 | import net.minecraft.server.management.UserListOpsEntry; 16 | import net.minecraft.util.math.BlockPos; 17 | 18 | import javax.annotation.Nullable; 19 | import java.util.*; 20 | 21 | public class WBCommand implements ICommand 22 | { 23 | static final String NAME = "wborder"; 24 | static final List ALIASES = Arrays.asList(NAME, "wb", "worldborder"); 25 | 26 | // map of all sub-commands with the command name (string) for quick reference 27 | public Map subCommands = new LinkedHashMap<>(); 28 | // ref. list of the commands which can have a world name in front of the command itself (ex. /wb _world_ radius 100) 29 | private Set subCommandsWithWorldNames = new LinkedHashSet<>(); 30 | 31 | private ArrayList subCommandNames = null; 32 | 33 | /** 34 | * Checks the server's registered commands in case any other commands override 35 | * WorldBorder's. Will print errors to the log if conflicts found. 36 | */ 37 | public static void checkRegistrations(MinecraftServer server) 38 | { 39 | List valid = new ArrayList<>( ALIASES.size() ); 40 | List conflict = new ArrayList<>( ALIASES.size() ); 41 | 42 | Map commands = server.getCommandManager().getCommands(); 43 | 44 | for (Object o : ALIASES) 45 | { 46 | String name = (String) o; 47 | Object value = commands.get(name); 48 | 49 | if (value == null) 50 | Log.error("Null handler for '/%s'! Please report this", name); 51 | else if (value instanceof WBCommand) 52 | valid.add("/" + name); 53 | else 54 | conflict.add( 55 | String.format( "/%s (from %s)", name, value.getClass().getName() ) 56 | ); 57 | } 58 | 59 | if (valid.size() == 0) 60 | { 61 | Log.error("All WorldBorder commands are being handled elsewhere:"); 62 | for (String c : conflict) Log.error("* %s", c); 63 | Log.error("It may be that another mod is attempting to provide world " + 64 | "border functionality. Consider removing or disabling that mod to " + 65 | "allow WorldBorder-Forge to work properly."); 66 | } 67 | else if (conflict.size() > 0) 68 | { 69 | Log.warn("The following WorldBorder commands are being handled elsewhere:"); 70 | for (String c : conflict) Log.warn("* %s", c); 71 | Log.warn("It may be that another mod is attempting to provide world " + 72 | "border functionality. Consider removing or disabling that mod to " + 73 | "allow WorldBorder-Forge to work. Alternatively, try these commands:"); 74 | for (String v : valid) Log.warn("* %s", v); 75 | } 76 | } 77 | 78 | // constructor 79 | public WBCommand () 80 | { 81 | addCmd(new CmdHelp()); // 1 example 82 | addCmd(new CmdSet()); // 4 examples for player, 3 for console 83 | addCmd(new CmdSetcorners()); // 1 84 | addCmd(new CmdRadius()); // 1 85 | addCmd(new CmdList()); // 1 86 | //----- 8 per page of examples 87 | addCmd(new CmdShape()); // 2 88 | addCmd(new CmdClear()); // 2 89 | addCmd(new CmdFill()); // 1 90 | addCmd(new CmdTrim()); // 1 91 | addCmd(new CmdBypass()); // 1 92 | addCmd(new CmdBypasslist()); // 1 93 | //----- 94 | addCmd(new CmdKnockback()); // 1 95 | addCmd(new CmdWrap()); // 1 96 | addCmd(new CmdWhoosh()); // 1 97 | addCmd(new CmdGetmsg()); // 1 98 | addCmd(new CmdSetmsg()); // 1 99 | addCmd(new CmdWshape()); // 3 100 | //----- 101 | addCmd(new CmdPreventPlace()); // 1 102 | addCmd(new CmdPreventSpawn()); // 1 103 | addCmd(new CmdDelay()); // 1 104 | addCmd(new CmdDynmap()); // 1 105 | addCmd(new CmdDynmapmsg()); // 1 106 | addCmd(new CmdRemount()); // 1 107 | addCmd(new CmdFillautosave()); // 1 108 | addCmd(new CmdDenypearl()); // 1 109 | //----- 110 | addCmd(new CmdReload()); // 1 111 | 112 | // this is the default command, which shows command example pages; should be last just in case 113 | addCmd(new CmdCommands()); 114 | } 115 | 116 | private void addCmd(WBCmd cmd) 117 | { 118 | subCommands.put(cmd.name, cmd); 119 | if (cmd.hasWorldNameInput) 120 | subCommandsWithWorldNames.add(cmd.name); 121 | } 122 | 123 | @Override 124 | public void execute(MinecraftServer server, ICommandSender sender, String[] split) throws CommandException { 125 | EntityPlayerMP player = sender instanceof EntityPlayerMP 126 | ? (EntityPlayerMP) sender 127 | : null; 128 | 129 | ArrayList params = Lists.newArrayList(split); 130 | 131 | String worldName = null; 132 | // is second parameter the command and first parameter a world name? 133 | if (params.size() > 1 && !subCommands.containsKey(params.get(0)) && subCommandsWithWorldNames.contains(params.get(1))) 134 | worldName = params.get(0); 135 | 136 | // no command specified? show command examples / help 137 | if (params.isEmpty()) 138 | params.add(0, "commands"); 139 | 140 | // determined the command name 141 | String cmdName = (worldName == null) ? params.get(0).toLowerCase() : params.get(1).toLowerCase(); 142 | 143 | // remove command name and (if there) world name from front of param array 144 | params.remove(0); 145 | if (worldName != null) 146 | params.remove(0); 147 | 148 | // make sure command is recognized, default to showing command examples / help if not; also check for specified page number 149 | if (!subCommands.containsKey(cmdName)) 150 | { 151 | int page = (player == null) ? 0 : 1; 152 | try 153 | { 154 | page = Integer.parseInt(cmdName); 155 | } 156 | catch(NumberFormatException ignored) 157 | { 158 | Util.chat(sender, WBCmd.C_ERR + "Command not recognized. Showing command list."); 159 | } 160 | cmdName = "commands"; 161 | params.add(0, Integer.toString(page)); 162 | } 163 | 164 | WBCmd subCommand = subCommands.get(cmdName); 165 | 166 | // if command requires world name when run by console, make sure that's in place 167 | if (player == null && subCommand.hasWorldNameInput && subCommand.consoleRequiresWorldName && worldName == null) 168 | { 169 | Util.chat(sender, WBCmd.C_ERR + "This command requires a world to be specified if run by the console."); 170 | subCommand.sendCmdHelp(sender); 171 | return; 172 | } 173 | 174 | // make sure valid number of parameters has been provided 175 | if (params.size() < subCommand.minParams || params.size() > subCommand.maxParams) 176 | { 177 | if (subCommand.maxParams == 0) 178 | Util.chat(sender, WBCmd.C_ERR + "This command does not accept any parameters."); 179 | else 180 | Util.chat(sender, WBCmd.C_ERR + "You have not provided a valid number of parameters."); 181 | subCommand.sendCmdHelp(sender); 182 | return; 183 | } 184 | 185 | // execute command 186 | subCommand.execute(sender, player, params, worldName); 187 | } 188 | 189 | public ArrayList getCommandNames() 190 | { 191 | if (subCommandNames != null) 192 | return subCommandNames; 193 | 194 | subCommandNames = new ArrayList<>( subCommands.keySet() ); 195 | // Remove "commands" as it's not normally shown or run like other commands 196 | subCommandNames.remove("commands"); 197 | Collections.sort(subCommandNames); 198 | 199 | return subCommandNames; 200 | } 201 | 202 | @Override 203 | public String getName() 204 | { 205 | return NAME; 206 | } 207 | 208 | @Override 209 | public String getUsage(ICommandSender sender) 210 | { 211 | return "/wborder help [n]"; 212 | } 213 | 214 | @Override 215 | public List getAliases() 216 | { 217 | return ALIASES; 218 | } 219 | 220 | @Override 221 | public boolean isUsernameIndex(String[] args, int idx) 222 | { 223 | return false; 224 | } 225 | 226 | @Override 227 | public boolean checkPermission(MinecraftServer server, ICommandSender sender) { 228 | if (sender instanceof DedicatedServer) 229 | return true; 230 | 231 | EntityPlayerMP player = (EntityPlayerMP) sender; 232 | GameProfile profile = player.getGameProfile(); 233 | UserListOpsEntry opEntry = (UserListOpsEntry) WorldBorder.SERVER 234 | .getPlayerList() 235 | .getOppedPlayers() 236 | .getEntry(profile); 237 | 238 | // Level 2 (out of 4) have general access to game-changing commands 239 | // TODO: Make this a configuration option 240 | return opEntry != null && opEntry.getPermissionLevel() > 2; 241 | } 242 | 243 | @Override 244 | public List getTabCompletions(MinecraftServer server, ICommandSender sender, String[] args, @Nullable BlockPos pos) { 245 | if (args.length <= 1) 246 | return CommandBase.getListOfStringsMatchingLastWord(args, getCommandNames()); 247 | 248 | String[] players = WorldBorder.SERVER.getOnlinePlayerNames(); 249 | return CommandBase.getListOfStringsMatchingLastWord(args, players); 250 | } 251 | 252 | @Override 253 | public int compareTo(ICommand o) { 254 | return o.getName().compareTo( getName() ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/BorderData.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder; 2 | 3 | import com.wimbli.WorldBorder.forge.Location; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Worlds; 6 | import net.minecraft.util.math.BlockPos; 7 | import net.minecraft.world.WorldServer; 8 | import net.minecraft.world.chunk.Chunk; 9 | 10 | import java.util.Arrays; 11 | import java.util.LinkedHashSet; 12 | 13 | public class BorderData 14 | { 15 | // the main data interacted with 16 | private double x = 0; 17 | private double z = 0; 18 | private int radiusX = 0; 19 | private int radiusZ = 0; 20 | 21 | private Boolean shapeRound = null; 22 | private boolean wrapping = false; 23 | 24 | // some extra data kept handy for faster border checks 25 | private double maxX; 26 | private double minX; 27 | private double maxZ; 28 | private double minZ; 29 | private double radiusXSquared; 30 | private double radiusZSquared; 31 | private double DefiniteRectangleX; 32 | private double DefiniteRectangleZ; 33 | private double radiusSquaredQuotient; 34 | 35 | // 36 | public BorderData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound, boolean wrap) 37 | { 38 | setData(x, z, radiusX, radiusZ, shapeRound, wrap); 39 | } 40 | 41 | public BorderData(double x, double z, int radiusX, int radiusZ) 42 | { 43 | setData(x, z, radiusX, radiusZ, null); 44 | } 45 | 46 | public BorderData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound) 47 | { 48 | setData(x, z, radiusX, radiusZ, shapeRound); 49 | } 50 | 51 | public BorderData(double x, double z, int radius) 52 | { 53 | setData(x, z, radius, null); 54 | } 55 | 56 | public BorderData(double x, double z, int radius, Boolean shapeRound) 57 | { 58 | setData(x, z, radius, shapeRound); 59 | } 60 | // 61 | 62 | // 63 | public final void setData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound, boolean wrap) 64 | { 65 | this.x = x; 66 | this.z = z; 67 | this.shapeRound = shapeRound; 68 | this.wrapping = wrap; 69 | this.setRadiusX(radiusX); 70 | this.setRadiusZ(radiusZ); 71 | } 72 | 73 | public final void setData(double x, double z, int radiusX, int radiusZ, Boolean shapeRound) 74 | { 75 | setData(x, z, radiusX, radiusZ, shapeRound, false); 76 | } 77 | 78 | public final void setData(double x, double z, int radius, Boolean shapeRound) 79 | { 80 | setData(x, z, radius, radius, shapeRound, false); 81 | } 82 | // 83 | 84 | // 85 | public double getX() 86 | { 87 | return x; 88 | } 89 | 90 | public void setX(double x) 91 | { 92 | this.x = x; 93 | this.maxX = x + radiusX; 94 | this.minX = x - radiusX; 95 | } 96 | 97 | public double getZ() 98 | { 99 | return z; 100 | } 101 | 102 | public void setZ(double z) 103 | { 104 | this.z = z; 105 | this.maxZ = z + radiusZ; 106 | this.minZ = z - radiusZ; 107 | } 108 | 109 | public int getRadiusX() 110 | { 111 | return radiusX; 112 | } 113 | 114 | public int getRadiusZ() 115 | { 116 | return radiusZ; 117 | } 118 | 119 | public void setRadiusX(int radiusX) 120 | { 121 | this.radiusX = radiusX; 122 | this.maxX = x + radiusX; 123 | this.minX = x - radiusX; 124 | 125 | this.radiusXSquared = (double) radiusX * (double) radiusX; 126 | this.radiusSquaredQuotient = this.radiusXSquared / this.radiusZSquared; 127 | this.DefiniteRectangleX = Math.sqrt(.5 * this.radiusXSquared); 128 | } 129 | 130 | public void setRadiusZ(int radiusZ) 131 | { 132 | this.radiusZ = radiusZ; 133 | this.maxZ = z + radiusZ; 134 | this.minZ = z - radiusZ; 135 | 136 | this.radiusZSquared = (double) radiusZ * (double) radiusZ; 137 | this.radiusSquaredQuotient = this.radiusXSquared / this.radiusZSquared; 138 | this.DefiniteRectangleZ = Math.sqrt(.5 * this.radiusZSquared); 139 | } 140 | 141 | public void setRadius(int radius) 142 | { 143 | setRadiusX(radius); 144 | setRadiusZ(radius); 145 | } 146 | 147 | public Boolean getShape() 148 | { 149 | return shapeRound; 150 | } 151 | 152 | public void setShape(Boolean shapeRound) 153 | { 154 | this.shapeRound = shapeRound; 155 | } 156 | 157 | public boolean getWrapping() 158 | { 159 | return wrapping; 160 | } 161 | 162 | public void setWrapping(boolean wrap) 163 | { 164 | this.wrapping = wrap; 165 | } 166 | // 167 | 168 | public BorderData copy() 169 | { 170 | return new BorderData(x, z, radiusX, radiusZ, shapeRound, wrapping); 171 | } 172 | 173 | @Override 174 | public String toString() 175 | { 176 | return String.format("radius %s at X: %s Z: %s%s%s", 177 | (radiusX == radiusZ) ? radiusX : radiusX + "*" + radiusZ, 178 | Config.COORD_FORMAT.format(x), 179 | Config.COORD_FORMAT.format(z), 180 | shapeRound != null 181 | ? String.format( " (shape override: %s)", Config.getShapeName(shapeRound) ) 182 | : "", 183 | wrapping ? " (wrapping)" : "" 184 | ); 185 | } 186 | 187 | /** This algorithm of course needs to be fast, since it will be run very frequently */ 188 | public boolean insideBorder(double xLoc, double zLoc, boolean round) 189 | { 190 | // if this border has a shape override set, use it 191 | if (shapeRound != null) 192 | round = shapeRound; 193 | 194 | // square border 195 | if (!round) 196 | return !(xLoc < minX || xLoc > maxX || zLoc < minZ || zLoc > maxZ); 197 | 198 | // round border 199 | else 200 | { 201 | // elegant round border checking algorithm is from rBorder by Reil with almost no changes, all credit to him for it 202 | double X = Math.abs(x - xLoc); 203 | double Z = Math.abs(z - zLoc); 204 | 205 | if (X < DefiniteRectangleX && Z < DefiniteRectangleZ) 206 | return true; // Definitely inside 207 | else if (X >= radiusX || Z >= radiusZ) 208 | return false; // Definitely outside 209 | else if (X * X + Z * Z * radiusSquaredQuotient < radiusXSquared) 210 | return true; // After further calculation, inside 211 | else 212 | return false; // Apparently outside, then 213 | } 214 | } 215 | public boolean insideBorder(double xLoc, double zLoc) 216 | { 217 | return insideBorder(xLoc, zLoc, Config.getShapeRound()); 218 | } 219 | 220 | public boolean insideBorder(Location loc) 221 | { 222 | return insideBorder(loc.posX, loc.posZ, Config.getShapeRound()); 223 | } 224 | 225 | public boolean insideBorder(CoordXZ coord, boolean round) 226 | { 227 | return insideBorder(coord.x, coord.z, round); 228 | } 229 | 230 | public boolean insideBorder(CoordXZ coord) 231 | { 232 | return insideBorder(coord.x, coord.z, Config.getShapeRound()); 233 | } 234 | 235 | public Location correctedPosition(Location loc, boolean round, boolean flying) 236 | { 237 | // if this border has a shape override set, use it 238 | if (shapeRound != null) 239 | round = shapeRound; 240 | 241 | double xLoc = loc.posX; 242 | double yLoc = loc.posY; 243 | double zLoc = loc.posZ; 244 | double knock = Config.getKnockBack(); 245 | 246 | // Make sure knockback is not too big for this border 247 | if (knock >= radiusX * 2 || knock >= radiusZ * 2) 248 | { 249 | Log.warn("Knockback %.2f is too big for border. Defaulting to 3.0.", knock); 250 | knock = 3.0; 251 | } 252 | 253 | // square border 254 | if (!round) 255 | { 256 | if (wrapping) 257 | { 258 | if (xLoc <= minX) 259 | xLoc = maxX - knock; 260 | else if (xLoc >= maxX) 261 | xLoc = minX + knock; 262 | if (zLoc <= minZ) 263 | zLoc = maxZ - knock; 264 | else if (zLoc >= maxZ) 265 | zLoc = minZ + knock; 266 | } 267 | else 268 | { 269 | if (xLoc <= minX) 270 | xLoc = minX + knock; 271 | else if (xLoc >= maxX) 272 | xLoc = maxX - knock; 273 | if (zLoc <= minZ) 274 | zLoc = minZ + knock; 275 | else if (zLoc >= maxZ) 276 | zLoc = maxZ - knock; 277 | } 278 | } 279 | 280 | // round border 281 | else 282 | { 283 | // algorithm originally from: http://stackoverflow.com/q/300871/3354920 284 | // modified by Lang Lukas to support elliptical border shape 285 | 286 | // Transform the ellipse to a circle with radius 1 (we need to transform the point the same way) 287 | double dX = xLoc - x; 288 | double dZ = zLoc - z; 289 | // Distance of the untransformed point from the center 290 | double dU = Math.sqrt(dX *dX + dZ * dZ); 291 | // Distance of the transformed point from the center 292 | double dT = Math.sqrt(dX *dX / radiusXSquared + dZ * dZ / radiusZSquared); 293 | // "Correction" factor for the distances 294 | double f = (1 / dT - knock / dU); 295 | 296 | if (wrapping) 297 | { 298 | xLoc = x - dX * f; 299 | zLoc = z - dZ * f; 300 | } 301 | else 302 | { 303 | xLoc = x + dX * f; 304 | zLoc = z + dZ * f; 305 | } 306 | } 307 | 308 | int ixLoc = Location.locToBlock(xLoc); 309 | int izLoc = Location.locToBlock(zLoc); 310 | int icxLoc = CoordXZ.blockToChunk(ixLoc); 311 | int icZLoc = CoordXZ.blockToChunk(izLoc); 312 | 313 | // Make sure the chunk we're checking in is actually loaded 314 | // TODO: should this be here? 315 | Chunk tChunk = loc.world.getChunkFromBlockCoords(new BlockPos(ixLoc, 0, izLoc)); 316 | if (!tChunk.isLoaded()) 317 | loc.world.getChunkProvider().loadChunk(icxLoc, icZLoc); 318 | 319 | yLoc = getSafeY(loc.world, ixLoc, Location.locToBlock(yLoc), izLoc, flying); 320 | if (yLoc == -1) 321 | return null; 322 | 323 | return new Location(loc.world, Math.floor(xLoc) + 0.5, yLoc, Math.floor(zLoc) + 0.5, loc.yaw, loc.pitch); 324 | } 325 | 326 | public Location correctedPosition(Location loc, boolean round) 327 | { 328 | return correctedPosition(loc, round, false); 329 | } 330 | 331 | public Location correctedPosition(Location loc) 332 | { 333 | return correctedPosition(loc, Config.getShapeRound(), false); 334 | } 335 | 336 | //these material IDs are acceptable for places to teleport player; breathable blocks and water 337 | public static final LinkedHashSet safeOpenBlocks = new LinkedHashSet<>(Arrays.asList( 338 | new Integer[] {0, 6, 8, 9, 27, 28, 30, 31, 32, 37, 38, 39, 40, 50, 55, 59, 63, 64, 65, 66, 68, 69, 70, 71, 72, 75, 76, 77, 78, 83, 90, 93, 94, 96, 104, 105, 106, 115, 131, 132, 141, 142, 149, 150, 157, 171} 339 | )); 340 | 341 | //these material IDs are ones we don't want to drop the player onto, like cactus or lava or fire or activated Ender portal 342 | public static final LinkedHashSet painfulBlocks = new LinkedHashSet<>(Arrays.asList( 343 | new Integer[] {10, 11, 51, 81, 119} 344 | )); 345 | 346 | // check if a particular spot consists of 2 breathable blocks over something relatively solid 347 | private boolean isSafeSpot(WorldServer world, int X, int Y, int Z, boolean flying) 348 | { 349 | boolean safe = safeOpenBlocks.contains( Worlds.getBlockID(world, X, Y, Z) ) // target block open and safe 350 | && safeOpenBlocks.contains( Worlds.getBlockID(world, X, Y + 1, Z) ); // above target block open and safe 351 | 352 | if (!safe || flying) 353 | return safe; 354 | 355 | int below = Worlds.getBlockID(world, X, Y - 1, Z); 356 | 357 | return 358 | !(below == 7 && world.provider.getDimension() == -1) // try not to place player above bedrock in nether 359 | && (!safeOpenBlocks.contains(below) || below == 8 || below == 9) // below target block not open/breathable (so presumably solid), or is water 360 | && !painfulBlocks.contains(below); // below target block not painful 361 | } 362 | 363 | private static final int limBot = 1; 364 | 365 | // find closest safe Y position from the starting position 366 | public double getSafeY(WorldServer world, int X, int Y, int Z, boolean flying) 367 | { 368 | final int limTop = world.getHeight() - 2; 369 | // Expanding Y search method adapted from Acru's code in the Nether plugin 370 | 371 | for(int y1 = Y, y2 = Y; (y1 > limBot) || (y2 < limTop); y1--, y2++) 372 | { 373 | // Look below. 374 | if (y1 > limBot) 375 | if ( isSafeSpot(world, X, y1, Z, flying) ) 376 | return (double) y1; 377 | 378 | // Look above. 379 | if (y2 < limTop && y2 != y1) 380 | if ( isSafeSpot(world, X, y2, Z, flying) ) 381 | return (double) y2; 382 | } 383 | 384 | // no safe Y location?!?!? Must be a rare spot in a Nether world or something 385 | return -1.0; 386 | } 387 | 388 | @Override 389 | public boolean equals(Object obj) 390 | { 391 | if (this == obj) 392 | return true; 393 | else if ( obj == null || obj.getClass() != this.getClass() ) 394 | return false; 395 | 396 | BorderData test = (BorderData) obj; 397 | return test.x == this.x 398 | && test.z == this.z 399 | && test.radiusX == this.radiusX 400 | && test.radiusZ == this.radiusZ; 401 | } 402 | 403 | @Override 404 | public int hashCode() 405 | { 406 | return ((int) (this.x * 10) << 4) 407 | + (int) this.z 408 | + (this.radiusX << 2) 409 | + (this.radiusZ << 3); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/task/WorldTrimTask.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.task; 2 | 3 | import com.wimbli.WorldBorder.*; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import com.wimbli.WorldBorder.forge.Worlds; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.entity.player.EntityPlayerMP; 9 | import net.minecraft.util.math.BlockPos; 10 | import net.minecraft.util.math.ChunkPos; 11 | import net.minecraft.world.WorldServer; 12 | import net.minecraftforge.fml.common.FMLCommonHandler; 13 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 14 | import net.minecraftforge.fml.common.gameevent.TickEvent; 15 | 16 | import java.io.File; 17 | import java.io.FileNotFoundException; 18 | import java.io.IOException; 19 | import java.io.RandomAccessFile; 20 | import java.nio.file.Files; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | /** 25 | * Singleton tick handler that performs a trim task over a long running series of ticks 26 | */ 27 | public class WorldTrimTask 28 | { 29 | private static WorldTrimTask INSTANCE = null; 30 | 31 | /** Gets the singleton instance of this task, or null if none exists */ 32 | public static WorldTrimTask getInstance() 33 | { 34 | return INSTANCE; 35 | } 36 | 37 | public static WorldTrimTask create( 38 | ICommandSender player, String worldName, 39 | int trimDistance, int chunksPerRun, int tickFrequency) 40 | { 41 | if (INSTANCE != null) 42 | throw new IllegalStateException("There can only be one WorldTrimTask"); 43 | else try 44 | { 45 | INSTANCE = new WorldTrimTask(player, worldName, trimDistance, chunksPerRun, tickFrequency); 46 | return INSTANCE; 47 | } 48 | catch (Exception e) 49 | { 50 | INSTANCE = null; 51 | throw e; 52 | } 53 | } 54 | 55 | // Per-task shortcut references 56 | private final WorldServer world; 57 | private final WorldFileData worldData; 58 | private final BorderData border; 59 | private final ICommandSender requester; 60 | 61 | // Per-task state variables 62 | private List regionChunks = new ArrayList<>(1024); 63 | private List trimChunks = new ArrayList<>(1024); 64 | 65 | private int tickFrequency = 1; 66 | private int chunksPerRun = 1; 67 | private boolean readyToGo = false; 68 | private boolean paused = false; 69 | private boolean deleteError = false; 70 | 71 | // Per-task state region progress tracking 72 | private int currentRegion = -1; // region(file) we're at in regionFiles 73 | private int currentChunk = 0; // chunk we've reached in the current region (regionChunks) 74 | 75 | private int regionX = 0; // X location value of the current region 76 | private int regionZ = 0; // X location value of the current region 77 | private int counter = 0; 78 | 79 | // Per-task state for progress reporting 80 | private long lastReport = Util.now(); 81 | private int reportTarget = 0; 82 | private int reportTotal = 0; 83 | 84 | private int reportTrimmedRegions = 0; 85 | private int reportTrimmedChunks = 0; 86 | 87 | /** Starts this task by registering the tick handler */ 88 | public void start() 89 | { 90 | if (INSTANCE != this) 91 | throw new IllegalStateException("Cannot start a stopped task"); 92 | 93 | FMLCommonHandler.instance().bus().register(this); 94 | } 95 | 96 | /** Stops this task by unregistering the tick handler and removing the instance */ 97 | public void stop() 98 | { 99 | if (INSTANCE != this) 100 | throw new IllegalStateException("Task has already been stopped"); 101 | else 102 | FMLCommonHandler.instance().bus().unregister(this); 103 | 104 | regionChunks.clear(); 105 | trimChunks.clear(); 106 | 107 | INSTANCE = null; 108 | } 109 | 110 | // TODO: Optimize this away 111 | public void pause() 112 | { 113 | pause(!this.paused); 114 | } 115 | 116 | public void pause(boolean pause) 117 | { 118 | this.paused = pause; 119 | if (pause) 120 | reportProgress(); 121 | } 122 | 123 | public boolean isPaused() 124 | { 125 | return this.paused; 126 | } 127 | 128 | private WorldTrimTask(ICommandSender player, String worldName, int trimDistance, int chunksPerRun, int tickFrequency) 129 | { 130 | this.requester = player; 131 | this.tickFrequency = tickFrequency; 132 | this.chunksPerRun = chunksPerRun; 133 | 134 | this.world = Worlds.getWorld(worldName); 135 | if (this.world == null) 136 | throw new IllegalArgumentException("World \"" + worldName + "\" not found!"); 137 | 138 | this.border = (Config.Border(worldName) == null) 139 | ? null 140 | : Config.Border(worldName).copy(); 141 | 142 | if (this.border == null) 143 | throw new IllegalStateException("No border found for world \"" + worldName + "\"!"); 144 | 145 | this.worldData = new WorldFileData(world, requester); 146 | 147 | this.border.setRadiusX(border.getRadiusX() + trimDistance); 148 | this.border.setRadiusZ(border.getRadiusZ() + trimDistance); 149 | 150 | // each region file covers up to 1024 chunks; with all operations we might need to do, let's figure 3X that 151 | this.reportTarget = worldData.regionFileCount() * 3072; 152 | 153 | // queue up the first file 154 | if (!nextFile()) 155 | return; 156 | 157 | this.readyToGo = true; 158 | } 159 | 160 | @SubscribeEvent 161 | public void onServerTick(TickEvent.ServerTickEvent event) 162 | { 163 | // Only run at start of tick 164 | if (event.phase == TickEvent.Phase.END) 165 | return; 166 | 167 | if (WorldBorder.SERVER.getTickCounter() % tickFrequency != 0) 168 | return; 169 | 170 | if (!readyToGo || paused) 171 | return; 172 | 173 | // TODO: make this less crude by kicking or teleporting players in dimension 174 | // if (DimensionManager.getWorld(world.provider.dimensionId) != null) 175 | // { 176 | // Log.debug( "Trying to unload dimension %s", Util.getWorldName(world) ); 177 | // DimensionManager.unloadWorld(world.provider.dimensionId); 178 | // return; 179 | // } 180 | 181 | // this is set so it only does one iteration at a time, no matter how frequently the timer fires 182 | readyToGo = false; 183 | // 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 184 | long loopStartTime = Util.now(); 185 | 186 | counter = 0; 187 | while (counter <= chunksPerRun) 188 | { 189 | // in case the task has been paused while we're repeating... 190 | if (paused) 191 | return; 192 | 193 | long now = Util.now(); 194 | 195 | // every 5 seconds or so, give basic progress report to let user know how it's going 196 | if (now > lastReport + 5000) 197 | reportProgress(); 198 | 199 | // 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 200 | if (now > loopStartTime + 45) 201 | { 202 | readyToGo = true; 203 | return; 204 | } 205 | 206 | if (regionChunks.isEmpty()) 207 | addCornerChunks(); 208 | else if (currentChunk == 4) 209 | { // determine if region is completely _inside_ border based on corner chunks 210 | if (trimChunks.isEmpty()) 211 | { // it is, so skip it and move on to next file 212 | counter += 4; 213 | nextFile(); 214 | continue; 215 | } 216 | addEdgeChunks(); 217 | addInnerChunks(); 218 | } 219 | else if (currentChunk == 124 && trimChunks.size() == 124) 220 | { // region is completely _outside_ border based on edge chunks, so delete file and move on to next 221 | counter += 16; 222 | trimChunks = regionChunks; 223 | unloadChunks(); 224 | reportTrimmedRegions++; 225 | File regionFile = worldData.regionFile(currentRegion); 226 | 227 | try 228 | { 229 | Files.delete( regionFile.toPath() ); 230 | 231 | Log.trace( 232 | "Deleted region file '%s' for world '%s'", 233 | regionFile.getAbsolutePath(), 234 | Worlds.getWorldName(world) 235 | ); 236 | } 237 | catch (Exception e) 238 | { 239 | Log.warn( 240 | "Exception when deleting region file '%s': %s", 241 | regionFile.getName(), 242 | e.getMessage().replaceAll("\n", "") 243 | ); 244 | 245 | deleteError = true; 246 | wipeChunks(); 247 | } 248 | 249 | // if DynMap is installed, re-render the trimmed region 250 | DynMapFeatures.renderRegion(world, new CoordXZ(regionX, regionZ)); 251 | 252 | nextFile(); 253 | continue; 254 | } 255 | else if (currentChunk == 1024) 256 | { // last chunk of the region has been checked, time to wipe out whichever chunks are outside the border 257 | counter += 32; 258 | unloadChunks(); 259 | wipeChunks(); 260 | nextFile(); 261 | continue; 262 | } 263 | 264 | // check whether chunk is inside the border or not, add it to the "trim" list if not 265 | CoordXZ chunk = regionChunks.get(currentChunk); 266 | if (!isChunkInsideBorder(chunk)) 267 | trimChunks.add(chunk); 268 | 269 | currentChunk++; 270 | counter++; 271 | } 272 | 273 | reportTotal += counter; 274 | 275 | // ready for the next iteration to run 276 | readyToGo = true; 277 | } 278 | 279 | // Advance to the next region file. Returns true if successful, false if the next file isn't accessible for any reason 280 | private boolean nextFile() 281 | { 282 | reportTotal = currentRegion * 3072; 283 | currentRegion++; 284 | regionX = regionZ = currentChunk = 0; 285 | regionChunks = new ArrayList<>(1024); 286 | trimChunks = new ArrayList<>(1024); 287 | 288 | // have we already handled all region files? 289 | if (currentRegion >= worldData.regionFileCount()) 290 | { // hey, we're done 291 | paused = true; 292 | readyToGo = false; 293 | finish(); 294 | return false; 295 | } 296 | 297 | counter += 16; 298 | 299 | // get the X and Z coordinates of the current region 300 | CoordXZ coord = worldData.regionFileCoordinates(currentRegion); 301 | if (coord == null) 302 | return false; 303 | 304 | regionX = coord.x; 305 | regionZ = coord.z; 306 | return true; 307 | } 308 | 309 | // add just the 4 corner chunks of the region; can determine if entire region is _inside_ the border 310 | private void addCornerChunks() 311 | { 312 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ))); 313 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ))); 314 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ) + 31)); 315 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ) + 31)); 316 | } 317 | 318 | // add all chunks along the 4 edges of the region (minus the corners); can determine if entire region is _outside_ the border 319 | private void addEdgeChunks() 320 | { 321 | int chunkX = 0, chunkZ; 322 | 323 | for (chunkZ = 1; chunkZ < 31; chunkZ++) 324 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 325 | 326 | chunkX = 31; 327 | for (chunkZ = 1; chunkZ < 31; chunkZ++) 328 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 329 | 330 | chunkZ = 0; 331 | for (chunkX = 1; chunkX < 31; chunkX++) 332 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 333 | 334 | chunkZ = 31; 335 | for (chunkX = 1; chunkX < 31; chunkX++) 336 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 337 | 338 | counter += 4; 339 | } 340 | 341 | // add the remaining interior chunks (after corners and edges) 342 | private void addInnerChunks() 343 | { 344 | for (int chunkX = 1; chunkX < 31; chunkX++) 345 | for (int chunkZ = 1; chunkZ < 31; chunkZ++) 346 | regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ)); 347 | 348 | counter += 32; 349 | } 350 | 351 | // make sure chunks set to be trimmed are not currently loaded by the server 352 | private void unloadChunks() 353 | { 354 | for (CoordXZ unload : trimChunks) 355 | ChunkUtil.unloadChunksIfNotNearSpawn(world, unload.x, unload.z); 356 | 357 | world.getChunkProvider().tick(); 358 | counter += trimChunks.size(); 359 | } 360 | 361 | // edit region file to wipe all chunk pointers for chunks outside the border 362 | private void wipeChunks() 363 | { 364 | File regionFile = worldData.regionFile(currentRegion); 365 | if (!regionFile.canWrite()) 366 | { 367 | if (!regionFile.setWritable(true)) 368 | throw new RuntimeException(); 369 | 370 | if (!regionFile.canWrite()) 371 | { 372 | sendMessage("Error! region file is locked and can't be trimmed: " + regionFile.getName()); 373 | return; 374 | } 375 | } 376 | 377 | // since our stored chunk positions are based on world, we need to offset those to positions in the region file 378 | int offsetX = CoordXZ.regionToChunk(regionX); 379 | int offsetZ = CoordXZ.regionToChunk(regionZ); 380 | int chunkCount = 0; 381 | long wipePos; 382 | 383 | try ( RandomAccessFile unChunk = new RandomAccessFile(regionFile, "rwd") ) 384 | { 385 | for (CoordXZ wipe : trimChunks) 386 | { 387 | // if the chunk pointer is empty (chunk doesn't technically exist), no need to wipe the already empty pointer 388 | if (!worldData.doesChunkExist(wipe.x, wipe.z)) 389 | continue; 390 | 391 | // wipe this extraneous chunk's pointer... note that this method isn't perfect since the actual chunk data is left orphaned, 392 | // but Minecraft will overwrite the orphaned data sector if/when another chunk is created in the region, so it's not so bad 393 | wipePos = 4 * ((wipe.x - offsetX) + ((wipe.z - offsetZ) * 32)); 394 | unChunk.seek(wipePos); 395 | unChunk.writeInt(0); 396 | chunkCount++; 397 | } 398 | 399 | // if DynMap is installed, re-render the trimmed chunks 400 | // TODO: check if this now works 401 | DynMapFeatures.renderChunks(world, trimChunks); 402 | 403 | reportTrimmedChunks += chunkCount; 404 | } 405 | catch (FileNotFoundException ex) 406 | { 407 | sendMessage("Error! Could not open region file to wipe individual chunks: "+regionFile.getName()); 408 | } 409 | catch (IOException ex) 410 | { 411 | sendMessage("Error! Could not modify region file to wipe individual chunks: "+regionFile.getName()); 412 | } 413 | 414 | counter += trimChunks.size(); 415 | } 416 | 417 | private boolean isChunkInsideBorder(CoordXZ chunk) 418 | { 419 | return border.insideBorder(CoordXZ.chunkToBlock(chunk.x) + 8, CoordXZ.chunkToBlock(chunk.z) + 8); 420 | } 421 | 422 | // for successful completion 423 | private void finish() 424 | { 425 | reportTotal = reportTarget; 426 | reportProgress(); 427 | sendMessage("Task successfully completed for world \"" + Worlds.getWorldName(world) + "\"!"); 428 | 429 | if (deleteError) 430 | sendMessage( 431 | "One or more region files could not be deleted. It may be that the world " + 432 | "spawn point covers those regions, or the server is running on Windows. " + 433 | "Restart the server and retry trimming without players logged in." 434 | ); 435 | 436 | this.stop(); 437 | } 438 | 439 | // let the user know how things are coming along 440 | private void reportProgress() 441 | { 442 | lastReport = Util.now(); 443 | double perc = ((double)(reportTotal) / (double)reportTarget) * 100; 444 | sendMessage(reportTrimmedRegions + " entire region(s) and " + reportTrimmedChunks + " individual chunk(s) trimmed so far (" + Config.COORD_FORMAT.format(perc) + "%% done" + ")"); 445 | } 446 | 447 | // send a message to the server console/log and possibly to an in-game player 448 | private void sendMessage(String text) 449 | { 450 | Log.info("[Trim] " + text); 451 | if (requester instanceof EntityPlayerMP) 452 | Util.chat(requester, "[Trim] " + text); 453 | } 454 | 455 | @Override 456 | protected void finalize() throws Throwable 457 | { 458 | super.finalize(); 459 | Log.debug( "WorldTrimTask cleaned up for %s", Worlds.getWorldName(world) ); 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /src/main/java/com/wimbli/WorldBorder/task/WorldFillTask.java: -------------------------------------------------------------------------------- 1 | package com.wimbli.WorldBorder.task; 2 | 3 | import com.wimbli.WorldBorder.*; 4 | import com.wimbli.WorldBorder.forge.Log; 5 | import com.wimbli.WorldBorder.forge.Util; 6 | import com.wimbli.WorldBorder.forge.Worlds; 7 | import net.minecraft.command.ICommandSender; 8 | import net.minecraft.entity.player.EntityPlayerMP; 9 | import net.minecraft.world.WorldServer; 10 | import net.minecraft.world.chunk.Chunk; 11 | import net.minecraft.world.gen.ChunkProviderServer; 12 | import net.minecraftforge.fml.common.FMLCommonHandler; 13 | import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; 14 | import net.minecraftforge.fml.common.gameevent.TickEvent; 15 | 16 | import java.util.*; 17 | 18 | /** 19 | * Singleton tick handler that performs a fill task over a long running series of ticks 20 | */ 21 | public class WorldFillTask 22 | { 23 | private static WorldFillTask INSTANCE = null; 24 | 25 | /** Gets the singleton instance of this task, or null if none exists */ 26 | public static WorldFillTask getInstance() 27 | { 28 | return INSTANCE; 29 | } 30 | 31 | /** Creates a singleton instance of this task, rethrowing any errors */ 32 | public static WorldFillTask create( 33 | ICommandSender requester, String worldName, 34 | boolean forceLoad, int fillDistance, int chunksPerRun, int tickFrequency) 35 | { 36 | if (INSTANCE != null) 37 | throw new IllegalStateException("There can only be one WorldFillTask"); 38 | else try 39 | { 40 | INSTANCE = new WorldFillTask(requester, worldName, forceLoad, fillDistance, chunksPerRun, tickFrequency); 41 | return INSTANCE; 42 | } 43 | catch (Exception e) 44 | { 45 | INSTANCE = null; 46 | throw e; 47 | } 48 | } 49 | 50 | // Per-task shortcut references 51 | private final WorldServer world; 52 | private final WorldFileData worldData; 53 | private final ChunkProviderServer provider; 54 | private final BorderData border; 55 | private final ICommandSender requester; 56 | 57 | // Per-task state variables 58 | private final List storedChunks = new LinkedList<>(); 59 | private final Set originalChunks = new HashSet<>(); 60 | private final CoordXZ lastChunk = new CoordXZ(0, 0); 61 | 62 | private int chunksPerRun = 1; 63 | private boolean readyToGo = false; 64 | private boolean paused = false; 65 | private boolean memoryPause = false; 66 | private boolean continueNotice = false; 67 | private boolean forceLoad = false; 68 | 69 | // Per-task state for the spiral fill pattern 70 | private int x = 0; 71 | private int z = 0; 72 | private boolean isZLeg = false; 73 | private boolean isNeg = false; 74 | private boolean inside = true; 75 | private int length = -1; 76 | private int current = 0; 77 | 78 | // Per-task state for progress reporting 79 | private long lastReport = Util.now(); 80 | private long lastAutosave = Util.now(); 81 | private int reportTarget = 0; 82 | private int reportTotal = 0; 83 | private int reportNum = 0; 84 | 85 | // Per-task persistent settings 86 | private int fillDistance = 208; 87 | private int tickFrequency = 1; 88 | private int refLength = -1; 89 | 90 | private int refX = 0, lastLegX = 0; 91 | private int refZ = 0, lastLegZ = 0; 92 | private int refTotal = 0, lastLegTotal = 0; 93 | 94 | // 95 | /** Gets X of last chunk to be processed */ 96 | public int getRefX() 97 | { 98 | return refX; 99 | } 100 | 101 | /** Gets Z of last chunk to be processed */ 102 | public int getRefZ() 103 | { 104 | return refZ; 105 | } 106 | 107 | /** Gets progress amount of chunks to process */ 108 | public int getRefLength() 109 | { 110 | return refLength; 111 | } 112 | 113 | /** Gets total amount of chunks to process */ 114 | public int getRefTotal() 115 | { 116 | return refTotal; 117 | } 118 | 119 | /** Gets configured fill distance of this task */ 120 | public int getFillDistance() 121 | { 122 | return fillDistance; 123 | } 124 | 125 | /** Gets configured how many ticks are each run */ 126 | public int getTickFrequency() 127 | { 128 | return tickFrequency; 129 | } 130 | 131 | /** Gets configured amount of chunks to fill per run */ 132 | public int getChunksPerRun() 133 | { 134 | return chunksPerRun; 135 | } 136 | 137 | /** Gets configured world of this task */ 138 | public String getWorld() 139 | { 140 | return Worlds.getWorldName(world); 141 | } 142 | 143 | /** Gets whether this task forces loading of existing chunks */ 144 | public boolean getForceLoad() 145 | { 146 | return forceLoad; 147 | } 148 | // 149 | 150 | /** Starts this task by registering the tick handler */ 151 | public void start() 152 | { 153 | if (INSTANCE != this) 154 | throw new IllegalStateException("Cannot start a stopped task"); 155 | 156 | FMLCommonHandler.instance().bus().register(this); 157 | } 158 | 159 | /** Starts this task by resuming from prior progress */ 160 | public void startFrom(int x, int z, int length, int totalDone) 161 | { 162 | this.x = x; 163 | this.z = z; 164 | this.length = length; 165 | this.reportTotal = totalDone; 166 | this.continueNotice = true; 167 | start(); 168 | } 169 | 170 | /** Stops this task by unregistering the tick handler and removing the instance */ 171 | public void stop() 172 | { 173 | if (INSTANCE != this) 174 | throw new IllegalStateException("Task has already been stopped"); 175 | else 176 | FMLCommonHandler.instance().bus().unregister(this); 177 | 178 | // Unload chunks that are still loaded 179 | while( !storedChunks.isEmpty() ) 180 | { 181 | CoordXZ coord = storedChunks.remove(0); 182 | 183 | if ( !originalChunks.contains(coord) ) 184 | ChunkUtil.unloadChunksIfNotNearSpawn(world, coord.x, coord.z); 185 | } 186 | 187 | originalChunks.clear(); 188 | 189 | INSTANCE = null; 190 | } 191 | 192 | // TODO: Optimize this away 193 | public void pause() 194 | { 195 | if(this.memoryPause) 196 | pause(false); 197 | else 198 | pause(!this.paused); 199 | } 200 | 201 | public void pause(boolean pause) 202 | { 203 | if (this.memoryPause && !pause) 204 | this.memoryPause = false; 205 | else 206 | this.paused = pause; 207 | if (this.paused) 208 | { 209 | Config.storeFillTask(); 210 | reportProgress(); 211 | } 212 | else 213 | Config.deleteFillTask(); 214 | } 215 | 216 | public boolean isPaused() 217 | { 218 | return this.paused || this.memoryPause; 219 | } 220 | 221 | private WorldFillTask(ICommandSender requester, String worldName, boolean forceLoad, int fillDistance, int chunksPerRun, int tickFrequency) 222 | { 223 | this.requester = requester; 224 | this.fillDistance = fillDistance; 225 | this.tickFrequency = tickFrequency; 226 | this.chunksPerRun = chunksPerRun; 227 | this.forceLoad = forceLoad; 228 | 229 | this.world = Worlds.getWorld(worldName); 230 | 231 | if (this.world == null) 232 | throw new IllegalArgumentException("World \"" + worldName + "\" not found!"); 233 | 234 | this.border = (Config.Border(worldName) == null) 235 | ? null 236 | : Config.Border(worldName).copy(); 237 | 238 | if (this.border == null) 239 | throw new IllegalStateException("No border found for world \"" + worldName + "\"!"); 240 | 241 | this.worldData = new WorldFileData(world, requester); 242 | 243 | this.border.setRadiusX(border.getRadiusX() + fillDistance); 244 | this.border.setRadiusZ(border.getRadiusZ() + fillDistance); 245 | this.x = CoordXZ.blockToChunk((int)border.getX()); 246 | this.z = CoordXZ.blockToChunk((int)border.getZ()); 247 | 248 | // We need to calculate the reportTarget with the bigger width, since the spiral 249 | // will only stop if it has a size of biggerWidth * biggerWidth 250 | int chunkWidthX = (int) Math.ceil((double)((border.getRadiusX() + 16) * 2) / 16); 251 | int chunkWidthZ = (int) Math.ceil((double)((border.getRadiusZ() + 16) * 2) / 16); 252 | int biggerWidth = (chunkWidthX > chunkWidthZ) ? chunkWidthX : chunkWidthZ; 253 | 254 | this.reportTarget = (biggerWidth * biggerWidth) + biggerWidth + 1; 255 | 256 | // Keep track of the chunks which are already loaded when the task starts, to not unload them 257 | this.provider = world.getChunkProvider(); 258 | Collection originals = provider.getLoadedChunks(); 259 | 260 | for (Chunk original : originals) 261 | originalChunks.add(new CoordXZ(original.x, original.z)); 262 | 263 | this.readyToGo = true; 264 | } 265 | 266 | @SubscribeEvent 267 | public void onServerTick(TickEvent.ServerTickEvent event) 268 | { 269 | if(!provider.canSave()) { 270 | provider.saveChunks(true); 271 | } 272 | 273 | // Only run at start of tick 274 | if (event.phase == TickEvent.Phase.END) 275 | return; 276 | 277 | if (WorldBorder.SERVER.getTickCounter() % tickFrequency != 0) 278 | return; 279 | 280 | if (continueNotice) 281 | { // notify user that task has continued automatically 282 | continueNotice = false; 283 | sendMessage("World map generation task automatically continuing."); 284 | sendMessage("Reminder: you can cancel at any time with \"wb fill cancel\", or pause/unpause with \"wb fill pause\"."); 285 | } 286 | 287 | if (memoryPause) 288 | { // if available memory gets too low, we automatically pause, so handle that 289 | if ( Config.isAvailableMemoryTooLow() ) 290 | return; 291 | 292 | memoryPause = false; 293 | readyToGo = true; 294 | sendMessage("Available memory is sufficient, automatically continuing."); 295 | } 296 | 297 | if (!readyToGo || paused) 298 | return; 299 | 300 | // this is set so it only does one iteration at a time, no matter how frequently the timer fires 301 | readyToGo = false; 302 | // 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 303 | long loopStartTime = Util.now(); 304 | 305 | for (int loop = 0; loop < chunksPerRun; loop++) 306 | { 307 | // in case the task has been paused while we're repeating... 308 | if (paused || memoryPause) 309 | return; 310 | 311 | long now = Util.now(); 312 | 313 | // every 5 seconds or so, give basic progress report to let user know how it's going 314 | if (now > lastReport + 5000) 315 | reportProgress(); 316 | 317 | // if this iteration has been running for 45ms (almost 1 tick) or more, stop to take a breather 318 | if (now > loopStartTime + 45) 319 | { 320 | readyToGo = true; 321 | return; 322 | } 323 | 324 | // if we've made it at least partly outside the border, skip past any such chunks 325 | while (!border.insideBorder(CoordXZ.chunkToBlock(x) + 8, CoordXZ.chunkToBlock(z) + 8)) 326 | if (!moveToNext()) 327 | return; 328 | 329 | inside = true; 330 | 331 | // skip past any chunks which are confirmed as fully generated using our super-special isChunkFullyGenerated routine 332 | if (!forceLoad) 333 | while (worldData.isChunkFullyGenerated(x, z)) 334 | { 335 | inside = true; 336 | if (!moveToNext()) 337 | return; 338 | } 339 | 340 | // load the target chunk and generate it if necessary 341 | provider.provideChunk(x, z); 342 | worldData.chunkExistsNow(x, z); 343 | 344 | // There need to be enough nearby chunks loaded to make the server populate a chunk with trees, snow, etc. 345 | // So, we keep the last few chunks loaded, and need to also temporarily load an extra inside chunk (neighbor closest to center of map) 346 | int popX = !isZLeg ? x : (x + (isNeg ? -1 : 1)); 347 | int popZ = isZLeg ? z : (z + (!isNeg ? -1 : 1)); 348 | // RoyCurtis: this originally specified "false" for chunk generation; things 349 | // may break now that it is true 350 | provider.provideChunk(popX, popZ); 351 | 352 | // make sure the previous chunk in our spiral is loaded as well (might have already existed and been skipped over) 353 | if (!storedChunks.contains(lastChunk) && !originalChunks.contains(lastChunk)) 354 | { 355 | provider.provideChunk(lastChunk.x, lastChunk.z); 356 | storedChunks.add(new CoordXZ(lastChunk.x, lastChunk.z)); 357 | } 358 | 359 | // Store the coordinates of these latest 2 chunks we just loaded, so we can unload them after a bit... 360 | storedChunks.add(new CoordXZ(popX, popZ)); 361 | storedChunks.add(new CoordXZ(x, z)); 362 | 363 | // If enough stored chunks are buffered in, go ahead and unload the oldest to free up memory 364 | while (storedChunks.size() > 8) 365 | { 366 | CoordXZ coord = storedChunks.remove(0); 367 | 368 | if (!originalChunks.contains(coord)) 369 | ChunkUtil.unloadChunksIfNotNearSpawn(world, coord.x, coord.z); 370 | } 371 | 372 | // move on to next chunk 373 | if (!moveToNext()) 374 | return; 375 | } 376 | 377 | // ready for the next iteration to run 378 | DynMapFeatures.renderRegion(world, new CoordXZ(x, z)); 379 | readyToGo = true; 380 | } 381 | 382 | // step through chunks in spiral pattern from center; returns false if we're done, otherwise returns true 383 | public boolean moveToNext() 384 | { 385 | if (paused || memoryPause) 386 | return false; 387 | 388 | reportNum++; 389 | 390 | // keep track of progress in case we need to save to config for restoring progress after server restart 391 | if (!isNeg && current == 0 && length > 3) 392 | { 393 | if (!isZLeg) 394 | { 395 | lastLegX = x; 396 | lastLegZ = z; 397 | lastLegTotal = reportTotal + reportNum; 398 | } 399 | else 400 | { 401 | refX = lastLegX; 402 | refZ = lastLegZ; 403 | refTotal = lastLegTotal; 404 | refLength = length - 1; 405 | } 406 | } 407 | 408 | // make sure of the direction we're moving (X or Z? negative or positive?) 409 | if (current < length) 410 | current++; 411 | else 412 | { // one leg/side of the spiral down... 413 | current = 0; 414 | isZLeg ^= true; 415 | if (isZLeg) 416 | { // every second leg (between X and Z legs, negative or positive), length increases 417 | isNeg ^= true; 418 | length++; 419 | } 420 | } 421 | 422 | // keep track of the last chunk we were at 423 | lastChunk.x = x; 424 | lastChunk.z = z; 425 | 426 | // move one chunk further in the appropriate direction 427 | if (isZLeg) 428 | z += (isNeg) ? -1 : 1; 429 | else 430 | x += (isNeg) ? -1 : 1; 431 | 432 | // if we've been around one full loop (4 legs)... 433 | if (isZLeg && isNeg && current == 0) 434 | { // see if we've been outside the border for the whole loop 435 | if (!inside) 436 | { // and finish if so 437 | finish(); 438 | return false; 439 | } // otherwise, reset the "inside border" flag 440 | else 441 | inside = false; 442 | } 443 | return true; 444 | 445 | /* reference diagram used, should move in this pattern: 446 | * 8 [>][>][>][>][>] etc. 447 | * [^][6][>][>][>][>][>][6] 448 | * [^][^][4][>][>][>][4][v] 449 | * [^][^][^][2][>][2][v][v] 450 | * [^][^][^][^][0][v][v][v] 451 | * [^][^][^][1][1][v][v][v] 452 | * [^][^][3][<][<][3][v][v] 453 | * [^][5][<][<][<][<][5][v] 454 | * [7][<][<][<][<][<][<][7] 455 | */ 456 | } 457 | 458 | private void finish() 459 | { 460 | this.paused = true; 461 | reportProgress(); 462 | Worlds.saveWorld(world); 463 | sendMessage("Task successfully completed for world \"" + getWorld() + "\"!"); 464 | this.stop(); 465 | } 466 | 467 | // let the user know how things are coming along 468 | private void reportProgress() 469 | { 470 | lastReport = Util.now(); 471 | double perc = ((double)(reportTotal + reportNum) / (double)reportTarget) * 100; 472 | if (perc > 100) perc = 100; 473 | sendMessage(reportNum + " more chunks processed (" + (reportTotal + reportNum) + " total, ~" + Config.COORD_FORMAT.format(perc) + "%%" + ")"); 474 | reportTotal += reportNum; 475 | reportNum = 0; 476 | 477 | // 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 478 | if (Config.getFillAutosaveFrequency() > 0 && lastAutosave + (Config.getFillAutosaveFrequency() * 1000) < lastReport) 479 | { 480 | lastAutosave = lastReport; 481 | sendMessage("Saving the world to disk, just to be on the safe side."); 482 | Worlds.saveWorld(world); 483 | } 484 | } 485 | 486 | // send a message to the server console/log and possibly to an in-game player 487 | private void sendMessage(String text) 488 | { 489 | // Due to chunk generation eating up memory and Java being too slow about GC, we need to track memory availability 490 | long availMem = Config.getAvailableMemory(); 491 | 492 | Log.info("[Fill] " + text + " (free mem: " + availMem + " MB)"); 493 | if (requester instanceof EntityPlayerMP) 494 | Util.chat(requester, "[Fill] " + text + " (free mem: " + availMem + " MB)"); 495 | 496 | if ( Config.isAvailableMemoryTooLow() ) 497 | { // running low on memory, auto-pause 498 | memoryPause = true; 499 | Config.storeFillTask(); 500 | text = "Available memory is very low, task is pausing. A cleanup will be attempted now, " + 501 | "and the task will automatically continue if/when sufficient memory is freed up.\n " + 502 | "Alternatively, if you restart the server, this task will automatically continue once " + 503 | "the server is back up."; 504 | 505 | Log.info("[Fill] " + text); 506 | if (requester instanceof EntityPlayerMP) 507 | Util.chat(requester, "[Fill] " + text); 508 | 509 | // Forced garbage-collection works well to immediately recover memory 510 | System.gc(); 511 | } 512 | } 513 | 514 | @Override 515 | protected void finalize() throws Throwable 516 | { 517 | super.finalize(); 518 | Log.debug( "WorldFillTask cleaned up for %s", getWorld() ); 519 | } 520 | } 521 | --------------------------------------------------------------------------------