Input:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.pl3x.map.core.Pl3xMap;
import net.pl3x.map.core.configuration.Config;
import net.pl3x.map.core.configuration.Lang;
import net.pl3x.map.core.configuration.PlayersLayerConfig;
import net.pl3x.map.core.configuration.WorldConfig;
import net.pl3x.map.core.image.io.IO;
import net.pl3x.map.core.markers.Point;
import net.pl3x.map.core.scheduler.Task;
import net.pl3x.map.core.util.FileUtil;
import net.pl3x.map.core.world.World;
import org.jetbrains.annotations.NotNull;

Context:
core/src/main/java/net/pl3x/map/core/renderer/task/UpdateSettingsData.java
/*
 * MIT License
 *
 * Copyright (c) 2020-2023 William Blake Galbreath
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package net.pl3x.map.core.renderer.task;

public class UpdateSettingsData extends Task {
    private final Gson gson = new GsonBuilder()
            //.setPrettyPrinting()
            .disableHtmlEscaping()
            .serializeNulls()
            .setLenient()
            .create();

    public UpdateSettingsData() {
        super(1, true);
    }

    @Override
    public void run() {
        try {
            parseSettings();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    private @NotNull List<@NotNull Object> parsePlayers() {
        if (!PlayersLayerConfig.ENABLED) {
            return Collections.emptyList();
        }
        List<Object> players = new ArrayList<>();
        Pl3xMap.api().getPlayerRegistry().forEach(player -> {
            // do not expose hidden players in the json
            if (player.isHidden() || player.isNPC()) {
                return;
            }
            if (PlayersLayerConfig.HIDE_SPECTATORS && player.isSpectator()) {
                return;
            }
            if (PlayersLayerConfig.HIDE_INVISIBLE && player.isInvisible()) {
                return;
            }

            Map<String, Object> playerEntry = new LinkedHashMap<>();

            playerEntry.put("name", player.getDecoratedName());
            playerEntry.put("uuid", player.getUUID().toString());
            playerEntry.put("displayName", player.getDecoratedName());
            playerEntry.put("world", player.getWorld().getName());
            playerEntry.put("position", player.getPosition());

            players.add(playerEntry);
        });
        return players;
    }

    private @NotNull List<@NotNull Map<@NotNull String, @NotNull Object>> parseWorlds() {
        List<Map<String, Object>> worldSettings = new ArrayList<>();
        Pl3xMap.api().getWorldRegistry().entrySet().forEach(entry -> {
            World world = entry.getValue();
            if (!world.isEnabled()) {
                return;
            }

            WorldConfig config = world.getConfig();

            Map<String, Object> spawn = new LinkedHashMap<>();
            Point point = world.getSpawn();
            spawn.put("x", point.x());
            spawn.put("z", point.z());

            Map<String, Object> zoom = new LinkedHashMap<>();
            zoom.put("default", config.ZOOM_DEFAULT);
            zoom.put("maxOut", config.ZOOM_MAX_OUT);
            zoom.put("maxIn", config.ZOOM_MAX_IN);

            Map<String, Object> ui = new LinkedHashMap<>();
            ui.put("link", config.UI_LINK);
            ui.put("coords", config.UI_COORDS);
            ui.put("blockinfo", config.UI_BLOCKINFO);
            ui.put("attribution", config.UI_ATTRIBUTION);

            Map<String, Object> settings = new LinkedHashMap<>();
            settings.put("name", world.getName().replace(":", "-"));
            settings.put("tileUpdateInterval", 10);
            settings.put("spawn", spawn);
            settings.put("zoom", zoom);
            settings.put("ui", ui);

            FileUtil.writeJson(this.gson.toJson(settings), world.getTilesDirectory().resolve("settings.json"));

            List<Object> renderers = new ArrayList<>();
            world.getRenderers().forEach((rendererKey, builder) -> {
                String icon = world.getConfig().RENDER_RENDERERS.get(rendererKey);
                renderers.add(Map.of("label", rendererKey, "value", builder.getName(), "icon", icon));
            });

            Map<String, Object> worldsList = new LinkedHashMap<>();

core/src/main/java/net/pl3x/map/core/util/FileUtil.java
public class FileUtil {
    public static @NotNull Path getTilesDir() {
        return getWebDir().resolve("tiles");
    }

    public static @NotNull Path getWebDir() {
        return Config.WEB_DIR.startsWith("/") ? Path.of(Config.WEB_DIR) : Pl3xMap.api().getMainDir().resolve(Config.WEB_DIR);
    }

    public static void extractFile(@NotNull Class<?> clazz, @NotNull String filename, @NotNull Path outDir, boolean replace) {
        try (InputStream in = clazz.getResourceAsStream("/" + filename)) {
            if (in == null) {
                throw new RuntimeException("Could not read file from jar! (" + filename + ")");
            }
            Path path = outDir.resolve(filename);
            if (!Files.exists(path) || replace) {
                Files.createDirectories(path.getParent());
                Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void extractDir(@NotNull String sourceDir, @NotNull Path outDir, boolean replace) {
        try (JarFile jarFile = new JarFile(Pl3xMap.api().getJarPath().toFile())) {
            Logger.debug("Extracting " + sourceDir + " directory from jar...");
            String path = sourceDir.substring(1);
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (!name.startsWith(path)) {
                    continue;
                }
                Path file = outDir.resolve(name.substring(path.length()));
                boolean exists = Files.exists(file);
                if (!replace && exists) {
                    Logger.debug("  <yellow>exists</yellow>   " + name);
                    continue;
                }
                if (entry.isDirectory()) {
                    if (!exists) {
                        try {
                            Files.createDirectories(file);
                            Logger.debug("  <green>creating</green> " + name);
                        } catch (IOException e) {
                            Logger.debug("  <red><bold>failed</bold></red>   " + name);
                        }
                    } else {
                        Logger.debug("  <yellow>exists</yellow>   " + name);
                    }
                    continue;
                }
                try (
                        InputStream in = new BufferedInputStream(jarFile.getInputStream(entry));
                        OutputStream out = new BufferedOutputStream(new FileOutputStream(file.toFile()))
                ) {
                    byte[] buffer = new byte[4096];
                    int readCount;
                    while ((readCount = in.read(buffer)) > 0) {
                        out.write(buffer, 0, readCount);
                    }
                    out.flush();
                    Logger.debug("  <green>writing</green>  " + name);
                } catch (IOException e) {
                    Logger.debug("  <red><bold>failed</bold></red>   " + name);
                    Logger.warn("Failed to extract file (" + name + ") from jar!");
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void writeJson(@NotNull String str, @NotNull Path file) {
        Path tmp = tmp(file);
        try (
                OutputStream fileOut = Files.newOutputStream(mkDirs(tmp));
                Writer writer = new OutputStreamWriter(fileOut)
        ) {
            writer.write(str);
            writer.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        try {
            atomicMove(tmp, file);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void saveGzip(@NotNull String json, @NotNull Path file) throws IOException {
        Path tmp = tmp(file);
        try (
                OutputStream fileOut = Files.newOutputStream(mkDirs(tmp));
                GZIPOutputStream gzipOut = new GZIPOutputStream(fileOut);
                Writer writer = new OutputStreamWriter(gzipOut)
        ) {
            writer.write(json);
            writer.flush();
        }
        atomicMove(tmp, file);
    }

    public static void saveGzip(byte[] bytes, @NotNull Path file) throws IOException {
        Path tmp = tmp(file);
        try (
                OutputStream fileOut = Files.newOutputStream(mkDirs(tmp));
                GZIPOutputStream gzipOut = new GZIPOutputStream(fileOut)
        ) {
            gzipOut.write(bytes);
            gzipOut.flush();
        }
        atomicMove(tmp, file);
    }

    public static void readGzip(@NotNull Path file, @NotNull ByteBuffer buffer) throws IOException {
        try (
                InputStream fileIn = Files.newInputStream(file);
                GZIPInputStream gzipIn = new GZIPInputStream(fileIn)
        ) {
            // try reading all bytes and closing stream _before_ putting into buffer
            byte[] bytes = gzipIn.readAllBytes();
            gzipIn.close();
            buffer.put(bytes);
        }
    }

    public static String readGzip(@NotNull Path file) throws IOException {
        try (
                InputStream fileIn = Files.newInputStream(file);
                GZIPInputStream gzipIn = new GZIPInputStream(fileIn);
                Reader reader = new InputStreamReader(gzipIn, StandardCharsets.UTF_8);
                Writer writer = new StringWriter()
        ) {
            char[] buffer = new char[4096];
            for (int length; (length = reader.read(buffer)) > 0; ) {
                writer.write(buffer, 0, length);
            }
            return writer.toString();
        }
    }

    public static Path tmp(Path file) {
        return file.resolveSibling("." + file.getFileName().toString() + ".tmp");
    }

    public static void atomicMove(Path source, Path target) throws IOException {
        try {
            atomicMove(source, target, 0);
        } catch (AccessDeniedException | NoSuchFileException ignore) {
        }
    }

    private static void atomicMove(Path source, Path target, int attempt) throws IOException {
        try {
            com.google.common.io.Files.move(source.toFile(), target.toFile());
            Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
        } catch (AtomicMoveNotSupportedException e) {
            Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
        } catch (AccessDeniedException e) {
            if (attempt < 5) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException ignore) {
                }
                atomicMove(source, target, ++attempt);
            } else if (source.getFileName().toString().endsWith(".tmp")) {
                try {
                    Files.delete(source);
                } catch (Throwable ignore) {
                }
            }
        }
    }

    public static @NotNull Path mkDirs(@NotNull Path file) throws IOException {
        if (!Files.exists(file)) {
            Files.createDirectories(file.getParent());
            Files.createFile(file);
        }
        return file;
    }

    public static void createDirs(@NotNull Path dirPath) {
        if (!Files.exists(dirPath)) {
            try {
                Files.createDirectories(dirPath);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    public static void deleteDirectory(@NotNull Path dir) throws IOException {
        try (Stream<Path> walk = Files.walk(dir)) {
            walk.sorted(Comparator.reverseOrder())
                    .map(Path::toFile)
                    .forEach(File::delete);
        }
    }

    public static @NotNull Collection<@NotNull Point> regionPathsToPoints(@NotNull World world, @Nullable Collection<@NotNull Path> paths, boolean ignoreTimestamp) {
        if (paths == null || paths.isEmpty()) {
            return Collections.emptyList();
        }
        List<Point> regions = new ArrayList<>();
        for (Path file : paths) {
            if (file.toFile().length() <= 0) {
                Logger.debug("Skipping zero length region file: " + file.getFileName());
                continue;
            }
            try {
                String[] split = file.getFileName().toString().split("\\.");
                int rX = Integer.parseInt(split[1]);
                int rZ = Integer.parseInt(split[2]);
                if (!world.visibleRegion(rX, rZ)) {
                    Logger.debug("Skipping region outside of visible areas: " + file.getFileName());
                    continue;
                }
                if (ignoreTimestamp) {
                    regions.add(Point.of(rX, rZ));
                    continue;
                }
                long storedModifiedTime = world.getRegionModifiedState().get(Mathf.asLong(rX, rZ));
                long actualModifiedTime = Files.getLastModifiedTime(file).toMillis();
                if (actualModifiedTime > storedModifiedTime) {
                    Logger.debug("Found modified region file: " + file.getFileName());
                    regions.add(Point.of(rX, rZ));
                } else {
                    //Logger.debug("Skipping unmodified region file: " + file.getFileName() + " " + actualModifiedTime + " <= " + storedModifiedTime);
                }
            } catch (NumberFormatException ignore) {
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return regions;
    }
}

core/src/main/java/net/pl3x/map/core/Pl3xMap.java
public abstract class Pl3xMap {
    public static @NotNull Pl3xMap api() {
        return Provider.api();
    }

    private final boolean isBukkit;

    private final Attributes manifestAttributes;
    private final HttpdServer httpdServer;
    private final RegionProcessor regionProcessor;
    private final RegionDoubleChecker regionDoubleChecker;
    private final Scheduler scheduler;

    private final BlockRegistry blockRegistry;
    private final EventRegistry eventRegistry;
    private final HeightmapRegistry heightmapRegistry;
    private final IconRegistry iconRegistry;
    private final PlayerRegistry playerRegistry;
    private final RendererRegistry rendererRegistry;
    private final WorldRegistry worldRegistry;

    private ExecutorService renderExecutor;

    private String commit;
    private Metrics metrics;
    private boolean enabled;

    public Pl3xMap(boolean isBukkit) {
        this.isBukkit = isBukkit;

        /*try {
            // Due to these bugs(?) in spi
            // * relocated libraries cant find their services (xnio fails)
            // * imageio fails to find twelvemonkeys spis at all
            // I am forced to load them all myself instead of relying on the META-INF
            SpiFix.forceRegisterSpis();
        } catch (Throwable ignore) {
        }*/

        try {
            Field api = Provider.class.getDeclaredField("api");
            api.setAccessible(true);
            api.set(null, this);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        Manifest manifest;
        try (JarFile jarFile = new JarFile(getJarPath().toFile())) {
            JarEntry entry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
            try (InputStream in = new BufferedInputStream(jarFile.getInputStream(entry))) {
                manifest = new Manifest(in);
            }
        } catch (Exception e) {
            manifest = new Manifest();
            e.printStackTrace();
        }
        this.manifestAttributes = manifest.getMainAttributes();

        // setup internal server
        this.httpdServer = new HttpdServer();

        // setup tasks
        this.regionProcessor = new RegionProcessor();
        this.regionDoubleChecker = new RegionDoubleChecker();
        this.scheduler = new Scheduler();

        // setup registries
        this.blockRegistry = new BlockRegistry();
        this.eventRegistry = new EventRegistry();
        this.heightmapRegistry = new HeightmapRegistry();
        this.iconRegistry = new IconRegistry();
        this.playerRegistry = new PlayerRegistry();
        this.rendererRegistry = new RendererRegistry();
        this.worldRegistry = new WorldRegistry();
    }

    public boolean isEnabled() {
        return this.enabled;
    }

    public @NotNull HttpdServer getHttpdServer() {
        return this.httpdServer;
    }

    public @NotNull RegionProcessor getRegionProcessor() {
        return this.regionProcessor;
    }

    public @NotNull RegionDoubleChecker getRegionDoubleChecker() {
        return this.regionDoubleChecker;
    }

    public @NotNull BlockRegistry getBlockRegistry() {
        return this.blockRegistry;
    }

    public @NotNull EventRegistry getEventRegistry() {
        return this.eventRegistry;
    }

    public @NotNull HeightmapRegistry getHeightmapRegistry() {
        return this.heightmapRegistry;
    }

    public @NotNull IconRegistry getIconRegistry() {
        return this.iconRegistry;
    }

    public @NotNull PlayerRegistry getPlayerRegistry() {
        return this.playerRegistry;
    }

    public @NotNull RendererRegistry getRendererRegistry() {
        return this.rendererRegistry;
    }

    public @NotNull WorldRegistry getWorldRegistry() {
        return this.worldRegistry;
    }

    public @NotNull ExecutorService getRenderExecutor() {
        return this.renderExecutor;
    }

    public @NotNull Scheduler getScheduler() {
        return this.scheduler;
    }

    public void enable() {
        // load up configs
        Logger.debug("Loading configs");
        Config.reload();
        Lang.reload();
        ColorsConfig.reload();
        PlayersLayerConfig.reload();
        SpawnLayerConfig.reload();
        WorldBorderLayerConfig.reload();

        // initialize block registry
        getBlockRegistry().init();

        // initialize icons
        getIconRegistry().init();

        // load blocks _after_ we loaded colors
        Logger.debug("Registering blocks");
        Blocks.registerDefaults();
        loadBlocks();

        // create the executor service
        Logger.debug("Creating services");
        this.renderExecutor = ThreadFactory.createService("Pl3xMap-Renderer", Config.RENDER_THREADS);

        // register built in tile image types
        Logger.debug("Registering tile image types");
        IO.register();

        // register built-in heightmaps
        Logger.debug("Registering heightmaps");
        getHeightmapRegistry().register();

        // register built-in renderers
        Logger.debug("Registering renderers");
        getRendererRegistry().register();

        this.enabled = true;

        new Pl3xMapEnabledEvent().callEvent();

        // load up already loaded worlds
        Logger.debug("Registering worlds");
        loadWorlds();

        // load up players already connected to the server
        Logger.debug("Registering players");
        loadPlayers();

        // start integrated server
        Logger.debug("Starting internal server");
        getHttpdServer().startServer();

        // start tasks
        Logger.debug("Starting region processor");
        getRegionProcessor().start(10000L);
        getRegionDoubleChecker().start(250000L);

        Logger.debug("Starting update settings data task");
        getScheduler().addTask(new UpdateSettingsData());

        Logger.info("Platform: " + getPlatform());
        Logger.info("Version: " + getVersion());

        try {
            this.metrics = new Metrics(this);
        } catch (IOException e) {
            e.printStackTrace();
        }

        Pl3xMap.api().getRegionProcessor().setPaused(false);
    }

    public void disable() {
        Pl3xMap.api().getRegionProcessor().setPaused(true);

        if (this.metrics != null) {
            this.metrics.shutdown();
            this.metrics = null;
        }

        // stop tasks
        Logger.debug("Stopping tasks");
        getScheduler().cancelAll();
        getRegionDoubleChecker().stop();
        getRegionProcessor().stop();
        if (this.renderExecutor != null) {
            this.renderExecutor.shutdownNow();
        }

        // stop integrated server
        Logger.debug("Stopping internal server");
        getHttpdServer().stopServer();

        this.enabled = false;

        new Pl3xMapDisabledEvent().callEvent();

        // unload all players
        Logger.debug("Unregistering players");
        getPlayerRegistry().unregister();

        // unload all map worlds
        Logger.debug("Unregistering worlds");
        getWorldRegistry().unregister();

        // unregister renderers
        Logger.debug("Unregistering renderers");
        getRendererRegistry().unregister();

        // unregister icons
        Logger.debug("Unregistering icons");
        getIconRegistry().unregister();

        // unregister heightmaps
        Logger.debug("Unregistering heightmaps");
        getHeightmapRegistry().unregister();

        // unregister tile image types
        Logger.debug("Unregistering tile image types");
        IO.unregister();

        // unregister blocks
        Logger.debug("Unregistering blocks");
        getBlockRegistry().unregister();

        System.gc();
    }

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

    public abstract @NotNull String getPlatform();

    public abstract @NotNull String getVersion();

    public @NotNull String getVersionCommit() {
        if (this.commit == null) {
            this.commit = this.manifestAttributes.getValue("Git-Commit");
            if (this.commit == null) {
                this.commit = "unknown";
            }
        }
        return this.commit;
    }

    public abstract int getMaxPlayers();

    public abstract boolean getOnlineMode();

    public abstract String getServerVersion();

    public abstract @NotNull AudienceProvider adventure();

    public abstract @NotNull Path getMainDir();

    public abstract @NotNull Path getJarPath();

    public abstract int getColorForPower(byte power);

    public abstract @Nullable Block getFlower(@NotNull World world, @NotNull Biome biome, int blockX, int blockY, int blockZ);

    protected abstract void loadBlocks();

    protected abstract void loadWorlds();

    protected abstract void loadPlayers();

    public abstract @NotNull World cloneWorld(@NotNull World world);

    protected static final class Provider {
        static Pl3xMap api;

        static @NotNull Pl3xMap api() {
            return Provider.api;
        }
    }

    public static final class ThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory {
        private final String name;
        private final int threads;

        private final AtomicInteger id = new AtomicInteger();

        public ThreadFactory(@NotNull String name, int threads) {
            this.name = name;
            this.threads = threads;
        }

        public static @NotNull ExecutorService createService(@NotNull String name) {
            return createService(new ThreadFactory(name, 1));
        }

        public static @NotNull ExecutorService createService(@NotNull String name, int threads) {
            int max = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
            int parallelism = Mathf.clamp(1, max, threads < 1 ? max : threads);
            return createService(new ThreadFactory(name, parallelism));
        }

        private static @NotNull ExecutorService createService(@NotNull ThreadFactory factory) {
            return new ForkJoinPool(factory.threads, factory, null, false);
        }

        @Override
        public @NotNull ForkJoinWorkerThread newThread(@NotNull ForkJoinPool pool) {
            ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
            // use current classloader, this fixes ClassLoading issues with forge
            thread.setContextClassLoader(Pl3xMap.class.getClassLoader());
            thread.setName(this.threads > 1 ? String.format("%s-%d", this.name, this.id.getAndIncrement()) : this.name);
            return thread;
        }
    }
}

core/src/main/java/net/pl3x/map/core/scheduler/Task.java
public abstract class Task implements Runnable {
    final int delay;
    final boolean repeat;

    boolean cancelled = false;
    int tick;

    /**
     * Creates a new schedulable task.
     *
     * @param delay Delay (in seconds) before task starts
     */
    public Task(int delay) {
        this(delay, false);
    }

    /**
     * Creates a new schedulable task.
     *
     * @param delay  Delay (in seconds) before task starts
     * @param repeat Whether this task should repeat
     */
    public Task(int delay, boolean repeat) {
        this.delay = delay;
        this.repeat = repeat;
    }

    /**
     * Mark task as cancelled.
     */
    public void cancel() {
        this.cancelled = true;
    }

    /**
     * Check if task is marked as cancelled.
     *
     * @return True if cancelled
     */
    public boolean cancelled() {
        return this.cancelled;
    }
}

core/src/main/java/net/pl3x/map/core/configuration/Lang.java
@SuppressWarnings("CanBeFinal")
public final class Lang extends AbstractConfig {
    @Key("prefix.command")
    public static String PREFIX_COMMAND = "<grey>[<gradient:#C028FF:#5B00FF>Pl3xMap</gradient>]</grey> ";
    @Key("command.base")
    public static String COMMAND_BASE = "View the map at '<grey><click:open_url:'<web-address>'><web-address></click></grey>'";

    @Key("command.event.click-for-help")
    public static String CLICK_FOR_HELP = "Click for help";
    @Key("command.event.click-to-confirm")
    public static String CLICK_TO_CONFIRM = "Click to confirm";

    @Key("httpd.started.success")
    public static String HTTPD_STARTED = "<green>Internal webserver running on <yellow><bind></yellow>:<yellow><port></yellow>";
    @Key("httpd.started.error")
    public static String HTTPD_START_ERROR = "<red>Internal webserver could not start";
    @Key("httpd.stopped.success")
    public static String HTTPD_STOPPED = "<green>Internal webserver stopped";
    @Key("httpd.stopped.error")
    public static String HTTPD_STOP_ERROR = "<red>An error occurred with the internal webserver";
    @Key("httpd.disabled")
    public static String HTTPD_DISABLED = "<green>Internal webserver is disabled";

    @Key("progress.eta.unknown")
    public static String PROGRESS_ETA_UNKNOWN = "Unknown";

    @Key("command.argument.optional-player")
    public static String COMMAND_ARGUMENT_OPTIONAL_PLAYER_DESCRIPTION = "Defaults to the executing player if unspecified (console must specify a player)";
    @Key("command.argument.optional-center")
    public static String COMMAND_ARGUMENT_OPTIONAL_CENTER_DESCRIPTION = "Defaults to (<white>0<gray>,</gray> 0</white>) if unspecified";
    @Key("command.argument.optional-zoom")
    public static String COMMAND_ARGUMENT_OPTIONAL_ZOOM_DESCRIPTION = "Map zoom level";
    @Key("command.argument.required-renderer")
    public static String COMMAND_ARGUMENT_REQUIRED_RENDERER_DESCRIPTION = "Renderer is required";
    @Key("command.argument.required-world")
    public static String COMMAND_ARGUMENT_REQUIRED_WORLD_DESCRIPTION = "World is required";

    @Key("command.confirm.description")
    public static String COMMAND_CONFIRM_DESCRIPTION = "Confirm a pending command";
    @Key("command.confirm.not-rendering")
    public static String COMMAND_CONFIRM_CONFIRMATION_REQUIRED_MESSAGE = "<red>Confirmation required. Confirm using <grey>/map confirm";
    @Key("command.confirm.success")
    public static String COMMAND_CONFIRM_NO_PENDING_MESSAGE = "<red>You don't have any pending confirmations";

    @Key("command.fullrender.description")
    public static String COMMAND_FULLRENDER_DESCRIPTION = "Fully render a world";
    @Key("command.fullrender.starting")
    public static String COMMAND_FULLRENDER_STARTING = "<green>Full render starting. Check <grey>/map status</grey> for more info";

    @Key("command.help.description")
    public static String COMMAND_HELP_DESCRIPTION = "Get help for Pl3xmap commands";
    @Key("command.help.argument.query")
    public static String COMMAND_HELP_ARGUMENT_QUERY_DESCRIPTION = "Help Query";

    @Key("command.hide.description")
    public static String COMMAND_HIDE_DESCRIPTION = "Hide a player from the map";
    @Key("command.hide.already-hidden")
    public static String COMMAND_HIDE_ALREADY_HIDDEN = "<grey><player> <red>is already hidden from the map";
    @Key("command.hide.success")
    public static String COMMAND_HIDE_SUCCESS = "<grey><player> <green>is now hidden from the map";

    @Key("command.pause.description")
    public static String COMMAND_PAUSE_DESCRIPTION = "Toggle the pause state of renderers";
    @Key("command.pause.paused")
    public static String COMMAND_PAUSE_PAUSED = "<green>Renderers are now paused";
    @Key("command.pause.unpaused")
    public static String COMMAND_PAUSE_UNPAUSED = "<green>Renderers are now unpaused";

    @Key("command.radiusrender.description")
    public static String COMMAND_RADIUSRENDER_DESCRIPTION = "Render a section of a world";
    @Key("command.radiusrender.starting")
    public static String COMMAND_RADIUSRENDER_STARTING = "<green>Radius render starting. Check <grey>/map status</grey> for more info";

    @Key("command.reload.description")
    public static String COMMAND_RELOAD_DESCRIPTION = "Reloads the plugin";
    @Key("command.reload.success")
    public static String COMMAND_RELOAD_SUCCESS = "<green>Pl3xMap <grey>v<version></grey> reloaded";

    @Key("command.resetmap.description")
    public static String COMMAND_RESETMAP_DESCRIPTION = "Cancel active render of a world";
    @Key("command.resetmap.begin")
    public static String COMMAND_RESETMAP_BEGIN = "<green>Map reset for <grey><world></grey> has begun";
    @Key("command.resetmap.success")
    public static String COMMAND_RESETMAP_SUCCESS = "<green>Successfully reset map for <grey><world>";
    @Key("command.resetmap.failed")
    public static String COMMAND_RESETMAP_FAILED = "<red>Could not reset map for <grey><world>";

    @Key("command.show.description")
    public static String COMMAND_SHOW_DESCRIPTION = "Show a player on the map";
    @Key("command.show.not-hidden")
    public static String COMMAND_SHOW_NOT_HIDDEN = "<grey><player> <red>is not hidden from the map";
    @Key("command.show.success")
    public static String COMMAND_SHOW_SUCCESS = "<grey><player> <green>is no longer hidden from the map";

    @Key("command.status.description")
    public static String COMMAND_STATUS_DESCRIPTION = "View the render status";

    @Key("command.stitch.description")
    public static String COMMAND_STITCH_DESCRIPTION = "Stitches tiles into one image";
    @Key("command.stitch.missing-directory")
    public static String COMMAND_STITCH_MISSING_DIRECTORY = "<red>Unable to find tiles directory.";
    @Key("command.stitch.error-reading-directory")
    public static String COMMAND_STITCH_ERROR_READING_DIRECTORY = "<red>There was a problem reading the tiles directory.";
    @Key("command.stitch.empty-directory")
    public static String COMMAND_STITCH_EMPTY_DIRECTORY = "<red>There are no tiles to stitch.";
    @Key("command.stitch.starting")
    public static String COMMAND_STITCH_STARTING = "<green>Started stitching <count> tiles..\n<green><italic>(min: <min-x>,<min-z> max: <max-x>,<max-z> size: <size-x>,<size-z>)";
    @Key("command.stitch.finished")
    public static String COMMAND_STITCH_FINISHED = "<green>Finished stitching <count> tiles!\n<green>You can find it at <grey>/tiles/<world>/stitched/<filename>";

    @Key("command.version.description")
    public static String COMMAND_VERSION_DESCRIPTION = "Get version information";
    @Key("command.version.please-wait")
    public static String COMMAND_VERSION_PLEASE_WAIT = "<italic>Checking version, please wait...";
    @Key("command.version.still-checking")
    public static String COMMAND_VERSION_STILL_CHECKING = "<italic>Still checking...";
    @Key("command.version.error.not-array")
    public static String COMMAND_VERSION_ERROR_NOT_ARRAY = "<red>Error: response not an array";
    @Key("command.version.error.corrupt-json")
    public static String COMMAND_VERSION_ERROR_CORRUPT_JSON = "<red>Error: response is corrupt json";
    @Key("command.version.error.unknown-version")
    public static String COMMAND_VERSION_ERROR_UNKNOWN_VERSION = "<red>Error: response has unknown version";
    @Key("command.version.error.unable-to-determine")
    public static String COMMAND_VERSION_ERROR_UNABLE_TO_DETERMINE = "<red>Error: Unable to determine latest build";
    @Key("command.version.success")
    public static String COMMAND_VERSION_SUCCESS = "Pl3xMap v3 <version> (<italic><platform></italic>) git-<commit>";
    @Key("command.version.snapshot")
    public static String COMMAND_VERSION_SNAPSHOT = "<yellow><italic>You are running a snapshot";
    @Key("command.version.latest-build-is")
    public static String COMMAND_VERSION_LATEST_BUILD_IS = "<yellow><italic>Latest build is <build>";
    @Key("command.version.running-latest-build")
    public static String COMMAND_VERSION_RUNNING_LATEST_BUILD = "<green><italic>You are running the latest build.";
    @Key("command.version.builds-behind")
    public static String COMMAND_VERSION_BUILDS_BEHIND = "<yellow><italic>You are <number> builds behind.";
    @Key("command.version.download")
    public static String COMMAND_VERSION_DOWNLOAD = "<yellow><italic>Download new build at: <gold><link>";
    @Key("command.version.time-traveler")
    public static String COMMAND_VERSION_TIME_TRAVELER = "<yellow><italic>Are you a time traveler?";

    @Key("error.must-specify-player")
    public static String ERROR_MUST_SPECIFY_PLAYER = "<red>You must specify the player";
    @Key("error.no-such-player")
    public static String ERROR_NO_SUCH_PLAYER = "<red>No such player <grey><player>";
    @Key("error.must-specify-renderer")
    public static String ERROR_MUST_SPECIFY_RENDERER = "<red>You must specify the renderer";
    @Key("error.no-such-renderer")
    public static String ERROR_NO_SUCH_RENDERER = "<red>No such renderer <grey><renderer>";
    @Key("error.must-specify-world")
    public static String ERROR_MUST_SPECIFY_WORLD = "<red>You must specify the world";
    @Key("error.no-such-world")
    public static String ERROR_NO_SUCH_WORLD = "<red>No such world <grey><world>";
    @Key("error.world-disabled")
    public static String ERROR_WORLD_DISABLED = "<red>Pl3xMap is disabled for world <grey><world>";
    @Key("error.not-valid-zoom-level")
    public static String ERROR_NOT_VALID_ZOOM_LEVEL = "<red>Not a valid zoom level";
    @Key("error.point-invalid-format")
    public static String ERROR_POINT_INVALID_FORMAT = "'<point>' is not a valid location. Required format is '<x> <z>'";

    @Key("ui.layer.players")
    public static String UI_LAYER_PLAYERS = "Players";
    @Key("ui.layer.spawn")
    public static String UI_LAYER_SPAWN = "Spawn";
    @Key("ui.layer.worldborder")
    public static String UI_LAYER_WORLDBORDER = "World Border";

    @Key("ui.title")
    public static String UI_TITLE = "Pl3xMap";
    @Key("ui.block-and-biome-lang-file")
    public static String UI_BLOCK_AND_BIOME_LANG_FILE = "en_us.json";
    @Key("ui.blockinfo.label")
    public static String UI_BLOCKINFO_LABEL = "BlockInfo";
    @Key("ui.blockinfo.value")
    public static String UI_BLOCKINFO_VALUE = "<block><br /><biome>";
    @Key("ui.coords.label")
    public static String UI_COORDS_LABEL = "Coordinates";
    @Key("ui.coords.value")
    public static String UI_COORDS_VALUE = "<x>, <y>, <z>";
    @Key("ui.link.label")
    public static String UI_LINK_LABEL = "Sharable Link";
    @Key("ui.link.value")
    public static String UI_LINK_VALUE = "";
    @Key("ui.markers.label")
    public static String UI_MARKERS_LABEL = "Markers";
    @Key("ui.markers.value")
    public static String UI_MARKERS_VALUE = "No markers have been configured";
    @Key("ui.players.label")
    public static String UI_PLAYERS_LABEL = "Players (<online>/<max>)";
    @Key("ui.players.value")
    public static String UI_PLAYERS_VALUE = "No players are currently online";
    @Key("ui.worlds.label")
    public static String UI_WORLDS_LABEL = "Worlds";
    @Key("ui.worlds.value")
    public static String UI_WORLDS_VALUE = "No worlds have been configured";
    @Key("ui.layers.label")
    public static String UI_LAYERS_LABEL = "Layers";
    @Key("ui.layers.value")
    public static String UI_LAYERS_VALUE = "No layers have been configured";

    private static final Lang CONFIG = new Lang();

    public static void reload() {
        Path localeDir = Pl3xMap.api().getMainDir().resolve("locale");

        // extract locale dir from jar
        FileUtil.extractDir("/locale/", localeDir, false);

        CONFIG.reload(localeDir.resolve(Config.LANGUAGE_FILE), Lang.class);
    }

    public static @NotNull Component parse(@NotNull String msg, @NotNull TagResolver.@NotNull Single... placeholders) {
        return MiniMessage.miniMessage().deserialize(msg, placeholders);
    }

    public static @NotNull String strip(@NotNull String msg) {
        return MiniMessage.miniMessage().stripTags(msg);
    }
}

core/src/main/java/net/pl3x/map/core/configuration/WorldConfig.java
@SuppressWarnings("CanBeFinal")
public final class WorldConfig extends AbstractConfig {
    @Key("enabled")
    @Comment("""
            Enables this world to be rendered on the map.""")
    public boolean ENABLED = true;

    @Key("render.renderers")
    @Comment("""
            Renderers to use. Each renderer will render a different
            type of map. The built-in renderers include:
            vintage_story, basic, biomes, flowermap, inhabited, night, and vanilla""")
    public Map<@NotNull String, @NotNull String> RENDER_RENDERERS = new LinkedHashMap<>() {{
        put("vintage_story", "overworld_basic");
    }};

    @Key("render.biome-blend")
    @Comment("""
            Enables blending of biome grass/foliage/water colors similar to
            the client's biome blending option.
            Values are clamped to 0-7""")
    public int RENDER_BIOME_BLEND = 3;

    @Key("render.skylight")
    @Comment("""
            World skylight value. This is used for the day/night cycle
            map (not yet implemented) .Values are clamped to 0-15
            with 0 being darkest and 15 being full bright.""")
    public int RENDER_SKYLIGHT = 15;

    @Key("render.translucent-fluids")
    @Comment("""
            Enable translucent fluids.
            This will make the fluids look fancier and translucent,
            so you can see the blocks below in shallow fluids.""")
    public boolean RENDER_TRANSLUCENT_FLUIDS = true;

    @Key("render.translucent-glass")
    @Comment("""
            Enable translucent glass.
            This will make the glass look fancier and translucent,
            so you can see the blocks below.""")
    public boolean RENDER_TRANSLUCENT_GLASS = true;

    @Key("render.heightmap-type")
    @Comment("""
            Type of heightmap to render.
            NONE has no heightmap drawn.
            EVEN_ODD makes every other Y layer darker, like Dynmap.
            HIGH_CONTRAST same as MODERN, but darker.
            LOW_CONTRAST same as MODERN, but lighter.
            MODERN is a clearer, more detailed view.
            OLD_SCHOOL is the old type from v1.
            VANILLA matches the in-game vanilla maps.
            EVEN_ODD_HIGH_CONTRAST mix of EVEN_ODD and HIGH_CONTRAST.
            EVEN_ODD_LOW_CONTRAST mix of EVEN_ODD and LOW_CONTRAST.
            EVEN_ODD_MODERN mix of EVEN_ODD and MODERN.
            EVEN_ODD_OLD_SCHOOL mix of EVEN_ODD and OLD_SCHOOL.
            EVEN_ODD_VANILLA mix of EVEN_ODD and VANILLA.""")
    public String RENDER_HEIGHTMAP_TYPE = "MODERN";

    @Key("ui.display-name")
    @Comment("""
            The display name of the world in the world list.
            Use <world> to use the official world name.""")
    public String DISPLAY_NAME = "<world>";

    @Key("ui.order")
    @Comment("""
            The order of the world in the world list""")
    public int ORDER = 0;

    @Key("ui.attribution")
    @Comment("""
            Shows the footer attributes""")
    public boolean UI_ATTRIBUTION = true;

    @Key("ui.blockinfo")
    @Comment("""
            The display position for the blockinfo box""")
    public String UI_BLOCKINFO = "bottomleft";

    @Key("ui.coords")
    @Comment("""
            The display position for the coordinates box""")
    public String UI_COORDS = "bottomcenter";

    @Key("ui.link")
    @Comment("""
            The display position for the link box""")
    public String UI_LINK = "bottomright";

    @Key("zoom.default")
    @Comment("""
            The default zoom when loading the map in browser.
            Normal sized tiles (1 pixel = 1 block) are
            always at zoom level 0.""")
    public int ZOOM_DEFAULT = 0;
    @Key("zoom.max-out")
    @Comment("""
            The maximum zoom out you can do on the map.
            Each additional level requires a new set of tiles
            to be rendered, so don't go too wild here.""")
    public int ZOOM_MAX_OUT = 3;
    @Key("zoom.max-in")
    @Comment("""
            Extra zoom in layers will stretch the original
            tile images so you can zoom in further without
            the extra cost of rendering more tiles.""")
    public int ZOOM_MAX_IN = 2;

    @Key("render.visible-areas")
    @Comment("""
            Visible areas of the world.""")
    public List<Area> VISIBLE_AREAS = new ArrayList<>(); // defaults added in ctor

    private final World world;

    public WorldConfig(@NotNull World world) {
        this.world = world;

        // we need an instance of the world to get the border
        VISIBLE_AREAS.add(new Border(world));

        reload();
    }

    public void reload() {
        reload(Pl3xMap.api().getMainDir().resolve("config.yml"), WorldConfig.class);

        RENDER_BIOME_BLEND = Mathf.clamp(0, 7, RENDER_BIOME_BLEND);
        RENDER_SKYLIGHT = Mathf.clamp(0, 15, RENDER_SKYLIGHT);
    }

    @Override
    protected @NotNull Object getClassObject() {
        return this;
    }

    @Override
    protected @Nullable Object getValue(@NotNull String path, @Nullable Object def) {
        if (getConfig().get("world-settings.default." + path) == null) {
            set("world-settings.default." + path, def);
        }
        return get("world-settings." + this.world.getName() + "." + path,
                get("world-settings.default." + path, def));
    }

    @Override
    protected void setComment(@NotNull String path, @Nullable String comment) {
        getConfig().setComment("world-settings.default." + path, comment);
    }

    @Override
    protected @Nullable Object get(@NotNull String path) {
        if (!path.contains("render.visible-areas")) {
            return super.get(path);
        }
        Object value = getConfig().get(path);
        if (!(value instanceof List<?>)) {
            return value;
        }
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> list = (List<Map<String, Object>>) value;
        List<Area> areas = new ArrayList<>();
        for (Map<String, Object> map : list) {
            areas.add(Area.deserialize(this.world, map));
        }
        return areas;
    }

    @Override
    protected void set(@NotNull String path, @Nullable Object value) {
        if (value != null && path.contains("render.visible-areas")) {
            @SuppressWarnings("unchecked")
            List<Area> list = (List<Area>) value;
            value = list.stream().map(Area::serialize).toList();
        }
        getConfig().set(path, value);
    }
}

core/src/main/java/net/pl3x/map/core/configuration/Config.java
@SuppressWarnings("CanBeFinal")
public final class Config extends AbstractConfig {
    @Key("settings.debug-mode")
    @Comment("""
            Extra logger/console output. (can be spammy)""")
    public static boolean DEBUG_MODE = false;

    @Key("settings.language-file")
    @Comment("""
            The language file to use from the locale folder.""")
    public static String LANGUAGE_FILE = "lang-en.yml";

    @Key("settings.web-address")
    @Comment("""
            Set the web address players use to connect to your map. This
            is only used for the client mod to know where to connect.""")
    public static String WEB_ADDRESS = "http://localhost:8080";

    @Key("settings.web-directory.path")
    @Comment("""
            The directory that houses the website and world tiles.
            This is a relative path from Pl3xMap's plugin directory,
            unless it starts with / in which case it will be treated
            as an absolute path.""")
    public static String WEB_DIR = "web/";
    @Key("settings.web-directory.read-only")
    @Comment("""
            Set to true if you don't want Pl3xMap to overwrite
            the website files on startup. (Good for servers that
            customize these files)
            Note: Be sure this is false when upgrading.""")
    public static boolean WEB_DIR_READONLY = false;
    @Key("settings.web-directory.tile-format")
    @Comment("""
            The image format for tile images.
            Built in types: bmp, gif, jpg, jpeg, png""")
    public static String WEB_TILE_FORMAT = "png";
    @Key("settings.web-directory.tile-quality")
    @Comment("""
            The quality for image tiles (0.0 - 1.0)
            0.0 is low quality, high compression, small file size
            1.0 is high quality, no compression, large file size
            Note: Not all image formats honor this setting.""")
    public static double WEB_TILE_QUALITY = 0.0D;

    @Key("settings.map.zoom.snap")
    @Comment("""
            Forces the map's zoom level to always be a multiple of this.
            By default, the zoom level snaps to the nearest integer; lower
            values (e.g. 0.5 or 0.1) allow for greater granularity. A
            value of 0 means the zoom level will not be snapped.""")
    public static double MAP_ZOOM_SNAP = 0.25D;
    @Key("settings.map.zoom.delta")
    @Comment("""
            Controls how much the map's zoom level will change after a zoom in,
            zoom out, pressing + or - on the keyboard, or using the zoom controls.
            Values smaller than 1 (e.g. 0.5) allow for greater granularity.""")
    public static double MAP_ZOOM_DELTA = 0.25D;
    @Key("settings.map.zoom.wheel")
    @Comment("""
            How many scroll pixels (as reported by L.DomEvent.getWheelDelta) mean
            a change of one full zoom level. Smaller values will make wheel-zooming
            faster (and vice versa).""")
    public static int MAP_ZOOM_WHEEL = 120;

    @Key("settings.internal-webserver.enabled")
    @Comment("""
            Enable the built-in web server.
            Disable this if you want to use a standalone web server
            such as apache or nginx.""")
    public static boolean HTTPD_ENABLED = true;
    @Key("settings.internal-webserver.bind")
    @Comment("""
            The interface the built-in web server should bind to.
            This is NOT always the same as your public facing IP.
            If you don't understand what this is,
            leave it set to 0.0.0.0""")
    public static String HTTPD_BIND = "0.0.0.0";
    @Key("settings.internal-webserver.port")
    @Comment("""
            The port the built-in web server listens to.
            Make sure the port is allocated if using Pterodactyl.""")
    public static int HTTPD_PORT = 8080;
    @Key("settings.internal-webserver.follow-symlinks")
    @Comment("""
            Allows the built-in web server to follow symlinks.
            It is generally advised against enabling this,
            for security reasons. But you do you, boo boo.""")
    public static boolean HTTPD_FOLLOW_SYMLINKS = false;

    @Key("settings.performance.render-threads")
    @Comment("""
            The number of process-threads to use for loading and scanning chunks.
            Value of -1 will use 50% of the available cpu-threads. (recommended)""")
    public static int RENDER_THREADS = -1;

    @Key("settings.performance.gc.when-finished")
    @Comment("""
            Runs the JVM GC after a render job stops to free up memory immediately.""")
    public static boolean GC_WHEN_FINISHED = true;

    @Key("settings.performance.gc.when-running")
    @Comment("""
            Runs the JVM GC aggressively while a render is running
            CAUTION: this _will_ slow down your renders!""")
    public static boolean GC_WHEN_RUNNING = false;

    private static final Config CONFIG = new Config();

    public static void reload() {
        Path mainDir = Pl3xMap.api().getMainDir();

        // extract default config from jar
        FileUtil.extractFile(Config.class, "config.yml", mainDir, false);

        CONFIG.reload(mainDir.resolve("config.yml"), Config.class);

        if (!Pl3xMap.api().isBukkit()) {
            tryRenamePath("world-settings.world", "world-settings.minecraft:overworld");
            tryRenamePath("world-settings.world_nether", "world-settings.minecraft:the_nether");
            tryRenamePath("world-settings.world_the_end", "world-settings.minecraft:the_end");
        }
    }

    private static void tryRenamePath(String oldPath, String newPath) {
        YamlFile config = CONFIG.getConfig();
        Object oldValue = config.get(oldPath);
        if (oldValue == null) {
            return; // old default doesn't exist; do nothing
        }
        if (config.get(newPath) != null) {
            return; // new default already set; do nothing
        }
        config.set(newPath, oldValue);
        config.set(oldPath, null);
        CONFIG.save();
    }
}

core/src/main/java/net/pl3x/map/core/image/io/IO.java
public abstract class IO {
    private static final Registry<@NotNull Type> TYPES = new Registry<>();

    public static void register() {
        IO.register("bmp", new Bmp());
        IO.register("gif", new Gif());
        IO.register("jpg", new Jpg());
        IO.register("jpeg", get("jpg"));
        IO.register("png", new Png());
    }

    public static void register(@NotNull String name, @NotNull Type type) {
        if (TYPES.has(name)) {
            throw new IllegalStateException(String.format("IO type %s already registered", name));
        }
        TYPES.register(name, type);
    }

    public static void unregister() {
        TYPES.unregister();
    }

    public static void unregister(@NotNull String name) {
        TYPES.unregister(name);
    }

    public static @NotNull Type get(@NotNull String format) {
        Type type = TYPES.get(format.toLowerCase(Locale.ROOT));
        if (type == null) {
            throw new IllegalStateException("Unknown or unsupported image format");
        }
        return type;
    }

    public abstract static class Type extends Keyed {
        public Type(@NotNull String key) {
            super(key);
        }

        public @NotNull BufferedImage createBuffer() {
            return new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB);
        }

        public int color(int argb) {
            return argb;
        }

        public @Nullable BufferedImage read(@NotNull Path path) {
            BufferedImage buffer = null;
            ImageReader reader = null;
            try (ImageInputStream in = ImageIO.createImageInputStream(Files.newInputStream(path))) {
                reader = ImageIO.getImageReadersBySuffix(getKey()).next();
                reader.setInput(in, false, true);
                buffer = reader.read(0);
                in.flush();
            } catch (IOException e) {
                Logger.warn("Could not read tile image: " + path);
                e.printStackTrace();
            } finally {
                if (reader != null) {
                    reader.dispose();
                }
            }
            return buffer;
        }

        public void write(@NotNull Path path, @NotNull BufferedImage buffer) {
            Path tmp = FileUtil.tmp(path);
            ImageWriter writer = null;
            try (ImageOutputStream out = ImageIO.createImageOutputStream(tmp.toFile())) {
                writer = ImageIO.getImageWritersBySuffix(getKey()).next();
                ImageWriteParam param = writer.getDefaultWriteParam();
                if (param.canWriteCompressed()) {
                    param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                    if (param.getCompressionType() == null) {
                        param.setCompressionType(param.getCompressionTypes()[0]);
                    }
                    param.setCompressionQuality((float) Config.WEB_TILE_QUALITY);
                }
                writer.setOutput(out);
                writer.write(null, new IIOImage(buffer, null, null), param);
                out.flush();
            } catch (IOException e) {
                Logger.warn("Could not write tile image: " + tmp);
                e.printStackTrace();
            } finally {
                if (writer != null) {
                    writer.dispose();
                }
            }
            try {
                FileUtil.atomicMove(tmp, path);
            } catch (IOException e) {
                Logger.warn("Could not write tile image: " + path);
                e.printStackTrace();
            }
        }
    }
}

core/src/main/java/net/pl3x/map/core/configuration/PlayersLayerConfig.java
@SuppressWarnings("CanBeFinal")
public final class PlayersLayerConfig extends AbstractConfig {
    @Key("settings.enabled")
    @Comment("""
            Show online players on the map and sidebar.""")
    public static boolean ENABLED = true;

    @Key("settings.hide.invisible")
    @Comment("""
            Should invisible players be hidden from the map.""")
    public static boolean HIDE_INVISIBLE = true;

    @Key("settings.hide.spectators")
    @Comment("""
            Should spectators be hidden from the map.""")
    public static boolean HIDE_SPECTATORS = true;

    @Key("settings.layer.update-interval")
    @Comment("""
            How often (in seconds) to update the marker.
            Setting to 0 is the same as setting it to 1.""")
    public static int UPDATE_INTERVAL = 0;
    @Key("settings.layer.show-controls")
    @Comment("""
            Whether the players layer control shows up in the layers list or not.""")
    public static boolean SHOW_CONTROLS = true;
    @Key("settings.layer.default-hidden")
    @Comment("""
            Whether the players layer should be hidden (toggled off) by default.""")
    public static boolean DEFAULT_HIDDEN = false;
    @Key("settings.layer.priority")
    @Comment("""
            Priority order players layer shows up in the layers list.
            (lower values = higher in the list)""")
    public static int PRIORITY = 20;
    @Key("settings.layer.z-index")
    @Comment("""
            Z-Index order players layer shows up in the map.
            (higher values are drawn on top of lower values)""")
    public static int Z_INDEX = 999;

    @Key("settings.icon")
    @Comment("""
            The player icon.
            Icon must be in the web/images/icon/ directory.""")
    public static String ICON = "players";

    @Key("settings.tooltip")
    @Comment("""
            Tooltip for player markers.
            Variables: uuid, name, decoratedName, health, armor""")
    public static String TOOLTIP = """
            <ul>
              <li><img src='images/skins/2D/<uuid>.png' class='head' alt='<name>' /></li>
              <li>
                <name>
                <img src='images/clear.png' class='health' style='background-position:0 calc(-<health>px * 9);' alt='Health <health>' />
                <img src='images/clear.png' class='armor' style='background-position:0 calc(-<armor>px * 9);' alt='Armor <armor>' />
              </li>
            </ul>""";

    @Key("settings.pane")
    @Comment("""
            The custom pane layer for the player tracker.
            This is used to make custom css styled tooltips.
            (see css setting below)""")
    public static String PANE = "nameplates";

    @Key("settings.css")
    @Comment("""
            Custom css for players marker layer.
            Class names use the pane name set above.""")
    public static String CSS = """
            div.leaflet-nameplates-pane div img.head {
              image-rendering: pixelated;
              image-rendering: -moz-crisp-edges;
              -ms-interpolation-mode: nearest-neighbor;
            }
            div.leaflet-nameplates-pane div {
              margin: 0;
              padding: 0;
              color: #ffffff;
              font-weight: 700;
              line-height: 1rem;
              background: rgba(0, 0, 0, 0.5);
              border-color: rgba(0, 0, 0, 0.75);
            }
            div.leaflet-nameplates-pane div ul {
              padding: 3px;
            }
            div.leaflet-nameplates-pane div:before {
              border-color: transparent;
            }
            div.leaflet-nameplates-pane div img.head {
              vertical-align: middle;
              width: 32px;
              height: 32px;
              border-radius: 5px;
              border: 1px solid black;
            }
            div.leaflet-nameplates-pane div img.health {
              margin-top: 3px;
            }
            div.leaflet-nameplates-pane div img.armor,
            div.leaflet-nameplates-pane div img.health {
              display: block;
              width: 81px;
              height: 9px;
              background-position: 0 0;
            }
            div.leaflet-nameplates-pane div img.armor {
              background: url('images/armor.png') no-repeat;
            }
            div.leaflet-nameplates-pane div img.health {
              background: url('images/health.png') no-repeat;
            }
            div.leaflet-nameplates-pane div,
            div.leaflet-marker-pane img {
              transition: all 0.25s;
            }""";

    private static final PlayersLayerConfig CONFIG = new PlayersLayerConfig();

    public static void reload() {
        CONFIG.reload(Pl3xMap.api().getMainDir().resolve("layers/players.yml"), PlayersLayerConfig.class);
    }
}

core/src/main/java/net/pl3x/map/core/world/World.java
public abstract class World extends Keyed {
    public static final PathMatcher JSON_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*.json");
    public static final PathMatcher MCA_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/r.*.*.mca");
    public static final PathMatcher PNG_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*_*.png");

    private final Path customMarkersDirectory;
    private final Path markersDirectory;
    private final Path regionDirectory;
    private final Path tilesDirectory;
    private final WorldConfig worldConfig;

    private final long seed;
    private final Point spawn;
    private final Type type;

    private final BiomeManager biomeManager;
    private final BiomeRegistry biomeRegistry;
    private final Registry<@NotNull Layer> layerRegistry;

    private final LoadingCache<@NotNull Long, @NotNull Region> regionCache;
    private final RegionModifiedState regionModifiedState;
    //private final RegionFileWatcher regionFileWatcher;
    private final UpdateMarkerData markerTask;
    private final Map<@NotNull String, Renderer.@NotNull Builder> renderers = new LinkedHashMap<>();

    public World(@NotNull String name, long seed, @NotNull Point spawn, @NotNull Type type, @NotNull Path regionDirectory) {
        super(name);

        this.seed = seed;
        this.spawn = spawn;
        this.type = type;

        String safeNameForDirectories = name.replace(":", "-");

        this.regionDirectory = regionDirectory;
        this.tilesDirectory = FileUtil.getTilesDir().resolve(safeNameForDirectories);
        this.customMarkersDirectory = Pl3xMap.api().getMainDir().resolve("markers").resolve(safeNameForDirectories);
        this.markersDirectory = getTilesDirectory().resolve("markers");

        FileUtil.createDirs(this.regionDirectory);
        FileUtil.createDirs(this.tilesDirectory);
        FileUtil.createDirs(this.customMarkersDirectory);
        FileUtil.createDirs(this.markersDirectory);

        this.worldConfig = new WorldConfig(this);

        this.biomeManager = new BiomeManager(hashSeed(getSeed()));
        this.biomeRegistry = new BiomeRegistry();
        this.layerRegistry = new Registry<>();

        this.regionCache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                .build(this::loadRegion);

        this.regionModifiedState = new RegionModifiedState(this);
        //this.regionFileWatcher = new RegionFileWatcher(this);
        this.markerTask = new UpdateMarkerData(this);
    }

    protected void init() {
        if (!isEnabled()) {
            return;
        }

        getBiomeRegistry().init(this);

        //this.regionFileWatcher.start();

        getConfig().RENDER_RENDERERS.forEach((id, icon) -> {
            Renderer.Builder renderer = Pl3xMap.api().getRendererRegistry().get(id);
            if (renderer == null) {
                return;
            }
            Path path = FileUtil.getWebDir().resolve("images/icon/" + icon + ".png");
            try {
                IconImage image = new IconImage(icon, ImageIO.read(path.toFile()), "png");
                Pl3xMap.api().getIconRegistry().register(image);
            } catch (IOException e) {
                Logger.severe("Cannot load world renderer icon " + path, e);
            }
            this.renderers.put(renderer.getKey(), renderer);
        });

        if (WorldBorderLayerConfig.ENABLED) {
            Logger.debug("Registering world border layer");
            getLayerRegistry().register(WorldBorderLayer.KEY, new WorldBorderLayer(this));
        }

        if (SpawnLayerConfig.ENABLED) {
            Logger.debug("Registering spawn layer");
            getLayerRegistry().register(SpawnLayer.KEY, new SpawnLayer(this));
        }

        if (PlayersLayerConfig.ENABLED) {
            Logger.debug("Registering player tracker layer");
            getLayerRegistry().register(PlayersLayer.KEY, new PlayersLayer(this));
        }

        Logger.debug("Checking all region files");
        Pl3xMap.api().getRegionProcessor().addRegions(this, listRegions(false));

        Logger.debug("Starting marker task");
        Pl3xMap.api().getScheduler().addTask(1, true, this.markerTask);

        // load up custom markers
        Logger.debug("Loading custom markers for " + getName());
        for (Path file : getCustomMarkerFiles()) {
            CustomLayer.load(this, file);
        }
    }

    public void cleanup() {
        this.regionCache.invalidateAll();
        getRegionModifiedState().save();
    }

    public @NotNull Path getCustomMarkersDirectory() {
        return this.customMarkersDirectory;
    }

    public @NotNull Path getMarkersDirectory() {
        return this.markersDirectory;
    }

    public @NotNull Path getRegionDirectory() {
        return this.regionDirectory;
    }

    public @NotNull Path getTilesDirectory() {
        return this.tilesDirectory;
    }

    public @NotNull WorldConfig getConfig() {
        return this.worldConfig;
    }

    public @NotNull RegionModifiedState getRegionModifiedState() {
        return this.regionModifiedState;
    }

    //public @NotNull RegionFileWatcher getRegionFileWatcher() {
    //    return this.regionFileWatcher;
    //}

    public @NotNull UpdateMarkerData getMarkerTask() {
        return this.markerTask;
    }

    public @NotNull Map<@NotNull String, Renderer.@NotNull Builder> getRenderers() {
        return Collections.unmodifiableMap(this.renderers);
    }

    /**
     * Get whether this world is enabled.
     *
     * @return true if enabled
     */
    public boolean isEnabled() {
        return getConfig().ENABLED;
    }

    public @NotNull String getName() {
        return getKey();
    }

    public long getSeed() {
        return this.seed;
    }

    public @NotNull Point getSpawn() {
        return this.spawn;
    }

    public int getSkylight() {
        return getConfig().RENDER_SKYLIGHT;
    }

    /**
     * Get the world's type.
     *
     * @return world type
     */
    public @NotNull Type getType() {
        return this.type;
    }

    public @NotNull BiomeManager getBiomeManager() {
        return this.biomeManager;
    }

    public @NotNull BiomeRegistry getBiomeRegistry() {
        return this.biomeRegistry;
    }

    public @NotNull Registry<Layer> getLayerRegistry() {
        return this.layerRegistry;
    }

    public abstract <T> @NotNull T getLevel();

    public abstract long hashSeed(long seed);

    public abstract boolean hasCeiling();

    public abstract int getMinBuildHeight();

    public abstract int getMaxBuildHeight();

    public abstract int getLogicalHeight();

    public abstract double getBorderMinX();

    public abstract double getBorderMinZ();

    public abstract double getBorderMaxX();

    public abstract double getBorderMaxZ();

    public abstract @NotNull Collection<@NotNull Player> getPlayers();

    public boolean visibleBlock(int blockX, int blockZ) {
        for (Area area : getConfig().VISIBLE_AREAS) {
            if (area.containsBlock(blockX, blockZ)) {
                return true;
            }
        }
        return getConfig().VISIBLE_AREAS.isEmpty();
    }

    public boolean visibleChunk(int chunkX, int chunkZ) {
        for (Area area : getConfig().VISIBLE_AREAS) {
            if (area.containsChunk(chunkX, chunkZ)) {
                return true;
            }
        }
        return getConfig().VISIBLE_AREAS.isEmpty();
    }

    public boolean visibleRegion(int regionX, int regionZ) {
        for (Area area : getConfig().VISIBLE_AREAS) {
            if (area.containsRegion(regionX, regionZ)) {
                return true;
            }
        }
        return getConfig().VISIBLE_AREAS.isEmpty();
    }

    public @NotNull Chunk getChunk(@Nullable Region region, int chunkX, int chunkZ) {
        return getRegion(region, chunkX >> 5, chunkZ >> 5).getChunk(chunkX, chunkZ);
    }

    public @NotNull Region getRegion(@Nullable Region region, int regionX, int regionZ) {
        if (region != null && region.getX() == regionX && region.getZ() == regionZ) {
            return region;
        }
        return getRegion(Mathf.asLong(regionX, regionZ));
    }

    private @NotNull Region getRegion(long pos) {
        return this.regionCache.get(pos);
    }

    public void unloadRegion(int regionX, int regionZ) {
        unloadRegion(Mathf.asLong(regionX, regionZ));
    }

    private void unloadRegion(long pos) {
        this.regionCache.invalidate(pos);
    }

    public @NotNull Collection<@NotNull Path> getRegionFiles() {
        if (!Files.exists(getRegionDirectory())) {
            return Collections.emptySet();
        }
        try (Stream<Path> stream = Files.list(getRegionDirectory())) {
            return stream.filter(MCA_MATCHER::matches).toList();
        } catch (IOException e) {
            throw new RuntimeException("Failed to list region files in directory '" + getRegionDirectory().toAbsolutePath() + "'", e);
        }
    }

    public @NotNull Collection<@NotNull Path> getCustomMarkerFiles() {
        if (!Files.exists(getCustomMarkersDirectory())) {
            return Collections.emptySet();
        }
        try (Stream<Path> stream = Files.list(getCustomMarkersDirectory())) {
            return stream.filter(JSON_MATCHER::matches).toList();
        } catch (IOException e) {
            throw new RuntimeException("Failed to list custom marker files in directory '" + getCustomMarkersDirectory().toAbsolutePath() + "'", e);
        }
    }

    public @NotNull Collection<@NotNull Point> listRegions(boolean ignoreTimestamp) {
        return FileUtil.regionPathsToPoints(this, getRegionFiles(), ignoreTimestamp);
    }

    private @NotNull Region loadRegion(long pos) {
        int x = Mathf.longToX(pos);
        int z = Mathf.longToZ(pos);
        return new Region(this, x, z, getMCAFile(x, z));
    }

    private @NotNull Path getMCAFile(int regionX, int regionZ) {
        return getRegionDirectory().resolve("r." + regionX + "." + regionZ + ".mca");
    }

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

    @Override
    public int hashCode() {
        return Objects.hash(getName());
    }

    @Override
    public abstract @NotNull String toString();

    /**
     * Represents a world's type.
     */
    public enum Type {
        OVERWORLD,
        NETHER,
        THE_END,
        CUSTOM;

        private final String name;

        Type() {
            this.name = name().toLowerCase(Locale.ROOT);
        }

        /**
         * Get the world type from a server level.
         *
         * @param dimension dimension name
         * @return world type
         */
        public static @NotNull Type get(@NotNull String dimension) {
            return switch (dimension) {
                case "minecraft:overworld" -> OVERWORLD;
                case "minecraft:the_nether" -> NETHER;
                case "minecraft:the_end" -> THE_END;
                default -> CUSTOM;
            };
        }

        @Override
        public @NotNull String toString() {
            return this.name;
        }
    }
}

Answer: worldsList.put("name", world.getName().replace(":", "-"));

<think>
In the parseWorlds method, after building each world's individual settings and the list of renderer entries, the code creates a new map called worldsList. The next logical step is to populate this map with the world's identifier and its renderers before adding it to the overall worldSettings list. The first entry to add is the world name (with colons replaced by hyphens) matching the naming used elsewhere. Hence the next line should put the name into worldsList.
</think>
worldsList.put("name", world.getName().replace(":", "-"));