@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();
        proxy.registerEventHandlers();
        channels = NetworkRegistry.INSTANCE.newChannel("JEDChannel", DimensionSyncChannelHandler.instance);

        DimensionConfig.init(event.getModConfigurationDirectory());
    }

    @Mod.EventHandler
    public void onServerAboutToStart(FMLServerAboutToStartEvent event)
    {
        File worldDir = new File(((AnvilSaveConverter) event.getServer().getActiveAnvilConverter()).savesDirectory, event.getServer().getFolderName());
        Configs.loadConfigsFromPerWorldConfigIfEnabled(worldDir);
        DimensionConfig.instance().readDimensionConfig(worldDir);
        PlayerInventoryStorage.INSTANCE.setWorldDir(worldDir);
        DataTracker.getInstance().readFromDisk(worldDir);
        lastWorldDir = worldDir;

        // This needs to be here so that we are able to override existing dimensions before
        // they get loaded during server start.
        // But on the other hand we don't want to register the rest of the dimensions yet,
        // otherwise they would be considered 'static dimensions' and get loaded on server start.
        DimensionConfig.instance().doEarlyDimensionRegistrations();

        // Handle template world copying for the overworld before the server starts
        WorldFileUtils.copyTemplateWorldIfApplicable(0, worldDir);
    }

    @Mod.EventHandler
    public void serverStarting(FMLServerStartingEvent event)
    {
        event.registerServerCommand(new CommandJED());
        event.registerServerCommand(new CommandTeleportJED());

        if (event.getServer().getAllowNether() == false && DimensionManager.getStaticDimensionIDs().length > 1)
        {
            logger.warn("******************************************************************************************");
            logger.warn("**  The 'disable-nether' option is currently disabled in 'server.properties'.");
            logger.warn("**  Just a friendly FYI, because this has been the cause of some weird and");
            logger.warn("**  hard to pinpoint issues for some users, before it was found out that the");
            logger.warn("**  above mentioned option was disabled in the hopes of simply disabling just the Nether.");
            logger.warn("**  That's not what that option does.");
            logger.warn("**  It actually prevents ANY DIMENSIONS except for dimension 0 from being ticked at all!!");
            logger.warn("**  Thus it basically makes any dimensions except for the overworld unusable,");
            logger.warn("**  and any player trying to go to other dimensions will get stuck in limbo.");
            logger.warn("******************************************************************************************");
        }
    }

    @Mod.EventHandler
    public void serverStarted(FMLServerStartedEvent event)
    {
        // This removes the WorldBorder listeners that WorldServerMulti adds from other dimensions to the overworld border.
        // Thus this needs to be called after the static dimensions have loaded, ie. from this event specifically.
        WorldBorderUtils.removeDefaultBorderListeners();

        // Register our custom (non-override) dimensions. This is in this event so that our custom dimensions
        // won't get auto-loaded on server start as 'static' dimensions.
        DimensionConfig.instance().registerNonOverrideDimensions();
    }

    @Mod.EventHandler
    public void serverStopped(FMLServerStoppedEvent event)
    {
        WorldUtils.removeTemporaryWorldIfApplicable(0, null, lastWorldDir, true);
        lastWorldDir = null;

        // Unregister custom dimensions. This is only useful in single player,
        // so that all the dimensions won't immediately load when joining a world again.
        DimensionConfig.instance().unregisterCustomDimensions();

        // (Re-)read the global configs after closing a world
        Configs.loadConfigsFromGlobalConfigFile();
    }

    public static void logInfo(String message, Object... params)
    {
        if (Configs.enableLoggingInfo)
        {
            logger.info(message, params);
        }
        else
        {
            logger.trace(message, params);
        }
    }

    @Mod.EventHandler
    public void onFingerPrintViolation(FMLFingerprintViolationEvent event)
    {
        // Not running in a dev environment
        if (event.isDirectory() == false)
        {
            logger.warn("*********************************************************************************************");
            logger.warn("*****                                    WARNING                                        *****");
            logger.warn("*****                                                                                   *****");
            logger.warn("*****   The signature of the mod file '{}' does not match the expected fingerprint!     *****", event.getSource().getName());
            logger.warn("*****   This might mean that the mod file has been tampered with!                       *****");
            logger.warn("*****   If you did not download the mod {} directly from Curse/CurseForge,       *****", Reference.MOD_NAME);
            logger.warn("*****   or using one of the well known launchers, and you did not                       *****");
            logger.warn("*****   modify the mod file at all yourself, then it's possible,                        *****");
            logger.warn("*****   that it may contain malware or other unwanted things!                           *****");
            logger.warn("*********************************************************************************************");
        }
    }

    // Register some default DimensionType entries early on, to try to avoid some issues
    // with mods that use a switch() with the DimensionType values (Optifine...).
    // Note that these still have some potential issues, the suffix being one.
    static
    {
        DimensionTypeEntry.cache(DimensionType.register("JED Surface",           "_dim7891", 7891, WorldProviderSurfaceJED.class,    false));
        DimensionTypeEntry.cache(DimensionType.register("JED Surface 0",         "",            0, WorldProviderSurfaceJED.class,    false));
        DimensionTypeEntry.cache(DimensionType.register("JED Surface Loaded 0",  "_dim0",       0, WorldProviderSurfaceJED.class,    true));
        DimensionTypeEntry.cache(DimensionType.register("JED Hell",              "_dim-1",     -1, WorldProviderHellJED.class,       false));
        DimensionTypeEntry.cache(DimensionType.register("JED End",               "_dim1",       1, WorldProviderEndJED.class,        false));
    }
} 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 MessageSyncDimensions implements IMessage
{
    private List<DimensionConfigEntry> dimensions = new ArrayList<DimensionConfigEntry>();

    public MessageSyncDimensions()
    {
    }

    public MessageSyncDimensions(Collection<DimensionConfigEntry> entries)
    {
        this.dimensions.addAll(entries);
    }

    @Override
    public void fromBytes(ByteBuf buf)
    {
        int count = buf.readInt();
        this.dimensions.clear();

        for (int i = 0 ; i < count ; i++)
        {
            DimensionConfigEntry entry = DimensionConfigEntry.fromByteBuf(buf);

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

    @Override
    public void toBytes(ByteBuf buf)
    {
        buf.writeInt(this.dimensions.size());

        for (DimensionConfigEntry entry : this.dimensions)
        {
            entry.writeToByteBuf(buf);
        }
    }

    public static class Handler implements IMessageHandler<MessageSyncDimensions, IMessage>
    {
        @Override
        public IMessage onMessage(final MessageSyncDimensions message, MessageContext ctx)
        {
            if (ctx.side != Side.CLIENT)
            {
                JustEnoughDimensions.logger.error("Wrong side in MessageSyncDimensions: " + ctx.side);
                return null;
            }

            Minecraft mc = FMLClientHandler.instance().getClient();
            if (mc == null)
            {
                JustEnoughDimensions.logger.error("Minecraft was null in MessageSyncDimensions");
                return null;
            }

            mc.addScheduledTask(new Runnable()
            {
                public void run()
                {
                    processMessage(message);
                }
            });

            return null;
        }

        protected void processMessage(final MessageSyncDimensions message)
        {
            String str = DimensionSyncPacket.registerDimensions(message.dimensions);
            JustEnoughDimensions.logInfo("MessageSyncDimensions: Registered dimensions: '" + str + "'");
        }
    }
} 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 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 ConfigFileUtils
{
    public static void createDirIfNotExists(File dir)
    {
        if (dir.exists() == false)
        {
            try
            {
                dir.mkdirs();
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.warn("Failed to create the directory '{}'", dir.getAbsolutePath(), e);
            }
        }
    }

    public static void tryCopyOrMoveConfigIfMissingOrOlder(File fileToReplace, File replacementFile, FileAction action, ConfigComparator configComparator)
    {
        if (replacementFile.exists() && replacementFile.isFile() && replacementFile.canRead() &&
            (fileToReplace.exists() == false || configComparator.shouldReplace(fileToReplace, replacementFile)))
        {
            try
            {
                if (action == FileAction.COPY)
                {
                    JustEnoughDimensions.logger.info("Copying the file '{}' to the new location '{}'",
                            replacementFile.getAbsolutePath(), fileToReplace.getAbsolutePath());

                    Files.copy(replacementFile, fileToReplace);
                }
                else if (action == FileAction.MOVE)
                {
                    JustEnoughDimensions.logger.info("Moving the file '{}' to the new location '{}'",
                            replacementFile.getAbsolutePath(), fileToReplace.getAbsolutePath());

                    Files.move(replacementFile, fileToReplace);
                }
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.warn("Failed to {} the file '{}' to the new location '{}'",
                        action.name().toLowerCase(), replacementFile.getAbsolutePath(), fileToReplace.getAbsolutePath(), e);
            }
        }
    }

    public static abstract class ConfigComparator
    {
        public abstract boolean shouldReplace(File fileToReplace, File replacementFile);
    }

    public enum FileAction
    {
        COPY,
        MOVE;
    }
} public static abstract class ConfigComparator
{
    public abstract boolean shouldReplace(File fileToReplace, File replacementFile);
} public enum FileAction
{
    COPY,
    MOVE;
} public class JEDJsonUtils
{
    public static final Gson GSON = new GsonBuilder().create();
    public static final Gson GSON_PRETTY = new GsonBuilder().setPrettyPrinting().create();

    public static boolean hasBoolean(JsonObject obj, String name)
    {
        JsonElement el = obj.get(name);

        if (el != null && el.isJsonPrimitive())
        {
            try
            {
                el.getAsBoolean();
                return true;
            }
            catch (Exception e) {}
        }

        return false;
    }

    public static boolean hasInteger(JsonObject obj, String name)
    {
        JsonElement el = obj.get(name);

        if (el != null && el.isJsonPrimitive())
        {
            try
            {
                el.getAsInt();
                return true;
            }
            catch (Exception e) {}
        }

        return false;
    }

    public static boolean hasFloat(JsonObject obj, String name)
    {
        JsonElement el = obj.get(name);

        if (el != null && el.isJsonPrimitive())
        {
            try
            {
                el.getAsFloat();
                return true;
            }
            catch (Exception e) {}
        }

        return false;
    }

    public static boolean hasDouble(JsonObject obj, String name)
    {
        JsonElement el = obj.get(name);

        if (el != null && el.isJsonPrimitive())
        {
            try
            {
                el.getAsDouble();
                return true;
            }
            catch (Exception e) {}
        }

        return false;
    }

    public static boolean hasString(JsonObject obj, String name)
    {
        JsonElement el = obj.get(name);

        if (el != null && el.isJsonPrimitive())
        {
            try
            {
                el.getAsString();
                return true;
            }
            catch (Exception e) {}
        }

        return false;
    }

    public static boolean hasObject(JsonObject obj, String name)
    {
        JsonElement el = obj.get(name);

        if (el != null && el.isJsonObject())
        {
            return true;
        }

        return false;
    }

    public static boolean hasArray(JsonObject obj, String name)
    {
        JsonElement el = obj.get(name);

        if (el != null && el.isJsonArray())
        {
            return true;
        }

        return false;
    }

    public static String getStringOrDefault(JsonObject obj, String name, String defaultValue, boolean allowEmpty)
    {
        if (obj.has(name) && obj.get(name).isJsonPrimitive())
        {
            try
            {
                String str = obj.get(name).getAsString();
                return allowEmpty == false && StringUtils.isBlank(str) ? defaultValue : str;
            }
            catch (Exception e) {}
        }

        return defaultValue;
    }

    public static byte getByteOrDefault(JsonObject obj, String name, byte defaultValue)
    {
        if (obj.has(name) && obj.get(name).isJsonPrimitive())
        {
            try
            {
                return obj.get(name).getAsByte();
            }
            catch (Exception e) {}
        }

        return defaultValue;
    }

    public static int getIntegerOrDefault(JsonObject obj, String name, int defaultValue)
    {
        if (obj.has(name) && obj.get(name).isJsonPrimitive())
        {
            try
            {
                return obj.get(name).getAsInt();
            }
            catch (Exception e) {}
        }

        return defaultValue;
    }

    public static float getFloatOrDefault(JsonObject obj, String name, float defaultValue)
    {
        if (obj.has(name) && obj.get(name).isJsonPrimitive())
        {
            try
            {
                return obj.get(name).getAsFloat();
            }
            catch (Exception e) {}
        }

        return defaultValue;
    }

    public static double getDoubleOrDefault(JsonObject obj, String name, double defaultValue)
    {
        if (obj.has(name) && obj.get(name).isJsonPrimitive())
        {
            try
            {
                return obj.get(name).getAsDouble();
            }
            catch (Exception e) {}
        }

        return defaultValue;
    }

    public static boolean getBooleanOrDefault(JsonObject obj, String name, boolean defaultValue)
    {
        if (obj.has(name) && obj.get(name).isJsonPrimitive())
        {
            try
            {
                return obj.get(name).getAsBoolean();
            }
            catch (Exception e) {}
        }

        return defaultValue;
    }

    public static boolean getBoolean(JsonObject obj, String name)
    {
        return getBooleanOrDefault(obj, name, false);
    }

    public static byte getByte(JsonObject obj, String name)
    {
        return getByteOrDefault(obj, name, (byte) 0);
    }

    public static int getInteger(JsonObject obj, String name)
    {
        return getIntegerOrDefault(obj, name, 0);
    }

    public static float getFloat(JsonObject obj, String name)
    {
        return getFloatOrDefault(obj, name, 0F);
    }

    public static double getDouble(JsonObject obj, String name)
    {
        return getDoubleOrDefault(obj, name, 0D);
    }

    public static String getString(JsonObject obj, String name)
    {
        return getStringOrDefault(obj, name, null, true);
    }

    @Nullable
    public static JsonObject getOrCreateNestedObject(JsonObject parent, String key)
    {
        return getNestedObject(parent, key, true);
    }

    @Nullable
    public static JsonObject getNestedObject(JsonObject parent, String key, boolean create)
    {
        if (parent.has(key) == false || parent.get(key).isJsonObject() == false)
        {
            if (create == false)
            {
                return null;
            }

            JsonObject obj = new JsonObject();
            parent.add(key, obj);
            return obj;
        }
        else
        {
            return parent.get(key).getAsJsonObject();
        }
    }

    public static Vec3d getVec3dOrDefault(JsonObject obj, String arrayName, Vec3d defaultValue)
    {
        if (obj.has(arrayName) && obj.get(arrayName).isJsonArray())
        {
            JsonArray arr = obj.get(arrayName).getAsJsonArray();

            try
            {
                Vec3d vec = new Vec3d(arr.get(0).getAsDouble(), arr.get(1).getAsDouble(), arr.get(2).getAsDouble());
                return vec;
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.warn("Failed to parse a Vec3d value '{}' from JSON", arrayName, e);
            }
        }

        return defaultValue;
    }

    // https://stackoverflow.com/questions/29786197/gson-jsonobject-copy-value-affected-others-jsonobject-instance
    @Nullable
    public static JsonObject deepCopy(@Nullable JsonObject jsonObject)
    {
        if (jsonObject != null)
        {
            JsonObject result = new JsonObject();

            for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet())
            {
                result.add(entry.getKey(), deepCopy(entry.getValue()));
            }

            return result;
        }

        return null;
    }

    @Nonnull
    public static JsonArray deepCopy(@Nonnull JsonArray jsonArray)
    {
        JsonArray result = new JsonArray();

        for (JsonElement e : jsonArray)
        {
            result.add(deepCopy(e));
        }

        return result;
    }

    @Nonnull
    public static JsonElement deepCopy(@Nonnull JsonElement jsonElement)
    {
        if (jsonElement.isJsonPrimitive() || jsonElement.isJsonNull())
        {
            return jsonElement; // these are immutable anyway
        }
        else if (jsonElement.isJsonObject())
        {
            return deepCopy(jsonElement.getAsJsonObject());
        }
        else if (jsonElement.isJsonArray())
        {
            return deepCopy(jsonElement.getAsJsonArray());
        }
        else
        {
            throw new UnsupportedOperationException("Unsupported element: " + jsonElement);
        }
    }

    /**
     * Converts the given JsonElement into its String representation
     * @param element
     * @return
     */
    public static String serialize(@Nonnull JsonElement element)
    {
        return GSON.toJson(element);
    }

    @Nullable
    public static JsonElement deserialize(String input) throws JsonParseException, JsonSyntaxException
    {
        JsonParser parser = new JsonParser();
        return parser.parse(input);
    }

    @Nullable
    public static JsonElement parseJsonFile(File file)
    {
        if (file != null && file.exists() && file.isFile() && file.canRead())
        {
            String fileName = file.getAbsolutePath();

            try
            {
                JsonParser parser = new JsonParser();
                return parser.parse(new FileReader(file));
            }
            catch (Exception e)
            {
                JustEnoughDimensions.logger.error("Failed to parse the JSON file '{}'", fileName, e);
            }
        }

        return null;
    }
} public class JEDWorldProperties
{
    private static final Map<Integer, JEDWorldProperties> PROPERTIES = new HashMap<>();
    private static JEDWorldProperties clientProperties = new JEDWorldProperties();

    @Nullable
    private JsonObject fullJEDTag;
    @Nullable
    private JsonObject colorData;
    @Nullable
    private JsonObject perBiomeFogData;

    private boolean disableDawnDuskColors;
    private boolean disableDragon;
    private boolean disableEndSpikes;
    private boolean dontAdvanceWeatherWhenSleeping;
    private boolean forceGameMode;
    private boolean generateFallbackSpawnBlock;
    private boolean useCustomCelestialAngleRange;
    private boolean useCustomDayCycle;
    private boolean useCustomDayTimeRange;
    private boolean worldBorderCenteredOnSpawn;
    private float celestialAngleMin = 0.0f;
    private float celestialAngleMax = 1.0f;
    private int cloudHeight = 128;
    private int dayCycleIncrement = 24000;
    private int dayLength = 12000;
    private int dayTimeMax = 24000 - 1;
    private int dayTimeMin = 0;
    private int nightLength = 12000;
    private int skyDisableFlags;
    private int skyRenderType;
    private int voidTeleportInterval = 40;

    private float[] customLightBrightnessTable = null;
    private boolean hasPerBiomeFog = false;
    private final IdentityHashMap<Biome, Boolean> foggyBiomes = new IdentityHashMap<>();

    private WorldProvider.WorldSleepResult canSleepHere = null;
    private Boolean canDoLightning = null;
    private Boolean canDoRainSnowIce = null;
    private Boolean canRespawnHere = null;
    private Boolean canSpawnHostiles = null;
    private Boolean canSpawnPeacefulMobs = null;
    private Boolean hasSkyLight = null;
    private Boolean hasXZFog = null;
    private Boolean ignoreSpawnSuitability = null;
    private Boolean isSurfaceWorld = null;
    private Boolean shouldClientCheckLight = null;
    private Boolean waterVaporizes = null;

    private Integer averageGroundLevel = null;
    private Integer respawnDimension = null;

    private Double horizon = null;
    private Double movementFactor = null;

    private Float moonScale = null;
    private Float skyBlendRatio = null;
    private Float skyColorLightBlendRatio = null;
    private Float sunBrightnessFactor = null;
    private Float sunBrightness = null;
    private Float sunScale = null;

    private Vec3d cloudColor = null;
    private Vec3d fogColor = null;
    private Vec3d moonColor = null;
    private Vec3d skyColor = null;
    private Vec3d sunColor = null;

    private String cloudRenderer = null;
    private String skyRenderer = null;
    private String weatherRenderer = null;
    private String playerInventoryGroup = PlayerInventoryStorage.DEFAULT_PLAYER_INVENTORY_GROUP;

    private String musicType = null;
    private String worldProviderOverrideClass = null;
    private String chunkGeneratorFactoryWorldProviderClassName = null;

    private SpawnPointSearch spawnPointSearchType = null;

    @Nullable
    public static JEDWorldProperties getPropertiesIfExists(World world)
    {
        JEDWorldProperties props = getPropertiesIfExists(world.provider.getDimension());

        if (props == null && world.isRemote)
        {
            props = clientProperties;
        }

        return props;
    }

    @Nullable
    public static JEDWorldProperties getPropertiesIfExists(int dimension)
    {
        return PROPERTIES.get(dimension);
    }

    /**
     * Gets the properties for the requested dimension.
     * If there are no properties for that dimension, then a new default instance is returned.
     * NOTE: If the default instance is created and returned, it is NOT added to the map.
     * @param dimension
     * @return the properties for the requested dimension, or a new default instance (which is NOT added to the map!)
     */
    public static JEDWorldProperties getOrCreateProperties(int dimension)
    {
        JEDWorldProperties props = PROPERTIES.get(dimension);

        if (props == null)
        {
            props = new JEDWorldProperties();
        }

        return props;
    }

    public static JEDWorldProperties createAndSetPropertiesForDimension(int dimension, @Nullable JsonObject obj)
    {
        if (obj != null)
        {
            JEDWorldProperties props = new JEDWorldProperties(obj);
            PROPERTIES.put(dimension, props);

            return props;
        }

        return null;
    }

    public static void setClientProperties(@Nullable JsonObject obj)
    {
        clientProperties = obj != null ? new JEDWorldProperties(obj) : new JEDWorldProperties();
    }

    public static JEDWorldProperties getClientProperties()
    {
        return clientProperties;
    }

    public static void removePropertiesFrom(int dimension)
    {
        PROPERTIES.remove(dimension);
    }

    public static void clearWorldProperties()
    {
        PROPERTIES.clear();
    }

    private JEDWorldProperties()
    {
    }

    private JEDWorldProperties(@Nonnull JsonObject obj)
    {
        this.fullJEDTag = JEDJsonUtils.deepCopy(obj);
        this.colorData = JEDJsonUtils.getNestedObject(obj, "Colors", false);
        this.perBiomeFogData = JEDJsonUtils.getNestedObject(obj, "FoggyBiomes", false);

        if (JEDJsonUtils.hasBoolean(obj, "CanDoLightning"))                 { this.canDoLightning                   = JEDJsonUtils.getBoolean(obj, "CanDoLightning"); }
        if (JEDJsonUtils.hasBoolean(obj, "CanDoRainSnowIce"))               { this.canDoRainSnowIce                 = JEDJsonUtils.getBoolean(obj, "CanDoRainSnowIce"); }
        if (JEDJsonUtils.hasBoolean(obj, "CanRespawnHere"))                 { this.canRespawnHere                   = JEDJsonUtils.getBoolean(obj, "CanRespawnHere"); }
        if (JEDJsonUtils.hasBoolean(obj, "CanSpawnHostiles"))               { this.canSpawnHostiles                 = JEDJsonUtils.getBoolean(obj, "CanSpawnHostiles"); }
        if (JEDJsonUtils.hasBoolean(obj, "CanSpawnPeacefulMobs"))           { this.canSpawnPeacefulMobs             = JEDJsonUtils.getBoolean(obj, "CanSpawnPeacefulMobs"); }
        if (JEDJsonUtils.hasBoolean(obj, "CustomDayCycle"))                 { this.useCustomDayCycle                = JEDJsonUtils.getBoolean(obj, "CustomDayCycle"); }
        if (JEDJsonUtils.hasBoolean(obj, "DisableDawnDuskColors"))          { this.disableDawnDuskColors            = JEDJsonUtils.getBoolean(obj, "DisableDawnDuskColors"); }
        if (JEDJsonUtils.hasBoolean(obj, "DisableEnderDragon"))             { this.disableDragon                    = JEDJsonUtils.getBoolean(obj, "DisableEnderDragon"); }
        if (JEDJsonUtils.hasBoolean(obj, "DisableEndSpikes"))               { this.disableEndSpikes                 = JEDJsonUtils.getBoolean(obj, "DisableEndSpikes"); }
        if (JEDJsonUtils.hasBoolean(obj, "DontAdvanceWeatherWhenSleeping")) { this.dontAdvanceWeatherWhenSleeping   = JEDJsonUtils.getBoolean(obj, "DontAdvanceWeatherWhenSleeping"); }
        if (JEDJsonUtils.hasBoolean(obj, "ForceGameMode"))                  { this.forceGameMode                    = JEDJsonUtils.getBoolean(obj, "ForceGameMode"); }
        if (JEDJsonUtils.hasBoolean(obj, "GenerateFallbackSpawnBlock"))     { this.generateFallbackSpawnBlock       = JEDJsonUtils.getBoolean(obj, "GenerateFallbackSpawnBlock"); }
        if (JEDJsonUtils.hasBoolean(obj, "HasSkyLight"))                    { this.hasSkyLight                      = JEDJsonUtils.getBoolean(obj, "HasSkyLight"); }
        if (JEDJsonUtils.hasBoolean(obj, "HasXZFog"))                       { this.hasXZFog                         = JEDJsonUtils.getBoolean(obj, "HasXZFog"); }
        if (JEDJsonUtils.hasBoolean(obj, "IgnoreSpawnSuitability"))         { this.ignoreSpawnSuitability           = JEDJsonUtils.getBoolean(obj, "IgnoreSpawnSuitability"); }
        if (JEDJsonUtils.hasBoolean(obj, "IsSurfaceWorld"))                 { this.isSurfaceWorld                   = JEDJsonUtils.getBoolean(obj, "IsSurfaceWorld"); }
        if (JEDJsonUtils.hasBoolean(obj, "ShouldClientCheckLight"))         { this.shouldClientCheckLight           = JEDJsonUtils.getBoolean(obj, "ShouldClientCheckLight"); }
        if (JEDJsonUtils.hasBoolean(obj, "WaterVaporizes"))                 { this.waterVaporizes                   = JEDJsonUtils.getBoolean(obj, "WaterVaporizes"); }
        if (JEDJsonUtils.hasBoolean(obj, "WorldBorderCenteredOnSpawn"))     { this.worldBorderCenteredOnSpawn       = JEDJsonUtils.getBoolean(obj, "WorldBorderCenteredOnSpawn"); }
        
        if (JEDJsonUtils.hasInteger(obj, "DayLength"))                      { this.dayLength                        = JEDJsonUtils.getInteger(obj, "DayLength"); }
        if (JEDJsonUtils.hasInteger(obj, "NightLength"))                    { this.nightLength                      = JEDJsonUtils.getInteger(obj, "NightLength"); }
        if (JEDJsonUtils.hasInteger(obj, "DayCycleIncrement"))              { this.dayCycleIncrement                = JEDJsonUtils.getInteger(obj, "DayCycleIncrement"); }
        if (JEDJsonUtils.hasInteger(obj, "CloudHeight"))                    { this.cloudHeight                      = JEDJsonUtils.getInteger(obj, "CloudHeight"); }
        if (JEDJsonUtils.hasInteger(obj, "SkyRenderType"))                  { this.skyRenderType                    = JEDJsonUtils.getInteger(obj, "SkyRenderType"); }
        if (JEDJsonUtils.hasInteger(obj, "SkyDisableFlags"))                { this.skyDisableFlags                  = JEDJsonUtils.getInteger(obj, "SkyDisableFlags"); }
        if (JEDJsonUtils.hasInteger(obj, "RespawnDimension"))               { this.respawnDimension                 = JEDJsonUtils.getInteger(obj, "RespawnDimension"); }
        if (JEDJsonUtils.hasInteger(obj, "AverageGroundLevel"))             { this.averageGroundLevel               = JEDJsonUtils.getInteger(obj, "AverageGroundLevel"); }
        if (JEDJsonUtils.hasInteger(obj, "VoidTeleportInterval"))           { this.voidTeleportInterval             = JEDJsonUtils.getInteger(obj, "VoidTeleportInterval"); }

        if (JEDJsonUtils.hasDouble(obj, "Horizon"))                         { this.horizon                          = JEDJsonUtils.getDouble(obj, "Horizon"); }
        if (JEDJsonUtils.hasDouble(obj, "MovementFactor"))                  { this.movementFactor                   = JEDJsonUtils.getDouble(obj, "MovementFactor"); }

        if (JEDJsonUtils.hasFloat(obj, "MoonScale"))                        { this.moonScale                        = JEDJsonUtils.getFloat(obj, "MoonScale"); }
        if (JEDJsonUtils.hasFloat(obj, "SkyBlend"))                         { this.skyBlendRatio                    = JEDJsonUtils.getFloat(obj, "SkyBlend"); }
        if (JEDJsonUtils.hasFloat(obj, "SkyLightBlend"))                    { this.skyColorLightBlendRatio          = JEDJsonUtils.getFloat(obj, "SkyLightBlend"); }
        if (JEDJsonUtils.hasFloat(obj, "SunBrightness"))                    { this.sunBrightness                    = JEDJsonUtils.getFloat(obj, "SunBrightness"); }
        if (JEDJsonUtils.hasFloat(obj, "SunBrightnessFactor"))              { this.sunBrightnessFactor              = JEDJsonUtils.getFloat(obj, "SunBrightnessFactor"); }
        if (JEDJsonUtils.hasFloat(obj, "SunScale"))                         { this.sunScale                         = JEDJsonUtils.getFloat(obj, "SunScale"); }

        if (JEDJsonUtils.hasString(obj, "CloudRenderer"))                   { this.cloudRenderer                    = JEDJsonUtils.getString(obj, "CloudRenderer"); }
        if (JEDJsonUtils.hasString(obj, "MusicType"))                       { this.musicType                        = JEDJsonUtils.getString(obj, "MusicType"); }
        if (JEDJsonUtils.hasString(obj, "PlayerInventoryGroup"))            { this.playerInventoryGroup             = JEDJsonUtils.getString(obj, "PlayerInventoryGroup"); }
        if (JEDJsonUtils.hasString(obj, "SkyRenderer"))                     { this.skyRenderer                      = JEDJsonUtils.getString(obj, "SkyRenderer"); }
        if (JEDJsonUtils.hasString(obj, "WeatherRenderer"))                 { this.weatherRenderer                  = JEDJsonUtils.getString(obj, "WeatherRenderer"); }
        if (JEDJsonUtils.hasString(obj, "WorldProviderOverride"))           { this.worldProviderOverrideClass       = JEDJsonUtils.getString(obj, "WorldProviderOverride"); }
        if (JEDJsonUtils.hasString(obj, "ChunkGenFactoryWorldProvider"))    { this.chunkGeneratorFactoryWorldProviderClassName = JEDJsonUtils.getString(obj, "ChunkGenFactoryWorldProvider"); }

        if (JEDJsonUtils.hasString(obj, "CloudColor"))                      { this.cloudColor                       = JEDStringUtils.hexStringToColor(JEDJsonUtils.getString(obj, "CloudColor")); }
        if (JEDJsonUtils.hasString(obj, "FogColor"))                        { this.fogColor                         = JEDStringUtils.hexStringToColor(JEDJsonUtils.getString(obj, "FogColor")); }
        if (JEDJsonUtils.hasString(obj, "MoonColor"))                       { this.moonColor                        = JEDStringUtils.hexStringToColor(JEDJsonUtils.getString(obj, "MoonColor")); }
        if (JEDJsonUtils.hasString(obj, "SkyColor"))                        { this.skyColor                         = JEDStringUtils.hexStringToColor(JEDJsonUtils.getString(obj, "SkyColor")); }
        if (JEDJsonUtils.hasString(obj, "SunColor"))                        { this.sunColor                         = JEDStringUtils.hexStringToColor(JEDJsonUtils.getString(obj, "SunColor")); }

        if (JEDJsonUtils.hasString(obj, "CanSleepHere"))
        {
            String value = JEDJsonUtils.getString(obj, "CanSleepHere");
            if (value.equalsIgnoreCase("allow"))                { this.canSleepHere = WorldProvider.WorldSleepResult.ALLOW; }
            else if (value.equalsIgnoreCase("deny"))            { this.canSleepHere = WorldProvider.WorldSleepResult.DENY; }
            else if (value.equalsIgnoreCase("bed_explodes"))    { this.canSleepHere = WorldProvider.WorldSleepResult.BED_EXPLODES; }
            else { JustEnoughDimensions.logger.warn("Invalid 'CanSleepHere' value: '{}'", value); }
        }

        if (JEDJsonUtils.hasObject(obj, "FoggyBiomes"))
        {
            JsonObject fogObj = obj.getAsJsonObject("FoggyBiomes");
            boolean listedHaveFog = JEDJsonUtils.getBoolean(fogObj, "listed_have_fog");
            Set<Biome> biomes = new HashSet<>();

            if (JEDJsonUtils.hasArray(fogObj, "biomes"))
            {
                JsonArray arr = fogObj.getAsJsonArray("biomes");
                final int size = arr.size();

                for (int i = 0; i < size; ++i)
                {
                    String name = arr.get(i).getAsString();
                    ResourceLocation rl = new ResourceLocation(name);
                    Biome biome = ForgeRegistries.BIOMES.getValue(rl);

                    if (biome != null)
                    {
                        biomes.add(biome);
                    }
                }
            }

            this.foggyBiomes.clear();

            if (listedHaveFog == false)
            {
                Set<Biome> allBiomes = new HashSet<>(ForgeRegistries.BIOMES.getValuesCollection());
                allBiomes.removeAll(biomes);
                biomes = allBiomes;
            }

            for (Biome biome : biomes)
            {
                this.foggyBiomes.put(biome, true);
            }

            this.hasPerBiomeFog = true;
        }

        if (JEDJsonUtils.hasDouble(obj, "CelestialAngleMin") && JEDJsonUtils.hasDouble(obj, "CelestialAngleMax"))
        {
            this.celestialAngleMin = MathHelper.clamp(JEDJsonUtils.getFloat(obj, "CelestialAngleMin"), 0f, 1f);
            this.celestialAngleMax = MathHelper.clamp(JEDJsonUtils.getFloat(obj, "CelestialAngleMax"), 0f, 1f);
            this.useCustomCelestialAngleRange = true;
        }

        if (JEDJsonUtils.hasInteger(obj, "DayTimeMin") && JEDJsonUtils.hasInteger(obj, "DayTimeMax"))
        {
            this.dayTimeMin = MathHelper.clamp(JEDJsonUtils.getInteger(obj, "DayTimeMin"), 0, 24000 - 1);
            this.dayTimeMax = MathHelper.clamp(JEDJsonUtils.getInteger(obj, "DayTimeMax"), 0, 24000 - 1);
            this.useCustomDayTimeRange = true;
        }

        if (JEDJsonUtils.hasObject(obj, "SpawnPointSearchType"))
        {
            this.spawnPointSearchType = SpawnPointSearch.fromJson(obj.get("SpawnPointSearchType").getAsJsonObject());
        }

        JsonElement el = obj.get("LightBrightness");

        if (el != null && el.isJsonArray())
        {
            JsonArray arr = el.getAsJsonArray();

            if (arr.size() == 16)
            {
                try
                {
                    float[] light = new float[16];

                    for (int i = 0; i < 16; i++)
                    {
                        light[i] = arr.get(i).getAsFloat();
                    }

                    this.customLightBrightnessTable = light;
                }
                catch (Exception e)
                {
                    JustEnoughDimensions.logger.warn("Failed to read a light brightness table from JSON", e);
                }
            }
        }

        if (this.dayLength   <= 0) { this.dayLength = 1; }
        if (this.nightLength <= 0) { this.nightLength = 1; }
    }

    public JsonObject getJEDPropsForClientSync()
    {
        JsonObject obj = new JsonObject();

        if (this.dayLength != 12000)        { obj.add("DayLength",          new JsonPrimitive(this.dayLength)); }
        if (this.nightLength != 12000)      { obj.add("NightLength",        new JsonPrimitive(this.nightLength)); }
        if (this.dayCycleIncrement != 24000){ obj.add("DayCycleIncrement",  new JsonPrimitive(this.dayCycleIncrement)); }
        if (this.cloudHeight != 128)        { obj.add("CloudHeight",        new JsonPrimitive(this.cloudHeight)); }
        if (this.skyRenderer != null)       { obj.add("SkyRenderer",        new JsonPrimitive(this.skyRenderer)); }
        if (this.cloudRenderer != null)     { obj.add("CloudRenderer",      new JsonPrimitive(this.cloudRenderer)); }
        if (this.weatherRenderer != null)   { obj.add("WeatherRenderer",    new JsonPrimitive(this.weatherRenderer)); }
        if (this.skyRenderType != 0)        { obj.add("SkyRenderType",      new JsonPrimitive(this.skyRenderType)); }
        if (this.skyDisableFlags != 0)      { obj.add("SkyDisableFlags",    new JsonPrimitive(this.skyDisableFlags)); }
        if (this.disableDawnDuskColors)     { obj.add("DisableDawnDuskColors", new JsonPrimitive(this.disableDawnDuskColors)); }
        if (this.useCustomDayCycle)         { obj.add("CustomDayCycle",     new JsonPrimitive(this.useCustomDayCycle)); }
        if (this.hasSkyLight != null)       { obj.add("HasSkyLight",        new JsonPrimitive(this.hasSkyLight)); }
        if (this.hasXZFog != null)          { obj.add("HasXZFog",           new JsonPrimitive(this.hasXZFog)); }
        if (this.horizon != null)           { obj.add("Horizon",            new JsonPrimitive(this.horizon)); }
        if (this.musicType != null)         { obj.add("MusicType",          new JsonPrimitive(this.musicType)); }

        if (this.cloudColor != null)        { obj.add("CloudColor",         new JsonPrimitive(JEDStringUtils.colorToHexString(this.cloudColor))); }
        if (this.fogColor != null)          { obj.add("FogColor",           new JsonPrimitive(JEDStringUtils.colorToHexString(this.fogColor))); }
        if (this.moonColor != null)         { obj.add("MoonColor",          new JsonPrimitive(JEDStringUtils.colorToHexString(this.moonColor))); }
        if (this.skyColor != null)          { obj.add("SkyColor",           new JsonPrimitive(JEDStringUtils.colorToHexString(this.skyColor))); }
        if (this.sunColor != null)          { obj.add("SunColor",           new JsonPrimitive(JEDStringUtils.colorToHexString(this.sunColor))); }

        if (this.skyBlendRatio != null)     { obj.add("SkyBlend",           new JsonPrimitive(this.skyBlendRatio)); }
        if (this.moonScale != null)         { obj.add("MoonScale",          new JsonPrimitive(this.moonScale)); }
        if (this.sunScale != null)          { obj.add("SunScale",           new JsonPrimitive(this.sunScale)); }
        if (this.skyColorLightBlendRatio != null) { obj.add("SkyLightBlend", new JsonPrimitive(this.skyColorLightBlendRatio)); }

        if (this.isSurfaceWorld != null)        { obj.add("IsSurfaceWorld",         new JsonPrimitive(this.isSurfaceWorld)); }
        if (this.shouldClientCheckLight != null){ obj.add("ShouldClientCheckLight", new JsonPrimitive(this.shouldClientCheckLight)); }
        if (this.sunBrightnessFactor != null)   { obj.add("SunBrightnessFactor",    new JsonPrimitive(this.sunBrightnessFactor)); }
        if (this.sunBrightness != null)         { obj.add("SunBrightness",          new JsonPrimitive(this.sunBrightness)); }
        if (this.worldProviderOverrideClass != null) { obj.add("WorldProviderOverride", new JsonPrimitive(this.worldProviderOverrideClass)); }

        if (this.useCustomCelestialAngleRange)
        {
            obj.add("CelestialAngleMin", new JsonPrimitive(this.celestialAngleMin));
            obj.add("CelestialAngleMax", new JsonPrimitive(this.celestialAngleMax));
        }

        if (this.useCustomDayTimeRange)
        {
            obj.add("DayTimeMin", new JsonPrimitive(this.dayTimeMin));
            obj.add("DayTimeMax", new JsonPrimitive(this.dayTimeMax));
        }

        if (this.colorData != null)
        {
            obj.add("Colors", JEDJsonUtils.deepCopy(this.colorData));
        }

        if (this.perBiomeFogData != null)
        {
            obj.add("FoggyBiomes", JEDJsonUtils.deepCopy(this.perBiomeFogData));
        }

        if (this.customLightBrightnessTable != null)
        {
            JsonArray arr = new JsonArray();

            for (int i = 0; i < this.customLightBrightnessTable.length; i++)
            {
                arr.add(this.customLightBrightnessTable[i]);
            }

            obj.add("LightBrightness", arr);
        }

        return obj;
    }

    @Nullable
    public JsonObject getFullJEDProperties()
    {
        return this.fullJEDTag;
    }

    @Nullable
    public JsonObject getNestedObject(String name)
    {
        if (this.fullJEDTag != null)
        {
            JsonObject obj = JEDJsonUtils.getNestedObject(this.fullJEDTag, name, false);
            return obj != null ? JEDJsonUtils.deepCopy(obj) : null;
        }

        return null;
    }

    @Nonnull
    private static NBTTagList writeFloats(float... values)
    {
        NBTTagList tagList = new NBTTagList();

        for (float f : values)
        {
            tagList.appendTag(new NBTTagFloat(f));
        }

        return tagList;
    }

    public boolean getDisableDawnDuskColors()
    {
        return this.disableDawnDuskColors;
    }

    public boolean getDisableDragon()
    {
        return this.disableDragon;
    }

    public boolean getDisableEndSpikes()
    {
        return this.disableEndSpikes;
    }

    public boolean getForceGameMode()
    {
        return this.forceGameMode;
    }

    public boolean getUseCustomDayCycle()
    {
        return this.useCustomDayCycle;
    }

    public boolean getUseCustomCelestialAngleRange()
    {
        return this.useCustomCelestialAngleRange;
    }

    public boolean getUseCustomDayTimeRange()
    {
        return this.useCustomDayTimeRange;
    }

    public boolean shouldWorldBorderBeCenteredOnSpawn()
    {
        return this.worldBorderCenteredOnSpawn;
    }

    public boolean getDontAdvanceWeatherWhenSleeping()
    {
        return this.dontAdvanceWeatherWhenSleeping;
    }

    public int getDayLength()
    {
        return this.dayLength;
    }

    public int getNightLength()
    {
        return this.nightLength;
    }

    public int getDayCycleIncrement()
    {
        return this.dayCycleIncrement;
    }

    public int getDayTimeMin()
    {
        return this.dayTimeMin;
    }

    public int getDayTimeMax()
    {
        return this.dayTimeMax;
    }

    public float getCelestialAngleMin()
    {
        return this.celestialAngleMin;
    }

    public float getCelestialAngleMax()
    {
        return this.celestialAngleMax;
    }

    public float[] getCustomLightBrightnessTable()
    {
        return this.customLightBrightnessTable;
    }

    public int getSkyRenderType()
    {
        return this.skyRenderType;
    }

    public int getSkyDisableFlags()
    {
        return this.skyDisableFlags;
    }

    public int getCloudHeight()
    {
        return this.cloudHeight;
    }

    public int getVoidTeleportInterval()
    {
        return this.voidTeleportInterval;
    }

    @Nullable
    public Vec3d getSkyColor()
    {
        return this.skyColor;
    }

    @Nullable
    public Vec3d getFogColor()
    {
        return this.fogColor;
    }

    @Nullable
    public Vec3d getCloudColor()
    {
        return this.cloudColor;
    }

    @Nullable
    public Float getSkyBlendRatio()
    {
        return this.skyBlendRatio;
    }

    @Nullable
    public Float getSkyColorLightBlendRatio()
    {
        return this.skyColorLightBlendRatio;
    }

    @Nullable
    public Vec3d getSunColor()
    {
        return this.sunColor;
    }

    @Nullable
    public Vec3d getMoonColor()
    {
        return this.moonColor;
    }

    @Nullable
    public Float getSunScale()
    {
        return this.sunScale;
    }

    @Nullable
    public Float getMoonScale()
    {
        return this.moonScale;
    }

    @Nullable
    public Boolean canRespawnHere()
    {
        return this.canRespawnHere;
    }

    @Nullable
    public WorldProvider.WorldSleepResult canSleepHere()
    {
        return this.canSleepHere;
    }

    @Nullable
    public Boolean canSpawnPeacefulMobs()
    {
        return this.canSpawnPeacefulMobs;
    }

    @Nullable
    public Boolean canSpawnHostiles()
    {
        return this.canSpawnHostiles;
    }

    @Nullable
    public Integer getRespawnDimension()
    {
        return this.respawnDimension;
    }

    @Nullable
    public Float getSunBrightnessFactor()
    {
        return this.sunBrightnessFactor;
    }

    @Nullable
    public Float getSunBrightness()
    {
        return this.sunBrightness;
    }

    @Nullable
    public Double getHorizon()
    {
        return this.horizon;
    }

    @Nullable
    public Double getMovementFactor()
    {
        return this.movementFactor;
    }

    @Nullable
    public Integer getAverageGroundLevel()
    {
        return this.averageGroundLevel;
    }

    @Nullable
    public Boolean isSurfaceWorld()
    {
        return this.isSurfaceWorld;
    }

    @Nullable
    public Boolean getHasSkyLight()
    {
        return this.hasSkyLight;
    }

    @Nullable
    public Boolean ignoreSpawnSuitability()
    {
        return this.ignoreSpawnSuitability;
    }

    @Nullable
    public Boolean getHasXZFog()
    {
        return this.hasXZFog;
    }

    public boolean getHasPerBiomeFog()
    {
        return this.hasPerBiomeFog;
    }

    public boolean doesBiomeHaveFog(Biome biome)
    {
        return this.foggyBiomes.containsKey(biome);
    }

    @Nullable
    public Boolean canDoLightning()
    {
        return this.canDoLightning;
    }

    @Nullable
    public Boolean canDoRainSnowIce()
    {
        return this.canDoRainSnowIce;
    }

    @Nullable
    public Boolean shouldClientCheckLight()
    {
        return this.shouldClientCheckLight;
    }

    @Nullable
    public Boolean doesWaterVaporize()
    {
        return this.waterVaporizes;
    }

    public boolean generateFallbackSpawnBlock()
    {
        return this.generateFallbackSpawnBlock;
    }

    public String getPlayerInventoryGroup(int dimension)
    {
        // Allow automatically setting the value to per-dimension groups by setting an empty name
        return this.playerInventoryGroup.isEmpty() ? ("dim_" + dimension) : this.playerInventoryGroup;
    }

    @Nullable
    public String getMusicType()
    {
        return this.musicType;
    }

    @Nullable
    public SpawnPointSearch getSpawnPointSearchType()
    {
        return this.spawnPointSearchType;
    }

    @Nullable
    public String getWorldProviderOverrideClassName()
    {
        return this.worldProviderOverrideClass;
    }

    @Nullable
    public String getChunkGeneratorFactoryWorldProviderClassName()
    {
        return this.chunkGeneratorFactoryWorldProviderClassName;
    }
} public class WorldProviderSurfaceJED extends WorldProviderJED
{
}
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
import net.minecraft.nbt.*;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.world.DimensionType;
import net.minecraft.world.WorldProvider;
import net.minecraftforge.common.DimensionManager;
import net.minecraftforge.common.util.Constants;
import fi.dy.masa.justenoughdimensions.JustEnoughDimensions;
import fi.dy.masa.justenoughdimensions.command.CommandJED;
import fi.dy.masa.justenoughdimensions.network.MessageSyncDimensions;
import fi.dy.masa.justenoughdimensions.network.PacketHandler;
import fi.dy.masa.justenoughdimensions.reference.Reference;
import fi.dy.masa.justenoughdimensions.util.ConfigFileUtils;
import fi.dy.masa.justenoughdimensions.util.ConfigFileUtils.ConfigComparator;
import fi.dy.masa.justenoughdimensions.util.ConfigFileUtils.FileAction;
import fi.dy.masa.justenoughdimensions.util.JEDJsonUtils;
import fi.dy.masa.justenoughdimensions.world.JEDWorldProperties;
import fi.dy.masa.justenoughdimensions.world.WorldProviderSurfaceJED;
            {
                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());