public class CommandJED extends CommandBase
{
    private final Map<ICommandSender, String> commandConfirmations = new HashMap<>();

    @Override
    public String getName()
    {
        return "jed";
    }

    @Override
    public int getRequiredPermissionLevel()
    {
        return 2;
    }

    @Override
    public String getUsage(ICommandSender sender)
    {
        return  "jed.commands.usage.generic";
    }

    @Override
    public List<String> getTabCompletions(MinecraftServer server, ICommandSender sender, String[] args, BlockPos pos)
    {
        if (args.length == 1)
        {
            return getListOfStringsMatchingLastWord(args,
                    "debug",
                    "defaultgamemode",
                    "delete-dimension",
                    "difficulty",
                    "dimbuilder",
                    "gamerule",
                    "list-loaded-dimensions",
                    "list-registered-dimensions",
                    "load-dimension",
                    "register",
                    "reload",
                    "reload-main-config",
                    "seed",
                    "setworldspawn",
                    "time",
                    "unload-empty-dimensions",
                    "unregister",
                    "unregister-remove",
                    "weather",
                    "worldborder"
                );
        }
        else if (args.length >= 2)
        {
            if (args[0].equals("dimbuilder"))
            {
                if (args.length == 2)
                {
                    return getListOfStringsMatchingLastWord(args,
                            "clear", "create-as", "dimtype", "list", "list-onetime", "read-from",
                            "remove", "remove-onetime", "save-as", "set", "set-onetime");
                }
                else
                {
                    return super.getTabCompletions(server, sender, args, pos);
                }
            }

            String cmd = args[0];
            args = dropFirstStrings(args, 1);

            try
            {
                parseInt(args[0]); // dimension

                // Bleh hacky check here to not mess up the tab completion ;_;
                if (cmd.equals("delete-dimension") == false)
                {
                    args = dropFirstStrings(args, 1);
                }
            }
            catch (NumberInvalidException e)
            {
                if (sender.getCommandSenderEntity() == null)
                {
                    return super.getTabCompletions(server, sender, args, pos);
                }
            }

            if (cmd.equals("weather") && args.length == 1)
            {
                return getListOfStringsMatchingLastWord(args, "clear", "rain", "thunder");
            }
            else if (cmd.equals("time"))
            {
                if (args.length == 1)
                {
                    return getListOfStringsMatchingLastWord(args, "add", "set", "query");
                }
                else if (args.length == 2 && args[0].equals("set"))
                {
                    return getListOfStringsMatchingLastWord(args, "day", "night");
                }
                else if (args.length == 2 && args[0].equals("query"))
                {
                    return getListOfStringsMatchingLastWord(args, "day", "daytime", "gametime");
                }
            }
            else if (cmd.equals("defaultgamemode") && args.length == 1)
            {
                return getListOfStringsMatchingLastWord(args, "survival", "creative", "adventure", "spectator");
            }
            else if (cmd.equals("delete-dimension") && args.length == 2)
            {
                return getListOfStringsMatchingLastWord(args, "confirm");
            }
            else if (cmd.equals("difficulty") && args.length == 1)
            {
                return getListOfStringsMatchingLastWord(args, "peaceful", "easy", "normal", "hard");
            }
            else if (cmd.equals("gamerule"))
            {
                if (args.length == 1)
                {
                    return getListOfStringsMatchingLastWord(args, this.getOverWorldGameRules(server).getRules());
                }
                else if (args.length == 2 && this.getOverWorldGameRules(server).areSameType(args[0], GameRules.ValueType.BOOLEAN_VALUE))
                {
                    return getListOfStringsMatchingLastWord(args, "true", "false");
                }
            }
            else if (cmd.equals("setworldspawn") && args.length == 1)
            {
                return getListOfStringsMatchingLastWord(args, "query");
            }
            else if (cmd.equals("unload-empty-dimensions") && args.length == 1)
            {
                return getListOfStringsMatchingLastWord(args, "unload-chunks");
            }
            else if (cmd.equals("worldborder"))
            {
                if (args.length == 1)
                {
                    return getListOfStringsMatchingLastWord(args, "set", "center", "damage", "warning", "add", "get");
                }
                else if (args[0].equals("damage") && args.length == 2)
                {
                    return getListOfStringsMatchingLastWord(args, "buffer", "amount");
                }
                else if (args[0].equals("warning") && args.length == 2)
                {
                    return getListOfStringsMatchingLastWord(args, "time", "distance");
                }
                else if (args[0].equals("center") && args.length <= 3)
                {
                    return getTabCompletionCoordinateXZ(args, 1, pos);
                }
            }
        }

        return super.getTabCompletions(server, sender, args, pos);
    }

    @Override
    public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException
    {
        if (args.length == 0)
        {
            throwUsage("generic");
        }

        String cmd = args[0];
        args = dropFirstStrings(args, 1);

        if (cmd.equals("reload"))
        {
            DimensionConfig.instance().unregisterCustomDimensions();
            DimensionConfig.instance().readDimensionConfig();
            DimensionConfig.instance().registerDimensions();
            notifyCommandListener(sender, this, "jed.commands.reloaded");
        }
        else if (cmd.equals("reload-main-config"))
        {
            if (Configs.reloadConfigsFromFile())
            {
                sender.sendMessage(new TextComponentTranslation("jed.commands.info.reload_main_config.success"));
            }
            else
            {
                throwCommand("reload_main_config.failure");
            }
        }
        else if (cmd.equals("list-registered-dimensions"))
        {
            Integer[] dims = DimensionManager.getStaticDimensionIDs();
            String[] dimsStr = new String[dims.length];
            for (int i = 0; i < dimsStr.length; i++) { dimsStr[i] = String.valueOf(dims[i]); }

            for (String line : DimensionDump.getFormattedRegisteredDimensionsDump())
            {
                JustEnoughDimensions.logger.info(line);
            }

            sender.sendMessage(new TextComponentTranslation("jed.commands.listdims.list", String.join(", ", dimsStr)));
            sender.sendMessage(new TextComponentTranslation("jed.commands.info.output.printed.to.console.full"));
        }
        else if (cmd.equals("list-loaded-dimensions"))
        {
            for (String line : DimensionDump.getFormattedLoadedDimensionsDump())
            {
                JustEnoughDimensions.logger.info(line);
            }

            sender.sendMessage(new TextComponentTranslation("jed.commands.info.output.printed.to.console"));
        }
        else if (cmd.equals("unload-empty-dimensions"))
        {
            int count = WorldUtils.unloadEmptyDimensions(args.length == 1 && args[0].equals("unload-chunks"));
            sender.sendMessage(new TextComponentTranslation("jed.commands.info.unloaded.dimensions", String.valueOf(count)));
        }
        else if (cmd.equals("load-dimension"))
        {
            if (args.length == 1)
            {
                int dimension = parseInt(args[0]);

                if (server.getWorld(dimension) != null)
                {
                    sender.sendMessage(new TextComponentTranslation("jed.commands.info.loaded.dimension", String.valueOf(dimension)));
                }
                else
                {
                    throwCommand("load_dimension_failed", String.valueOf(dimension));
                }
            }
            else
            {
                throwUsage("load_dimension");
            }
        }
        else if (cmd.equals("dimbuilder"))
        {
            this.dimBuilder(args, sender);
        }
        else if (cmd.equals("register"))
        {
            this.register(args, sender);
        }
        else if (cmd.equals("unregister"))
        {
            if (args.length == 1)
            {
                int dimension = parseInt(args[0]);

                if (this.unregister(dimension))
                {
                    notifyCommandListener(sender, this, "jed.commands.unregister", Integer.valueOf(dimension));
                }
                else
                {
                    throwCommand("dimension.cannotunregisteroverworld");
                }
            }
            else
            {
                throw new WrongUsageException("/jed unregister <dimension id>");
            }
        }
        else if (cmd.equals("unregister-remove"))
        {
            if (args.length == 1)
            {
                int dimension = parseInt(args[0]);
                this.unregister(dimension);
                DimensionConfig.instance().removeDimensionAndSaveConfig(dimension);
                notifyCommandListener(sender, this, "jed.commands.unregister.remove", Integer.valueOf(dimension));
            }
            else
            {
                throw new WrongUsageException("/jed unregister-remove <dimension id>");
            }
        }
        else if (cmd.equals("delete-dimension"))
        {
            if (args.length == 1)
            {
                int dimension = parseInt(args[0]);

                if (dimension == 0)
                {
                    throwCommand("delete_dimension.cant_delete_overworld");
                }

                String str = cmd + String.join(" ", args);
                this.commandConfirmations.put(sender, str);
                notifyCommandListener(sender, this, "jed.commands.generic.confirmation_command_cached");
                return;
            }
            else if (args.length == 2)
            {
                if (args[1].equals("confirm"))
                {
                    int dimension = parseInt(args[0]);
                    String cached = this.commandConfirmations.remove(sender);

                    if (cached != null && cached.equals(cmd + args[0]))
                    {
                        if (WorldUtils.tryDeleteDimension(dimension, sender))
                        {
                            notifyCommandListener(sender, this, "jed.commands.delete_dimension.success", Integer.valueOf(dimension));
                            return;
                        }
                        else
                        {
                            throwCommand("delete_dimension.failed", dimension);
                        }
                    }
                }
            }

            throw new WrongUsageException("/jed delete-dimension <dimension id> [confirm]");
        }
        else if (cmd.equals("broadcast"))
        {
            // NO-OP - This is a dummy command used by other mods (at least World Primer)
            // to get a notification of certain JED events, via the CommandEvent (just to avoid
            // adding an actual API and dependencies just for this...)
        }
        else if (cmd.equals("debug"))
        {
            World world = null;

            if (args.length == 1)
            {
                try
                {
                    int dim = parseInt(args[0]);
                    world = DimensionManager.getWorld(dim);
                }
                catch (Exception e) { }
            }

            if (world == null)
            {
                Entity ent = sender.getCommandSenderEntity();
                if (ent != null) { world = ent.getEntityWorld(); }
            }

            if (world != null)
            {
                IChunkProvider cp = world.getChunkProvider();
                DimensionType dimType = null;

                try
                {
                    dimType = DimensionManager.getProviderType(world.provider.getDimension());
                }
                catch (Exception e)
                {
                    dimType = world.provider.getDimensionType();
                }

                JustEnoughDimensions.logger.info("============= JED DEBUG START ==========");
                JustEnoughDimensions.logger.info("DIM: {}", world.provider.getDimension());

                String clazzName = "?";
                try
                {
                    Class <? extends WorldProvider > clazz = DimensionTypeEntry.getProviderClassFrom(dimType);
                    clazzName = clazz.getName();
                }
                catch (Exception e) {}

                JustEnoughDimensions.logger.info(String.format("DimensionType: ID: %d, name: '%s', suffix: '%s', " +
                                                  "shouldLoadSpawn: %s, WorldProvider class: '%s'",
                                                  dimType.getId(), dimType.getName(), dimType.getSuffix(), dimType.shouldLoadSpawn(), clazzName));

                JustEnoughDimensions.logger.info("DimensionType.toString(): {}", dimType.toString());
                JustEnoughDimensions.logger.info("Seed: {}", world.getWorldInfo().getSeed());
                JustEnoughDimensions.logger.info("getSpawnPoint(): {}", world.getSpawnPoint());
                JustEnoughDimensions.logger.info("getSpawnCoordinate(): {}", world.provider.getSpawnCoordinate());
                JustEnoughDimensions.logger.info("World class: {}", world.getClass().getName());
                WorldType type = world.getWorldInfo().getTerrainType();
                JustEnoughDimensions.logger.info("WorldType: '{}' (class: {})", type.getName(), type.getClass().getName());
                JustEnoughDimensions.logger.info("WorldInfo class: '{}'", world.getWorldInfo().getClass().getName());
                JustEnoughDimensions.logger.info("WorldProvider: {}", world.provider.getClass().getName());
                JustEnoughDimensions.logger.info("ChunkProvider: {}", cp.getClass().getName());
                JustEnoughDimensions.logger.info("ChunkProviderServer.chunkGenerator: {}",
                        ((cp instanceof ChunkProviderServer) ? ((ChunkProviderServer) cp).chunkGenerator.getClass().getName() : "null"));
                JustEnoughDimensions.logger.info("BiomeProvider: {}", world.getBiomeProvider().getClass().getName());

                JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(world);
                if (props != null)
                {
                    JustEnoughDimensions.logger.info("Dimension has JED properties");
                    String str = JEDJsonUtils.serialize(props.getFullJEDProperties());
                    JustEnoughDimensions.logger.info("JED properties tag: {}", str);
                }
                else
                {
                    JustEnoughDimensions.logger.info("Dimension doesn't have JED properties");
                }

                NBTTagCompound tag = world.getWorldInfo().cloneNBTCompound(new NBTTagCompound());
                JustEnoughDimensions.logger.info("Vanilla level NBT: {}", tag.toString());
                JustEnoughDimensions.logger.info("============= JED DEBUG END ==========");

                sender.sendMessage(new TextComponentTranslation("jed.commands.info.output.printed.to.console"));
            }
        }
        else
        {
            Entity entity = sender.getCommandSenderEntity();
            boolean dimensionKnown = entity != null;
            int dimension = entity != null ? entity.getEntityWorld().provider.getDimension() : 0;

            if (args.length >= 1)
            {
                try
                {
                    dimension = parseInt(args[0]);
                    args = dropFirstStrings(args, 1);
                    dimensionKnown = true;
                }
                catch (NumberInvalidException e) { }
            }

            if (dimensionKnown == false)
            {
                throwUsage("generic");
            }

            if (cmd.equals("defaultgamemode"))
            {
                CommandJEDDefaultGameType.execute(this, dimension, args, server, sender);
            }
            else if (cmd.equals("difficulty"))
            {
                CommandJEDDifficulty.execute(this, dimension, args, sender);
            }
            else if (cmd.equals("gamerule"))
            {
                CommandJEDGameRule.execute(this, dimension, args, server, sender);
            }
            else if (cmd.equals("seed"))
            {
                this.commandSeed(dimension, sender);
            }
            else if (cmd.equals("setworldspawn"))
            {
                CommandJEDSetWorldSpawn.execute(this, dimension, args, sender);
            }
            else if (cmd.equals("time"))
            {
                CommandJEDTime.execute(this, dimension, args, sender);
            }
            else if (cmd.equals("weather"))
            {
                CommandJEDWeather.execute(this, dimension, args, sender);
            }
            else if (cmd.equals("worldborder"))
            {
                CommandJEDWorldBorder.execute(this, dimension, args, server, sender);
            }
            else
            {
                throwUsage("generic");
            }
        }
    }

    private void commandSeed(int dimension, ICommandSender sender) throws CommandException
    {
        World world = DimensionManager.getWorld(dimension);

        if (world == null)
        {
            throwNumber("dimension.notloaded", Integer.valueOf(dimension));
        }

        sender.sendMessage(new TextComponentTranslation("jed.commands.seed.success", Long.valueOf(dimension), world.getWorldInfo().getSeed()));
    }

    private GameRules getOverWorldGameRules(MinecraftServer server)
    {
        return server.getWorld(0).getGameRules();
    }

    private void register(String[] args, ICommandSender sender) throws CommandException
    {
        if (args.length == 1)
        {
            int dimension = parseInt(args[0]);
            String str = DimensionConfig.instance().registerDimensionFromConfig(dimension);
            notifyCommandListener(sender, this, "jed.commands.register.from.config", Integer.valueOf(dimension), str);
        }
        else if (args.length == 2 && args[1].equals("create"))
        {
            int dimension = parseInt(args[0]);
            String str = DimensionConfig.instance().registerNewDimension(dimension);
            notifyCommandListener(sender, this, "jed.commands.register.create.simple", Integer.valueOf(dimension), str);
        }
        else if (args.length == 5 || args.length == 6)
        {
            int dimension = parseInt(args[0]);
            boolean keepLoaded = Boolean.parseBoolean(args[3]);
            boolean override = args.length == 6 ? Boolean.parseBoolean(args[5]) : false;
            String str = DimensionConfig.instance().registerNewDimension(dimension, args[1], args[2], keepLoaded, args[4], override);
            notifyCommandListener(sender, this, "jed.commands.register.custom", Integer.valueOf(dimension), str);
        }
        else
        {
            throwUsage("register");
        }
    }

    private boolean unregister(int dimension) throws CommandException
    {
        if (DimensionManager.isDimensionRegistered(dimension))
        {
            if (DimensionManager.getWorld(dimension) == null)
            {
                if (dimension != 0)
                {
                    DimensionManager.unregisterDimension(dimension);
                    return true;
                }
            }
            else
            {
                throwNumber("dimension.loaded", Integer.valueOf(dimension));
            }
        }
        else
        {
            throwNumber("dimension.notregistered", Integer.valueOf(dimension));
        }

        return false;
    }

    private void dimBuilder(String[] args, ICommandSender sender) throws CommandException
    {
        if (args.length < 1)
        {
            this.dimBuilderPrintHelp(sender);
        }
        else if (args[0].equals("dimtype"))
        {
            if (args.length == 6)
            {
                DimensionConfig.instance().dimbuilderDimtype(parseInt(args[1]), args[2], args[3], args[4], args[5]);
                notifyCommandListener(sender, this, "jed.commands.dimbuilder.dimtype.success");
            }
            else
            {
                throw new WrongUsageException("/jed dimbuilder dimtype <DimensionType id> <name> <suffix> <keeploaded true/false> <worldprovidername>");
            }
        }
        else if (args[0].equals("clear"))
        {
            if (args.length == 1)
            {
                DimensionConfig.instance().dimbuilderClear();
                notifyCommandListener(sender, this, "jed.commands.dimbuilder.clear.success");
            }
            else
            {
                throw new WrongUsageException("/jed dimbuilder clear");
            }
        }
        else if (args[0].equals("set") || args[0].equals("set-onetime"))
        {
            if (args.length >= 3)
            {
                WorldInfoType type = args[0].equals("set-onetime") ? WorldInfoType.ONE_TIME : WorldInfoType.REGULAR;
                String[] valueParts = dropFirstStrings(args, 2);
                String value = String.join(" ", valueParts);
                DimensionConfig.instance().dimbuilderSet(args[1], value, type);
                notifyCommandListener(sender, this, "jed.commands.dimbuilder.set.success", args[1], value);
            }
            else
            {
                throw new WrongUsageException("/jed dimbuilder <set | set-onetime> <key> <value>");
            }
        }
        else if (args[0].equals("remove") || args[0].equals("remove-onetime"))
        {
            if (args.length >= 2)
            {
                WorldInfoType type = args[0].equals("remove-onetime") ? WorldInfoType.ONE_TIME : WorldInfoType.REGULAR;

                for (int i = 1; i < args.length; i++)
                {
                    if (DimensionConfig.instance().dimbuilderRemove(args[i], type))
                    {
                        notifyCommandListener(sender, this, "jed.commands.dimbuilder.remove.success", args[i]);
                    }
                    else
                    {
                        throwCommand("dimbuilder.remove.fail", args[i]);
                    }
                }
            }
            else
            {
                throw new WrongUsageException("/jed dimbuilder <remove | remove-onetime> <key> [key] ...");
            }
        }
        else if (args[0].equals("list") || args[0].equals("list-onetime"))
        {
            WorldInfoType type = args[0].equals("list-onetime") ? WorldInfoType.ONE_TIME : WorldInfoType.REGULAR;

            if (args.length > 1)
            {
                for (int i = 1; i < args.length; i++)
                {
                    DimensionConfig.instance().dimbuilderList(args[i], type, sender);
                }
            }
            else
            {
                DimensionConfig.instance().dimbuilderList(null, type, sender);
            }
        }
        else if (args[0].equals("read-from"))
        {
            if (args.length == 2)
            {
                int dimension = parseInt(args[1]);
                if (DimensionConfig.instance().dimbuilderReadFrom(dimension))
                {
                    notifyCommandListener(sender, this, "jed.commands.dimbuilder.read.from.success", Integer.valueOf(dimension));
                }
                else
                {
                    notifyCommandListener(sender, this, "jed.commands.dimbuilder.read.from.fail", Integer.valueOf(dimension));
                }
            }
            else
            {
                throw new WrongUsageException("/jed dimbuilder read-from <dimension id>");
            }
        }
        else if (args[0].equals("save-as"))
        {
            if (args.length == 2)
            {
                int dimension = parseInt(args[1]);
                DimensionConfig.instance().dimbuilderSaveAs(dimension);
                notifyCommandListener(sender, this, "jed.commands.dimbuilder.save.as.success", Integer.valueOf(dimension));
            }
            else
            {
                throw new WrongUsageException("/jed dimbuilder save-as <dimension id>");
            }
        }
        else if (args[0].equals("create-as"))
        {
            if (args.length == 2)
            {
                int dimension = parseInt(args[1]);
                DimensionConfig.instance().dimbuilderCreateAs(dimension);
                notifyCommandListener(sender, this, "jed.commands.dimbuilder.create.as.success", Integer.valueOf(dimension));
            }
            else
            {
                throw new WrongUsageException("/jed dimbuilder create-as <dimension id>");
            }
        }
        else
        {
            this.dimBuilderPrintHelp(sender);
        }
    }

    private void dimBuilderPrintHelp(ICommandSender sender)
    {
        sender.sendMessage(new TextComponentString("/jed dimbuilder clear"));
        sender.sendMessage(new TextComponentString("/jed dimbuilder create-as <dim id>"));
        sender.sendMessage(new TextComponentString("/jed dimbuilder dimtype <DimensionType ID> <name> <suffix> <keeploaded> <worldprovider>"));
        sender.sendMessage(new TextComponentString("/jed dimbuilder <list | list-onetime> [key1] [key2] ..."));
        sender.sendMessage(new TextComponentString("/jed dimbuilder <remove | remove-onetime> <key>"));
        sender.sendMessage(new TextComponentString("/jed dimbuilder read-from <dim id>"));
        sender.sendMessage(new TextComponentString("/jed dimbuilder save-as <dim id>"));
        sender.sendMessage(new TextComponentString("/jed dimbuilder <set | set-onetime> <key> <value which can have spaces>"));
    }

    public static String[] dropFirstStrings(String[] input, int toDrop)
    {
        if (toDrop > input.length)
        {
            return new String[0];
        }

        String[] arr = new String[input.length - toDrop];
        System.arraycopy(input, toDrop, arr, 0, input.length - toDrop);
        return arr;
    }

    public static void throwUsage(String type, Object... params) throws CommandException
    {
        throw new WrongUsageException("jed.commands.usage." + type, params);
    }

    public static void throwNumber(String type, Object... params) throws CommandException
    {
        throw new NumberInvalidException("jed.commands.error." + type, params);
    }

    public static void throwCommand(String type, Object... params) throws CommandException
    {
        throw new CommandException("jed.commands.error." + type, params);
    }

    public static void runBroadcastCommand(ICommandSender sender, String commandType, Object... args)
    {
        String fullCommand = "jed broadcast " + commandType + " " + StringUtils.join(args, ' ');

        try
        {
            JustEnoughDimensions.logInfo("Running a broadcast command '{}'", fullCommand);
            FMLCommonHandler.instance().getMinecraftServerInstance().getCommandManager().executeCommand(sender, fullCommand);
        }
        catch (Exception e)
        {
            JustEnoughDimensions.logger.warn("Exception while executing a broadcast command '{}'", fullCommand, e);
        }
    }
} public class CommandTeleportJED extends CommandBase
{
    private MethodHandle methodHandle_Entity_copyDataFromOld;
    private static CommandTeleportJED instance;

    public CommandTeleportJED()
    {
        try
        {
            this.methodHandle_Entity_copyDataFromOld = MethodHandleUtils.getMethodHandleVirtual(
                    Entity.class, new String[] { "func_180432_n", "copyDataFromOld" }, Entity.class);
        }
        catch (UnableToFindMethodHandleException e)
        {
            JustEnoughDimensions.logger.error("CommandTeleportJED: Failed to get MethodHandle for Entity#copyDataFromOld()", e);
        }

        instance = this;
    }

    public static CommandTeleportJED instance()
    {
        return instance;
    }

    @Override
    public String getName()
    {
        return "tpj";
    }

    @Override
    public String getUsage(ICommandSender sender)
    {
        return "jed.commands.usage.tp";
    }

    @Override
    public int getRequiredPermissionLevel()
    {
        return 2;
    }

    @Override
    public boolean isUsernameIndex(String[] args, int index)
    {
        return index == 0;
    }

    @Override
    public List<String> getTabCompletions(MinecraftServer server, ICommandSender sender, String[] strArr, BlockPos pos)
    {
        if (strArr.length == 1 || strArr.length == 2)
        {
            return getListOfStringsMatchingLastWord(strArr, server.getOnlinePlayerNames());
        }

        return new ArrayList<String>();
    }

    @Override
    public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException
    {
        TeleportData data = this.parseArguments(server, sender, args);
        Entity entity = this.teleportEntityToLocation(data.getEntity(), data, server);

        if (entity != null)
        {
            notifyCommandListener(sender, this, "jed.commands.teleport.success.coordinates",
                    entity.getName(),
                    String.format("%.1f", entity.posX),
                    String.format("%.1f", entity.posY),
                    String.format("%.1f", entity.posZ),
                    Integer.valueOf(entity.getEntityWorld().provider.getDimension()));
        }
    }

    private TeleportData parseArguments(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException
    {
        if (args.length == 0)
        {
            if ((sender.getCommandSenderEntity() instanceof Entity))
            {
                int dim = sender.getCommandSenderEntity().getEntityWorld().provider.getDimension();
                sender.sendMessage(new TextComponentTranslation("jed.commands.info.current.dimension", Integer.valueOf(dim)));
            }

            CommandJED.throwUsage("tp");
        }

        // <to-entity> OR <dimensionId>
        if (args.length == 1)
        {
            Entity entityDest = null;

            try
            {
                entityDest = getEntity(server, sender, args[0]);
            }
            catch (Exception e) { }

            // Used from the console and an invalid entity selector for the first entity
            if (entityDest == null && (sender.getCommandSenderEntity() instanceof Entity) == false)
            {
                CommandJED.throwUsage("invalid.entity", args[0]);
            }

            if (entityDest != null)
            {
                return new TeleportData(sender.getCommandSenderEntity(), entityDest);
            }
            else
            {
                return new TeleportData(sender.getCommandSenderEntity(), parseInt(args[0]), true, server);
            }
        }

        // args.length >= 2 at this point
        Entity target = null;
        int dimension = 0;
        int argIndex = 0;

        try
        {
            target = getEntity(server, sender, args[argIndex++]);
        }
        catch (Exception e)
        {
            if (sender.getCommandSenderEntity() == null)
            {
                CommandJED.throwUsage("no.targetentity");
            }

            argIndex--;
            target = sender.getCommandSenderEntity();
        }

        // <entity> <to-entity>
        if (argIndex == 1 && args.length == 2)
        {
            try
            {
                Entity destEntity = getEntity(server, sender, args[1]);
                return new TeleportData(target, destEntity);
            }
            // The second argument is not an entity, but possibly the dimension
            catch (Exception e) { }
        }

        if (args.length >= (argIndex + 1))
        {
            dimension = parseInt(args[argIndex++]);
        }

        if (args.length >= (argIndex + 3))
        {
            Vec3d pos = target.getPositionVector();
            double x = parseCoordinate(pos.x, args[argIndex++], true).getResult();
            double y = parseCoordinate(pos.y, args[argIndex++], false).getResult();
            double z = parseCoordinate(pos.z, args[argIndex++], true).getResult();
            float yaw = target.rotationYaw;
            float pitch = target.rotationPitch;

            if (args.length >= (argIndex + 1))
            {
                yaw = (float) parseDouble(args[argIndex++]);
            }

            if (args.length >= (argIndex + 1))
            {
                pitch = (float) parseDouble(args[argIndex++]);
            }

            if (args.length > argIndex)
            {
                CommandJED.throwUsage("tp");
            }

            return new TeleportData(target, dimension, x, y, z, yaw, pitch);
        }
        else if (args.length > argIndex)
        {
            CommandJED.throwUsage("tp");
        }

        return new TeleportData(target, dimension, true, server);
    }

    public Entity teleportEntityToLocation(Entity entity, TeleportData data, MinecraftServer server) throws CommandException
    {
        // TODO hook up the mounted entity TP code from Ender Utilities?
        entity.dismountRidingEntity();
        entity.removePassengers();

        if (entity.getEntityWorld().provider.getDimension() != data.getDimension())
        {
            return this.teleportEntityToDimension(entity, data, server);
        }
        else
        {
            return this.teleportEntityInsideSameDimension(entity, data);
        }
    }

    private Entity teleportEntityInsideSameDimension(Entity entity, TeleportData data)
    {
        Vec3d pos = data.getPosition(entity.getEntityWorld());

        // Load the chunk first
        entity.getEntityWorld().getChunk((int) Math.floor(pos.x / 16D), (int) Math.floor(pos.z / 16D));

        entity.setLocationAndAngles(pos.x, pos.y, pos.z, data.getYaw(), data.getPitch());
        entity.setPositionAndUpdate(pos.x, pos.y, pos.z);
        return entity;
    }

    private Entity teleportEntityToDimension(Entity entity, TeleportData data, MinecraftServer server) throws CommandException
    {
        WorldServer worldDst = server.getWorld(data.getDimension());

        if (worldDst == null)
        {
            CommandJED.throwNumber("unable.to.load.world", Integer.valueOf(data.getDimension()));
        }

        Vec3d pos = data.getPosition(worldDst);
        double x = pos.x;
        double y = pos.y;
        double z = pos.z;

        // Load the chunk first
        worldDst.getChunk((int) Math.floor(x / 16D), (int) Math.floor(z / 16D));

        if (entity instanceof EntityPlayerMP)
        {
            EntityPlayerMP player = (EntityPlayerMP) entity;
            World worldOld = player.getEntityWorld();
            TeleporterJED teleporter = new TeleporterJED(worldDst, data);

            player.setLocationAndAngles(x, y, z, data.getYaw(), data.getPitch());
            server.getPlayerList().transferPlayerToDimension(player, data.getDimension(), teleporter);

            // See PlayerList#transferEntityToWorld()
            if (worldOld.provider.getDimension() == 1)
            {
                worldDst.spawnEntity(player);
            }

            // Teleporting FROM The End
            if (worldOld.provider instanceof WorldProviderEnd)
            {
                this.removeDragonBossBarHack(player, (WorldProviderEnd) worldOld.provider);
            }

            player.setPositionAndUpdate(x, y, z);
            worldDst.updateEntityWithOptionalForce(player, false);
            player.addExperience(0);
            player.setPlayerHealthUpdated();
        }
        else
        {
            WorldServer worldSrc = (WorldServer) entity.getEntityWorld();

            worldSrc.removeEntity(entity);
            entity.isDead = false;
            worldSrc.updateEntityWithOptionalForce(entity, false);

            Entity entityNew = EntityList.newEntity(entity.getClass(), worldDst);

            if (entityNew != null)
            {
                this.copyDataFromOld(entityNew, entity);
                entityNew.setLocationAndAngles(x, y, z, data.getYaw(), data.getPitch());

                boolean flag = entityNew.forceSpawn;
                entityNew.forceSpawn = true;
                worldDst.spawnEntity(entityNew);
                entityNew.forceSpawn = flag;

                worldDst.updateEntityWithOptionalForce(entityNew, false);
                entity.isDead = true;

                worldSrc.resetUpdateEntityTick();
                worldDst.resetUpdateEntityTick();
            }

            entity = entityNew;
        }

        return entity;
    }

    public static Vec3d getClampedDestinationPosition(Vec3d posIn, World worldDst)
    {
        return getClampedDestinationPosition(posIn.x, posIn.y, posIn.z, worldDst);
    }

    public static Vec3d getClampedDestinationPosition(double x, double y, double z, World worldDst)
    {
        WorldBorder border = worldDst.getWorldBorder();

        x = MathHelper.clamp(x, border.minX() + 2, border.maxX() - 2);
        z = MathHelper.clamp(z, border.minZ() + 2, border.maxZ() - 2);

        return new Vec3d(x, y, z);
    }

    private void removeDragonBossBarHack(EntityPlayerMP player, WorldProviderEnd provider)
    {
        // FIXME 1.9 - Somewhat ugly way to clear the Boss Info stuff when teleporting FROM The End
        DragonFightManager manager = provider.getDragonFightManager();

        if (manager != null)
        {
            try
            {
                BossInfoServer bossInfo = ObfuscationReflectionHelper.getPrivateValue(DragonFightManager.class, manager, "field_186109_c"); // bossInfo

                if (bossInfo != null)
                {
                    bossInfo.removePlayer(player);
                }
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.warn("tpj: Failed to get DragonFightManager#bossInfo");
            }
        }
    }

    private void copyDataFromOld(Entity target, Entity old)
    {
        try
        {
            this.methodHandle_Entity_copyDataFromOld.invokeExact(target, old);
        }
        catch (Throwable e)
        {
            JustEnoughDimensions.logger.error("Error while trying invoke Entity#copyDataFromOld()", e);
        }
    }

    public static class TeleportData
    {
        private final Entity entity;
        private int dimension;
        private double posX;
        private double posY;
        private double posZ;
        private float yaw;
        private float pitch;

        public TeleportData(Entity entity, int dimension, boolean useSpawn, MinecraftServer server)
        {
            this.entity = entity;
            this.dimension = dimension;
            this.posX = entity.posX;
            this.posY = entity.posY;
            this.posZ = entity.posZ;
            this.yaw = entity.rotationYaw;
            this.pitch = entity.rotationPitch;

            if (useSpawn)
            {
                WorldServer world = server.getWorld(dimension);

                if (world != null)
                {
                    BlockPos spawn = world.getSpawnCoordinate();

                    // If the spawn point of End type dimensions (that are using separate WorldInfo) has been moved,
                    // then use that manually set spawn point instead
                    if (spawn == null || (world.getSpawnPoint().equals(spawn) == false && world.getWorldInfo() instanceof WorldInfoJED))
                    {
                        spawn = world.getSpawnPoint();
                    }

                    if (spawn != null)
                    {
                        this.posX = spawn.getX() + 0.5;
                        this.posY = spawn.getY();
                        this.posZ = spawn.getZ() + 0.5;
                    }
                }
            }
        }

        public TeleportData(Entity entity, int dimension, double x, double y, double z)
        {
            this.entity = entity;
            this.dimension = dimension;
            this.posX = x;
            this.posY = y;
            this.posZ = z;
            this.yaw = entity.rotationYaw;
            this.pitch = entity.rotationPitch;
        }

        public TeleportData(Entity entity, int dimension, double x, double y, double z, float yaw, float pitch)
        {
            this.entity = entity;
            this.dimension = dimension;
            this.posX = x;
            this.posY = y;
            this.posZ = z;
            this.yaw = yaw;
            this.pitch = pitch;
        }

        public TeleportData(Entity entity, Entity otherEntity)
        {
            this.entity = entity;
            this.dimension = otherEntity.getEntityWorld().provider.getDimension();
            this.posX = otherEntity.posX;
            this.posY = otherEntity.posY;
            this.posZ = otherEntity.posZ;
            this.yaw = otherEntity.rotationYaw;
            this.pitch = otherEntity.rotationPitch;
        }

        public Entity getEntity() { return this.entity;    }
        public int getDimension() { return this.dimension; }
        public double getX()      { return this.posX;      }
        public double getY()      { return this.posY;      }
        public double getZ()      { return this.posZ;      }
        public float getYaw()     { return this.yaw;       }
        public float getPitch()   { return this.pitch;     }

        public Vec3d getPosition(World world)
        {
            return getClampedDestinationPosition(this.posX, this.posY, this.posZ, world);
        }

        @Override
        public String toString()
        {
            return String.format("TeleportData:{dim=%d,x=%.2f,y=%.2f,z=%.2f,yaw=%.2f,pitch=%.2f,entity=%s}\n",
                    this.dimension, this.posX, this.posY, this.posZ, this.yaw, this.pitch, this.entity.getName());
        }
    }

    private static class TeleporterJED extends Teleporter
    {
        private final WorldServer world;
        private final TeleportData data;

        public TeleporterJED(WorldServer worldIn, TeleportData data)
        {
            super(worldIn);

            this.world = worldIn;
            this.data = data;
        }

        @Override
        public boolean makePortal(Entity entityIn)
        {
            return true;
        }

        @Override
        public boolean placeInExistingPortal(Entity entityIn, float rotationYaw)
        {
            Vec3d pos = this.data.getPosition(entityIn.getEntityWorld());
            entityIn.setLocationAndAngles(pos.x, pos.y, pos.z, this.data.getYaw(), this.data.getPitch());
            return true;
        }

        @Override
        public void removeStalePortalLocations(long worldTime)
        {
            // NO-OP
        }

        @Override
        public void placeInPortal(Entity entityIn, float rotationYaw)
        {
            BlockPos spawnCoord = this.world.provider.getSpawnCoordinate();

            // For End type dimensions, generate the platform and place the entity there,
            // UNLESS the world has a different spawn point set.
            if ((this.world.provider.getDimensionType().getId() == 1 || this.world.provider instanceof WorldProviderEnd)
                 && this.world.getSpawnPoint().equals(spawnCoord))
            {
                IBlockState obsidian = Blocks.OBSIDIAN.getDefaultState();
                IBlockState air = Blocks.AIR.getDefaultState();
                int spawnX = spawnCoord.getX();
                int spawnY = spawnCoord.getY();
                int spawnZ = spawnCoord.getZ();

                for (int zOff = -2; zOff <= 2; ++zOff)
                {
                    for (int xOff = -2; xOff <= 2; ++xOff)
                    {
                        for (int yOff = -1; yOff < 3; yOff++)
                        {
                            this.world.setBlockState(new BlockPos(spawnX + xOff, spawnY + yOff, spawnZ + zOff), yOff < 0 ? obsidian : air);
                        }
                    }
                }

                entityIn.setLocationAndAngles((double)spawnX, (double)spawnY, (double)spawnZ, entityIn.rotationYaw, 0.0F);
                entityIn.motionX = 0.0D;
                entityIn.motionY = 0.0D;
                entityIn.motionZ = 0.0D;
            }
        }
    }
} public class Configs
{
    private static File configDirGlobal;
    private static File configFileGlobal;
    public static String configurationFileName;
    public static Configuration config;
    
    public static final String CATEGORY_CLIENT = "Client";
    public static final String CATEGORY_CONFIG_HANDLING = "ConfigHandling";
    public static final String CATEGORY_GENERIC = "Generic";
    public static final String CATEGORY_VERSION = "Version";

    public static boolean enableColorOverrides;
    public static boolean enableCommandRedirecting;
    public static boolean enableForcedGameModes;
    public static boolean enableInitialSpawnDimensionOverride;
    public static boolean enableLoggingInfo;
    public static boolean enableOverrideBiomeProvider;
    public static boolean enableOverrideWorldProvider;
    public static boolean enablePlayerInventoryGroups;
    public static boolean enableReplacingRegisteredDimensions;
    public static boolean enableSeparateWorldBorders;
    public static boolean enableSeparateWorldInfo;
    public static boolean enableUnregisteringDimensions;

    public static boolean copyDimensionConfigToWorld;
    public static boolean copyMainConfigToWorld;
    public static boolean usePerWorldDimensionConfig;
    public static boolean usePerWorldMainConfig;

    public static int initialSpawnDimensionId;
    public static GameType normalGameMode = GameType.SURVIVAL;

    @SubscribeEvent
    public void onConfigChangedEvent(OnConfigChangedEvent event)
    {
        if (Reference.MOD_ID.equals(event.getModID()) == true)
        {
            loadConfigs(config);
        }
    }

    public static File getConfigDir()
    {
        return configDirGlobal;
    }

    public static void loadConfigsFromMainConfigFile(File configDirCommon)
    {
        File configDir = new File(configDirCommon, Reference.MOD_ID);
        File configFile = new File(configDir, Reference.MOD_ID + ".cfg");
        configDirGlobal = configDir;
        configFileGlobal = configFile;

        ConfigFileUtils.createDirIfNotExists(configDir);

        // Automatic config moving from the old location to the new location
        File oldFile = new File(configDirCommon, "justenoughdimensions.cfg");
        ConfigFileUtils.tryCopyOrMoveConfigIfMissingOrOlder(configFile, oldFile, FileAction.MOVE, new ConfigComparatorMainConfig());

        loadConfigsFromFile(configFile);
    }

    public static void loadConfigsFromPerWorldConfigIfEnabled(@Nullable File worldDir)
    {
        if (worldDir != null)
        {
            File configDir = new File(new File(worldDir, "data"), Reference.MOD_ID);
            File configFile = new File(configDir, Reference.MOD_ID + ".cfg");

            if (copyMainConfigToWorld)
            {
                ConfigFileUtils.createDirIfNotExists(configDir);
                ConfigFileUtils.tryCopyOrMoveConfigIfMissingOrOlder(configFile, configFileGlobal, FileAction.COPY, new ConfigComparatorMainConfig());
            }

            if (usePerWorldMainConfig && configFile.exists() && configFile.isFile() && configFile.canRead())
            {
                loadConfigsFromFile(configFile);
                return;
            }
        }

        loadConfigsFromFile(configFileGlobal);
    }

    public static void loadConfigsFromGlobalConfigFile()
    {
        loadConfigsFromFile(configFileGlobal);
    }

    private static void loadConfigsFromFile(File configFile)
    {
        configurationFileName = configFile.toString();
        config = new Configuration(configFile, null, true);

        reloadConfigsFromFile();
    }

    public static class ConfigComparatorMainConfig extends ConfigComparator
    {
        @Override
        public boolean shouldReplace(File fileToReplace, File replacementFile)
        {
            if (fileToReplace.exists() == false)
            {
                return true;
            }

            if ((fileToReplace.exists() && fileToReplace.isFile() && fileToReplace.canRead() &&
                replacementFile.exists() && replacementFile.isFile() && replacementFile.canRead()))
            {
                Configuration confOld = new Configuration(fileToReplace, null, true);
                Configuration confNew = new Configuration(replacementFile, null, true);

                if (confOld.hasKey(CATEGORY_VERSION, "configId") && confNew.hasKey(CATEGORY_VERSION, "configId"))
                {
                    String idOld = confOld.get(CATEGORY_VERSION, "configId", "").getString();
                    String idNew = confNew.get(CATEGORY_VERSION, "configId", "").getString();
                    int versionOld = confOld.get(CATEGORY_VERSION, "version", 0).getInt();
                    int versionNew = confNew.get(CATEGORY_VERSION, "version", 0).getInt();

                    return idOld.equals(idNew) && versionNew > versionOld;
                }
            }

            return false;
        }
    }

    public static boolean reloadConfigsFromFile()
    {
        if (config != null)
        {
            JustEnoughDimensions.logger.info("Reloading the main configs from file '{}'", config.getConfigFile().getAbsolutePath());
            config.load();
            loadConfigs(config);

            return true;
        }

        return false;
    }

    public static void loadConfigs(Configuration conf)
    {
        Property prop;

        prop = conf.get(CATEGORY_CONFIG_HANDLING, "copyDimensionConfigToWorld", true).setRequiresWorldRestart(true).setRequiresMcRestart(false);
        prop.setComment("If enabled, then the dimension config ('dimensions.json')\n" +
                        "will be copied to each save and used from there, to avoid changes in the\n" +
                        "global/pack config from breaking the saves due to possibly differing settings between worlds.\n" +
                        "NOTE: This option ONLY affects whether or not the config is automatically _copied to_ each world.");
        copyDimensionConfigToWorld = prop.getBoolean();

        prop = conf.get(CATEGORY_CONFIG_HANDLING, "copyMainConfigToWorld", true).setRequiresWorldRestart(true).setRequiresMcRestart(false);
        prop.setComment("If enabled, then the \"main config\" ('justenoughdimensions.cfg')\n" +
                        "will be copied to each save and used from there, to avoid changes in the\n" +
                        "global/pack config from breaking the saves due to possibly differing settings between worlds.\n" +
                        "NOTE: This option ONLY affects whether or not the config is automatically _copied to_ each world.");
        copyMainConfigToWorld = prop.getBoolean();

        prop = conf.get(CATEGORY_CONFIG_HANDLING, "usePerWorldDimensionConfig", false).setRequiresWorldRestart(true).setRequiresMcRestart(false);
        prop.setComment("If enabled, then the dimension config ('dimensions.json')\n" +
                        "will read from within each world/save, if it exists there.\n" +
                        "Also see the option 'copyDimensionConfigToWorld' to enable automatically copying it there.");
        usePerWorldDimensionConfig = prop.getBoolean();

        prop = conf.get(CATEGORY_CONFIG_HANDLING, "usePerWorldMainConfig", false).setRequiresWorldRestart(true).setRequiresMcRestart(false);
        prop.setComment("If enabled, then the \"main config\" ('justenoughdimensions.cfg')\n" +
                        "will read from within each world/save, if it exists there.\n" +
                        "Also see the option 'copyMainConfigToWorld' to enable automatically copying it there.");
        usePerWorldMainConfig = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableCommandRedirecting", true).setRequiresMcRestart(false);
        prop.setComment("Enables redirecting the vanilla /time, /weather etc. commands to the JED variants in WorldInfo-overridden dimensions");
        enableCommandRedirecting = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableForcedGameModes", false).setRequiresMcRestart(false);
        prop.setComment("Enables switching players' gamemode when they enter a dimension which has the ForceGameMode option set to true");
        enableForcedGameModes = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableInitialSpawnDimensionOverride", false).setRequiresMcRestart(false);
        prop.setComment("Enables overriding the initial spawning dimension to something other than dim 0 (overworld).");
        enableInitialSpawnDimensionOverride = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableLoggingInfo", false).setRequiresMcRestart(false);
        prop.setComment("Enables a bunch of extra logging on the INFO level for registrations etc.");
        enableLoggingInfo = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableOverrideBiomeProvider", true).setRequiresMcRestart(false);
        prop.setComment("If enabled, then a '\"biome\": \"registrynameofbiome\"' value in the dimensions.json config will override the\n" +
                        "BiomeProvider of that dimension with BiomeProviderSingle, using the biome given as the value.\n" +
                        "This means that the entire dimension will use only that one biome set in the config.\n" +
                        "To get the registry names of biomes, you can use the TellMe mod (the command '/tellme dump biomes').");
        enableOverrideBiomeProvider = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableOverrideWorldProvider", false);
        prop.setComment("If enabled, then a '\"WorldProviderOverride\": \"class name\"' value in JED options in the dimensions.json config\n" +
                        "will override the WorldProvider of that dimension with one created by the provided class name.\n" +
                        "NOTE: This is normally not recommended!!!\n" +
                        "This is provided for certain mod compatibility cases,\n" +
                        "where the other mod is checking the DimensionType against one of the vanilla values, and thus\n" +
                        "you have to use an \"existing_dimensiontype\" key in the \"dimensiontype\" object, instead of\n" +
                        "registering/defining a new custom entry as you would do normally.");
        enableOverrideWorldProvider = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enablePlayerInventoryGroups", false).setRequiresMcRestart(false);
        prop.setComment("Enables \"player inventory groups\".\n" +
                        "These groups can be used to separate the player inventories in some dimensions\n" +
                        "or groups of dimensions. Whenever a player switches dimensions, if the groups\n" +
                        "in the from and to dimensions are different, then the player inventories will be\n" +
                        "stored and swapped. The groups can be set in \"jed\": { \"PlayerInventoryGroup\": \"FooBar\" }.\n" +
                        "You can use an empty string value to automatically set it to \"dim_<id>\".\n" +
                        "The default group name for dimensions that don't define anything for it is \"__default\".");
        enablePlayerInventoryGroups = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableReplacingRegisteredDimensions", true).setRequiresMcRestart(false);
        prop.setComment("If enabled, then an 'override: true' boolean value for the dimension in\n" +
                        "the dimensions.json config can be used to override an existing dimension.");
        enableReplacingRegisteredDimensions = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableUnregisteringDimensions", false).setRequiresMcRestart(false);
        prop.setComment("If enabled, then an 'unregister: true' boolean value for the dimension in\n" +
                        "the dimensions.json config can be used to unregister existing dimension.");
        enableUnregisteringDimensions = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableSeparateWorldBorders", false).setRequiresWorldRestart(true).setRequiresMcRestart(false);
        prop.setComment("If enabled, adds custom WorldBorder syncing and removes default linking from other dimensions to the overworld border.");
        enableSeparateWorldBorders = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "enableSeparateWorldInfo", true).setRequiresWorldRestart(true).setRequiresMcRestart(false);
        prop.setComment("If enabled, all dimensions that exist in dimensions.json and have either a" +
                        "\"worldinfo\" or a \"worldinfo_onetime\" value present (an empty object is enough),\n" +
                        "will use separate WorldInfo instances (separate time, weather, world border, gamerules etc.).\n" +
                        "This works even if the dimension in question isn't registered by this mod\n" +
                        "(so vanilla, or other mod dimensions can have it too).");
        enableSeparateWorldInfo = prop.getBoolean();

        prop = conf.get(CATEGORY_GENERIC, "initialSpawnDimensionId", 0).setRequiresWorldRestart(true).setRequiresMcRestart(false);
        prop.setComment("If enabled with the enableInitialSpawnDimensionOverride option, this will be used as the initial spawn dimension ID");
        initialSpawnDimensionId = prop.getInt();

        prop = conf.get(CATEGORY_GENERIC, "normalGameMode", "survival");
        prop.setComment("If the players first join into a ForceGameMode dimension, then this option sets the \"normal\"\n" +
                        "game mode they should get when they leave that dimension for a non-ForceGameMode dimension.\n" +
                        "Valid values are: adventure, creative, spectator and survival");
        setNormalGameMode(prop.getString());

        prop = conf.get(CATEGORY_VERSION, "configId", "__default").setRequiresMcRestart(false);
        prop.setComment("For the config file copying/replacement to happen, this id\n" +
                        "in the old per-world config must match the id in the current global/common config,\n" +
                        "for the automatic config upgrade/override from the global config to the per-world config to happen.");

        prop = conf.get(CATEGORY_VERSION, "version", 0).setRequiresMcRestart(false);
        prop.setComment("Config version tracking.\nIf you are a mod pack developer and need to force a config change\n" +
                        "for users (when using the per-world configs option), increase the version number here\n" +
                        "to tell the mod to copy this config version over an existing, older, per-world config.\n" +
                        "Also note that the 'configId' value should be set to something mod pack specific,\n" +
                        "basically to tie the per-world configs to the pack in question,\n" +
                        "and not overwrite the per-world config with one from a different pack.\n" +
                        "(Although it probably would be extremely rare for anyone to try to load the same world in a different pack...)");

        // Client stuff

        prop = conf.get(CATEGORY_CLIENT, "enableColorOverrides", true).setRequiresMcRestart(false);
        prop.setComment("Enables the Grass/Foliage/Water color customizations. This controls whether or not the event handlers get registered.");
        enableColorOverrides = prop.getBoolean();

        if (enableColorOverrides)
        {
            JustEnoughDimensions.proxy.registerClientEventHandler();
        }
        else
        {
            JustEnoughDimensions.proxy.unregisterClientEventHandler();
        }

        if (conf.hasChanged())
        {
            conf.save();
        }
    }

    private static void setNormalGameMode(String name)
    {
        if (name.equalsIgnoreCase("adventure"))
        {
            normalGameMode = GameType.ADVENTURE;
        }
        else if (name.equalsIgnoreCase("creative"))
        {
            normalGameMode = GameType.CREATIVE;
        }
        else if (name.equalsIgnoreCase("spectator"))
        {
            normalGameMode = GameType.SPECTATOR;
        }
        else if (name.equalsIgnoreCase("survival"))
        {
            normalGameMode = GameType.SURVIVAL;
        }
        else
        {
            JustEnoughDimensions.logger.warn("Configs: Invalid normalGameMode value '{}', falling back to 'survival'", name);
            normalGameMode = GameType.SURVIVAL;
        }
    }
} public class DimensionConfig
{
    private static DimensionConfig instance;
    private final File configDirJED;
    private final File dimensionConfigFileGlobal;
    private final Set<Integer> registeredDimensions = new HashSet<Integer>();
    private final Map<Integer, DimensionConfigEntry> dimensions = new HashMap<Integer, DimensionConfigEntry>();
    private final Map<Integer, NBTTagCompound> customWorldInfo = new HashMap<Integer, NBTTagCompound>();
    private final Map<Integer, NBTTagCompound> onetimeWorldInfo = new HashMap<Integer, NBTTagCompound>();
    private final Map<String, Integer> worldInfoKeys = new HashMap<String, Integer>();
    private final Map<String, Integer> jedKeys = new HashMap<String, Integer>();
    private final Map<String, Integer> jedKeysListTypes = new HashMap<String, Integer>();
    private JsonObject dimBuilderData = new JsonObject();
    private File currentDimensionConfigFile;
    private String currentConfigId = "__default";
    private int currentConfigVersion;

    private DimensionConfig(File configDirCommon)
    {
        instance = this;

        this.configDirJED = new File(configDirCommon, Reference.MOD_ID);
        this.dimensionConfigFileGlobal = new File(this.configDirJED, "dimensions.json");

        this.initWorldInfoKeys();
    }

    public static DimensionConfig init(File configDirCommon)
    {
        return new DimensionConfig(configDirCommon);
    }

    public static DimensionConfig instance()
    {
        return instance;
    }

    public File getGlobalJEDConfigDir()
    {
        return this.configDirJED;
    }

    public Collection<DimensionConfigEntry> getRegisteredDimensions()
    {
        List<DimensionConfigEntry> list = new ArrayList<>();

        for (int dim : this.registeredDimensions)
        {
            DimensionConfigEntry entry = this.dimensions.get(dim);

            if (entry != null)
            {
                list.add(entry);
            }
        }

        return list;
    }

    @Nullable
    public DimensionConfigEntry getDimensionConfigFor(int dimension)
    {
        return this.dimensions.get(dimension);
    }

    private void initWorldInfoKeys()
    {
        // These are the keys/values in a vanilla level.dat
        this.worldInfoKeys.put("RandomSeed",           Constants.NBT.TAG_LONG);
        this.worldInfoKeys.put("generatorName",        Constants.NBT.TAG_STRING);
        this.worldInfoKeys.put("generatorVersion",     Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("generatorOptions",     Constants.NBT.TAG_STRING);
        this.worldInfoKeys.put("GameType",             Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("MapFeatures",          Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("SpawnX",               Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("SpawnY",               Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("SpawnZ",               Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("Time",                 Constants.NBT.TAG_LONG);
        this.worldInfoKeys.put("DayTime",              Constants.NBT.TAG_LONG);
        this.worldInfoKeys.put("LastPlayed",           Constants.NBT.TAG_LONG);
        this.worldInfoKeys.put("SizeOnDisk",           Constants.NBT.TAG_LONG);
        this.worldInfoKeys.put("LevelName",            Constants.NBT.TAG_STRING);
        this.worldInfoKeys.put("version",              Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("clearWeatherTime",     Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("rainTime",             Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("raining",              Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("thunderTime",          Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("thundering",           Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("hardcore",             Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("initialized",          Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("allowCommands",        Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("Difficulty",           Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("DifficultyLocked",     Constants.NBT.TAG_BYTE);
        this.worldInfoKeys.put("BorderCenterX",        Constants.NBT.TAG_DOUBLE);
        this.worldInfoKeys.put("BorderCenterZ",        Constants.NBT.TAG_DOUBLE);
        this.worldInfoKeys.put("BorderSize",           Constants.NBT.TAG_DOUBLE);
        this.worldInfoKeys.put("BorderSizeLerpTime",   Constants.NBT.TAG_LONG);
        this.worldInfoKeys.put("BorderSizeLerpTarget", Constants.NBT.TAG_DOUBLE);
        this.worldInfoKeys.put("BorderSafeZone",       Constants.NBT.TAG_DOUBLE);
        this.worldInfoKeys.put("BorderDamagePerBlock", Constants.NBT.TAG_DOUBLE);
        this.worldInfoKeys.put("BorderWarningBlocks",  Constants.NBT.TAG_INT);
        this.worldInfoKeys.put("BorderWarningTime",    Constants.NBT.TAG_INT);

        // Custom JED properties
        this.jedKeys.put("CanRespawnHere",      Constants.NBT.TAG_BYTE);
        this.jedKeys.put("CloudColor",          Constants.NBT.TAG_STRING);
        this.jedKeys.put("CloudHeight",         Constants.NBT.TAG_INT);
        this.jedKeys.put("Colors",              Constants.NBT.TAG_LIST);
        this.jedKeys.put("CustomDayCycle",      Constants.NBT.TAG_BYTE);
        this.jedKeys.put("DayLength",           Constants.NBT.TAG_INT);
        this.jedKeys.put("FogColor",            Constants.NBT.TAG_STRING);
        this.jedKeys.put("ForceGameMode",       Constants.NBT.TAG_BYTE);
        this.jedKeys.put("LightBrightness",     Constants.NBT.TAG_LIST);
        this.jedKeys.put("NightLength",         Constants.NBT.TAG_INT);
        this.jedKeys.put("RespawnDimension",    Constants.NBT.TAG_INT);
        this.jedKeys.put("SkyColor",            Constants.NBT.TAG_STRING);
        this.jedKeys.put("SkyDisableFlags",     Constants.NBT.TAG_BYTE);
        this.jedKeys.put("SkyRenderer",         Constants.NBT.TAG_STRING);
        this.jedKeys.put("SkyRenderType",       Constants.NBT.TAG_BYTE);

        this.jedKeysListTypes.put("Colors",             Constants.NBT.TAG_COMPOUND);
        this.jedKeysListTypes.put("LightBrightness",    Constants.NBT.TAG_FLOAT);
    }

    public boolean useCustomWorldInfoFor(int dimension)
    {
        return this.customWorldInfo.containsKey(dimension) || this.onetimeWorldInfo.containsKey(dimension);
    }

    public void setWorldInfoValues(int dimension, NBTTagCompound tagIn, WorldInfoType type)
    {
        Map<Integer, NBTTagCompound> map = type == WorldInfoType.ONE_TIME ? this.onetimeWorldInfo : this.customWorldInfo;
        NBTTagCompound dimNBT = map.get(dimension);

        if (dimNBT != null)
        {
            // Support randomized seeds that get randomized every time the values are applied
            if (dimNBT.hasKey("RandomSeed", Constants.NBT.TAG_STRING))
            {
                dimNBT = dimNBT.copy();
                dimNBT.removeTag("RandomSeed");
                dimNBT.setLong("RandomSeed", JustEnoughDimensions.RAND.nextLong());
            }

            tagIn.merge(dimNBT);
        }
    }

    /**
     * Only call this method after the overworld has been loaded
     */
    public void readDimensionConfig()
    {
        this.readDimensionConfig(DimensionManager.getCurrentSaveRootDirectory());
    }

    public void readDimensionConfig(File worldDir)
    {
        if (worldDir != null)
        {
            File configDir = new File(new File(worldDir, "data"), Reference.MOD_ID);
            File configFile = new File(configDir, "dimensions.json");

            if (Configs.copyDimensionConfigToWorld)
            {
                ConfigFileUtils.createDirIfNotExists(configDir);
                File configGlobal = this.dimensionConfigFileGlobal;

                if (configGlobal.exists() && configGlobal.isFile() && configGlobal.canRead())
                {
                    ConfigFileUtils.tryCopyOrMoveConfigIfMissingOrOlder(configFile, configGlobal,
                            FileAction.COPY, new ConfigComparatorDimensionConfig());
                }
            }

            if (Configs.usePerWorldDimensionConfig && configFile.exists() && configFile.isFile() && configFile.canRead())
            {
                this.currentDimensionConfigFile = configFile;
                this.readDimensionConfigFromFile(this.currentDimensionConfigFile);
                return;
            }
        }

        this.currentDimensionConfigFile = this.dimensionConfigFileGlobal;
        this.readDimensionConfigFromFile(this.currentDimensionConfigFile);
    }

    private void readDimensionConfigFromFile(File configFile)
    {
        this.customWorldInfo.clear();
        this.onetimeWorldInfo.clear();
        this.dimensions.clear();
        this.currentConfigId = "__default";
        this.currentConfigVersion = 0;
        JEDWorldProperties.clearWorldProperties();

        if (configFile != null && configFile.exists() && configFile.isFile() && configFile.canRead())
        {
            String fileName = configFile.getAbsolutePath();
            JsonElement rootElement = JEDJsonUtils.parseJsonFile(configFile);

            if (rootElement != null)
            {
                JustEnoughDimensions.logInfo("Reading the dimension config from file '{}'", fileName);
                this.parseDimensionConfig(rootElement);
            }
            else
            {
                JustEnoughDimensions.logger.warn("The dimension config in file '{}' was empty or invalid", fileName);
            }
        }
        else
        {
            JustEnoughDimensions.logger.warn("No 'dimensions.json' file found; neither global nor per-world");
        }

        this.restoreMissingVanillaDimensions();
    }

    private void restoreMissingVanillaDimensions()
    {
        for (int dim = -1; dim < 2; dim++)
        {
            // Not currently in the JED configs, and not registered,
            // assume they have been unregistered by JED in a per-world config for another world (in single player)
            if (this.dimensions.containsKey(dim) == false &&
                DimensionManager.isDimensionRegistered(dim) == false)
            {
                JustEnoughDimensions.logInfo("Dimension {} was not registered, and not found in the current JED config. " +
                                             "Registering the normal vanilla dimension for it.", dim);
                DimensionManager.registerDimension(dim, this.getVanillaDimensionType(dim));
            }
        }
    }

    private DimensionType getVanillaDimensionType(int dim)
    {
        switch (dim)
        {
            case  1: return DimensionType.THE_END;
            case -1: return DimensionType.NETHER;
            default: return DimensionType.OVERWORLD;
        }
    }

    public void registerDimensions()
    {
        for (DimensionConfigEntry entry : this.dimensions.values())
        {
            this.registerDimension(entry.getDimension(), entry);
        }

        PacketHandler.INSTANCE.sendToAll(new MessageSyncDimensions(this.getRegisteredDimensions()));
    }

    public void registerNonOverrideDimensions()
    {
        for (DimensionConfigEntry entry : this.dimensions.values())
        {
            if (DimensionManager.isDimensionRegistered(entry.getDimension()) == false)
            {
                this.registerDimension(entry.getDimension(), entry);
            }
        }
    }

    public void doEarlyDimensionRegistrations()
    {
        for (DimensionConfigEntry entry : this.dimensions.values())
        {
            if (DimensionManager.isDimensionRegistered(entry.getDimension()))
            {
                if (Configs.enableUnregisteringDimensions && entry.getUnregister())
                {
                    JustEnoughDimensions.logInfo("Unregistering dimension {}...", entry.getDimension());
                    DimensionManager.unregisterDimension(entry.getDimension());
                }
                else if (Configs.enableReplacingRegisteredDimensions && entry.getOverride())
                {
                    this.registerDimension(entry.getDimension(), entry);
                }
            }
            else if (entry.getShouldLoadOnStart())
            {
                this.registerDimension(entry.getDimension(), entry);
            }
        }
    }

    private boolean registerDimension(int dimension, DimensionConfigEntry entry)
    {
        if (entry.getUnregister() || entry.hasDimensionTypeEntry() == false)
        {
            return false;
        }

        if (DimensionManager.isDimensionRegistered(dimension) == false)
        {
            JustEnoughDimensions.logInfo("Registering a dimension with ID {}", dimension);
            DimensionType type = entry.getDimensionTypeEntry().getOrRegisterDimensionType(dimension);
            DimensionManager.registerDimension(dimension, type);
            this.registeredDimensions.add(dimension);
            return true;
        }
        else if (Configs.enableReplacingRegisteredDimensions && entry.getOverride())
        {
            if (DimensionManager.getWorld(dimension) == null)
            {
                JustEnoughDimensions.logInfo("Overriding dimension {}", dimension);
                DimensionManager.unregisterDimension(dimension);
                DimensionManager.registerDimension(dimension, entry.getDimensionTypeEntry().getOrRegisterDimensionType(dimension));
                this.registeredDimensions.add(dimension);
                return true;
            }
            else
            {
                JustEnoughDimensions.logger.warn("Dimension {} is already registered and currently loaded, can't override it", dimension);
            }
        }
        else
        {
            JustEnoughDimensions.logger.warn("Dimension {} is already registered, skipping it", dimension);
        }

        return false;
    }

    public String registerDimensionFromConfig(int dimension) throws CommandException
    {
        DimensionConfigEntry entry = this.dimensions.get(dimension);

        if (entry != null)
        {
            if (entry.getUnregister())
            {
                CommandJED.throwNumber("register.from.config.unregister.set", Integer.valueOf(dimension));
            }

            if (this.registerDimension(dimension, entry))
            {
                PacketHandler.INSTANCE.sendToAll(new MessageSyncDimensions(this.getRegisteredDimensions()));
                return entry.getDescription();
            }
            else
            {
                CommandJED.throwNumber("register.from.config", Integer.valueOf(dimension));
            }
        }

        CommandJED.throwNumber("register.not.in.config", Integer.valueOf(dimension));
        return "";
    }

    public String registerNewDimension(int dimension) throws CommandException
    {
        DimensionConfigEntry entry = new DimensionConfigEntry(dimension);
        entry.setDimensionTypeEntry(this.createDefaultDimensionTypeEntry(dimension));
        return this.registerNewDimension(dimension, entry);
    }

    private String registerNewDimension(int dimension, DimensionConfigEntry entry) throws CommandException
    {
        boolean success = this.registerDimension(dimension, entry);

        if (success == false)
        {
            CommandJED.throwNumber("dimension.already.registered", Integer.valueOf(dimension));
        }

        this.dimensions.put(dimension, entry);
        this.saveConfig();
        PacketHandler.INSTANCE.sendToAll(new MessageSyncDimensions(this.getRegisteredDimensions()));

        return entry.getDescription();
    }

    public String registerNewDimension(int dimension, String name, String suffix, boolean keepLoaded,
            String providerClassName, boolean override) throws CommandException
    {
        Class<? extends WorldProvider> providerClass = DimensionTypeEntry.getProviderClass(providerClassName);

        if (providerClass == null)
        {
            CommandJED.throwCommand("invalid.worldprovider.name", providerClassName);
        }

        DimensionConfigEntry entry = new DimensionConfigEntry(dimension);
        entry.setDimensionTypeEntry(new DimensionTypeEntry(dimension, name, suffix, keepLoaded, providerClass));
        entry.setOverride(override);

        return this.registerNewDimension(dimension, entry);
    }

    public void unregisterCustomDimensions()
    {
        Iterator<Integer> iter = this.registeredDimensions.iterator();

        while (iter.hasNext())
        {
            int dimension = iter.next();

            if (dimension != 0 && DimensionManager.isDimensionRegistered(dimension) && DimensionManager.getWorld(dimension) == null)
            {
                JustEnoughDimensions.logInfo("Unregistering dimension {}", dimension);
                DimensionManager.unregisterDimension(dimension);
                iter.remove();
            }
        }
    }

    public void removeDimensionAndSaveConfig(int dimension)
    {
        this.removeDimension(dimension);
        this.saveConfig();
    }

    private void removeDimension(int dimension)
    {
        this.dimensions.remove(dimension);
        this.customWorldInfo.remove(dimension);
        this.onetimeWorldInfo.remove(dimension);
        JEDWorldProperties.removePropertiesFrom(dimension);
    }

    private void saveConfig()
    {
        List<DimensionConfigEntry> dims = new ArrayList<>(this.dimensions.values());
        Collections.sort(dims);

        JsonObject root = new JsonObject();
        JsonArray array = new JsonArray();

        for (DimensionConfigEntry dimEntry : dims)
        {
            array.add(dimEntry.toJson());
        }

        JsonObject objVersion = new JsonObject();
        objVersion.add("id", new JsonPrimitive(this.currentConfigId));
        objVersion.add("version", new JsonPrimitive(this.currentConfigVersion));

        root.add("config_version", objVersion);
        root.add("dimensions", array);

        try
        {
            FileWriter writer = new FileWriter(this.currentDimensionConfigFile);
            writer.write(JEDJsonUtils.GSON_PRETTY.toJson(root));
            writer.close();
        }
        catch (IOException e)
        {
            JustEnoughDimensions.logger.warn("Failed to write dimension config to file '{}'",
                    this.currentDimensionConfigFile.getAbsolutePath(), e);
        }
    }

    @Nullable
    private static Pair<String, Integer> getConfigVersion(JsonElement rootElement)
    {
        if (rootElement != null && rootElement.isJsonObject())
        {
            JsonObject root = rootElement.getAsJsonObject();
            JsonObject objVersion = JEDJsonUtils.getNestedObject(root, "config_version", false);

            if (objVersion != null)
            {
                String id = JEDJsonUtils.getStringOrDefault(objVersion, "id", "__default", true);
                int ver = JEDJsonUtils.getIntegerOrDefault(objVersion, "version", 0);

                return Pair.of(id, ver);
            }
        }

        return null;
    }

    private void parseDimensionConfig(JsonElement rootElement) throws IllegalStateException
    {
        if (rootElement == null || rootElement.isJsonObject() == false)
        {
            JustEnoughDimensions.logger.warn("The dimension config is missing the root object!");
            return;
        }

        JsonObject root = rootElement.getAsJsonObject();
        Pair<String, Integer> version = getConfigVersion(rootElement);

        if (version != null)
        {
            this.currentConfigId = version.getLeft();
            this.currentConfigVersion = version.getRight();
        }

        if (root.has("dimensions") && root.get("dimensions").isJsonArray())
        {
            JsonArray array = rootElement.getAsJsonObject().get("dimensions").getAsJsonArray();
            int count = 0;

            for (JsonElement el : array)
            {
                if (el.isJsonObject())
                {
                    JsonObject object = el.getAsJsonObject();

                    if (object.has("dim") && object.get("dim").isJsonPrimitive())
                    {
                        this.parseDimensionConfigEntry(object.get("dim").getAsInt(), object);
                        count++;
                    }
                }
            }

            JustEnoughDimensions.logInfo("Read {} dimension entries from the config", count);
        }
    }

    private void parseDimensionConfigEntry(int dimension, final JsonObject object)
    {
        DimensionConfigEntry configEntry = DimensionConfigEntry.fromJson(dimension, object);

        if (configEntry.getWorldInfoJson() != null)
        {
            this.customWorldInfo.put(dimension, this.parseAndGetCustomWorldInfoValues(dimension, configEntry.getWorldInfoJson()));
        }

        if (configEntry.getOneTimeWorldInfoJson() != null)
        {
            this.onetimeWorldInfo.put(dimension, this.parseAndGetCustomWorldInfoValues(dimension, configEntry.getOneTimeWorldInfoJson()));
        }

        this.dimensions.put(dimension, configEntry);
    }

    private DimensionTypeEntry createDefaultDimensionTypeEntry(int dimension)
    {
        DimensionTypeEntry dte = new DimensionTypeEntry(dimension, "DIM" + dimension, "dim_" + dimension, false, WorldProviderSurfaceJED.class);
        JustEnoughDimensions.logInfo("Created a default DimensionTypeEntry for dimension {}: {}", dimension, dte.getDescription());
        return dte;
    }

    public void dimbuilderClear()
    {
        this.dimBuilderData = new JsonObject();
    }

    public void dimbuilderDimtype(int id, String name, String suffix, String keepLoaded, String worldProvider)
    {
        JsonObject dimType = JEDJsonUtils.getOrCreateNestedObject(this.dimBuilderData, "dimensiontype");
        dimType.add("id", new JsonPrimitive(id));
        dimType.add("name", new JsonPrimitive(name));
        dimType.add("suffix", new JsonPrimitive(suffix));
        dimType.add("keeploaded", new JsonPrimitive(keepLoaded));
        dimType.add("worldprovider", new JsonPrimitive(worldProvider));
    }

    public void dimbuilderSet(String key, String value, WorldInfoType type)
    {
        JsonObject obj = this.dimBuilderData;

        if (key.equals("override") || key.equals("unregister") || key.equals("biome") ||
            key.equals("disable_teleporting_from") || key.equals("disable_teleporting_to"))
        {
            obj.add(key, new JsonPrimitive(value));
        }
        else if (key.equals("id") || key.equals("name") || key.equals("suffix") || key.equals("keeploaded") ||
                 key.equals("worldprovider") || key.equals("existing_dimensiontype"))
        {
            JEDJsonUtils.getOrCreateNestedObject(obj, "dimensiontype").add(key, new JsonPrimitive(value));
        }
        else if (key.equals("worldinfo") || key.equals("worldinfo_onetime"))
        {
            JEDJsonUtils.getOrCreateNestedObject(obj, type.getKeyName());
        }
        else
        {
            if (this.isJEDProperty(key))
            {
                obj = JEDJsonUtils.getOrCreateNestedObject(obj, "jed");
            }
            else
            {
                obj = JEDJsonUtils.getOrCreateNestedObject(obj, type.getKeyName());

                // Not a JED property and not a (direct) vanilla level.dat key, so let's assume it's a GameRule then
                if (this.worldInfoKeys.get(key) == null)
                {
                    obj = JEDJsonUtils.getOrCreateNestedObject(obj, "GameRules");
                }
            }

            obj.add(key, new JsonPrimitive(value));
        }
    }

    public boolean dimbuilderRemove(String key, WorldInfoType type)
    {
        JsonObject obj = this.dimBuilderData;

        if (key.equals("override") ||
            key.equals("unregister") ||
            key.equals("biome") ||
            key.equals("disable_teleporting_from") ||
            key.equals("disable_teleporting_to") ||
            key.equals("jed") ||
            key.equals("worldinfo") ||
            key.equals("worldinfo_onetime"))
        {
            return obj.remove(key) != null;
        }
        else if (key.equals("id") ||
                 key.equals("name") ||
                 key.equals("suffix") ||
                 key.equals("keeploaded") ||
                 key.equals("worldprovider") ||
                 key.equals("existing_dimensiontype"))
        {
            obj = JEDJsonUtils.getNestedObject(obj, "dimensiontype", false);
            return obj != null ? obj.remove(key) != null : false;
        }
        else
        {
            if (this.isJEDProperty(key))
            {
                obj = JEDJsonUtils.getNestedObject(obj, "jed", false);
                boolean success = obj != null ? obj.remove(key) != null : false;

                if (obj != null && obj.size() == 0)
                {
                    this.dimBuilderData.remove("jed");
                }

                return success;
            }
            else
            {
                obj = JEDJsonUtils.getNestedObject(obj, type.getKeyName(), false);

                if (obj != null)
                {
                    // vanilla level.dat properties
                    if (obj.has(key))
                    {
                        return obj.remove(key) != null;
                    }
                    // GameRules
                    else
                    {
                        obj = JEDJsonUtils.getNestedObject(obj, "GameRules", false);
                        return obj != null && obj.remove(key) != null;
                    }
                }
            }

            return false;
        }
    }

    public void dimbuilderList(@Nullable String key, WorldInfoType type, ICommandSender sender) throws CommandException
    {
        if (key != null)
        {
            JsonPrimitive prim = this.getDimbuilderPrimitive(key, type);

            if (prim != null)
            {
                sender.sendMessage(new TextComponentString(key + " = " + prim.getAsString()));
            }
            else
            {
                CommandJED.throwCommand("dimbuilder.list", key);
            }
        }
        else
        {
            JustEnoughDimensions.logger.info("==== Dim Builder list start ====");
            JustEnoughDimensions.logger.info("\n" + JEDJsonUtils.GSON_PRETTY.toJson(this.dimBuilderData));
            JustEnoughDimensions.logger.info("==== Dim Builder list end ====");
            sender.sendMessage(new TextComponentTranslation("jed.commands.info.output.printed.to.console"));
        }
    }

    public boolean dimbuilderReadFrom(int dimension)
    {
        DimensionConfigEntry entry = this.dimensions.get(dimension);

        if (entry != null)
        {
            this.dimBuilderData = entry.toJson();
            this.dimBuilderData.remove("dim");
            return true;
        }

        return false;
    }

    public void dimbuilderSaveAs(int dimension)
    {
        this.removeDimension(dimension);
        this.parseDimensionConfigEntry(dimension, this.dimBuilderData);
        this.saveConfig();
    }

    public void dimbuilderCreateAs(int dimension) throws CommandException
    {
        this.dimbuilderSaveAs(dimension);
        this.registerDimensionFromConfig(dimension);
    }

    @Nullable
    private JsonPrimitive getDimbuilderPrimitive(String key, WorldInfoType type)
    {
        JsonObject obj = this.dimBuilderData;

        if ((key.equals("override") ||
             key.equals("unregister") ||
             key.equals("biome") ||
             key.equals("disable_teleporting_from") ||
             key.equals("disable_teleporting_to")) &&
                obj.has(key) && obj.get(key).isJsonPrimitive())
        {
            return obj.get(key).getAsJsonPrimitive();
        }

        if (key.equals("id") ||
            key.equals("name") ||
            key.equals("suffix") ||
            key.equals("keeploaded") ||
            key.equals("worldprovider") ||
            key.equals("existing_dimensiontype"))
        {
            if (obj.has("dimensiontype") && obj.get("dimensiontype").isJsonObject())
            {
                obj = obj.get("dimensiontype").getAsJsonObject();
                return obj.has(key) && obj.get(key).isJsonPrimitive() ? obj.get(key).getAsJsonPrimitive() : null;
            }
            else
            {
                return null;
            }
        }

        if (this.isJEDProperty(key))
        {
            obj = JEDJsonUtils.getNestedObject(obj, "jed", false);
            return obj != null && obj.has(key) && obj.get(key).isJsonPrimitive() ? obj.get(key).getAsJsonPrimitive() : null;
        }
        else if (obj.has(type.getKeyName()) && obj.get(type.getKeyName()).isJsonObject())
        {
            obj = obj.get(type.getKeyName()).getAsJsonObject();

            // The requested key is a value directly inside the worldinfo object
            if (obj.has(key) && obj.get(key).isJsonPrimitive())
            {
                return obj.get(key).getAsJsonPrimitive();
            }
            else if (obj.has("GameRules") && obj.get("GameRules").isJsonObject())
            {
                obj = obj.get("GameRules").getAsJsonObject();
                return obj.has(key) && obj.get(key).isJsonPrimitive() ? obj.get(key).getAsJsonPrimitive() : null;
            }
        }

        return null;
    }

    private NBTTagCompound parseAndGetCustomWorldInfoValues(int dimension, JsonObject object) throws IllegalStateException
    {
        NBTTagCompound nbt = new NBTTagCompound();

        for (Map.Entry<String, JsonElement> entry : object.entrySet())
        {
            JsonElement element = entry.getValue();
            String key = entry.getKey();
            NBTBase tag = this.getTagForWorldInfoValue(key, element);

            if (tag != null)
            {
                nbt.setTag(key, tag);
            }
        }

        return nbt;
    }

    private boolean isJEDProperty(String key)
    {
        return this.jedKeys.get(key) != null;
    }

    @Nullable
    private NBTBase getTagForWorldInfoValue(String key, JsonElement element)
    {
        if (key.equals("RandomSeed") && element.isJsonPrimitive())
        {
            try
            {
                long seed = Long.parseLong(element.getAsString());
                return new NBTTagLong(seed);
            }
            catch (NumberFormatException e)
            {
                String seedStr = element.getAsString();

                if (seedStr.isEmpty())
                {
                    return new NBTTagString("RAND");
                }
                else
                {
                    return new NBTTagLong(seedStr.hashCode());
                }
            }
        }
        else if (element.isJsonObject())
        {
            NBTTagCompound tag = new NBTTagCompound();
            JsonObject obj = element.getAsJsonObject();

            if (key.equals("GameRules"))
            {
                for (Map.Entry<String, JsonElement> entry : obj.entrySet())
                {
                    JsonElement el = entry.getValue();

                    if (el.isJsonPrimitive())
                    {
                        tag.setString(entry.getKey(), el.getAsString());
                    }
                    else
                    {
                        JustEnoughDimensions.logger.warn("Invalid GameRule value: '{} = {}'", entry.getKey(), el);
                    }
                }
            }

            return tag;
        }

        Integer type = this.worldInfoKeys.get(key);

        // Keys from the vanilla level.dat
        if (type != null)
        {
            return this.getTagForType(key, type, element);
        }

        JustEnoughDimensions.logger.warn("Unrecognized option in worldinfo.values: '{} = {}'", key, element);
        return null;
    }

    @Nullable
    private NBTBase getTagForType(String key, int type, JsonElement element)
    {
        try
        {
            switch (type)
            {
                case Constants.NBT.TAG_BYTE:
                    try
                    {
                        String str = element.getAsString();
                        if (str != null && (str.equals("true") || str.equals("false")))
                        {
                            return new NBTTagByte(element.getAsBoolean() ? (byte) 1 : 0);
                        }
                    }
                    catch (Exception e) {}
                    return new NBTTagByte(element.getAsByte());

                case Constants.NBT.TAG_SHORT:
                    return new NBTTagShort(element.getAsShort());

                case Constants.NBT.TAG_INT:
                    return new NBTTagInt(element.getAsInt());

                case Constants.NBT.TAG_LONG:
                    return new NBTTagLong(element.getAsLong());

                case Constants.NBT.TAG_FLOAT:
                    return new NBTTagFloat(element.getAsFloat());

                case Constants.NBT.TAG_DOUBLE:
                    return new NBTTagDouble(element.getAsDouble());

                case Constants.NBT.TAG_STRING:
                    return new NBTTagString(element.getAsString());

                case Constants.NBT.TAG_LIST:
                    if (element.isJsonArray() && this.jedKeysListTypes.containsKey(key))
                    {
                        JsonArray arr = element.getAsJsonArray();
                        NBTTagList list = new NBTTagList();
                        int listType = this.jedKeysListTypes.get(key);

                        for (JsonElement el : arr)
                        {
                            NBTBase tag = this.getTagForType("", listType, el);

                            if (tag != null)
                            {
                                list.appendTag(tag);
                            }
                        }

                        return list;
                    }
            }
        }
        catch (Exception e)
        {
            JustEnoughDimensions.logger.warn("Invalid/unexpected value: '{}' for key '{}'", key, element);
        }

        return null;
    }

    private static class ConfigComparatorDimensionConfig extends ConfigComparator
    {
        @Override
        public boolean shouldReplace(File fileToReplace, File replacementFile)
        {
            if (fileToReplace.exists() == false)
            {
                return true;
            }

            if ((fileToReplace.exists() && fileToReplace.isFile() && fileToReplace.canRead() &&
                replacementFile.exists() && replacementFile.isFile() && replacementFile.canRead()))
            {
                JsonElement rootElementOld = JEDJsonUtils.parseJsonFile(fileToReplace);
                JsonElement rootElementNew = JEDJsonUtils.parseJsonFile(replacementFile);
                Pair<String, Integer> versionOld = rootElementOld != null ? getConfigVersion(rootElementOld) : null;
                Pair<String, Integer> versionNew = rootElementNew != null ? getConfigVersion(rootElementNew) : null;

                if (versionOld != null && versionNew != null)
                {
                    return versionNew.getLeft().equals(versionOld.getLeft()) &&
                           versionNew.getRight() > versionOld.getRight();
                }
            }

            return false;
        }
    }

    public enum WorldInfoType
    {
        REGULAR ("worldinfo"),
        ONE_TIME ("worldinfo_onetime");

        private final String keyName;

        private WorldInfoType(String keyName)
        {
            this.keyName = keyName;
        }

        public String getKeyName()
        {
            return this.keyName;
        }
    }

    public enum ColorType
    {
        FOLIAGE ("FoliageColors"),
        GRASS   ("GrassColors"),
        WATER   ("WaterColors");

        private final String keyName;

        private ColorType(String keyName)
        {
            this.keyName = keyName;
        }

        public String getKeyName()
        {
            return this.keyName;
        }
    }
} public class DimensionTypeEntry implements Comparable<DimensionTypeEntry>
{
    private static final Field field_DimensionType_clazz = ObfuscationReflectionHelper.findField(DimensionType.class, "field_186077_g"); // clazz
    private static final List<DimensionType> DIMENSION_TYPE_CACHE = new ArrayList<>();

    private int dimensionTypeId;
    private String name;
    private String suffix;
    private boolean keepLoaded;
    private Class<? extends WorldProvider> providerClass;
    private String dimensionTypeName;
    private boolean forceRegister;
    private boolean allowDifferentId = true;
    private boolean requireExactMatch;

    public static void cache(DimensionType entry)
    {
        if (DIMENSION_TYPE_CACHE.contains(entry) == false)
        {
            DIMENSION_TYPE_CACHE.add(entry);
        }
    }

    @SuppressWarnings("unchecked")
    @Nullable
    public static Class<? extends WorldProvider> getProviderClassFrom(DimensionType type)
    {
        try
        {
            return (Class<? extends WorldProvider>) field_DimensionType_clazz.get(type);
        }
        catch (Exception e)
        {
            return null;
        }
    }

    public DimensionTypeEntry(String existingDimensionTypeName)
    {
        this.dimensionTypeName = existingDimensionTypeName;
    }

    public DimensionTypeEntry(int dimTypeId, String name, String suffix, boolean keepLoaded, @Nonnull Class<? extends WorldProvider> providerClass)
    {
        this.dimensionTypeId = dimTypeId;
        this.name = name;
        this.suffix = suffix;
        this.keepLoaded = dimTypeId == 0 ? true : keepLoaded;
        this.providerClass = providerClass;
        this.dimensionTypeName = null;
    }

    /**
     * Set whether or not a new DimensionType should always be registered,
     * or can an existing similar DimensionType be used.
     * @param forceRegister
     * @return
     */
    public DimensionTypeEntry setForceRegister(boolean forceRegister)
    {
        this.forceRegister = forceRegister;
        return this;
    }

    /**
     * Set whether or not an existing DimensionType with identical other values,
     * (except for the name and suffix, which are not checked) but a different ID can be used
     * instead of registering a new entry.
     * @param allowDifferentId
     * @return
     */
    public DimensionTypeEntry setAllowDifferentId(boolean allowDifferentId)
    {
        this.allowDifferentId = allowDifferentId;
        return this;
    }

    /**
     * Set whether or not an exact match of an existing DimensionType is required
     * to avoid registering a new entry.
     * @param requireExactMatch
     * @return
     */
    public DimensionTypeEntry setRequireExactMatch(boolean requireExactMatch)
    {
        this.requireExactMatch = requireExactMatch;
        return this;
    }

    public int getDimensionTypeId()
    {
        return this.dimensionTypeId;
    }

    public String getDimensionTypeName()
    {
        return this.name;
    }

    public String getDimensionTypeSuffix()
    {
        return this.suffix;
    }

    public boolean getDimensionTypeKeepLoaded()
    {
        return this.keepLoaded;
    }

    public Class<? extends WorldProvider> getProviderClass()
    {
        return this.providerClass;
    }

    public DimensionType getOrRegisterDimensionType(int dimension)
    {
        if (this.forceRegister == false)
        {
            DimensionType entry = this.getExistingMatchingEntry(DIMENSION_TYPE_CACHE);

            if (entry == null)
            {
                entry = this.getExistingMatchingEntry(Arrays.asList(DimensionType.values()));
            }

            if (entry != null)
            {
                JustEnoughDimensions.logInfo("Using an existing DimensionType '{}', for dimension {}", entry, dimension);
                return entry;
            }
        }

        return this.registerDimensionType(dimension);
    }

    @Nullable
    private DimensionType getExistingMatchingEntry(List<DimensionType> list)
    {
        DimensionType entry = null;

        // Try to find a suitable existing entry,
        // to try to avoid modifying the DimensionType enum unnecessarily.
        for (DimensionType tmp : list)
        {
            if (tmp.shouldLoadSpawn() == this.keepLoaded && getProviderClassFrom(tmp) == this.providerClass)
            {
                if (this.requireExactMatch)
                {
                    if (tmp.getId() == this.dimensionTypeId &&
                        tmp.getSuffix().equals(this.suffix) &&
                        tmp.getName().equals(this.name))
                    {
                        entry = tmp;
                        break;
                    }
                }
                else if ((tmp.getId() == this.dimensionTypeId || this.allowDifferentId))
                {
                    entry = tmp;

                    // "Exact"/best match, stop searching
                    if (tmp.getId() == this.dimensionTypeId)
                    {
                        break;
                    }
                }
            }
        }

        return entry;
    }

    private DimensionType registerDimensionType(int dimension)
    {
        DimensionType entry = null;

        if (this.dimensionTypeName != null)
        {
            try
            {
                entry = DimensionType.byName(this.dimensionTypeName);
                JustEnoughDimensions.logInfo("Using a vanilla DimensionType (or some other existing one) '{}' for dimension {}", entry, dimension);
            }
            catch (IllegalArgumentException e)
            {
                entry = DimensionType.OVERWORLD;
                JustEnoughDimensions.logger.warn("Failed to get a DimensionType by the name '{}' for dimension {}, falling back to {}",
                        this.dimensionTypeName, dimension, entry);
            }
        }
        else
        {
            JustEnoughDimensions.logInfo("Registering a new DimensionType with values '{}' for dimension {}", this.getDescription(), dimension);
            entry =  DimensionType.register(this.name, this.suffix, this.dimensionTypeId, this.providerClass, this.keepLoaded);
        }

        // Cache the registered entries internally, because DimensionType.values() at some point gets JIT'd and
        // stops returning the new registered entries.
        // The contains check doesn't really help with anything except reference equality when 'force_register' is used
        // with the 'existing_dimensiontype' option, to avoid caching duplicate instances.
        if (this.forceRegister == false || DIMENSION_TYPE_CACHE.contains(entry) == false)
        {
            DIMENSION_TYPE_CACHE.add(entry);
        }

        return entry;
    }

    public void writeToByteBuf(ByteBuf buf)
    {
        if (this.dimensionTypeName != null)
        {
            buf.writeByte(1);
            ByteBufUtils.writeUTF8String(buf, this.dimensionTypeName);
        }
        else
        {
            buf.writeByte(0);
            buf.writeInt(this.dimensionTypeId);
            buf.writeBoolean(this.forceRegister);
            buf.writeBoolean(this.allowDifferentId);
            buf.writeBoolean(this.requireExactMatch);
            ByteBufUtils.writeUTF8String(buf, this.name);
            ByteBufUtils.writeUTF8String(buf, this.suffix);
            ByteBufUtils.writeUTF8String(buf, this.providerClass.getName());
        }
    }

    public static DimensionTypeEntry fromByteBuf(ByteBuf buf)
    {
        final byte type = buf.readByte();

        if (type == (byte) 1)
        {
            DimensionTypeEntry entry = new DimensionTypeEntry(ByteBufUtils.readUTF8String(buf));
            return entry;
        }
        else
        {
            final int dimTypeId = buf.readInt();
            boolean forceRegister = buf.readBoolean();
            boolean allowDifferentId = buf.readBoolean();
            boolean requireExactMatch = buf.readBoolean();
            String name = ByteBufUtils.readUTF8String(buf);
            String suffix = ByteBufUtils.readUTF8String(buf);
            String providerClassName = ByteBufUtils.readUTF8String(buf);

            try
            {
                @SuppressWarnings("unchecked")
                Class<? extends WorldProvider> providerClass = (Class<? extends WorldProvider>) Class.forName(providerClassName);
                DimensionTypeEntry entry = new DimensionTypeEntry(dimTypeId, name, suffix, false, providerClass);
                entry.setForceRegister(forceRegister);
                entry.setAllowDifferentId(allowDifferentId);
                entry.setRequireExactMatch(requireExactMatch);
                return entry;
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.error("Failed to read dimension info from packet for dimension {}" +
                    " - WorldProvider class {} not found", dimTypeId, providerClassName);
                return null;
            }
        }

    }

    @Nonnull
    public static DimensionTypeEntry fromJson(int dimension, @Nonnull JsonObject objDimType)
    {
        String existing = JEDJsonUtils.getStringOrDefault(objDimType, "existing_dimensiontype", null, false);

        if (existing != null)
        {
            JustEnoughDimensions.logInfo("Using an existing DimensionType '{}' for dimension {}", existing, dimension);
            return new DimensionTypeEntry(existing);
        }

        int dimensionTypeId = JEDJsonUtils.getIntegerOrDefault(objDimType, "id", dimension);
        String name =   JEDJsonUtils.getStringOrDefault(objDimType, "name", "DIM" + dimension, false);
        String suffix = JEDJsonUtils.getStringOrDefault(objDimType, "suffix", "_dim" + dimension, false);

        boolean keepLoaded =        JEDJsonUtils.getBooleanOrDefault(objDimType, "keeploaded", false);
        boolean forceRegister =     JEDJsonUtils.getBooleanOrDefault(objDimType, "force_register", false);
        boolean allowDifferentId =  JEDJsonUtils.getBooleanOrDefault(objDimType, "allow_different_id", true);
        boolean requireExactMatch = JEDJsonUtils.getBooleanOrDefault(objDimType, "require_exact_match", false);

        Class<? extends WorldProvider> providerClass = WorldProviderSurfaceJED.class;
        String providerName = "";

        if (objDimType.has("worldprovider") && objDimType.get("worldprovider").isJsonPrimitive())
        {
            providerName = objDimType.get("worldprovider").getAsString();

            // Don't allow using the vanilla surface or hell providers for the vanilla end dimension,
            // because that will lead to a crash in the teleport code (null returned from getSpawnCoordinate() for
            // other vanilla providers than the End).
            if (dimension == 1)
            {
                if (providerName.equals("WorldProviderSurface") || providerName.equals("net.minecraft.world.WorldProviderSurface"))
                {
                    providerName = WorldProviderSurfaceJED.class.getSimpleName();
                    JustEnoughDimensions.logger.warn("Changing the provider for DIM1 to {} to prevent a vanilla crash", providerName);
                }
                else if (providerName.equals("WorldProviderHell") || providerName.equals("net.minecraft.world.WorldProviderHell"))
                {
                    providerName = WorldProviderHellJED.class.getSimpleName();
                    JustEnoughDimensions.logger.warn("Changing the provider for DIM1 to {} to prevent a vanilla crash", providerName);
                }
            }

            providerClass = getProviderClass(providerName);
        }

        if (providerClass == null)
        {
            providerClass = WorldProviderSurfaceJED.class;

            JustEnoughDimensions.logger.warn("Failed to get a WorldProvider for name '{}', using {} as a fall-back",
                    providerName, getNameForWorldProvider(providerClass));
        }

        DimensionTypeEntry entry = new DimensionTypeEntry(dimensionTypeId, name, suffix, keepLoaded, providerClass);
        entry.setForceRegister(forceRegister);
        entry.setAllowDifferentId(allowDifferentId);
        entry.setRequireExactMatch(requireExactMatch);

        return entry;
    }

    public JsonObject toJson()
    {
        JsonObject dimensionType = new JsonObject();

        if (this.dimensionTypeName != null)
        {
            dimensionType.addProperty("existing_dimensiontype", this.dimensionTypeName);
        }
        else
        {
            dimensionType.addProperty("id", this.dimensionTypeId);
            dimensionType.addProperty("name", this.name);
            dimensionType.addProperty("suffix", this.suffix);
            dimensionType.addProperty("keeploaded", this.keepLoaded);
            dimensionType.addProperty("worldprovider", getNameForWorldProvider(this.providerClass));

            // only include these when they are not at the default value
            if (this.forceRegister)
            {
                dimensionType.addProperty("force_register", this.forceRegister);
            }

            if (this.allowDifferentId == false)
            {
                dimensionType.addProperty("allow_different_id", this.allowDifferentId);
            }

            if (this.requireExactMatch)
            {
                dimensionType.addProperty("require_exact_match", this.requireExactMatch);
            }
        }

        return dimensionType;
    }

    public String getDescription()
    {
        if (this.dimensionTypeName != null)
        {
            return String.format("{existing_dimensiontype=%s}", this.dimensionTypeName);
        }
        else
        {
            return String.format("{id=%d,name=\"%s\",suffix=\"%s\",keepLoaded=%s,WorldProvider:\"%s\",force_register=%s,allow_different_id=%s}",
                    this.dimensionTypeId, this.name, this.suffix, this.keepLoaded,
                    getNameForWorldProvider(this.providerClass), this.forceRegister, this.allowDifferentId);
        }
    }

    @Override
    public int compareTo(DimensionTypeEntry other)
    {
        if (this.providerClass == other.providerClass)
        {
            return 0;
        }

        return this.dimensionTypeId > other.dimensionTypeId ? 1 : -1;
    }

    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + this.dimensionTypeId;
        return result;
    }

    @Override
    public boolean equals(Object other)
    {
        if (this == other) { return true; }
        if (other == null) { return false; }
        if (getClass() != other.getClass()) { return false; }

        return this.providerClass == ((DimensionTypeEntry) other).providerClass;
    }

    public static String getNameForWorldProvider(@Nonnull Class<? extends WorldProvider> clazz)
    {
        String provName = clazz.getName();

        // These ones are supported by their simple class names in this code
        if (provName.startsWith("net.minecraft.world.") ||
            provName.equals(WorldProviderSurfaceJED.class.getName()) ||
            provName.equals(WorldProviderHellJED.class.getName()) ||
            provName.equals(WorldProviderEndJED.class.getName()))
        {
            return clazz.getSimpleName();
        }

        return provName;
    }

    @Nullable
    @SuppressWarnings("unchecked")
    public static Class<? extends WorldProvider> getProviderClass(String providerClassName)
    {
        Class<? extends WorldProvider> providerClass;

        if (providerClassName.equals("WorldProviderSurfaceJED"))
        {
            providerClass = WorldProviderSurfaceJED.class;
        }
        else if (providerClassName.equals("WorldProviderHellJED"))
        {
            providerClass = WorldProviderHellJED.class;
        }
        else if (providerClassName.equals("WorldProviderEndJED"))
        {
            providerClass = WorldProviderEndJED.class;
        }
        else if (providerClassName.equals("WorldProviderSurface"))
        {
            providerClass = WorldProviderSurface.class;
        }
        else if (providerClassName.equals("WorldProviderHell"))
        {
            providerClass = WorldProviderHell.class;
        }
        else if (providerClassName.equals("WorldProviderEnd"))
        {
            providerClass = WorldProviderEnd.class;
        }
        else
        {
            try
            {
                providerClass = (Class<? extends WorldProvider>) Class.forName(providerClassName);
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.error("Failed to get a WorldProvider class for name '{}'", providerClassName, e);
                return null;
            }
        }

        return providerClass;
    }
} public class DataTracker
{
    private static DataTracker instance;
    private Map<UUID, PlayerData> playerData = new HashMap<>();
    private boolean dirty = false;

    public static DataTracker getInstance()
    {
        if (instance == null)
        {
            instance = new DataTracker();
        }

        return instance;
    }

    private PlayerData getOrCreatePlayerData(UUID uuid)
    {
        PlayerData data = this.playerData.get(uuid);

        if (data == null)
        {
            data = new PlayerData();
            this.playerData.put(uuid, data);
        }

        return data;
    }

    public void playerLoginOrRespawn(EntityPlayer playerIn)
    {
        if (EntityUtils.isValidPlayerMP(playerIn))
        {
            PlayerData data = this.playerData.get(playerIn.getUniqueID());

            if (data != null)
            {
                int dimFrom = data.dimension;
                int dimTo = playerIn.getEntityWorld().provider.getDimension();

                if (dimFrom != dimTo)
                {
                    this.playerChangedDimension(playerIn, dimFrom, dimTo);
                }
            }

            this.storePlayerDimension(playerIn);
        }
    }

    public void playerDied(EntityPlayer player)
    {
        if (player.getEntityWorld().getWorldInfo().isHardcoreModeEnabled())
        {
            this.getOrCreatePlayerData(player.getUniqueID()).deadInHardcore = true;
            this.dirty = true;
        }
    }

    public void playerInitialSpawn(EntityPlayer player)
    {
        UUID uuid = player.getUniqueID();

        // If players first join into a ForceGameMode dimension, set a "normal game mode"
        // for them from the main configuration.
        if (EntityUtils.isValidPlayerMP(player) &&
            this.dimensionHasForcedGameMode(player.dimension) &&
            this.playerData.containsKey(uuid) == false)
        {
            PlayerData data = this.getOrCreatePlayerData(uuid);
            data.normalGameMode = Configs.normalGameMode;
            data.dimension = player.dimension;
            this.dirty = true;

            JustEnoughDimensions.logInfo("DataTracker: Set the \"normal game mode\" of player '{}' to '{}' after their initial join to a ForceGameMode dimension {}",
                    player.getName(), Configs.normalGameMode, player.dimension);
        }
    }

    /**
     * The basic idea here is to store the "normal" gamemode of a player in a non-forced-gamemode
     * dimension, so that we know what to set the player's gamemode to when they leave a forced-gamemode-dimension.
     * Note that they may be changing between different forced-gamemode dimension before returning to
     * a non-forced-gamemode dimension.
     */
    public void playerChangedDimension(EntityPlayer playerIn, int dimFrom, int dimTo)
    {
        if (EntityUtils.isValidPlayerMP(playerIn))
        {
            this.storePlayerDimension(playerIn);

            if (Configs.enableForcedGameModes)
            {
                PlayerData data = this.getOrCreatePlayerData(playerIn.getUniqueID());

                if (data.deadInHardcore)
                {
                    return;
                }

                EntityPlayerMP player = (EntityPlayerMP) playerIn;
                boolean forcedFrom = this.dimensionHasForcedGameMode(dimFrom);
                boolean forcedTo = this.dimensionHasForcedGameMode(dimTo);

                if (forcedTo)
                {
                    // The gamemode only needs to be stored when changing to a forced-gamemode dimension
                    // from a non-forced-gamemode dimension. Ie. the gamemode won't be touched when switching
                    // between non-forced-gamemode dimensions, and the stored one won't be changed if switching
                    // between multiple forced ones.
                    if (forcedFrom == false)
                    {
                        this.storeNonForcedGameMode(player);
                    }

                    // The player is in the destination world at this point, so we get the gamemode from there
                    this.setPlayerGameMode(player, player.getEntityWorld().getWorldInfo().getGameType());

                    JustEnoughDimensions.logInfo("DataTracker: Set gamemode '{}' for player '{}'",
                            player.interactionManager.getGameType(), player.getName());
                }
                // When switching to a non-forced-gamemode dimension from a forced-gamemode dimension,
                // ie. we have a stored gamemode for the player.
                else
                {
                    this.restoreStoredGameModeIfExists(player);
                }
            }

            if (Configs.enablePlayerInventoryGroups)
            {
                String groupFrom = this.getPlayerInventoryGroup(dimFrom);
                String groupTo = this.getPlayerInventoryGroup(dimTo);

                if (groupTo.equals(groupFrom) == false)
                {
                    JustEnoughDimensions.logInfo("DataTracker: Swapping the player inventories of player '{}' from group '{}' to group '{}'",
                            playerIn.getName(), groupFrom, groupTo);

                    PlayerInventoryStorage.INSTANCE.writePlayerInventoryDataForGroup(playerIn, groupFrom);
                    PlayerInventoryStorage.INSTANCE.readPlayerInventoryDataForGroup(playerIn, groupTo);
                    playerIn.inventoryContainer.detectAndSendChanges();
                }
            }
        }
    }

    private boolean dimensionHasForcedGameMode(int dimension)
    {
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(dimension);
        return props != null && props.getForceGameMode();
    }

    private String getPlayerInventoryGroup(int dimension)
    {
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(dimension);
        return props != null ? props.getPlayerInventoryGroup(dimension) : PlayerInventoryStorage.DEFAULT_PLAYER_INVENTORY_GROUP;
    }

    private void storeNonForcedGameMode(EntityPlayerMP player)
    {
        this.getOrCreatePlayerData(player.getUniqueID()).normalGameMode = player.interactionManager.getGameType();
        this.dirty = true;

        JustEnoughDimensions.logInfo("DataTracker: Stored a non-forced gamemode '{}' for player '{}'",
                player.interactionManager.getGameType(), player.getName());
    }

    private void restoreStoredGameModeIfExists(EntityPlayerMP player)
    {
        PlayerData data = this.getOrCreatePlayerData(player.getUniqueID());

        if (data.normalGameMode != null)
        {
            this.setPlayerGameMode(player, data.normalGameMode);
            data.normalGameMode = null;
            this.dirty = true;

            JustEnoughDimensions.logInfo("DataTracker: Restored gamemode '{}' for player '{}'",
                    player.interactionManager.getGameType(), player.getName());
        }
    }

    private void setPlayerGameMode(EntityPlayerMP player, GameType type)
    {
        player.setGameType(type);
        player.sendMessage(new TextComponentTranslation("jed.info.gamemode.changed", type.toString()));
    }

    private void storePlayerDimension(EntityPlayer player)
    {
        int dimension = player.getEntityWorld().provider.getDimension();
        this.getOrCreatePlayerData(player.getUniqueID()).dimension = dimension;
        this.dirty = true;

        JustEnoughDimensions.logInfo("DataTracker: Stored dimension '{}' for player '{}'", dimension, player.getName());
    }

    @Nullable
    public Integer getPlayersDimension(EntityPlayer player)
    {
        return this.getOrCreatePlayerData(player.getUniqueID()).dimension;
    }

    public int getPlayerCountInDimension(int dimension)
    {
        int count = 0;

        for (PlayerData data : this.playerData.values())
        {
            if (data.dimension == dimension)
            {
                count++;
            }
        }

        return count;
    }

    public void readFromDisk(@Nullable File worldDir)
    {
        // Clear the data structures when reading the data for a world/save, so that data
        // from another world won't carry over to a world/save that doesn't have the file yet.
        this.playerData.clear();

        if (worldDir != null)
        {
            File jedDataDir = WorldFileUtils.getWorldJEDDataDirectory(worldDir);
            File file = new File(jedDataDir, "data_tracker.dat");

            if (file.exists() && file.isFile() && file.canRead())
            {
                try
                {
                    FileInputStream is = new FileInputStream(file);
                    this.readFromNBT(CompressedStreamTools.readCompressed(is));
                    is.close();
                }
                catch (Exception e)
                {
                    JustEnoughDimensions.logger.warn("Failed to read DataTracker data from file '{}'", file.getAbsolutePath());
                }
            }
        }
    }

    public void writeToDisk()
    {
        if (this.dirty)
        {
            try
            {
                File saveDir = DimensionManager.getCurrentSaveRootDirectory();

                if (saveDir == null)
                {
                    return;
                }

                File jedDataDir = WorldFileUtils.getWorldJEDDataDirectory(saveDir);

                if (jedDataDir.exists() == false && jedDataDir.mkdirs() == false)
                {
                    JustEnoughDimensions.logger.warn("Failed to create the save directory '{}'", jedDataDir.getAbsolutePath());
                    return;
                }

                final NBTTagCompound nbt = this.writeToNBT(new NBTTagCompound());

                ThreadedFileIOBase.getThreadedIOInstance().queueIO(() ->
                {
                    File fileTmp = new File(jedDataDir, "data_tracker.dat.tmp");
                    File fileReal = new File(jedDataDir, "data_tracker.dat");

                    try
                    {
                        FileOutputStream os = new FileOutputStream(fileTmp);
                        CompressedStreamTools.writeCompressed(nbt, os);
                        os.close();

                        if (fileReal.exists())
                        {
                            fileReal.delete();
                        }

                        fileTmp.renameTo(fileReal);
                    }
                    catch (Exception e)
                    {
                        JustEnoughDimensions.logger.warn("Failed to write DataTracker data to file", e);
                    }

                    return false;
                });

                this.dirty = false;
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.warn("Failed to write DataTracker data to file", e);
            }
        }
    }

    private void readFromNBTOld(NBTTagCompound nbt)
    {
        if (nbt != null)
        {
            if (nbt.hasKey("PlayerGameModes", Constants.NBT.TAG_LIST))
            {
                NBTTagList tagList = nbt.getTagList("PlayerGameModes", Constants.NBT.TAG_COMPOUND);
                final int count = tagList.tagCount();

                for (int i = 0; i < count; ++i)
                {
                    NBTTagCompound tag = tagList.getCompoundTagAt(i);

                    if (tag.hasKey("UUIDM", Constants.NBT.TAG_LONG) &&
                        tag.hasKey("UUIDL", Constants.NBT.TAG_LONG) &&
                        tag.hasKey("GameMode", Constants.NBT.TAG_BYTE))
                    {
                        GameType type = GameType.getByID(tag.getByte("GameMode"));

                        if (type != GameType.NOT_SET)
                        {
                            this.getOrCreatePlayerData(new UUID(tag.getLong("UUIDM"), tag.getLong("UUIDL"))).normalGameMode = type;
                        }
                    }
                }
            }

            if (nbt.hasKey("PlayerDimensions", Constants.NBT.TAG_LIST))
            {
                NBTTagList tagList = nbt.getTagList("PlayerDimensions", Constants.NBT.TAG_COMPOUND);
                final int count = tagList.tagCount();

                for (int i = 0; i < count; ++i)
                {
                    NBTTagCompound tag = tagList.getCompoundTagAt(i);

                    if (tag.hasKey("UUIDM", Constants.NBT.TAG_LONG) &&
                        tag.hasKey("UUIDL", Constants.NBT.TAG_LONG) &&
                        tag.hasKey("Dimension", Constants.NBT.TAG_INT))
                    {
                        this.getOrCreatePlayerData(new UUID(tag.getLong("UUIDM"), tag.getLong("UUIDL"))).dimension = tag.getInteger("Dimension");
                    }
                }
            }
        }
    }

    private void readFromNBT(NBTTagCompound nbt)
    {
        if (nbt != null)
        {
            if (nbt.hasKey("PlayerData", Constants.NBT.TAG_LIST))
            {
                NBTTagList tagList = nbt.getTagList("PlayerData", Constants.NBT.TAG_COMPOUND);
                final int count = tagList.tagCount();

                for (int i = 0; i < count; ++i)
                {
                    NBTTagCompound tag = tagList.getCompoundTagAt(i);

                    if (tag.hasKey("UUIDM", Constants.NBT.TAG_LONG) &&
                        tag.hasKey("UUIDL", Constants.NBT.TAG_LONG))
                    {
                        PlayerData data = new PlayerData();
                        data.dimension = tag.getInteger("Dimension");
                        data.deadInHardcore = tag.getBoolean("HardcoreDead");

                        if (tag.hasKey("GameMode", Constants.NBT.TAG_BYTE))
                        {
                            GameType type = GameType.getByID(tag.getByte("GameMode"));

                            if (type != GameType.NOT_SET)
                            {
                                data.normalGameMode = type;
                            }
                        }

                        this.playerData.put(new UUID(tag.getLong("UUIDM"), tag.getLong("UUIDL")), data);
                    }
                }
            }
            else
            {
                this.readFromNBTOld(nbt);
            }
        }
    }

    private NBTTagCompound writeToNBT(NBTTagCompound nbt)
    {
        NBTTagList tagList = new NBTTagList();

        for (Map.Entry<UUID, PlayerData> entry : this.playerData.entrySet())
        {
            PlayerData data = entry.getValue();
            NBTTagCompound tag = new NBTTagCompound();
            tag.setLong("UUIDM", entry.getKey().getMostSignificantBits());
            tag.setLong("UUIDL", entry.getKey().getLeastSignificantBits());
            tag.setInteger("Dimension", data.dimension);
            tag.setBoolean("HardcoreDead", data.deadInHardcore);

            if (data.normalGameMode != null)
            {
                tag.setByte("GameMode", (byte) data.normalGameMode.getID());
            }

            tagList.appendTag(tag);
        }

        nbt.setTag("PlayerData", tagList);

        return nbt;
    }

    private static class PlayerData
    {
        public boolean deadInHardcore;
        public int dimension;
        @Nullable
        public GameType normalGameMode;
    }
} public class DimensionSyncChannelHandler extends FMLIndexedMessageToMessageCodec<DimensionSyncPacket> {

    public static DimensionSyncChannelHandler instance = new DimensionSyncChannelHandler();

    public DimensionSyncChannelHandler()
    {
        addDiscriminator(0, DimensionSyncPacket.class);
    }

    @Override
    public void encodeInto(ChannelHandlerContext ctx, DimensionSyncPacket msg, ByteBuf target) throws Exception
    {
        target.writeBytes(msg.getData());
    }

    @Override
    public void decodeInto(ChannelHandlerContext ctx, ByteBuf source, DimensionSyncPacket msg)
    {
        msg.consumePacket(source);

        switch(FMLCommonHandler.instance().getEffectiveSide())
        {
            case CLIENT:
                msg.execute();
                break;
            case SERVER:
                break;
        }
    }
} public class PacketHandler
{
    public static final SimpleNetworkWrapper INSTANCE = NetworkRegistry.INSTANCE.newSimpleChannel(Reference.MOD_ID.toLowerCase());

    public static void init()
    {
        INSTANCE.registerMessage(MessageSyncDimensions.Handler.class, MessageSyncDimensions.class, 0, Side.CLIENT);
        INSTANCE.registerMessage(MessageSyncWorldProperties.Handler.class, MessageSyncWorldProperties.class, 1, Side.CLIENT);
    }
} public class CommonProxy
{
    public void registerEventHandlers()
    {
        MinecraftForge.EVENT_BUS.register(new JEDEventHandler());
    }

    public void registerClientEventHandler()
    {
    }

    public void unregisterClientEventHandler()
    {
    }
} public class Reference
{
    public static final String MOD_ID = "justenoughdimensions";
    public static final String MOD_NAME = "Just Enough Dimensions";
    public static final String MOD_VERSION = "@MOD_VERSION@";
    public static final String FINGERPRINT = "@FINGERPRINT@";

    public static final String PROXY_CLIENT = "fi.dy.masa.justenoughdimensions.proxy.ClientProxy";
    public static final String PROXY_SERVER = "fi.dy.masa.justenoughdimensions.proxy.CommonProxy";
} public class PlayerInventoryStorage
{
    public static final PlayerInventoryStorage INSTANCE = new PlayerInventoryStorage();
    public static final String DEFAULT_PLAYER_INVENTORY_GROUP = "__default";

    @Nullable private File playerFileDirRoot;

    public void setWorldDir(@Nullable File worldDir)
    {
        if (worldDir != null)
        {
            this.playerFileDirRoot = new File(new File(worldDir, Reference.MOD_ID), "player_inventories");
        }
    }

    public void readPlayerInventoryDataForGroup(EntityPlayer player, String group)
    {
        if (this.playerFileDirRoot != null)
        {
            NBTTagCompound nbt = null;
            File dir = new File(this.playerFileDirRoot, group);

            if (dir.exists() && dir.isDirectory())
            {
                File file = new File(dir, player.getUniqueID().toString() + ".dat");

                if (file.exists() && file.isFile() && file.canRead())
                {
                    try
                    {
                        FileInputStream is = new FileInputStream(file);
                        nbt = CompressedStreamTools.readCompressed(is);
                        is.close();
                    }
                    catch (Exception e)
                    {
                        JustEnoughDimensions.logger.warn("Failed to read player inventory data from file '{}'", file.getAbsolutePath());
                    }
                }
            }

            // Always at least clear the inventory, even if there is no saved inventory yet for this group
            PlayerInventoryHandler.INSTANCE.restorePlayerInventories(player, nbt);
        }
    }

    public void writePlayerInventoryDataForGroup(EntityPlayer player, String group)
    {
        if (this.playerFileDirRoot != null)
        {
            File dir = new File(this.playerFileDirRoot, group);

            if (dir.exists() == false && dir.mkdirs() == false)
            {
                JustEnoughDimensions.logger.warn("Failed to create directory for player inventory group: '{}'", dir.getAbsolutePath());
            }

            if (dir.exists() && dir.isDirectory())
            {
                File fileTmp = new File(dir, player.getUniqueID().toString() + ".dat.tmp");
                File fileReal = new File(dir, player.getUniqueID().toString() + ".dat");
                NBTTagCompound nbt = new NBTTagCompound();
                PlayerInventoryHandler.INSTANCE.savePlayerInventories(player, nbt);

                ThreadedFileIOBase.getThreadedIOInstance().queueIO(() ->
                {
                    try
                    {
                        FileOutputStream os = new FileOutputStream(fileTmp);
                        CompressedStreamTools.writeCompressed(nbt, os);
                        os.close();

                        if (fileReal.exists())
                        {
                            fileReal.delete();
                        }

                        fileTmp.renameTo(fileReal);
                    }
                    catch (Exception e)
                    {
                        JustEnoughDimensions.logger.warn("Failed to write player inventory data to file '{}'", fileTmp.getAbsolutePath(), e);
                    }

                    return false;
                });
            }
        }
    }
} public class WorldBorderUtils
{
    private static Field field_WorldBorder_listeners = null;
    private static Field field_WorldServerMulti_borderListener = null;

    static
    {
        try
        {
            field_WorldBorder_listeners = ObfuscationReflectionHelper.findField(WorldBorder.class, "field_177758_a"); // listeners
            field_WorldServerMulti_borderListener = ObfuscationReflectionHelper.findField(WorldServerMulti.class, "borderListener"); // borderListener
        }
        catch (Exception e)
        {
            JustEnoughDimensions.logger.error("WorldBorderUtils: Reflection failed!!", e);
        }
    }

    public static void sendWorldBorder(EntityPlayer player)
    {
        if (Configs.enableSeparateWorldBorders && player.getEntityWorld().isRemote == false && (player instanceof EntityPlayerMP))
        {
            JustEnoughDimensions.logInfo("WorldBorderUtils.sendWorldBorder(): Sending the border to player '{}'", player.getName());
            ((EntityPlayerMP) player).connection.sendPacket(
                    new SPacketWorldBorder(player.getEntityWorld().getWorldBorder(), SPacketWorldBorder.Action.INITIALIZE));
        }
    }

    public static void removeDefaultBorderListeners()
    {
        World overworld = DimensionManager.getWorld(0);

        if (Configs.enableSeparateWorldBorders && overworld != null)
        {
            JustEnoughDimensions.logInfo("WorldBorderUtils.removeDefaultBorderListeners()");

            try
            {
                @SuppressWarnings("unchecked")
                List<IBorderListener> listeners = (List<IBorderListener>) field_WorldBorder_listeners.get(overworld.getWorldBorder());

                if (listeners != null)
                {
                    listeners.clear();
                    overworld.getWorldBorder().addListener(new JEDBorderListener(0));
                }
            }
            catch (IllegalArgumentException e)
            {
                JustEnoughDimensions.logger.warn("Failed to clear default WorldBorder listeners");
            }
            catch (IllegalAccessException e)
            {
                JustEnoughDimensions.logger.warn("Failed to clear default WorldBorder listeners");
            }
        }
    }

    public static void removeOverworldBorderListener(World world)
    {
        World overworld = DimensionManager.getWorld(0);

        if (Configs.enableSeparateWorldBorders && overworld != null && (world instanceof WorldServerMulti))
        {
            JustEnoughDimensions.logInfo("WorldBorderUtils.removeOverworldBorderListener() in dimension {}", world.provider.getDimension());

            try
            {
                @SuppressWarnings("unchecked")
                List<IBorderListener> overworldListeners = (List<IBorderListener>) field_WorldBorder_listeners.get(overworld.getWorldBorder());
                IBorderListener listener = (IBorderListener) field_WorldServerMulti_borderListener.get(world);

                if (overworldListeners != null)
                {
                    overworldListeners.remove(listener);
                }
            }
            catch (IllegalArgumentException e)
            {
                JustEnoughDimensions.logger.warn("Failed to clear default WorldBorder listeners");
            }
            catch (IllegalAccessException e)
            {
                JustEnoughDimensions.logger.warn("Failed to clear default WorldBorder listeners");
            }
        }
    }

    public static void setWorldBorderValues(World world)
    {
        WorldBorder border = world.getWorldBorder();
        WorldInfo info = world.getWorldInfo();

        border.setCenter(info.getBorderCenterX(), info.getBorderCenterZ());
        border.setDamageAmount(info.getBorderDamagePerBlock());
        border.setDamageBuffer(info.getBorderSafeZone());
        border.setWarningDistance(info.getBorderWarningDistance());
        border.setWarningTime(info.getBorderWarningTime());

        if (info.getBorderLerpTime() > 0L)
        {
            border.setTransition(info.getBorderSize(), info.getBorderLerpTarget(), info.getBorderLerpTime());
        }
        else
        {
            border.setTransition(info.getBorderSize());
        }
    }
} public class WorldFileUtils
{
    private static final String JED_LEVEL_FILENAME = "jed_level.dat";
    private static final FileFilter FILE_FILTER_NO_LEVEL = new FileFilter()
    {
        public boolean accept(File name)
        {
            return name.getName().equals("level.dat") == false;
        }
    };

    @Nullable
    public static File getWorldDirectory(World world)
    {
        IChunkProvider chunkProvider = world.getChunkProvider();

        if (chunkProvider instanceof ChunkProviderServer)
        {
            ChunkProviderServer chunkProviderServer = (ChunkProviderServer) chunkProvider;
            IChunkLoader chunkLoader = chunkProviderServer.chunkLoader;

            if (chunkLoader instanceof AnvilChunkLoader)
            {
                return ((AnvilChunkLoader) chunkLoader).chunkSaveLocation;
            }

            return null;
        }
        // If this method gets called before ChunkProviderServer has been set yet,
        // then we mimic the vanilla code in AnvilSaveHandler#getChunkLoader() to get the directory.
        else
        {
            return getWorldDirectoryDirectly(world, true);
        }
    }

    private static File getWorldDirectoryDirectly(World world, boolean mkDirs)
    {
        File mainWorldDir = world.getSaveHandler().getWorldDirectory();
        File dimensionDir = mainWorldDir;
        String dimensionDirName = world.provider.getSaveFolder();

        if (dimensionDirName != null)
        {
            dimensionDir = new File(mainWorldDir, dimensionDirName);

            if (mkDirs)
            {
                dimensionDir.mkdirs();
            }
        }

        return dimensionDir;
    }

    public static boolean jedLevelFileExists(World world)
    {
        File worldDir = WorldFileUtils.getWorldDirectory(world);

        if (worldDir != null)
        {
            File levelFile = new File(worldDir, JED_LEVEL_FILENAME);
            return levelFile.exists() && levelFile.isFile();
        }

        return false;
    }

    /**
     * 
     * @param world
     * @return true if worldinfo_onetime should be applied even though the jed_level.dat file already exists
     */
    public static void copyTemplateWorldIfApplicable(World world)
    {
        final int dimension = world.provider.getDimension();

        // This method can't be used for the overworld.
        // For the overworld the template copying needs to happen before the server is started.
        if (dimension != 0)
        {
            File dimensionDir = getWorldDirectoryDirectly(world, false);
            copyTemplateWorldIfApplicable(dimension, dimensionDir);
        }
    }

    /**
     * 
     * @param dimension
     * @param dimensionDir
     * @return true if worldinfo_onetime should be applied even though the jed_level.dat file already exists
     */
    public static void copyTemplateWorldIfApplicable(final int dimension, File dimensionDir)
    {
        DimensionConfigEntry entry = DimensionConfig.instance().getDimensionConfigFor(dimension);

        // For the overworld we need to detect a new world being created differently,
        // because the save directory gets created earlier.
        if (entry != null && entry.getWorldTemplate() != null && isNewWorld(dimensionDir))
        {
            File templateWorld = new File(new File(Configs.getConfigDir(), "world_templates"), entry.getWorldTemplate());

            if (templateWorld.exists() && templateWorld.isDirectory() && templateWorld.canRead())
            {
                try
                {
                    File levelFile = new File(templateWorld, "level.dat");
                    boolean hasLevelFile = levelFile.exists() && levelFile.isFile() && levelFile.canRead();

                    // For overworld templates, a level.dat is required to get the ID map.
                    if (dimension != 0 || hasLevelFile)
                    {
                        JustEnoughDimensions.logInfo("Copying a template world from '{}' to '{}'",
                                templateWorld.getAbsolutePath(), dimensionDir.getAbsolutePath());

                        FileUtils.copyDirectory(templateWorld, dimensionDir, FILE_FILTER_NO_LEVEL, true);

                        if (hasLevelFile && dimension == 0)
                        {
                            Files.copy(levelFile, new File(dimensionDir, "level.dat"));
                        }
                    }
                    else if (dimension == 0 && hasLevelFile == false)
                    {
                        // TODO 1.13: Remove
                        JustEnoughDimensions.logger.warn("Template world '{}' doesn't have the level.dat file (required for the ID map)",
                                templateWorld.getAbsolutePath());
                    }
                }
                catch (Exception e)
                {
                    JustEnoughDimensions.logger.warn("Failed to copy a template world from '{}' to '{}'",
                            templateWorld.getAbsolutePath(), dimensionDir.getAbsolutePath());
                }
            }
            else
            {
                JustEnoughDimensions.logger.warn("Template world '{}' doesn't exist or is not readable",
                        templateWorld.getAbsolutePath());
            }
        }
    }

    public static void createTemporaryWorldMarkerIfApplicable(World world)
    {
        final int dimension = world.provider.getDimension();
        DimensionConfigEntry entry = DimensionConfig.instance().getDimensionConfigFor(dimension);

        if (entry != null && entry.isTemporaryDimension())
        {
            File worldDir = getWorldDirectoryDirectly(world, false);

            if (isNewWorld(worldDir))
            {
                File jedDataDir = getWorldJEDDataDirectory(worldDir);
                File markerFile = getTemporaryDimensionMarkerFile(jedDataDir);

                if (markerFile.exists() == false)
                {
                    try
                    {
                        JustEnoughDimensions.logInfo("Creating a temporary dimension marker file '{}'", markerFile.getAbsolutePath());
                        jedDataDir.mkdirs();
                        markerFile.createNewFile();
                    }
                    catch (Exception e)
                    {
                        JustEnoughDimensions.logger.warn("Failed to create a temporary dimension marker file '{}'",
                                markerFile.getAbsolutePath(), e);
                    }
                }
            }
        }
    }

    public static File getWorldJEDDataDirectory(File worldDir)
    {
        return new File(new File(worldDir, "data"), Reference.MOD_ID);
    }

    public static File getTemporaryDimensionMarkerFile(File jedDataDir)
    {
        return new File(jedDataDir, "jed_temporary_dimension.txt");
    }

    /**
     * @param worldDir
     * @return true if the given world hasn't yet been created/initialized (no region directory)
     */
    private static boolean isNewWorld(File worldDir)
    {
        File regionDir = new File(worldDir, "region");
        return regionDir.exists() == false;
    }

    public static NBTTagCompound loadWorldInfoFromFile(World world, @Nullable File worldDir)
    {
        if (worldDir == null)
        {
            JustEnoughDimensions.logInfo("WorldFileUtils.loadWorldInfoFromFile(): null worldDir");
            return null;
        }

        File levelFile = new File(worldDir, JED_LEVEL_FILENAME);

        if (levelFile.exists())
        {
            try
            {
                NBTTagCompound nbt = CompressedStreamTools.readCompressed(new FileInputStream(levelFile));
                nbt = world.getMinecraftServer().getDataFixer().process(FixTypes.LEVEL, nbt.getCompoundTag("Data"));
                //FMLCommonHandler.instance().handleWorldDataLoad((SaveHandler) world.getSaveHandler(), info, nbt);
                JustEnoughDimensions.logInfo("WorldFileUtils.loadWorldInfoFromFile(): Read world info from file '{}'", levelFile.getPath());
                return nbt;
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.warn("Exception reading " + levelFile.getPath(), e);
                return null;
            }

            //return SaveFormatOld.loadAndFix(fileLevel, world.getMinecraftServer().getDataFixer(), (SaveHandler) world.getSaveHandler());
        }

        JustEnoughDimensions.logInfo("WorldFileUtils.loadWorldInfoFromFile(): '{}' didn't exist for dimension {}",
                JED_LEVEL_FILENAME, world.provider.getDimension());
        return null;
    }

    private static void saveWorldInfoToFile(World world, @Nullable File worldDir)
    {
        if (worldDir == null)
        {
            JustEnoughDimensions.logger.warn("WorldFileUtils.saveWorldInfoToFile(): No worldDir found");
            return;
        }

        WorldInfo info = world.getWorldInfo();
        info.setBorderSize(world.getWorldBorder().getDiameter());
        info.getBorderCenterX(world.getWorldBorder().getCenterX());
        info.getBorderCenterZ(world.getWorldBorder().getCenterZ());
        info.setBorderSafeZone(world.getWorldBorder().getDamageBuffer());
        info.setBorderDamagePerBlock(world.getWorldBorder().getDamageAmount());
        info.setBorderWarningDistance(world.getWorldBorder().getWarningDistance());
        info.setBorderWarningTime(world.getWorldBorder().getWarningTime());
        info.setBorderLerpTarget(world.getWorldBorder().getTargetSize());
        info.setBorderLerpTime(world.getWorldBorder().getTimeUntilTarget());

        NBTTagCompound rootTag = new NBTTagCompound();
        NBTTagCompound playerNBT = world.getMinecraftServer().getPlayerList().getHostPlayerData();
        rootTag.setTag("Data", info.cloneNBTCompound(playerNBT));

        ThreadedFileIOBase.getThreadedIOInstance().queueIO(() ->
        {
            try
            {
                /*
                if (world.getSaveHandler() instanceof SaveHandler)
                {
                    FMLCommonHandler.instance().handleWorldDataSave((SaveHandler) world.getSaveHandler(), info, rootTag);
                }
                */

                File fileNew = new File(worldDir, JED_LEVEL_FILENAME + "_new");
                File fileOld = new File(worldDir, JED_LEVEL_FILENAME + "_old");
                File fileCurrent = new File(worldDir, JED_LEVEL_FILENAME);
                CompressedStreamTools.writeCompressed(rootTag, new FileOutputStream(fileNew));

                if (fileOld.exists())
                {
                    fileOld.delete();
                }

                fileCurrent.renameTo(fileOld);

                if (fileCurrent.exists())
                {
                    JustEnoughDimensions.logger.error("Failed to rename file '{}' to '{}'", fileCurrent.getAbsolutePath(), fileOld.getAbsolutePath());
                    return false;
                }

                fileNew.renameTo(fileCurrent);

                if (fileNew.exists())
                {
                    JustEnoughDimensions.logger.error("Failed to rename file '{}' to '{}'", fileNew.getAbsolutePath(), fileCurrent.getAbsolutePath());
                    return false;
                }
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.error("WorldFileUtils.saveWorldInfoToFile(): Failed to save world "+
                                                  "info to file for dimension {}", world.provider.getDimension(), e);
            }

            return false;
        });
    }

    public static void saveCustomWorldInfoToFile(World world)
    {
        if (Configs.enableSeparateWorldInfo && world.isRemote == false)
        {
            saveWorldInfoToFile(world, getWorldDirectory(world));
        }
    }
} public class WorldUtils
{
    private static final String JED_RESPAWN_DIM_TAG = "justenoughdimensions:respawndimension";
    //private static Field field_WorldProvider_terrainType;
    //private static Field field_WorldProvider_generatorSettings;
    private static Field field_World_provider = null;
    private static Field field_WorldProvider_biomeProvider = null;
    private static Field field_ChunkProviderServer_chunkGenerator = null;

    static
    {
        try
        {
            //field_WorldProvider_terrainType = ReflectionHelper.findField(WorldProvider.class, "field_76577_b", "terrainType");
            //field_WorldProvider_generatorSettings = ReflectionHelper.findField(WorldProvider.class, "field_82913_c", "generatorSettings");
            field_World_provider                     = ObfuscationReflectionHelper.findField(World.class, "field_73011_w"); // provider
            field_WorldProvider_biomeProvider        = ObfuscationReflectionHelper.findField(WorldProvider.class, "field_76578_c"); // biomeProvider
            field_ChunkProviderServer_chunkGenerator = ObfuscationReflectionHelper.findField(ChunkProviderServer.class, "field_186029_c"); // chunkGenerator
        }
        catch (Exception e)
        {
            JustEnoughDimensions.logger.error("WorldUtils: Reflection failed!!", e);
        }
    }

    public static BlockPos getWorldSpawn(WorldServer world)
    {
        BlockPos spawn = world.getSpawnCoordinate();

        // Only the End has a spawn coordinate
        if (spawn == null)
        {
            spawn = world.getSpawnPoint();
        }

        return spawn;
    }

    public static int getLoadedChunkCount(WorldServer world)
    {
        return world.getChunkProvider().getLoadedChunkCount();
    }

    /**
     * Unloads all empty dimensions (with no chunks loaded)
     * @param tryUnloadChunks if true, then tries to first save and unload all non-player-loaded and non-force-loaded chunks
     * @return the number of dimensions successfully unloaded
     */
    public static int unloadEmptyDimensions(boolean tryUnloadChunks)
    {
        int count = 0;
        Integer[] dims = DimensionManager.getIDs();

        JustEnoughDimensions.logInfo("WorldUtils.unloadEmptyDimensions(): Trying to unload empty dimensions, tryUnloadChunks = {}", tryUnloadChunks);

        for (int dim : dims)
        {
            JustEnoughDimensions.logInfo("WorldUtils.unloadEmptyDimensions(): Trying to unload dimension {}", dim);
            WorldServer world = DimensionManager.getWorld(dim);

            if (world == null)
            {
                continue;
            }

            ChunkProviderServer chunkProviderServer = world.getChunkProvider();
            int loadedCountBefore = chunkProviderServer.getLoadedChunkCount();

            if (tryUnloadChunks && loadedCountBefore > 0)
            {
                JustEnoughDimensions.logInfo("WorldUtils.unloadEmptyDimensions(): Trying to unload chunks for " +
                                             "dimension {}, currently loaded chunks: {}", dim, loadedCountBefore);
                boolean disable = world.disableLevelSaving;
                world.disableLevelSaving = false;

                try
                {
                    // This also tries to unload all chunks that are not loaded by players
                    world.saveAllChunks(true, (IProgressUpdate) null);
                }
                catch (MinecraftException e)
                {
                    JustEnoughDimensions.logger.warn("WorldUtils.unloadEmptyDimensions(): Exception while "+
                                                     "trying to save chunks for dimension {}", dim, e);
                }

                // This would flush the chunks to disk from the AnvilChunkLoader. Probably not what we want to do.
                //world.saveChunkData();

                world.disableLevelSaving = disable;

                // This will unload the dimension, if it unloaded at least one chunk, and it has no loaded chunks anymore
                chunkProviderServer.tick();

                int loadedCountAfter = chunkProviderServer.getLoadedChunkCount();
                JustEnoughDimensions.logInfo("WorldUtils.unloadEmptyDimensions(): Unloaded {} chunks in dimension {}", loadedCountBefore - loadedCountAfter, dim);

                if (loadedCountAfter == 0)
                {
                    JustEnoughDimensions.logInfo("WorldUtils.unloadEmptyDimensions(): Likely unloaded dimension {}", dim);
                    count++;
                }
            }
            else if (chunkProviderServer.getLoadedChunkCount() == 0 &&
                world.provider.getDimensionType().shouldLoadSpawn() == false &&
                ForgeChunkManager.getPersistentChunksFor(world).size() == 0)
            {
                JustEnoughDimensions.logInfo("WorldUtils.unloadEmptyDimensions(): Unloading dimension {}", dim);
                DimensionManager.unloadWorld(world.provider.getDimension());
                count++;
            }
        }

        return count;
    }

    public static boolean removeTemporaryWorldIfApplicable(World world)
    {
        final int dimension = world.provider.getDimension();
        File worldDir = WorldFileUtils.getWorldDirectory(world);
        return removeTemporaryWorldIfApplicable(dimension, world, worldDir, false);
    }

    public static boolean removeTemporaryWorldIfApplicable(int dimension, @Nullable World world, File worldDir, boolean isServerStop)
    {
        DimensionConfigEntry entry = DimensionConfig.instance().getDimensionConfigFor(dimension);

        if (entry != null && entry.isTemporaryDimension() &&
            (dimension != 0 || isServerStop) &&
            worldDir != null && worldDir.exists() &&
            DataTracker.getInstance().getPlayerCountInDimension(dimension) == 0)
        {
            File jedDataDir = WorldFileUtils.getWorldJEDDataDirectory(worldDir);
            File markerFile = WorldFileUtils.getTemporaryDimensionMarkerFile(jedDataDir);

            if (markerFile.exists())
            {
                if (dimension == 0)
                {
                    JustEnoughDimensions.logInfo("Trying to remove a temporary world '{}'", worldDir.getAbsolutePath());
                }
                else
                {
                    JustEnoughDimensions.logInfo("Trying to remove a temporary dimension (DIM {}) from '{}'",
                            dimension, worldDir.getAbsolutePath());
                }

                try
                {
                    if (world instanceof WorldServer)
                    {
                        WorldServer worldServer = (WorldServer) world; 
                        worldServer.getChunkProvider().queueUnloadAll();
                        worldServer.saveAllChunks(true, null);
                        worldServer.flush();
                    }

                    FileUtils.deleteDirectory(worldDir);

                    MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance();
                    String cmd = entry.getPostDimensionDeletionCommand();

                    if (server != null && cmd != null)
                    {
                        server.getCommandManager().executeCommand(server, cmd);
                    }

                    return true;
                }
                catch (Exception e)
                {
                    JustEnoughDimensions.logger.warn("Exception while trying to remove a temporary dimension {}", dimension, e);
                }
            }
        }

        return false;
    }

    public static boolean tryDeleteDimension(int dimension, ICommandSender sender)
    {
        if (dimension != 0)
        {
            MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance();
            World world = server != null ? server.getWorld(dimension) : null;

            if (world == null)
            {
                JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: Could not load dimension {} (to get the directory)", dimension);
                return false;
            }

            if (world.playerEntities.size() > 0)
            {
                JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: There are currently players in dimension {}, can't delete it", dimension);
                return false;
            }

            File dir = WorldFileUtils.getWorldDirectory(world);

            if (dir.exists() == false || dir.isDirectory() == false)
            {
                JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: Failed to get the directory for dimension {}", dimension);
                return false;
            }

            if (world instanceof WorldServer)
            {
                WorldServer worldServer = (WorldServer) world;

                try
                {
                    worldServer.getChunkProvider().queueUnloadAll();
                    worldServer.saveAllChunks(true, null);
                    worldServer.flush();

                    DimensionManager.setWorld(dimension, null, worldServer.getMinecraftServer());
                    FileUtils.deleteDirectory(dir);
                    CommandJED.runBroadcastCommand(sender, "delete-dimension", Integer.valueOf(dimension));

                    JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: Successfully deleted dimension {}", dimension);

                    return true;
                }
                catch (MinecraftException e)
                {
                    JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: Exception while trying to unload dimension {}", dimension, e);
                }
                catch (IOException e)
                {
                    JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: Exception while trying to delete the directory of dimension {}", dimension, e);
                }
            }
            else
            {
                JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: Not a server world?!");
                return false;
            }
        }
        else
        {
            JustEnoughDimensions.logger.warn("WorldUtils.tryDeleteDimension: Can't delete dimension 0, it would delete the entire save");
        }

        return false;
    }

    public static void syncWorldProviderProperties(EntityPlayer player)
    {
        if (player instanceof EntityPlayerMP)
        {
            JustEnoughDimensions.logInfo("WorldUtils.syncWorldProviderProperties: Syncing WorldProvider properties " +
                                         "of dimension {} to player '{}'", player.getEntityWorld().provider.getDimension(), player.getName());
            PacketHandler.INSTANCE.sendTo(new MessageSyncWorldProperties(player.getEntityWorld()), (EntityPlayerMP) player);
        }
    }

    /*
    public static void overrideWorldProviderSettings(World world, WorldProvider provider)
    {
        WorldInfo info = world.getWorldInfo();

        if (info instanceof WorldInfoJED)
        {
            try
            {
                JustEnoughDimensions.logInfo("WorldUtils.overrideWorldProviderSettings(): Trying to override the "+
                                             "WorldType and generatorSettings for dimension {}", provider.getDimension());
                field_WorldProvider_terrainType.set(provider, info.getTerrainType());
                field_WorldProvider_generatorSettings.set(provider, info.getGeneratorOptions());
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.error("WorldUtils.overrideWorldProviderSettings(): Failed to override " +
                                                  "WorldProvider settings for dimension {}", provider.getDimension());
            }
        }
    }
    */

    public static void overrideWorldProviderIfApplicable(World world, boolean tryStealChunkGenerator)
    {
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(world);

        if (props == null)
        {
            return;
        }

        WorldProvider originalWorldProvider = world.provider;
        String worldProviderClassName = props.getChunkGeneratorFactoryWorldProviderClassName();
        IChunkGenerator chunkGenerator = null;
        boolean useFactory = false;

        // If there is a "chunk generator factory" defined, first temporarily override the World Provider with that type,
        // and then steal the ChunkGenerator from that provider and use that after changing to the final World Provider.
        if (worldProviderClassName != null &&
            world.getChunkProvider() instanceof ChunkProviderServer &&
            overrideWorldProvider(world, worldProviderClassName, false))
        {
            chunkGenerator = ((ChunkProviderServer) world.getChunkProvider()).chunkGenerator;
            useFactory = true;
        }

        final int dimension = world.provider.getDimension();
        worldProviderClassName = props.getWorldProviderOverrideClassName();

        if (worldProviderClassName != null)
        {
            overrideWorldProvider(world, worldProviderClassName, false);
        }
        else if (useFactory)
        {
            overrideWorldProvider(dimension, world, originalWorldProvider, dimension == 0);
        }

        if (useFactory && chunkGenerator != null)
        {
            overrideChunkGenerator(dimension, (ChunkProviderServer) world.getChunkProvider(), chunkGenerator);
        }
    }

    private static boolean overrideWorldProvider(World world, String newClassName, boolean isChunkGenFactory)
    {
        Class<? extends WorldProvider> newProviderClass = DimensionTypeEntry.getProviderClass(newClassName);

        if (newProviderClass == null)
        {
            JustEnoughDimensions.logger.warn("WorldUtils.overrideWorldProvider: Failed to create a WorldProvider from name '{}'", newClassName);
            return false;
        }

        final int dim = world.provider.getDimension();

        if (newProviderClass != world.provider.getClass())
        {
            String oldName = world.provider.getClass().getName();

            if (isChunkGenFactory)
            {
                JustEnoughDimensions.logInfo("WorldUtils.overrideWorldProvider: Trying to create a temporary WorldProvider (to create a ChunkGenerator with) of type '{}' in dimension {} with '{}'", oldName, dim, newClassName);
            }
            else
            {
                JustEnoughDimensions.logInfo("WorldUtils.overrideWorldProvider: Trying to override the WorldProvider of type '{}' in dimension {} with '{}'", oldName, dim, newClassName);
            }

            try
            {
                Constructor <? extends WorldProvider> constructor = newProviderClass.getConstructor();
                WorldProvider newProvider = constructor.newInstance();

                overrideWorldProvider(dim, world, newProvider, dim == 0);

                return true;
            }
            catch (Exception e)
            {
            }
        }
        else
        {
            JustEnoughDimensions.logger.warn("WorldUtils.overrideWorldProvider: The WorldProvider in dim '{}' was already of the type '{}'", dim, newClassName);
        }

        return false;
    }

    private static boolean overrideWorldProvider(int dim, World world, WorldProvider newProvider, boolean generatorChangedForOverworld)
    {
        try
        {
            field_World_provider.set(world, newProvider);
            world.provider.setWorld(world);
            world.provider.setDimension(dim);

            JustEnoughDimensions.logInfo("WorldUtils.overrideWorldProvider: Overrode the WorldProvider in dimension {} with '{}'", dim, newProvider.getClass().getName());

            reCreateChunkGenerator(world, generatorChangedForOverworld);

            return true;
        }
        catch (Exception e)
        {
            JustEnoughDimensions.logger.error("WorldUtils.overrideWorldProvider: Failed to override the WorldProvider of dimension {}", dim);
        }

        return false;
    }

    public static void overrideBiomeProvider(World world)
    {
        // For WorldProviderSurfaceJED the BiomeProvider has already been set in WorldProviderSurfaceJED#init()
        if ((world.provider instanceof WorldProviderSurfaceJED) == false)
        {
            int dimension = world.provider.getDimension();
            DimensionConfigEntry entry = DimensionConfig.instance().getDimensionConfigFor(dimension);

            if (entry != null)
            {
                BiomeProvider biomeProvider = null;

                if (entry.getBiome() != null)
                {
                    Biome biome = Biome.REGISTRY.getObject(new ResourceLocation(entry.getBiome()));

                    if (biome != null && ((world.provider.getBiomeProvider() instanceof BiomeProviderSingle) == false ||
                        world.provider.getBiomeProvider().getBiome(BlockPos.ORIGIN) != biome))
                    {
                        biomeProvider = new BiomeProviderSingle(biome);

                        JustEnoughDimensions.logInfo("WorldUtils.overrideBiomeProvider: Overriding the BiomeProvider of dimension {} with '{}' using the biome '{}'",
                                dimension, biomeProvider.getClass().getName(), entry.getBiome());
                    }
                }
                else if (entry.getBiomeProvider() != null)
                {
                    JustEnoughDimensions.logInfo("WorldUtils.overrideBiomeProvider: Trying to create a BiomeProvider for dimension {} from the class name '{}'",
                            dimension, entry.getBiomeProvider());
                    biomeProvider = createBiomeProviderForName(entry.getBiomeProvider(), world);
                }
                else if (entry.shouldUseNormalBiomes() && world.provider.getBiomeProvider() instanceof BiomeProviderSingle)
                {
                    biomeProvider = new BiomeProvider(world.getWorldInfo());

                    JustEnoughDimensions.logInfo("WorldUtils.overrideBiomeProvider: Overriding the BiomeProvider of dimension {} with '{}'",
                            dimension, biomeProvider.getClass().getName());
                }

                if (biomeProvider != null)
                {
                    try
                    {
                        field_WorldProvider_biomeProvider.set(world.provider, biomeProvider);
                    }
                    catch (Exception e)
                    {
                        JustEnoughDimensions.logger.error("Failed to override the BiomeProvider of dimension {}", dimension);
                    }
                }
            }
        }
    }

    @Nullable
    public static BiomeProvider createBiomeProviderForName(String name, World world)
    {
        try
        {
            @SuppressWarnings("unchecked")
            Class<? extends BiomeProvider> clazz = (Class<? extends BiomeProvider>) Class.forName(name);
            BiomeProvider provider = clazz.getConstructor(WorldInfo.class).newInstance(world.getWorldInfo());

            try
            {
                // This is for Painted Biomes compatibility as of PB 0.6.0
                Method method = clazz.getDeclaredMethod("init", World.class);
                method.invoke(provider, world);
            }
            catch (Exception e)
            {
                // ignore
            }

            return provider;
        }
        catch (Exception e)
        {
            JustEnoughDimensions.logger.warn("WorldUtils.createBiomeProviderForName(): Failed to create a BiomeProvider from class name '{}' ({})", name, e.getClass().getName());
        }

        return null;
    }

    public static void reCreateChunkGenerator(World world, boolean generatorChangedForOverworld)
    {
        if (world instanceof WorldServer && world.getChunkProvider() instanceof ChunkProviderServer)
        {
            final int dimension = world.provider.getDimension();
            WorldInfo info = world.getWorldInfo();
            World overworld = DimensionManager.getWorld(0);

            if (dimension == 0 && generatorChangedForOverworld == false)
            {
                JustEnoughDimensions.logInfo("No need to re-create the ChunkProvider in dimension {}", dimension);
                return;
            }
            else if (dimension != 0 && overworld != null)
            {
                WorldInfo infoOverworld = overworld.getWorldInfo();

                if (infoOverworld.getTerrainType() == info.getTerrainType() &&
                    infoOverworld.isMapFeaturesEnabled() == info.isMapFeaturesEnabled() &&
                    infoOverworld.getGeneratorOptions().equals(info.getGeneratorOptions()) &&
                    infoOverworld.getSeed() == info.getSeed())
                {
                    JustEnoughDimensions.logInfo("No need to re-create the ChunkProvider in dimension {}", dimension);
                    return;
                }
            }

            // This sets the new WorldType, generatorOptions and creates the BiomeProvider based on the seed for the WorldProvider
            world.provider.setWorld(world);

            ChunkProviderServer chunkProviderServer = (ChunkProviderServer) world.getChunkProvider();
            IChunkGenerator newChunkGenerator = world.provider.createChunkGenerator();

            overrideChunkGenerator(dimension, chunkProviderServer, newChunkGenerator);
        }
    }

    private static boolean overrideChunkGenerator(int dimension, ChunkProviderServer chunkProviderServer, IChunkGenerator newChunkGenerator)
    {
        if (newChunkGenerator == null)
        {
            JustEnoughDimensions.logger.warn("Failed to re-create the ChunkProvider for dimension {}", dimension);
            return false;
        }

        try
        {
            field_ChunkProviderServer_chunkGenerator.set(chunkProviderServer, newChunkGenerator);

            JustEnoughDimensions.logInfo("WorldUtils.reCreateChunkProvider: Re-created/overwrote the ChunkProvider " +
                                         "(of type '{}') in dimension {} with '{}'",
                    chunkProviderServer.chunkGenerator.getClass().getName(), dimension, newChunkGenerator.getClass().getName());

            return true;
        }
        catch (Exception e)
        {
            JustEnoughDimensions.logger.warn("Failed to re-create the ChunkProvider for dimension {} with {}",
                    dimension, newChunkGenerator.getClass().getName(), e);
        }

        return false;
    }

    public static void centerWorldBorderIfApplicable(World world)
    {
        int dimension = world.provider.getDimension();
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(dimension);
        WorldBorder border = world.getWorldBorder();
        BlockPos spawn = world.getSpawnPoint();

        if (props != null && props.shouldWorldBorderBeCenteredOnSpawn() &&
            (spawn.getX() != border.getCenterX() || spawn.getZ() != border.getCenterZ()))
        {
            border.setCenter(spawn.getX(), spawn.getZ());
            JustEnoughDimensions.logInfo("WorldUtils.centerWorldBorderIfApplicable: Moved the WorldBorder of dimension {} to the spawn point @ {}", dimension, spawn);
        }
    }

    public static void findAndSetWorldSpawnIfApplicable(World world)
    {
        if (WorldFileUtils.jedLevelFileExists(world) == false)
        {
            findAndSetWorldSpawn(world);
        }
    }

    private static void findAndSetWorldSpawn(World world)
    {
        final int dimension = world.provider.getDimension();
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(dimension);
        SpawnPointSearch searchType = props != null ? props.getSpawnPointSearchType() : null;
        NBTTagCompound nbt = WorldInfoUtils.getWorldInfoTag(world, dimension, false, false);
        @Nullable BlockPos newSpawn = null;

        if (nbt.hasKey("SpawnX") && nbt.hasKey("SpawnZ"))
        {
            if (nbt.hasKey("SpawnY"))
            {
                newSpawn = new BlockPos(nbt.getInteger("SpawnX"), nbt.getInteger("SpawnY"), nbt.getInteger("SpawnZ"));
                JustEnoughDimensions.logInfo("WorldUtils.findAndSetWorldSpawn: An exact spawn point {} defined in the " +
                                             "dimension config for dimension {}, skipping the search", newSpawn, dimension);
                generateFallbackSpawnBlockIfEnabled(world, newSpawn);
            }
            else
            {
                newSpawn = new BlockPos(nbt.getInteger("SpawnX"), 72, nbt.getInteger("SpawnZ"));
                JustEnoughDimensions.logInfo("WorldUtils.findAndSetWorldSpawn: A spawn point XZ-location 'x = {}, z = {}' defined in the " +
                                             "dimension config for dimension {}, searching for a suitable y-location",
                                             newSpawn.getX(), newSpawn.getZ(), dimension);
                newSpawn = getSuitableSpawnBlockInColumn(world, newSpawn, true, true);
            }
        }
        else if ((dimension != 0 && Configs.enableSeparateWorldInfo &&
                  DimensionConfig.instance().useCustomWorldInfoFor(dimension)) ||
                 (searchType != null && searchType.getType() != SpawnPointSearch.Type.NONE))
        {
            JustEnoughDimensions.logInfo("WorldUtils.findAndSetWorldSpawn: Trying to find a world spawn for dimension {}...", dimension);
            newSpawn = findSuitableSpawnpoint(world, searchType);
        }

        if (newSpawn != null)
        {
            if (world.getSpawnPoint().equals(newSpawn) == false)
            {
                world.setSpawnPoint(newSpawn);
                JustEnoughDimensions.logInfo("WorldUtils.findAndSetWorldSpawn: Set the world spawnpoint of dimension {} to {}", dimension, newSpawn);
            }

            WorldBorder border = world.getWorldBorder();

            if (border.contains(newSpawn) == false)
            {
                border.setCenter(newSpawn.getX(), newSpawn.getZ());
                JustEnoughDimensions.logInfo("WorldUtils.findAndSetWorldSpawn: Moved the WorldBorder of dimension {} " +
                                             "to the world's spawn, because the spawn was outside the border", dimension);
            }
        }
    }

    @Nonnull
    public static BlockPos findSuitableSpawnpoint(World world)
    {
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(world.provider.getDimension());
        SpawnPointSearch searchType = props != null ? props.getSpawnPointSearchType() : null;

        return findSuitableSpawnpoint(world, searchType);
    }

    @Nonnull
    public static BlockPos findSuitableSpawnpoint(World world, @Nullable SpawnPointSearch searchType)
    {
        WorldProvider provider = world.provider;
        BlockPos pos;

        if (searchType != null)
        {
            JustEnoughDimensions.logInfo("WordlUtils.findSuitableSpawnpoint: Using a customized spawn point search type '{}' for DIM {}",
                    searchType.toString(), provider.getDimension());

            switch (searchType.getType())
            {
                case OVERWORLD:
                    return findOverworldSpawnpoint(world, searchType);
                case CAVERN:
                    return findCavernSpawnpoint(world, searchType);
                case NONE:
                    JustEnoughDimensions.logInfo("WorldUtils.findSuitableSpawnpoint: SpawnPointSearch.Type == NONE, using the existing spawn point in DIM {}", provider.getDimension());
                    return world.getSpawnPoint();
            }
        }

        // Likely end type dimensions
        if (provider.getDimensionType() == DimensionType.THE_END ||
            provider instanceof WorldProviderEnd)
        {
            pos = provider.getSpawnCoordinate();

            if (pos == null)
            {
                pos = getSuitableSpawnBlockInColumn(world, new BlockPos(0, 72, 0), true, true);
            }
        }
        // Likely nether type dimensions
        else if (provider.getDimensionType() == DimensionType.NETHER ||
                 provider.isNether() ||
                 provider instanceof WorldProviderHell ||
                 provider instanceof WorldProviderHellJED)
        {
            pos = findCavernSpawnpoint(world, searchType);
        }
        else if (world.getWorldInfo().getTerrainType() == WorldType.DEBUG_ALL_BLOCK_STATES)
        {
            pos = BlockPos.ORIGIN.up(64);
        }
        // Mostly overworld type dimensions
        else
        {
            pos = findOverworldSpawnpoint(world, searchType);
        }

        generateFallbackSpawnBlockIfEnabled(world, pos);

        return pos;
    }

    @Nonnull
    private static BlockPos findCavernSpawnpoint(World world, @Nullable SpawnPointSearch searchType)
    {
        @Nullable final Integer yRangeMax = searchType != null ? searchType.getMaxY() : null;
        final int minY = searchType != null && searchType.getMinY() != null ? Math.max(1, searchType.getMinY()) : 30;
        Random random = new Random(world.getSeed());
        int x = 0;
        int z = 0;
        int iterations = 0;

        while (iterations < 200)
        {
            Chunk chunk = world.getChunk(x >> 4, z >> 4);
            int maxY = 120;

            if (yRangeMax != null)
            {
                maxY = yRangeMax.intValue();
            }

            BlockPos pos = new BlockPos(x, maxY, z);

            while (pos.getY() >= minY)
            {
                IBlockState stateBelow = chunk.getBlockState(pos.down(2));
                IBlockState state1 = chunk.getBlockState(pos.down(1));
                IBlockState state2 = chunk.getBlockState(pos);

                if (state1.getBlock().isAir(state1, world, pos) &&
                    state2.getBlock().isAir(state2, world, pos) &&
                    stateBelow.getMaterial().blocksMovement())
                {
                    return pos.down();
                }

                pos = pos.down();
            }

            x += random.nextInt(32) - random.nextInt(32);
            z += random.nextInt(32) - random.nextInt(32);
            iterations++;
        }

        BlockPos pos = new BlockPos(0, 72, 0);
        JustEnoughDimensions.logger.warn("Unable to find a cavern type spawn point for dimension {}, defaulting to x = {}, y = {}, z = {}",
                world.provider.getDimension(), pos.getX(), pos.getY(), pos.getZ());

        generateFallbackSpawnBlockIfEnabled(world, pos);

        return pos;
    }

    @Nonnull
    private static BlockPos findOverworldSpawnpoint(World world, @Nullable SpawnPointSearch searchType)
    {
        WorldProvider provider = world.provider;
        BiomeProvider biomeProvider = provider.getBiomeProvider();
        List<Biome> list = biomeProvider.getBiomesToSpawnIn();
        Random random = new Random(world.getSeed());
        int x = 8;
        int z = 8;
        final int minY = searchType != null && searchType.getMinY() != null ? Math.max(1, searchType.getMinY()) : 1;
        @Nullable final Integer yRangeMax = searchType != null ? searchType.getMaxY() : null;

        // This will not generate chunks, but only check the biome ID from the genBiomes.getInts() output
        BlockPos pos = biomeProvider.findBiomePosition(0, 0, 512, list, random);

        if (pos != null)
        {
            x = pos.getX();
            z = pos.getZ();
        }
        else
        {
            JustEnoughDimensions.logger.warn("Unable to find spawn biome for dimension {}", provider.getDimension());
        }

        int iterations = 0;

        // Note: This will generate chunks! Also note that the returned position might
        // still end up inside a tree or something, since decoration hasn't necessarily been done yet.
        while (iterations < 1000)
        {
            if (provider.canCoordinateBeSpawn(x, z))
            {
                Chunk chunk = world.getChunk(x >> 4, z >> 4);
                int maxY = chunk.getTopFilledSegment() + 15 + 1;

                if (yRangeMax != null)
                {
                    maxY = Math.min(yRangeMax.intValue(), maxY);
                }

                pos = new BlockPos(x, maxY, z);

                while (pos.getY() >= minY)
                {
                    if (isSuitableSpawnPosition(world, chunk, pos, false))
                    {
                        return pos;
                    }

                    pos = pos.down();
                }
            }

            x += random.nextInt(32) - random.nextInt(32);
            z += random.nextInt(32) - random.nextInt(32);

            iterations++;
        }

        pos = new BlockPos(x, 72, z);

        return getSuitableSpawnBlockInColumn(world, pos, true, true);
    }

    /**
     * Tries to find a suitable spawn position in the given XZ-column. If none are found, then the
     * original input position is returned.
     * @param world
     * @param originalPos
     * @param leanient if true, then leaves and foliage are allowed for the block below,
     * and fluids are allowed for the two blocks at the spawn location
     * @return
     */
    @Nonnull
    public static BlockPos getSuitableSpawnBlockInColumn(World world, BlockPos originalPos, boolean leanient)
    {
        return getSuitableSpawnBlockInColumn(world, originalPos, leanient, false);
    }

    @Nonnull
    public static BlockPos getSuitableSpawnBlockInColumn(World world, BlockPos originalPos, boolean leanient, boolean generateFallbackBlock)
    {
        Chunk chunk = world.getChunk(originalPos);
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(world.provider.getDimension());
        SpawnPointSearch searchType = props != null ? props.getSpawnPointSearchType() : null;
        @Nullable final Integer yRangeMax = searchType != null ? searchType.getMaxY() : null;
        final int minY = searchType != null && searchType.getMinY() != null ? Math.max(1, searchType.getMinY()) : 1;
        int maxY = chunk.getTopFilledSegment() + 15 + 1;

        if (yRangeMax != null)
        {
            maxY = MathHelper.clamp(maxY, minY, yRangeMax.intValue());
        }

        BlockPos posTop = new BlockPos(originalPos.getX(), maxY, originalPos.getZ());
        BlockPos pos = posTop;

        while (pos.getY() >= minY)
        {
            if (isSuitableSpawnPosition(world, chunk, pos, leanient))
            {
                return pos;
            }

            pos = pos.down();
        }

        if (generateFallbackBlock)
        {
            generateFallbackSpawnBlockIfEnabled(world, originalPos);
        }

        return originalPos;
    }

    /**
     * Check if the given position is suitable for the spawn point.
     * @param world
     * @param chunk
     * @param pos
     * @param leanient if true, then leaves and foliage are allowed for the block below,
     * and fluids are allowed for the two blocks at the spawn location
     * @return
     */
    private static boolean isSuitableSpawnPosition(World world, Chunk chunk, BlockPos pos, boolean leanient)
    {
        IBlockState state =    chunk.getBlockState(pos.down(1));
        Material materialUp1 = chunk.getBlockState(pos).getMaterial();
        Material materialUp2 = chunk.getBlockState(pos.up(1)).getMaterial();

        return state.getMaterial().blocksMovement() &&
               (leanient || state.getBlock().isLeaves(state, world, pos) == false) &&
               (leanient || state.getBlock().isFoliage(world, pos) == false) &&
               materialUp1.blocksMovement() == false && (leanient || materialUp1.isLiquid() == false) &&
               materialUp2.blocksMovement() == false && (leanient || materialUp2.isLiquid() == false);
    }

    private static void generateFallbackSpawnBlockIfEnabled(World world, BlockPos spawnPos)
    {
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(world.provider.getDimension());

        // Fallback - avoid falling to the void
        if (props != null && props.generateFallbackSpawnBlock() && world.isAirBlock(spawnPos.down()))
        {
            int dim = world.provider.getDimension();
            JustEnoughDimensions.logInfo("Didn't find suitable spawn location in dim {}, generating a glass block under the spawn point", dim);
            world.setBlockState(spawnPos.down(), Blocks.GLASS.getDefaultState());
        }
    }

    public static void createBonusChest(World world)
    {
        WorldInfo info = world.getWorldInfo();
        WorldGeneratorBonusChest gen = new WorldGeneratorBonusChest();

        for (int i = 0; i < 10; ++i)
        {
            int x = info.getSpawnX() + world.rand.nextInt(6) - world.rand.nextInt(6);
            int z = info.getSpawnZ() + world.rand.nextInt(6) - world.rand.nextInt(6);
            BlockPos pos = world.getTopSolidOrLiquidBlock(new BlockPos(x, 0, z)).up();

            if (gen.generate(world, world.rand, pos))
            {
                break;
            }
        }
    }

    private static void loadChunks(World world, int chunkX1, int chunkZ1, int chunkX2, int chunkZ2)
    {
        final int xStart = Math.min(chunkX1, chunkX2);
        final int zStart = Math.min(chunkZ1, chunkZ2);
        final int xEnd = Math.max(chunkX1, chunkX2);
        final int zEnd = Math.max(chunkZ1, chunkZ2);
        final int dimension = world.provider.getDimension();

        JustEnoughDimensions.logInfo("Attempting to load chunks [{},{}] to [{},{}] in dimension {}", xStart, zStart, xEnd, zEnd, dimension);

        for (int x = xStart; x <= xEnd; x++)
        {
            for (int z = zStart; z <= zEnd; z++)
            {
                if (world.isBlockLoaded(new BlockPos(x << 4, 0, z << 4)) == false)
                {
                    //JustEnoughDimensions.logInfo("Loading chunk [{},{}] in dimension {}", x, z, dimension);
                    world.getChunk(x, z);
                }
            }
        }
    }

    private static void loadChunks(World world, BlockPos pos, BlockPos size, int expand)
    {
        int cx1 = (pos.getX() - expand) >> 4;
        int cz1 = (pos.getZ() - expand) >> 4;
        int cx2 = (pos.getX() + size.getX() + expand) >> 4;
        int cz2 = (pos.getZ() + size.getZ() + expand) >> 4;

        WorldUtils.loadChunks(world, cx1, cz1, cx2, cz2);
    }

    public static void placeSpawnStructureIfApplicable(World world)
    {
        final int dimension = world.provider.getDimension();
        final boolean isDimensionInit = WorldFileUtils.jedLevelFileExists(world) == false;
        DimensionConfigEntry entry = DimensionConfig.instance().getDimensionConfigFor(dimension);

        if (isDimensionInit && entry != null)
        {
            JsonObject spawnStructureJson = entry.getSpawnStructureJson();

            if (spawnStructureJson != null)
            {
                StructurePlacement placement = StructurePlacement.fromJson(spawnStructureJson);

                if (placement != null)
                {
                    MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance();
                    StructureType type = StructureType.fromFileName(placement.getFile().getName());
                    BlockPos pos = world.getSpawnPoint().add(placement.getOffset());
                    boolean success = false;

                    if (type == StructureType.STRUCTURE)
                    {
                        JustEnoughDimensions.logInfo("WorldUtils.placeSpawnStructure: Trying to place the spawn structure at {}", pos);
                        success = tryPlaceVanillaStructure(server, world, pos, placement);
                    }
                    else if (type == StructureType.SCHEMATIC)
                    {
                        JustEnoughDimensions.logInfo("WorldUtils.placeSpawnStructure: Trying to place the spawn schematic at {}", pos);
                        success = tryPlaceSchematic(server, world, pos, placement);
                    }
                    else if (type == StructureType.INVALID)
                    {
                        JustEnoughDimensions.logger.warn("WorldUtils.placeSpawnStructure: Invalid structure type '{}'", placement.getFile().getAbsolutePath());
                    }

                    if (success)
                    {
                        JustEnoughDimensions.logInfo("WorldUtils.placeSpawnStructure: Successfully placed the spawn structure in dimension {}", dimension);
                    }
                    else
                    {
                        JustEnoughDimensions.logger.warn("WorldUtils.placeSpawnStructure: Failed to place the spawn structure in dimension {}", dimension);
                    }
                }
                else
                {
                    JustEnoughDimensions.logger.warn("WorldUtils.placeSpawnStructure: Spawn structure defined but failed to load in dimension {}", dimension);
                }
            }
        }
    }

    private static boolean tryPlaceVanillaStructure(MinecraftServer server, World world, BlockPos pos, StructurePlacement placement)
    {
        File file = placement.getFile();

        if (file.exists() == false)
        {
            return false;
        }

        String name = file.getName();
        name = name.substring(0, name.lastIndexOf("."));
        Template template = getTemplateManager().getTemplate(server, new ResourceLocation(name));

        if (template != null)
        {
            PlacementSettings placementSettings = new PlacementSettings();
            placementSettings.setRotation(placement.getRotation());
            placementSettings.setMirror(placement.getMirror());
            placementSettings.setReplacedBlock(Blocks.STRUCTURE_VOID);

            if (placement.isCentered())
            {
                BlockPos size = Template.transformedBlockPos(placementSettings, template.getSize());
                pos = pos.add(-(size.getX() / 2), 0, -(size.getZ() / 2));
            }

            WorldUtils.loadChunks(world, pos, template.getSize(), placement.getLoadRangeAround());
            template.addBlocksToWorld(world, pos, placementSettings, 0x12);

            return true;
        }

        return false;
    }

    private static boolean tryPlaceSchematic(MinecraftServer server, World world, BlockPos pos, StructurePlacement placement)
    {
        File file = new File(StructurePlacement.getStructureDirectory(), placement.getFile().getName());
        Schematic schematic = Schematic.createFromFile(file);

        if (schematic != null)
        {
            PlacementSettings placementSettings = new PlacementSettings();
            placementSettings.setRotation(placement.getRotation());
            placementSettings.setMirror(placement.getMirror());
            placementSettings.setReplacedBlock(Blocks.STRUCTURE_VOID);
            placementSettings.setIgnoreEntities(false);

            if (placement.isCentered())
            {
                BlockPos size = Template.transformedBlockPos(placementSettings, schematic.getSize());
                pos = pos.add(-(size.getX() / 2), 0, -(size.getZ() / 2));
            }

            WorldUtils.loadChunks(world, pos, schematic.getSize(), placement.getLoadRangeAround());
            schematic.placeSchematicToWorld(world, pos, placementSettings, 2);

            return true;
        }

        return false;
    }

    private static TemplateManager getTemplateManager()
    {
        return new TemplateManager(StructurePlacement.getStructureDirectory().toString(), FMLCommonHandler.instance().getDataFixer());
    }

    /**
     * This will set the spawnDimension field on the player, if the current world has
     * JED world properties, and they say that respawning there should be allowed, but the
     * WorldProvider says it's not. This should support respawning in the same,
     * normally not respawnable dimension, even without a JED WorldProvider.
     * @param player
     */
    public static void setupRespawnDimension(EntityPlayer player)
    {
        World world = player.getEntityWorld();
        final int dim = world.provider.getDimension();
        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(dim);

        if (world.provider.canRespawnHere() == false &&
            props != null &&
            props.canRespawnHere() != null &&
            props.canRespawnHere().booleanValue())
        {
            JustEnoughDimensions.logInfo("WorldUtils.setupRespawnDimension: Setting the respawn dimension of player '{}' to: {}", player.getName(), dim);
            player.setSpawnDimension(dim);
            player.addTag(JED_RESPAWN_DIM_TAG);
        }
        else if (player.getTags().contains(JED_RESPAWN_DIM_TAG))
        {
            JustEnoughDimensions.logInfo("WorldUtils.setupRespawnDimension: Removing the respawn dimension data from player '{}'", player.getName());
            player.setSpawnDimension(null);
            player.removeTag(JED_RESPAWN_DIM_TAG);
        }
    }

    public static boolean canBlockFreeze(World world, BlockPos pos, boolean noWaterAdj)
    {
        if (pos.getY() >= 0 && pos.getY() < 256 && world.getLightFor(EnumSkyBlock.BLOCK, pos) < 10)
        {
            IBlockState state = world.getBlockState(pos);
            Block block = state.getBlock();

            if ((block == Blocks.WATER || block == Blocks.FLOWING_WATER) && state.getValue(BlockLiquid.LEVEL).intValue() == 0)
            {
                if (noWaterAdj == false)
                {
                    return true;
                }

                return isWater(world, pos.west()) == false ||
                       isWater(world, pos.east()) == false ||
                       isWater(world, pos.north()) == false ||
                       isWater(world, pos.south()) == false;
            }
        }

        return false;
    }

    public static boolean isWater(World world, BlockPos pos)
    {
        return world.getBlockState(pos).getMaterial() == Material.WATER;
    }

    public static boolean canSnowAt(World world, BlockPos pos)
    {
        if (pos.getY() >= 0 && pos.getY() < 256 && world.getLightFor(EnumSkyBlock.BLOCK, pos) < 10)
        {
            IBlockState state = world.getBlockState(pos);

            return state.getBlock().isAir(state, world, pos) && Blocks.SNOW_LAYER.canPlaceBlockAt(world, pos);
        }

        return false;
    }
} public class WorldProviderEndJED extends WorldProviderEnd implements IWorldProviderJED
{
    private static final Field field_dragonFightManager = ObfuscationReflectionHelper.findField(WorldProviderEnd.class, "field_186064_g"); // dragonFightManager

    protected JEDWorldProperties properties;
    protected VoidTeleportData voidTeleport = null;
    protected VoidTeleportData skyTeleport = null;
    protected int teleportCounter;
    private boolean worldInfoSet;
    private boolean shouldSkipSpawnSearch;

    @Override
    public boolean getWorldInfoHasBeenSet()
    {
        return this.worldInfoSet;
    }

    @Override
    public boolean getShouldSkipSpawnSearch()
    {
        return this.shouldSkipSpawnSearch;
    }

    @Override
    public void setDimension(int dimension)
    {
        super.setDimension(dimension);

        this.properties = JEDWorldProperties.getOrCreateProperties(dimension);

        // This method gets called the first time from DimensionManager.createProviderFor(),
        // at which time the world hasn't been set yet. The second call comes from the WorldServer
        // constructor, and there the world has just been set.
        if (this.world != null && this.getWorldInfoHasBeenSet() == false)
        {
            BlockPos spawnOrig = this.getSpawnPoint();

            WorldInfoUtils.loadAndSetCustomWorldInfo(this.world);
            this.shouldSkipSpawnSearch = spawnOrig.equals(this.getSpawnPoint()) == false;

            this.hasSkyLight = this.properties.getHasSkyLight() != null ? this.properties.getHasSkyLight().booleanValue() : this.hasSkyLight;
            this.worldInfoSet = true;

            this.skyTeleport =  VoidTeleportData.fromJson(this.properties.getNestedObject("sky_teleport"), this.getDimension());
            this.voidTeleport = VoidTeleportData.fromJson(this.properties.getNestedObject("void_teleport"), this.getDimension());

            if (this.world.isRemote == false && this.properties.getDisableDragon())
            {
                NBTTagCompound tag = this.world.getWorldInfo().getDimensionData(dimension);

                try
                {
                    JustEnoughDimensions.logInfo("Trying to override the DragonFightManager in dimension {}...", dimension);
                    field_dragonFightManager.set(this, new DragonFightManagerDummy((WorldServer) this.world, tag.getCompoundTag("DragonFight")));
                    JustEnoughDimensions.logInfo("Overrode the DragonFightManager with '{}'", DragonFightManagerDummy.class.getName());
                }
                catch (Exception e)
                {
                    JustEnoughDimensions.logger.warn("Failed to override the DragonFightManager in dimension {}", dimension);
                }
            }
        }
    }

    @Override
    public IChunkGenerator createChunkGenerator()
    {
        IChunkGenerator generator = WorldProviderJED.createChunkGeneratorInstance(this.world, this);

        if (generator != null)
        {
            return generator;
        }

        if (this.properties.getDisableEndSpikes())
        {
            JustEnoughDimensions.logInfo("Using ChunkGenerator '{}' in dimension {}", ChunkGeneratorEndJED.class.getName(), this.getDimension());
            return new ChunkGeneratorEndJED(this.world, this.world.getWorldInfo().isMapFeaturesEnabled(), this.world.getSeed(), this.getSpawnCoordinate());
        }
        else
        {
            return super.createChunkGenerator();
        }
    }

    @Override
    public DimensionType getDimensionType()
    {
        DimensionType type = null;

        try
        {
            type = DimensionManager.getProviderType(this.getDimension());
        }
        catch (IllegalArgumentException e)
        {
        }

        return type != null ? type : super.getDimensionType();
    }

    @Override
    public void setJEDProperties(JEDWorldProperties properties)
    {
        this.properties = properties;
        ClientUtils.setRenderersFrom(this, this.properties.getFullJEDProperties());
    }

    @Override
    public WorldSleepResult canSleepAt(EntityPlayer player, BlockPos pos)
    {
        WorldSleepResult val = this.properties.canSleepHere();
        return val != null ? val : super.canSleepAt(player, pos);
    }

    @Override
    public boolean canRespawnHere()
    {
        return WorldProviderJED.getBooleanOrDefault(this.properties.canRespawnHere(), false);
    }

    @Override
    public int getRespawnDimension(EntityPlayerMP player)
    {
        if (this.properties.getRespawnDimension() != null)
        {
            return this.properties.getRespawnDimension();
        }
        else
        {
            return this.canRespawnHere() ? this.getDimension() : 0;
        }
    }

    @Override
    public void onWorldUpdateEntities()
    {
        super.onWorldUpdateEntities();

        if (++this.teleportCounter >= this.properties.getVoidTeleportInterval())
        {
            VoidTeleport.tryVoidTeleportEntities(this.world, this.voidTeleport, this.skyTeleport);
            this.teleportCounter = 0;
        }
    }

    @Override
    public float[] getLightBrightnessTable()
    {
        if (this.properties.getCustomLightBrightnessTable() != null)
        {
            return this.properties.getCustomLightBrightnessTable();
        }

        return super.getLightBrightnessTable();
    }

    public int getDayCycleLength()
    {
        return this.properties.getDayLength() + this.properties.getNightLength();
    }

    @Override
    public int getMoonPhase(long worldTime)
    {
        long cycleLength = this.getDayCycleLength();
        return (int)(worldTime / cycleLength % 8L + 8L) % 8;
    }

    @Override
    public void setWorldTime(long time)
    {
        time = WorldProviderJED.getNewWorldTime(time, this.getWorldTime(), this.properties);
        super.setWorldTime(time);
    }

    @Override
    public float calculateCelestialAngle(long worldTime, float partialTicks)
    {
        if (this.properties.getUseCustomDayCycle())
        {
            return WorldProviderJED.calculateCelestialAngle(this.world, this.properties, this.getDayCycleLength(), worldTime, partialTicks);
        }
        else if (this.properties.getUseCustomCelestialAngleRange())
        {
            return WorldProviderJED.getCustomCelestialAngleValue(this.world, this.properties, this.getDayCycleLength(), worldTime, partialTicks);
        }

        return super.calculateCelestialAngle(worldTime, partialTicks);
    }

    @Override
    public boolean canDropChunk(int x, int z)
    {
        return this.world.isSpawnChunk(x, z) == false || this.getDimensionType().shouldLoadSpawn() == false;
    }

    @Override
    public void setAllowedSpawnTypes(boolean allowHostileIn, boolean allowPeacefulIn)
    {
        // This fixes the custom dimensions being unable to spawn hostile mobs if the overworld is set to Peaceful
        // See Minecraft#runTick(), the call to this.world.setAllowedSpawnTypes(),
        // and also MinecraftServer#setDifficultyForAllWorlds()
        boolean allowHostile = this.world.getWorldInfo().getDifficulty() != EnumDifficulty.PEACEFUL;
        boolean allowPeaceful = allowPeacefulIn;

        JEDWorldProperties props = JEDWorldProperties.getPropertiesIfExists(this.getDimension());

        if (props != null)
        {
            Boolean hostiles = props.canSpawnHostiles();
            Boolean peaceful = props.canSpawnPeacefulMobs();

            if (hostiles != null)
            {
                allowHostile = hostiles.booleanValue();
            }

            if (peaceful != null)
            {
                allowPeaceful = peaceful.booleanValue();
            }
        }

        super.setAllowedSpawnTypes(allowHostile, allowPeaceful);
    }

    @Override
    public boolean canCoordinateBeSpawn(int x, int z)
    {
        Boolean ignore = this.properties.ignoreSpawnSuitability();

        if (ignore != null && ignore.booleanValue())
        {
            return true;
        }

        return super.canCoordinateBeSpawn(x, z);
    }

    @Override
    public boolean canDoLightning(net.minecraft.world.chunk.Chunk chunk)
    {
        return WorldProviderJED.getBooleanOrDefault(this.properties.canDoLightning(), false);
    }

    @Override
    public boolean canDoRainSnowIce(net.minecraft.world.chunk.Chunk chunk)
    {
        return WorldProviderJED.getBooleanOrDefault(this.properties.canDoRainSnowIce(), false);
    }

    @Override
    public boolean canBlockFreeze(BlockPos pos, boolean noWaterAdj)
    {
        Boolean val = this.properties.canDoRainSnowIce();

        if (val != null)
        {
            return val.booleanValue() && WorldUtils.canBlockFreeze(this.world, pos, noWaterAdj);
        }

        return super.canBlockFreeze(pos, noWaterAdj);
    }

    @Override
    public boolean canSnowAt(BlockPos pos, boolean checkLight)
    {
        Boolean val = this.properties.canDoRainSnowIce();

        if (val != null)
        {
            return val.booleanValue() && WorldUtils.canSnowAt(this.world, pos);
        }

        return super.canSnowAt(pos, checkLight);
    }

    @Override
    public boolean doesWaterVaporize()
    {
        return WorldProviderJED.getBooleanOrDefault(this.properties.doesWaterVaporize(), super.doesWaterVaporize());
    }

    @Override
    public boolean doesXZShowFog(int x, int z)
    {
        return WorldProviderJED.getBooleanOrDefault(this.properties.getHasXZFog(), super.doesXZShowFog(x, z));
    }

    @Override
    public boolean isSurfaceWorld()
    {
        return WorldProviderJED.getBooleanOrDefault(this.properties.isSurfaceWorld(), false);
    }

    @Override
    public int getAverageGroundLevel()
    {
        return WorldProviderJED.getIntegerOrDefault(this.properties.getAverageGroundLevel(), super.getAverageGroundLevel());
    }

    @Override
    public double getHorizon()
    {
        return WorldProviderJED.getDoubleOrDefault(this.properties.getHorizon(), super.getHorizon());
    }

    @Override
    public double getMovementFactor()
    {
        return WorldProviderJED.getDoubleOrDefault(this.properties.getMovementFactor(), 1.0D);
    }

    @Override
    public float getSunBrightness(float partialTicks)
    {
        return WorldProviderJED.getFloatOrDefault(this.properties.getSunBrightness(), super.getSunBrightness(partialTicks));
    }

    @Override
    public float getSunBrightnessFactor(float partialTicks)
    {
        return WorldProviderJED.getFloatOrDefault(this.properties.getSunBrightnessFactor(), super.getSunBrightnessFactor(partialTicks));
    }

    @Override
    public boolean shouldClientCheckLighting()
    {
        return WorldProviderJED.getBooleanOrDefault(this.properties.shouldClientCheckLight(), true);
    }

    @Override
    public boolean shouldMapSpin(String entity, double x, double y, double z)
    {
        return this.isSurfaceWorld() == false;
    }

    @SideOnly(Side.CLIENT)
    @Override
    @Nullable
    public MusicType getMusicType()
    {
        MusicType music = ClientUtils.getMusicTypeFromProperties(this.properties);
        return music != null ? music : null;
    }

    @Override
    public boolean isSkyColored()
    {
        return this.properties.getSkyColor() != null;
    }

    @Override
    public float getCloudHeight()
    {
        return (float) this.properties.getCloudHeight();
    }

    @Override
    public Vec3d getSkyColor(Entity entity, float partialTicks)
    {
        Vec3d skyColor = this.properties.getSkyColor();

        if (skyColor == null)
        {
            return super.getSkyColor(entity, partialTicks);
        }

        float f1 = MathHelper.cos(this.world.getCelestialAngle(partialTicks) * ((float)Math.PI * 2F)) * 2.0F + 0.5F;
        f1 = MathHelper.clamp(f1, 0.0F, 1.0F);
        int x = MathHelper.floor(entity.posX);
        int y = MathHelper.floor(entity.posY);
        int z = MathHelper.floor(entity.posZ);
        BlockPos blockpos = new BlockPos(x, y, z);
        int blendColour = net.minecraftforge.client.ForgeHooksClient.getSkyBlendColour(this.world, blockpos);
        float r = (float)((blendColour >> 16 & 255) / 255.0F * skyColor.x);
        float g = (float)((blendColour >>  8 & 255) / 255.0F * skyColor.y);
        float b = (float)((blendColour       & 255) / 255.0F * skyColor.z);
        r = r * f1;
        g = g * f1;
        b = b * f1;

        return new Vec3d(r, g, b);
    }

    @Override
    public Vec3d getFogColor(float celestialAngle, float partialTicks)
    {
        Vec3d fogColor = this.properties.getFogColor();

        if (fogColor == null)
        {
            return super.getFogColor(celestialAngle, partialTicks);
        }

        float f = MathHelper.cos(celestialAngle * ((float)Math.PI * 2F)) * 2.0F + 0.5F;
        f = MathHelper.clamp(f, 0.0F, 1.0F);
        float r = (float) fogColor.x;
        float g = (float) fogColor.y;
        float b = (float) fogColor.z;
        r = r * (f * 0.94F + 0.06F);
        g = g * (f * 0.94F + 0.06F);
        b = b * (f * 0.91F + 0.09F);
        return new Vec3d(r, g, b);
    }

    @Override
    public Vec3d getCloudColor(float partialTicks)
    {
        Vec3d cloudColor = this.properties.getCloudColor();
        return cloudColor != null ? cloudColor : super.getCloudColor(partialTicks);
    }
} public class WorldProviderHellJED extends WorldProviderJED
{
    @Override
    public void init()
    {
        this.doesWaterVaporize = true;
        this.nether = true;

        // Initialize the default values (used if the properties haven't been set via the config)
        this.canRespawnHere = false;
        this.isSurfaceWorld = false;
        this.hasXZFog = true;
        this.movementFactor = 8.0D;

        this.setBiomeProviderIfConfigured();

        if (this.biomeProvider == null)
        {
            this.biomeProvider = new BiomeProviderSingle(Biomes.HELL);
        }
    }

    @Override
    public float calculateCelestialAngle(long worldTime, float partialTicks)
    {
        if (this.properties.getUseCustomDayCycle())
        {
            return super.calculateCelestialAngle(worldTime, partialTicks);
        }
        else if (this.properties.getUseCustomCelestialAngleRange())
        {
            return getCustomCelestialAngleValue(this.world, this.properties, this.getDayCycleLength(), worldTime, partialTicks);
        }

        return 0.5F;
    }

    @SideOnly(Side.CLIENT)
    @Override
    @Nullable
    public MusicType getMusicType()
    {
        MusicType music = ClientUtils.getMusicTypeFromProperties(this.properties);
        return music != null ? music : MusicType.NETHER;
    }

    @Override
    public Vec3d getFogColor(float celestialAngle, float partialTicks)
    {
        if (this.properties.getFogColor() == null)
        {
            return new Vec3d(0.2, 0.03, 0.03);
        }

        return super.getFogColor(1f, partialTicks);
    }

    @Override
    protected void generateLightBrightnessTable()
    {
        for (int i = 0; i <= 15; ++i)
        {
            float f1 = 1.0F - (float)i / 15.0F;
            this.lightBrightnessTable[i] = (1.0F - f1) / (f1 * 3.0F + 1.0F) * 0.9F + 0.1F;
        }
    }

    @Override
    public IChunkGenerator createChunkGenerator()
    {
        IChunkGenerator generator = createChunkGeneratorInstance(this.world, this);
        return generator != null ? generator : new ChunkGeneratorHell(this.world, this.world.getWorldInfo().isMapFeaturesEnabled(), this.world.getSeed());
    }
} public class WorldProviderSurfaceJED extends WorldProviderJED
{
}
import java.io.File;
import java.util.EnumMap;
import java.util.Random;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.minecraft.world.DimensionType;
import net.minecraft.world.chunk.storage.AnvilSaveConverter;
import net.minecraftforge.common.DimensionManager;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.SidedProxy;
import net.minecraftforge.fml.common.event.FMLFingerprintViolationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.event.FMLServerAboutToStartEvent;
import net.minecraftforge.fml.common.event.FMLServerStartedEvent;
import net.minecraftforge.fml.common.event.FMLServerStartingEvent;
import net.minecraftforge.fml.common.event.FMLServerStoppedEvent;
import net.minecraftforge.fml.common.network.FMLEmbeddedChannel;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import net.minecraftforge.fml.relauncher.Side;
import fi.dy.masa.justenoughdimensions.command.CommandJED;
import fi.dy.masa.justenoughdimensions.command.CommandTeleportJED;
import fi.dy.masa.justenoughdimensions.config.Configs;
import fi.dy.masa.justenoughdimensions.config.DimensionConfig;
import fi.dy.masa.justenoughdimensions.config.DimensionTypeEntry;
import fi.dy.masa.justenoughdimensions.event.DataTracker;
import fi.dy.masa.justenoughdimensions.network.DimensionSyncChannelHandler;
import fi.dy.masa.justenoughdimensions.network.PacketHandler;
import fi.dy.masa.justenoughdimensions.proxy.CommonProxy;
import fi.dy.masa.justenoughdimensions.reference.Reference;
import fi.dy.masa.justenoughdimensions.util.PlayerInventoryStorage;
import fi.dy.masa.justenoughdimensions.util.world.WorldBorderUtils;
import fi.dy.masa.justenoughdimensions.util.world.WorldFileUtils;
import fi.dy.masa.justenoughdimensions.util.world.WorldUtils;
import fi.dy.masa.justenoughdimensions.world.WorldProviderEndJED;
import fi.dy.masa.justenoughdimensions.world.WorldProviderHellJED;
import fi.dy.masa.justenoughdimensions.world.WorldProviderSurfaceJED;
package fi.dy.masa.justenoughdimensions;


@Mod(modid = Reference.MOD_ID, name = Reference.MOD_NAME, version = Reference.MOD_VERSION, certificateFingerprint = Reference.FINGERPRINT,
    guiFactory = "fi.dy.masa.justenoughdimensions.config.JustEnoughDimensionsGuiFactory",
    updateJSON = "https://raw.githubusercontent.com/maruohon/justenoughdimensions/master/update.json",
    acceptedMinecraftVersions = "1.12")
public class JustEnoughDimensions
{
    @Mod.Instance(Reference.MOD_ID)
    public static JustEnoughDimensions instance;

    @SidedProxy(clientSide = Reference.PROXY_CLIENT, serverSide = Reference.PROXY_SERVER)
    public static CommonProxy proxy;

    public static final Random RAND = new Random();
    public static final Logger logger = LogManager.getLogger(Reference.MOD_ID);
    public static EnumMap<Side, FMLEmbeddedChannel> channels;

    private static File lastWorldDir;

    @Mod.EventHandler
    public void preInit(FMLPreInitializationEvent event)
    {
        instance = this;

        Configs.loadConfigsFromMainConfigFile(event.getModConfigurationDirectory());
        PacketHandler.init();