All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pepsoft.worldpainter.layers.BiomesPanel Maven / Gradle / Ivy

There is a newer version: 2.23.2
Show newest version
package org.pepsoft.worldpainter.layers;

import com.google.common.collect.ImmutableSet;
import org.pepsoft.util.DesktopUtils;
import org.pepsoft.util.IconUtils;
import org.pepsoft.util.swing.BetterJPopupMenu;
import org.pepsoft.worldpainter.App;
import org.pepsoft.worldpainter.ColourScheme;
import org.pepsoft.worldpainter.Platform;
import org.pepsoft.worldpainter.World2;
import org.pepsoft.worldpainter.biomeschemes.*;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.*;
import java.util.function.Consumer;

import static java.lang.Boolean.TRUE;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Collections.emptySet;
import static java.util.EnumSet.noneOf;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import static javax.swing.BoxLayout.PAGE_AXIS;
import static org.pepsoft.worldpainter.App.KEY_PAINT_ID;
import static org.pepsoft.worldpainter.Platform.Capability.*;
import static org.pepsoft.worldpainter.biomeschemes.Minecraft1_20Biomes.*;
import static org.pepsoft.worldpainter.layers.BiomesPanel.BiomeOption.*;
import static org.pepsoft.worldpainter.painting.PaintFactory.createDiscreteLayerPaintId;

/**
 * Created by pepijn on 27-05-15.
 */
public class BiomesPanel extends JPanel implements CustomBiomeManager.CustomBiomeListener {
    public BiomesPanel(CustomBiomeManager customBiomeManager, Listener listener, ButtonGroup buttonGroup) {
        this.customBiomeManager = customBiomeManager;
        this.listener = listener;
        this.buttonGroup = buttonGroup;

        initComponents();

        customBiomeManager.addListener(this);
    }

    public void loadBiomes(Platform platform, ColourScheme colourScheme) {
        biomeHelper = new BiomeHelper(colourScheme, customBiomeManager, platform);
        BiomesSet desiredSet;
        // TODO move this stuff to BiomeScheme/PlatformProvider
        if (platform.capabilities.contains(NAMED_BIOMES)) {
            desiredSet = MINECRAFT_1_20_BIOMES;
            showIds = false;
        } else if (platform.capabilities.contains(BIOMES) || platform.capabilities.contains(BIOMES_3D)) {
            desiredSet = MINECRAFT_1_17_BIOMES;
            showIds = true;
        } else {
            desiredSet = null;
        }
        if (biomesSet != desiredSet) {
            loadBiomes(desiredSet, colourScheme);
        }
    }

    public void selectBiome(int biomeId) {
        if (selectedBiome == biomeId) {
            // Biome already selected
            selectCurrentBaseBiomeButton();
            notifyListener();
        } else {
            final BiomeDescriptor descriptor = findBiomeDescriptor(biomeId);
            if (descriptor.baseId == selectedBaseBiome) {
                // The corresponding base biome is already selected; just update the options if necessary
                selectCurrentBaseBiomeButton();
                forEveryOption(checkBox -> {
                    if (descriptor.options.contains(checkBox.getClientProperty(KEY_BIOME_OPTION))) {
                        // Checkbox should be checked
                        if (! checkBox.isSelected()) {
                            checkBox.setSelected(true);
                        }
                    } else {
                        // Checkbox should be unchecked
                        if (checkBox.isSelected()) {
                            checkBox.setSelected(false);
                        }
                    }
                });
                updateOptions();
                updateLabels();
            } else {
                // Base biome also needs to be updated
                selectedBaseBiome = descriptor.baseId;
                selectCurrentBaseBiomeButton();
                resetOptions();
                forEveryOption(checkBox -> {
                    if (descriptor.options.contains(checkBox.getClientProperty(KEY_BIOME_OPTION))) {
                        checkBox.setSelected(true);
                    }
                });
                updateOptions();
                updateLabels();
            }
            notifyListener();
        }
    }

    // CustomBiomeListener

    @Override
    public void customBiomeAdded(CustomBiome customBiome) {
        addButton(customBiome);
    }

    @Override
    public void customBiomeChanged(CustomBiome customBiome) {
        for (Component component: grid.getComponents()) {
            if ((component instanceof JToggleButton) && (((Integer) ((JToggleButton) component).getClientProperty(KEY_BIOME)) == customBiome.getId())) {
                final JToggleButton button = (JToggleButton) component;
                final BufferedImage pattern = customBiome.getPattern();
                button.setIcon((pattern != null) ? new ImageIcon(pattern) : IconUtils.createScaledColourIcon(customBiome.getColour()));
                button.setToolTipText(customBiome.getName());
                return;
            }
        }
    }

    @Override
    public void customBiomeRemoved(CustomBiome customBiome) {
        for (Component component: grid.getComponents()) {
            if ((component instanceof JToggleButton) && (((Integer) ((JToggleButton) component).getClientProperty(KEY_BIOME)) == customBiome.getId())) {
                JToggleButton button = (JToggleButton) component;
                if (button.isSelected()) {
                    button.setSelected(false);
                    selectedBiome = BIOME_PLAINS;
                    notifyListener();
                }
                grid.remove(component);
                forceRepaint();
                return;
            }
        }
    }

    private void initComponents() {
        setLayout(new BoxLayout(this, PAGE_AXIS));

        label1.setHorizontalTextPosition(JLabel.LEADING);
        label1.setAlignmentX(0.0f);
        add(label1);
        label2.setAlignmentX(0.0f);
        add(label2);

        JButton addCustomBiomeButton = new JButton(IconUtils.loadScaledIcon("org/pepsoft/worldpainter/icons/plus.png"));
        addCustomBiomeButton.putClientProperty(KEY_ADD_BUTTON, TRUE);
        addCustomBiomeButton.setMargin(App.BUTTON_INSETS);
        addCustomBiomeButton.setToolTipText("Add a custom biome");
        addCustomBiomeButton.addActionListener(e -> {
            final World2 world = App.getInstance().getWorld();
            if (world == null) {
                DesktopUtils.beep();
                return;
            }
            final Window parent = SwingUtilities.getWindowAncestor(BiomesPanel.this);
            final int id = customBiomeManager.getNextId();
            if (id == -1) {
                JOptionPane.showMessageDialog(parent, "Maximum number of custom biomes reached", "Maximum Reached", JOptionPane.ERROR_MESSAGE);
                return;
            }
            final Platform platform = world.getPlatform();
            CustomBiome customBiome = new CustomBiome(platform.capabilities.contains(NAMED_BIOMES) ? "namespace:biome" : "Custom", id, Color.ORANGE.getRGB());
            CustomBiomeDialog dialog = new CustomBiomeDialog(parent, customBiome, true, platform);
            dialog.setVisible(true);
            if (! dialog.isCancelled()) {
                customBiomeManager.addCustomBiome(parent, customBiome);
            }
        });
        grid.add(addCustomBiomeButton);
        grid.setAlignmentX(0.0f);
        add(grid);

        optionsPanel.setLayout(new BoxLayout(optionsPanel, PAGE_AXIS));
        add(optionsPanel);
    }

    private void loadBiomes(BiomesSet biomesSet, ColourScheme colourScheme) {
        if (this.biomesSet != null) {
            while ((((JComponent) grid.getComponent(0)).getClientProperty(KEY_ADD_BUTTON) == null)
                    && (((JComponent) grid.getComponent(0)).getClientProperty(KEY_CUSTOM_BIOME) == null)) {
                // The first component is not the "add custom biome" button or a custom biome; keep removing components until it is
                grid.remove(0);
            }
        }
        this.biomesSet = biomesSet;
        if (biomesSet != null) {
            int index = 0;
            for (final int biome: biomesSet.biomeOrder) {
                if (biome != -1) {
                    final JToggleButton button = new JToggleButton(new ImageIcon(BiomeSchemeManager.createImage(StaticBiomeInfo.INSTANCE, biome, colourScheme)));
                    button.putClientProperty(KEY_BIOME, biome);
                    button.setMargin(App.BUTTON_INSETS);
                    StringBuilder tooltip = new StringBuilder();
                    tooltip.append(biomesSet.displayNames[biome]);
                    if (showIds) {
                        tooltip.append(" (");
                        List variantIds = findVariants(biome);
                        tooltip.append(variantIds.stream().map(i -> Integer.toString(i)).collect(joining(", ")));
                        tooltip.append(')');
                    } else {
                        Set options = findVariantOptions(biome);
                        if (! options.isEmpty()) {
                            tooltip.append(" (options: ");
                            tooltip.append(options.stream().map(this::createOptionName).collect(joining(", ")));
                            tooltip.append(')');
                        }
                    }
                    button.setToolTipText(tooltip.toString());
                    buttonGroup.add(button);
                    if (biome == selectedBiome) {
                        button.setSelected(true);
                    }
                    button.addActionListener(e -> {
                        if (button.isSelected()) {
                            selectBaseBiome(biome);
                        }
                    });
                    grid.add(button, index++);
                } else {
                    grid.add(Box.createGlue(), index++);
                }
            }
        }
        resetOptions();
        forceRepaint();
    }

    private void selectBaseBiome(int biome) {
        selectedBaseBiome = biome;
        selectedBiome = biome;
        notifyListener();
        resetOptions();
        updateLabels();
    }

    private void selectCurrentBaseBiomeButton() {
        for (Component component: grid.getComponents()) {
            if (component instanceof JToggleButton) {
                final JToggleButton button = (JToggleButton) component;
                if ((int) button.getClientProperty(KEY_BIOME) == selectedBaseBiome) {
                    button.setSelected(true);
                    return;
                }
            }
        }
        throw new IllegalArgumentException("No button found for currently selected base biome ID " + selectedBaseBiome);
    }

    private void resetOptions() {
        Set availableOptions = findAvailableOptions(selectedBaseBiome);
        optionsPanel.removeAll();
        optionsPanel.add(new JLabel("Variations:"));
        for (BiomeOption option: availableOptions) {
            JCheckBox checkBox = new JCheckBox(createOptionName(option));
            checkBox.addActionListener(event -> updateOptions());
            checkBox.putClientProperty(KEY_BIOME_OPTION, option);
            checkBox.setEnabled(findBiome(selectedBaseBiome, EnumSet.of(option)) != -1);
            optionsPanel.add(checkBox);
        }
    }

    private String createOptionName(BiomeOption option) {
        return stream(option.name().split("_"))
                .map(s -> s.charAt(0) + s.substring(1).toLowerCase())
                .collect(joining(" "));
    }

    private void updateOptions() {
        final Set selectedOptions = getSelectedOptions();
        selectedBiome = findBiome(selectedBaseBiome, selectedOptions);
        if (selectedBiome == -1) {
            // This means the new combination of selected options is no longer valid. This can happen when an option
            // becomes available only after selecting another option and then the other option is deselected. Just
            // deselect everything when this happens
            forEveryOption(checkBox -> {
                if (checkBox.isSelected()) {
                    checkBox.setSelected(false);
                }
            });
            selectedOptions.clear();
            selectedBiome = selectedBaseBiome;
        }
        notifyListener();
        forEveryOption(checkBox -> {
            BiomeOption biomeOption = (BiomeOption) checkBox.getClientProperty(KEY_BIOME_OPTION);
            if (selectedOptions.contains(biomeOption)) {
                checkBox.setEnabled(true);
            } else {
                EnumSet optionsCopy = EnumSet.copyOf(selectedOptions);
                optionsCopy.add(biomeOption);
                checkBox.setEnabled(findBiome(selectedBaseBiome, optionsCopy) != -1);
            }
        });
        updateLabels();
    }

    private Set getSelectedOptions() {
        final Set selectedOptions = noneOf(BiomeOption.class);
        forEveryOption(checkBox -> {
            if (checkBox.isSelected()) {
                selectedOptions.add((BiomeOption) checkBox.getClientProperty(KEY_BIOME_OPTION));
            }
        });
        return selectedOptions;
    }

    /**
     * Find the actual biome ID for a specific base biome and a set of selected
     * options.
     *
     * @param baseId The base ID of the biome.
     * @param options The selected options.
     * @return The actual biome ID for the specified base biome and options, or
     * -1 if the specified base ID or options are invalid or don't specify an
     * existing actual biome.
     */
    private int findBiome(int baseId, Set options) {
        for (BiomeDescriptor descriptor: biomesSet.descriptors) {
            if ((descriptor.getBaseId() == baseId) && descriptor.getOptions().equals(options)) {
                return descriptor.getId();
            }
        }
        return -1;
    }

    private void updateLabels() {
        label1.setText("Selected biome: " + (showIds ? selectedBiome : ""));
        label1.setIcon(biomeHelper.getBiomeIcon(selectedBiome));
        label2.setText(biomeHelper.getBiomeNameWithoutId(selectedBiome));
    }

    private void addButton(CustomBiome customBiome) {
        final int biome = customBiome.getId();
        final BufferedImage pattern = customBiome.getPattern();
        final JToggleButton button = new JToggleButton((pattern != null) ? new ImageIcon(pattern) : IconUtils.createScaledColourIcon(customBiome.getColour()));
        button.putClientProperty(KEY_BIOME, biome);
        button.putClientProperty(KEY_PAINT_ID, createDiscreteLayerPaintId(Biome.INSTANCE, biome));
        button.putClientProperty(KEY_CUSTOM_BIOME, TRUE);
        button.setMargin(App.BUTTON_INSETS);
        button.setToolTipText(customBiome.getName() + " (" + biome + "); right-click for options");
        button.addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    if (e.isPopupTrigger()) {
                        showPopupMenu(e);
                    }
                }

                @Override
                public void mouseClicked(MouseEvent e) {
                    if (e.isPopupTrigger()) {
                        showPopupMenu(e);
                    }
                }
                
                @Override
                public void mouseReleased(MouseEvent e) {
                    if (e.isPopupTrigger()) {
                        showPopupMenu(e);
                    }
                }

                private void showPopupMenu(MouseEvent e) {
                    JPopupMenu popup = new BetterJPopupMenu();
                    
                    JMenuItem item = new JMenuItem("Edit...");
                    item.addActionListener(actionEvent -> {
                        CustomBiomeDialog dialog = new CustomBiomeDialog(SwingUtilities.getWindowAncestor(button), customBiome, false, App.getInstance().getWorld().getPlatform());
                        dialog.setVisible(true);
                        if (! dialog.isCancelled()) {
                            customBiomeManager.editCustomBiome(customBiome);
                        }
                    });
                    popup.add(item);
                    
                    item = new JMenuItem("Remove...");
                    item.addActionListener(actionEvent -> {
                        if (JOptionPane.showConfirmDialog(button, "Are you sure you want to remove custom biome \"" + customBiome.getName() + "\" (ID: " + customBiome.getId() + ")?\nAny occurrences will be replaced with Automatic Biomes", "Confirm Removal", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
                            customBiomeManager.removeCustomBiome(customBiome);
                        }
                    });
                    popup.add(item);
                    
                    popup.show(button, e.getX(), e.getY());
                }
            });
        buttonGroup.add(button);
        button.addItemListener(e -> {
            if (e.getStateChange() == ItemEvent.SELECTED) {
                selectBaseBiome(biome);
            }
        });
        grid.add(button, grid.getComponentCount() - 1);
        forceRepaint();
    }

    private void forceRepaint() {
        // Not sure why this is necessary. Swing bug?
        Window parent = SwingUtilities.getWindowAncestor(this);
        if (parent != null) {
            parent.validate();
        }
    }

    /**
     * Find the available biome options given a particular base biome.
     *
     * @param baseId The ID of the base biome.
     * @return The total available options for the specified base biome. May be
     * empty, but not {@code null}.
     */
    private Set findAvailableOptions(int baseId) {
        if ((biomesSet != null) && (baseId < biomesSet.displayNames.length) && (biomesSet.displayNames[baseId] != null)) {
            Set availableOptions = noneOf(BiomeOption.class);
            for (BiomeDescriptor descriptor: biomesSet.descriptors) {
                if (descriptor.getBaseId() == baseId) {
                    availableOptions.addAll(descriptor.getOptions());
                }
            }
            return availableOptions;
        } else {
            return emptySet();
        }
    }

    /**
     * Find the IDs of all variants of the specified base biome.
     *
     * @param baseId The ID of the base biome.
     * @return The IDs of all variants of the specified base biome (including
     *     the base biome itself).
     */
    private List findVariants(int baseId) {
        List variants = new ArrayList<>();
        for (BiomeDescriptor descriptor: biomesSet.descriptors) {
            if (descriptor.getBaseId() == baseId) {
                variants.add(descriptor.getId());
            }
        }
        return variants;
    }

    /**
     * Find the options of all variants of the specified base biome.
     *
     * @param baseId The ID of the base biome.
     * @return The options of all variants of the specified base biome (including the base biome itself).
     */
    private Set findVariantOptions(int baseId) {
        Set options = noneOf(BiomeOption.class);
        for (BiomeDescriptor descriptor: biomesSet.descriptors) {
            if (descriptor.getBaseId() == baseId) {
                options.addAll(descriptor.getOptions());
            }
        }
        return options;
    }

    private BiomeDescriptor findBiomeDescriptor(int biomeId) {
        for (BiomeDescriptor descriptor: biomesSet.descriptors) {
            if (descriptor.id == biomeId) {
                return descriptor;
            }
        }
        throw new IllegalArgumentException("Biome ID not found in current biomes set: " + biomeId);
    }

    private void notifyListener() {
        listener.biomeSelected(selectedBiome);
    }

    private void forEveryOption(Consumer consumer) {
        stream(optionsPanel.getComponents())
                .filter(c -> c instanceof JCheckBox)
                .forEach(c -> consumer.accept((JCheckBox) c));
    }

    private final JPanel grid = new JPanel(new GridLayout(0, 5)), optionsPanel = new JPanel();
    private final ButtonGroup buttonGroup;
    private final JLabel label1 = new JLabel("Selected biome: 1"), label2 = new JLabel("Plains");

    private final CustomBiomeManager customBiomeManager;
    private final Listener listener;
    private BiomeHelper biomeHelper;
    private BiomesSet biomesSet;
    private int selectedBiome = BIOME_PLAINS, selectedBaseBiome = BIOME_PLAINS;
    private boolean showIds;

    // TODO move this stuff to BiomeScheme/PlatformProvider

    /**
     * Display order of the biomes on the biomes panel. This only contains the base biomes that get their own button;
     * all other biomes are presumed to be variants of the base biomes.
     */
    private static final int[] MC_117_BIOME_ORDER = {
            BIOME_PLAINS, BIOME_FOREST, BIOME_SWAMP, BIOME_JUNGLE, BIOME_BAMBOO_JUNGLE,
            BIOME_BIRCH_FOREST, BIOME_DARK_FOREST, BIOME_TAIGA, BIOME_GIANT_TREE_TAIGA, BIOME_GIANT_SPRUCE_TAIGA,
            BIOME_MOUNTAINS, BIOME_MUSHROOM_FIELDS, BIOME_DESERT, BIOME_SAVANNA, BIOME_BADLANDS,
            BIOME_SNOWY_TUNDRA, BIOME_ICE_SPIKES, BIOME_OCEAN, BIOME_RIVER, BIOME_BEACH,
            BIOME_STONE_SHORE, BIOME_DRIPSTONE_CAVES, BIOME_LUSH_CAVES, BIOME_THE_END, BIOME_THE_VOID,
            BIOME_NETHER_WASTES, BIOME_SOUL_SAND_VALLEY, BIOME_CRIMSON_FOREST, BIOME_WARPED_FOREST, BIOME_BASALT_DELTAS,
    };

    /**
     * Display order of the biomes on the biomes panel. This only contains the base biomes that get their own button;
     * all other biomes are presumed to be variants of the base biomes.
     */
    private static final int[] MC_120_BIOME_ORDER = {
            BIOME_PLAINS, BIOME_FOREST, BIOME_SWAMP, BIOME_JUNGLE, BIOME_BAMBOO_JUNGLE,
            BIOME_BIRCH_FOREST, BIOME_DARK_FOREST, BIOME_MANGROVE_SWAMP, BIOME_OLD_GROWTH_PINE_TAIGA, BIOME_OLD_GROWTH_SPRUCE_TAIGA,
            BIOME_WINDSWEPT_HILLS, BIOME_DESERT, BIOME_SAVANNA, BIOME_BADLANDS, BIOME_TAIGA,
            BIOME_SNOWY_PLAINS, BIOME_ICE_SPIKES, BIOME_OCEAN, BIOME_RIVER, BIOME_BEACH,
            BIOME_DRIPSTONE_CAVES, BIOME_MEADOW, BIOME_STONY_PEAKS, BIOME_JAGGED_PEAKS, BIOME_STONY_SHORE,
            BIOME_LUSH_CAVES, BIOME_MUSHROOM_FIELDS, BIOME_SNOWY_SLOPES, BIOME_GROVE, BIOME_FROZEN_PEAKS,
            BIOME_DEEP_DARK, BIOME_THE_VOID, BIOME_CHERRY_GROVE, -1, -1,
            BIOME_THE_END, BIOME_END_BARRENS, BIOME_END_HIGHLANDS, BIOME_END_MIDLANDS, BIOME_SMALL_END_ISLANDS,
            BIOME_NETHER_WASTES, BIOME_SOUL_SAND_VALLEY, BIOME_CRIMSON_FOREST, BIOME_WARPED_FOREST, BIOME_BASALT_DELTAS
    };

    private static final String KEY_BIOME = BiomesPanel.class.getName() + ".biome";
    private static final String KEY_BIOME_OPTION = BiomesPanel.class.getName() + ".biomeOption";
    private static final String KEY_ADD_BUTTON = BiomesPanel.class.getName() + ".addButton";
    private static final String KEY_CUSTOM_BIOME = BiomesPanel.class.getName() + ".customBiome";

    private static final Set MC_117_DESCRIPTORS = ImmutableSet.of(
        new BiomeDescriptor(BIOME_OCEAN),
        new BiomeDescriptor(BIOME_PLAINS),
        new BiomeDescriptor(BIOME_DESERT),
        new BiomeDescriptor(BIOME_MOUNTAINS),
        new BiomeDescriptor(BIOME_FOREST),
        new BiomeDescriptor(BIOME_TAIGA),
        new BiomeDescriptor(BIOME_SWAMP),
        new BiomeDescriptor(BIOME_RIVER),
        new BiomeDescriptor(BIOME_NETHER_WASTES),
        new BiomeDescriptor(BIOME_THE_END),

        new BiomeDescriptor(BIOME_FROZEN_OCEAN, 0, FROZEN),
        new BiomeDescriptor(BIOME_FROZEN_RIVER, 7, FROZEN),
        new BiomeDescriptor(BIOME_SNOWY_TUNDRA),
        new BiomeDescriptor(BIOME_SNOWY_MOUNTAINS, 3, SNOWY), // Double
        new BiomeDescriptor(BIOME_MUSHROOM_FIELDS),
        new BiomeDescriptor(BIOME_MUSHROOM_FIELD_SHORE, 14, SHORE), // Double
        new BiomeDescriptor(BIOME_BEACH),
        new BiomeDescriptor(BIOME_DESERT_HILLS, 2, HILLS),
        new BiomeDescriptor(BIOME_WOODED_HILLS, 4, HILLS),
        new BiomeDescriptor(BIOME_TAIGA_HILLS, 5, HILLS),

        new BiomeDescriptor(BIOME_MOUNTAIN_EDGE, 3, EDGE),
        new BiomeDescriptor(BIOME_JUNGLE),
        new BiomeDescriptor(BIOME_JUNGLE_HILLS, 21, HILLS),
        new BiomeDescriptor(BIOME_JUNGLE_EDGE, 21, EDGE),
        new BiomeDescriptor(BIOME_DEEP_OCEAN, 0, DEEP),
        new BiomeDescriptor(BIOME_STONE_SHORE),
        new BiomeDescriptor(BIOME_SNOWY_BEACH, 16, SNOWY),
        new BiomeDescriptor(BIOME_BIRCH_FOREST),
        new BiomeDescriptor(BIOME_BIRCH_FOREST_HILLS, 27, HILLS),
        new BiomeDescriptor(BIOME_DARK_FOREST),

        new BiomeDescriptor(BIOME_SNOWY_TAIGA, 5, SNOWY),
        new BiomeDescriptor(BIOME_SNOWY_TAIGA_HILLS, 5, SNOWY, HILLS),
        new BiomeDescriptor(BIOME_GIANT_TREE_TAIGA),
        new BiomeDescriptor(BIOME_GIANT_TREE_TAIGA_HILLS, 32, HILLS),
        new BiomeDescriptor(BIOME_WOODED_MOUNTAINS, 3, WOODED),
        new BiomeDescriptor(BIOME_SAVANNA),
        new BiomeDescriptor(BIOME_SAVANNA_PLATEAU, 35, PLATEAU),
        new BiomeDescriptor(BIOME_BADLANDS),
        new BiomeDescriptor(BIOME_WOODED_BADLANDS_PLATEAU, 37, WOODED, PLATEAU),
        new BiomeDescriptor(BIOME_BADLANDS_PLATEAU, 37, PLATEAU),

        new BiomeDescriptor(BIOME_SMALL_END_ISLANDS, 9, SMALL_ISLANDS),
        new BiomeDescriptor(BIOME_END_MIDLANDS, 9, MIDLANDS),
        new BiomeDescriptor(BIOME_END_HIGHLANDS, 9, HIGHLANDS),
        new BiomeDescriptor(BIOME_END_BARRENS, 9, BARRENS),
        new BiomeDescriptor(BIOME_WARM_OCEAN, 0, WARM),
        new BiomeDescriptor(BIOME_LUKEWARM_OCEAN, 0, LUKEWARM),
        new BiomeDescriptor(BIOME_COLD_OCEAN, 0, COLD),
        new BiomeDescriptor(BIOME_DEEP_WARM_OCEAN, 0, DEEP, WARM),
        new BiomeDescriptor(BIOME_DEEP_LUKEWARM_OCEAN, 0, DEEP, LUKEWARM),
        new BiomeDescriptor(BIOME_DEEP_COLD_OCEAN, 0, DEEP, COLD),

        new BiomeDescriptor(BIOME_DEEP_FROZEN_OCEAN, 0, DEEP, FROZEN),

        new BiomeDescriptor(BIOME_THE_VOID),
        new BiomeDescriptor(BIOME_SUNFLOWER_PLAINS, 1, FLOWERS),

        new BiomeDescriptor(BIOME_DESERT_LAKES, 2, LAKES),
        new BiomeDescriptor(BIOME_GRAVELLY_MOUNTAINS, 3, GRAVELLY),
        new BiomeDescriptor(BIOME_FLOWER_FOREST, 4, FLOWERS),
        new BiomeDescriptor(BIOME_TAIGA_MOUNTAINS, 5, MOUNTAINOUS),
        new BiomeDescriptor(BIOME_SWAMP_HILLS, 6, HILLS),

        new BiomeDescriptor(BIOME_ICE_SPIKES),
        new BiomeDescriptor(BIOME_MODIFIED_JUNGLE, 21, MODIFIED),

        new BiomeDescriptor(BIOME_MODIFIED_JUNGLE_EDGE, 21, MODIFIED, EDGE),
        new BiomeDescriptor(BIOME_TALL_BIRCH_FOREST, 27, TALL),
        new BiomeDescriptor(BIOME_TALL_BIRCH_HILLS, 27, HILLS, TALL),
        new BiomeDescriptor(BIOME_DARK_FOREST_HILLS, 29, HILLS),
        new BiomeDescriptor(BIOME_SNOWY_TAIGA_MOUNTAINS, 5, SNOWY, MOUNTAINOUS),

        new BiomeDescriptor(BIOME_GIANT_SPRUCE_TAIGA),
        new BiomeDescriptor(BIOME_GIANT_SPRUCE_TAIGA_HILLS, 160, HILLS),
        new BiomeDescriptor(BIOME_MODIFIED_GRAVELLY_MOUNTAINS, 3, GRAVELLY, VARIANT),
        new BiomeDescriptor(BIOME_SHATTERED_SAVANNA, 35, SHATTERED),
        new BiomeDescriptor(BIOME_SHATTERED_SAVANNA_PLATEAU, 35, SHATTERED, PLATEAU),
        new BiomeDescriptor(BIOME_ERODED_BADLANDS, 37, ERODED),
        new BiomeDescriptor(BIOME_MODIFIED_WOODED_BADLANDS_PLATEAU, 37, MODIFIED, WOODED, PLATEAU),
        new BiomeDescriptor(BIOME_MODIFIED_BADLANDS_PLATEAU, 37, MODIFIED, PLATEAU),
        new BiomeDescriptor(BIOME_BAMBOO_JUNGLE),
        new BiomeDescriptor(BIOME_BAMBOO_JUNGLE_HILLS, 168, HILLS),

        new BiomeDescriptor(BIOME_SOUL_SAND_VALLEY),
        new BiomeDescriptor(BIOME_CRIMSON_FOREST),
        new BiomeDescriptor(BIOME_WARPED_FOREST),
        new BiomeDescriptor(BIOME_BASALT_DELTAS),
        new BiomeDescriptor(BIOME_DRIPSTONE_CAVES),
        new BiomeDescriptor(BIOME_LUSH_CAVES)
    );

    private static final List MC_120_DESCRIPTORS = asList(
        new BiomeDescriptor(BIOME_OCEAN),
        new BiomeDescriptor(BIOME_PLAINS),
        new BiomeDescriptor(BIOME_DESERT),
        new BiomeDescriptor(BIOME_WINDSWEPT_HILLS),
        new BiomeDescriptor(BIOME_FOREST),
        new BiomeDescriptor(BIOME_TAIGA),
        new BiomeDescriptor(BIOME_SWAMP),
        new BiomeDescriptor(BIOME_RIVER),
        new BiomeDescriptor(BIOME_NETHER_WASTES),
        new BiomeDescriptor(BIOME_THE_END),

        new BiomeDescriptor(BIOME_FROZEN_OCEAN, BIOME_OCEAN, FROZEN),
        new BiomeDescriptor(BIOME_FROZEN_RIVER, BIOME_RIVER, FROZEN),
        new BiomeDescriptor(BIOME_SNOWY_PLAINS),
        // BIOME_SNOWY_MOUNTAINS no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_MUSHROOM_FIELDS),
        // BIOME_MUSHROOM_FIELD_SHORE no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_BEACH),
        // BIOME_DESERT_HILLS no longer exists separately in Minecraft 1.18+
        // BIOME_WOODED_HILLS no longer exists separately in Minecraft 1.18+
        // BIOME_TAIGA_HILLS no longer exists separately in Minecraft 1.18+

        // BIOME_MOUNTAIN_EDGE no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_JUNGLE),
        // BIOME_JUNGLE_HILLS no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_SPARSE_JUNGLE, BIOME_JUNGLE, SPARSE),
        new BiomeDescriptor(BIOME_DEEP_OCEAN, BIOME_OCEAN, DEEP),
        new BiomeDescriptor(BIOME_STONY_SHORE),
        new BiomeDescriptor(BIOME_SNOWY_BEACH, BIOME_BEACH, SNOWY),
        new BiomeDescriptor(BIOME_BIRCH_FOREST),
        // BIOME_BIRCH_FOREST_HILLS no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_DARK_FOREST),

        new BiomeDescriptor(BIOME_SNOWY_TAIGA, BIOME_TAIGA, SNOWY),
        // BIOME_SNOWY_TAIGA_HILLS no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_OLD_GROWTH_PINE_TAIGA),
        // BIOME_GIANT_TREE_TAIGA_HILLS no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_WINDSWEPT_FOREST, BIOME_FOREST, WINDSWEPT),
        new BiomeDescriptor(BIOME_SAVANNA),
        new BiomeDescriptor(BIOME_SAVANNA_PLATEAU, BIOME_SAVANNA, PLATEAU),
        new BiomeDescriptor(BIOME_BADLANDS),
        new BiomeDescriptor(BIOME_WOODED_BADLANDS, BIOME_BADLANDS, WOODED),
        // BIOME_BADLANDS_PLATEAU no longer exists separately in Minecraft 1.18+

        new BiomeDescriptor(BIOME_SMALL_END_ISLANDS),
        new BiomeDescriptor(BIOME_END_MIDLANDS),
        new BiomeDescriptor(BIOME_END_HIGHLANDS),
        new BiomeDescriptor(BIOME_END_BARRENS),
        new BiomeDescriptor(BIOME_WARM_OCEAN, BIOME_OCEAN, WARM),
        new BiomeDescriptor(BIOME_LUKEWARM_OCEAN, BIOME_OCEAN, LUKEWARM),
        new BiomeDescriptor(BIOME_COLD_OCEAN, BIOME_OCEAN, COLD),
        // BIOME_DEEP_WARM_OCEAN no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_DEEP_LUKEWARM_OCEAN, BIOME_OCEAN, DEEP, LUKEWARM),
        new BiomeDescriptor(BIOME_DEEP_COLD_OCEAN, BIOME_OCEAN, DEEP, COLD),

        new BiomeDescriptor(BIOME_DEEP_FROZEN_OCEAN, BIOME_OCEAN, DEEP, FROZEN),

        new BiomeDescriptor(BIOME_THE_VOID),
        new BiomeDescriptor(BIOME_SUNFLOWER_PLAINS, BIOME_PLAINS, FLOWERS),

        // BIOME_DESERT_LAKES no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_WINDSWEPT_GRAVELLY_HILLS, BIOME_WINDSWEPT_HILLS, GRAVELLY),
        new BiomeDescriptor(BIOME_FLOWER_FOREST, BIOME_FOREST, FLOWERS),
        // BIOME_TAIGA_MOUNTAINS no longer exists separately in Minecraft 1.18+
        // BIOME_SWAMP_HILLS no longer exists separately in Minecraft 1.18+

        new BiomeDescriptor(BIOME_ICE_SPIKES),
        // BIOME_MODIFIED_JUNGLE no longer exists separately in Minecraft 1.18+

        // BIOME_MODIFIED_JUNGLE_EDGE no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_OLD_GROWTH_BIRCH_FOREST, BIOME_BIRCH_FOREST, OLD_GROWTH),
        // BIOME_TALL_BIRCH_HILLS no longer exists separately in Minecraft 1.18+
        // BIOME_DARK_FOREST_HILLS no longer exists separately in Minecraft 1.18+
        // BIOME_SNOWY_TAIGA_MOUNTAINS no longer exists separately in Minecraft 1.18+

        new BiomeDescriptor(BIOME_OLD_GROWTH_SPRUCE_TAIGA),
        // BIOME_GIANT_SPRUCE_TAIGA_HILLS no longer exists separately in Minecraft 1.18+
        // BIOME_MODIFIED_GRAVELLY_MOUNTAINS no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_WINDSWEPT_SAVANNA, BIOME_SAVANNA, WINDSWEPT),
        // BIOME_SHATTERED_SAVANNA_PLATEAU no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_ERODED_BADLANDS, BIOME_BADLANDS, ERODED),
        // BIOME_MODIFIED_WOODED_BADLANDS_PLATEAU no longer exists separately in Minecraft 1.18+
        // BIOME_MODIFIED_BADLANDS_PLATEAU no longer exists separately in Minecraft 1.18+
        new BiomeDescriptor(BIOME_BAMBOO_JUNGLE),
        // BIOME_BAMBOO_JUNGLE_HILLS no longer exists separately in Minecraft 1.18+

        new BiomeDescriptor(BIOME_SOUL_SAND_VALLEY),
        new BiomeDescriptor(BIOME_CRIMSON_FOREST),
        new BiomeDescriptor(BIOME_WARPED_FOREST),
        new BiomeDescriptor(BIOME_BASALT_DELTAS),
        new BiomeDescriptor(BIOME_DRIPSTONE_CAVES),
        new BiomeDescriptor(BIOME_LUSH_CAVES),

        // Minecraft 1.18+ biomes, with synthetic numerical ID's that do not correspond to pre-1.18 biome ID's:
        new BiomeDescriptor(BIOME_CHERRY_GROVE),
        new BiomeDescriptor(BIOME_MANGROVE_SWAMP),
        new BiomeDescriptor(BIOME_DEEP_DARK),
        new BiomeDescriptor(BIOME_FROZEN_PEAKS),

        new BiomeDescriptor(BIOME_GROVE),
        new BiomeDescriptor(BIOME_JAGGED_PEAKS),
        new BiomeDescriptor(BIOME_MEADOW),
        new BiomeDescriptor(BIOME_SNOWY_SLOPES),
        new BiomeDescriptor(BIOME_STONY_PEAKS)
    );

    static {
        // Sanity checks
        final Set modernIdsEncountered = new HashSet<>();
        for (int biomeId = 0; biomeId <= HIGHEST_BIOME_ID; biomeId++) {
            if ((biomeId < Minecraft1_17Biomes.BIOME_NAMES.length) && (Minecraft1_17Biomes.BIOME_NAMES[biomeId] != null)) {
                boolean found = false;
                for (BiomeDescriptor descriptor: MC_117_DESCRIPTORS) {
                    if (descriptor.id == biomeId) {
                        found = true;
                        // Check that it is reachable
                        if ((descriptor.baseId != biomeId) && MC_117_DESCRIPTORS.stream().noneMatch(d -> d.baseId == d.id && d.id == descriptor.baseId)) {
                            throw new IllegalArgumentException("Biome " + biomeId + " (" + Minecraft1_17Biomes.BIOME_NAMES[biomeId] + ") is not reachable in MC_117_DESCRIPTORS");
                        }
                        break;
                    }
                }
                if (! found) {
                    throw new IllegalArgumentException("MC_117_DESCRIPTORS is missing biome " + biomeId + " (" + Minecraft1_17Biomes.BIOME_NAMES[biomeId] + ")");
                }
            }
            if (Minecraft1_20Biomes.BIOME_NAMES[biomeId] != null) {
                if (modernIdsEncountered.contains(MODERN_IDS[biomeId])) {
                    continue;
                } else {
                    modernIdsEncountered.add(MODERN_IDS[biomeId]);
                }
                boolean found = false;
                for (BiomeDescriptor descriptor: MC_120_DESCRIPTORS) {
                    if (descriptor.id == biomeId) {
                        found = true;
                        // Check that it is reachable
                        if ((descriptor.baseId != biomeId) && MC_120_DESCRIPTORS.stream().noneMatch(d -> d.baseId == d.id && d.id == descriptor.baseId)) {
                            throw new IllegalArgumentException("Biome " + biomeId + " (" + Minecraft1_20Biomes.BIOME_NAMES[biomeId] + ") is not reachable in MC_120_DESCRIPTORS");
                        }
                        break;
                    }
                }
                if (! found) {
                    throw new IllegalArgumentException("MC_120_DESCRIPTORS is missing biome " + biomeId + " (" + Minecraft1_20Biomes.BIOME_NAMES[biomeId] + ")");
                }
            }
        }
        MC_117_DESCRIPTORS.forEach(desc -> {
            if (Minecraft1_17Biomes.BIOME_NAMES[desc.id] == null) {
                throw new IllegalArgumentException("MC_117_DESCRIPTORS contains non-existent biome ID " + desc.id);
            }
        });
        modernIdsEncountered.clear();
        MC_120_DESCRIPTORS.forEach(desc -> {
            if (Minecraft1_20Biomes.BIOME_NAMES[desc.id] == null) {
                throw new IllegalArgumentException("MC_120_DESCRIPTORS contains non-existent biome ID " + desc.id);
            }
            if (modernIdsEncountered.contains(MODERN_IDS[desc.id])) {
                System.err.println("MC_120_DESCRIPTORS contains duplicate descriptor for biome ID " + desc.id + " (" + Minecraft1_20Biomes.BIOME_NAMES[desc.id] + ") for modern ID " + MODERN_IDS[desc.id]);
            } else {
                modernIdsEncountered.add(MODERN_IDS[desc.id]);
            }
        });
        Set uniqueIds = stream(MC_117_BIOME_ORDER).filter(i -> i != -1).boxed().collect(toSet());
        if (uniqueIds.size() != stream(MC_117_BIOME_ORDER).filter(i -> i != -1).count()) {
            throw new IllegalArgumentException("MC_117_BIOME_ORDER has duplicate IDs");
        }
        uniqueIds = MC_117_DESCRIPTORS.stream().map(d -> d.id).collect(toSet());
        if (uniqueIds.size() != MC_117_DESCRIPTORS.size()) {
            throw new IllegalArgumentException("MC_117_DESCRIPTORS has duplicate IDs");
        }
        uniqueIds = stream(MC_120_BIOME_ORDER).filter(i -> i != -1).boxed().collect(toSet());
        if (uniqueIds.size() != stream(MC_120_BIOME_ORDER).filter(i -> i != -1).count()) {
            throw new IllegalArgumentException("MC_120_BIOME_ORDER has duplicate IDs");
        }
        uniqueIds = MC_120_DESCRIPTORS.stream().map(d -> d.id).collect(toSet());
        if (uniqueIds.size() != MC_120_DESCRIPTORS.size()) {
            throw new IllegalArgumentException("MC_120_DESCRIPTORS has duplicate IDs");
        }
    }

    private static final BiomesSet MINECRAFT_1_17_BIOMES = new BiomesSet(MC_117_BIOME_ORDER, MC_117_DESCRIPTORS, Minecraft1_17Biomes.BIOME_NAMES);
    private static final BiomesSet MINECRAFT_1_20_BIOMES = new BiomesSet(MC_120_BIOME_ORDER, new HashSet<>(MC_120_DESCRIPTORS), Minecraft1_20Biomes.BIOME_NAMES);

    public enum BiomeOption {HILLS, SHORE, EDGE, PLATEAU, MOUNTAINOUS, VARIANT, FROZEN, SNOWY, DEEP, WOODED, WARM,
        LUKEWARM, COLD, TALL, FLOWERS, LAKES, GRAVELLY, SHATTERED, SMALL_ISLANDS, MIDLANDS, HIGHLANDS, BARRENS,
        MODIFIED, ERODED, WINDSWEPT, SPARSE, OLD_GROWTH}

    public static class BiomeDescriptor {
        public BiomeDescriptor(int id) {
            this(id, id);
        }

        public BiomeDescriptor(int id, int baseId, BiomeOption... options) {
            this.id = id;
            this.baseId = baseId;
            this.options = ((options != null) && (options.length > 0)) ? EnumSet.copyOf(asList(options)) : emptySet();
        }

        public int getId() {
            return id;
        }

        public int getBaseId() {
            return baseId;
        }

        public Set getOptions() {
            return options;
        }

        private final int id, baseId;
        private final Set options;
    }

    public interface Listener {
        void biomeSelected(int biomeId);
    }

    static class BiomesSet {
        BiomesSet(int[] biomeOrder, Set descriptors, String[] displayNames) {
            this.biomeOrder = biomeOrder;
            this.descriptors = descriptors;
            this.displayNames = displayNames;
        }

        final int[] biomeOrder;
        final Set descriptors;
        final String[] displayNames;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy