diff --git a/core/src/main/java/com/volmit/iris/core/gui/BiomePalette.java b/core/src/main/java/com/volmit/iris/core/gui/BiomePalette.java new file mode 100644 index 000000000..326f490c3 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/BiomePalette.java @@ -0,0 +1,121 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.volmit.iris.core.gui; + +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.engine.object.IrisBiomePaletteLayer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * One of the block palettes a biome places: a surface, sea or cave ceiling layer, or its wall or slab blocks. + */ +record BiomePalette(String biomeKey, Kind kind, int index) { + private static final String ID_PREFIX = "palette:"; + private static final char ID_SEPARATOR = '#'; + + enum Kind { + LAYER("Layer", true), + SEA_LAYER("Sea layer", true), + CAVE_CEILING_LAYER("Cave ceiling layer", true), + WALL("Wall", false), + SLAB("Slab", false); + + private final String label; + private final boolean stacked; + + Kind(String label, boolean stacked) { + this.label = label; + this.stacked = stacked; + } + + /** + * Whether the palette is a stack of blocks with a thickness, rather than single blocks on walls or slopes. + */ + boolean isStacked() { + return stacked; + } + + // A biome without wall or slab blocks still has those palettes, just empty. + List palettesOf(IrisBiome biome) { + return switch (this) { + case LAYER -> biome.getLayers(); + case SEA_LAYER -> biome.getSeaLayers(); + case CAVE_CEILING_LAYER -> biome.getCaveCeilingLayers(); + case WALL -> biome.getWall().getPalette().isEmpty() ? List.of() : List.of(biome.getWall()); + case SLAB -> biome.getSlab().getPalette().isEmpty() ? List.of() : List.of(biome.getSlab()); + }; + } + } + + static List allOf(String biomeKey, IrisBiome biome) { + List palettes = new ArrayList<>(); + for (Kind kind : Kind.values()) { + List layers = kind.palettesOf(biome); + for (int index = 0; index < layers.size(); index++) { + if (!layers.get(index).getPalette().isEmpty()) { + palettes.add(new BiomePalette(biomeKey, kind, index)); + } + } + } + return List.copyOf(palettes); + } + + static BiomePalette fromId(String id) { + if (id == null || !id.startsWith(ID_PREFIX)) { + return null; + } + int indexSeparator = id.lastIndexOf(ID_SEPARATOR); + int kindSeparator = id.lastIndexOf(ID_SEPARATOR, indexSeparator - 1); + if (kindSeparator < ID_PREFIX.length()) { + return null; + } + return new BiomePalette( + id.substring(ID_PREFIX.length(), kindSeparator), + Kind.valueOf(id.substring(kindSeparator + 1, indexSeparator)), + Integer.parseInt(id.substring(indexSeparator + 1)) + ); + } + + static boolean belongsTo(String id, String biomeKey) { + return id != null && id.startsWith(ID_PREFIX + biomeKey + ID_SEPARATOR); + } + + String id() { + return ID_PREFIX + biomeKey + ID_SEPARATOR + kind.name() + ID_SEPARATOR + index; + } + + String label() { + return kind.isStacked() ? kind.label + " " + index : kind.label; + } + + String lowercaseLabel() { + return label().toLowerCase(Locale.ROOT); + } + + /** + * The palette in the given copy of the biome, or null when a hotload removed it. + */ + IrisBiomePaletteLayer resolve(IrisBiome biome) { + List layers = kind.palettesOf(biome); + return index < layers.size() ? layers.get(index) : null; + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/BiomePalettePlacement.java b/core/src/main/java/com/volmit/iris/core/gui/BiomePalettePlacement.java new file mode 100644 index 000000000..c18e34336 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/BiomePalettePlacement.java @@ -0,0 +1,170 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.volmit.iris.core.gui; + +import com.volmit.iris.core.loader.IrisData; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.engine.object.IrisBiomePaletteLayer; +import com.volmit.iris.engine.object.IrisDimension; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.noise.CNG; + +/** + * How thick one biome palette is at a spot and which noise picks its block there, sampled exactly the way the + * generator does for that kind of palette. The generator seeds each palette's noise from the first call it makes, + * so these use the same random sources the terrain, carving and post passes use. + */ +abstract class BiomePalettePlacement { + protected final IrisBiomePaletteLayer layer; + protected final IrisData data; + + private BiomePalettePlacement(IrisBiomePaletteLayer layer, IrisData data) { + this.layer = layer; + this.data = data; + } + + static BiomePalettePlacement of(Engine engine, IrisBiome biome, BiomePalette palette, IrisBiomePaletteLayer layer) { + IrisData data = engine.getData(); + RNG terrainRandom = new RNG(engine.getSeedManager().getTerrain()); + RNG carveRandom = new RNG(engine.getSeedManager().getCarve()); + RNG postRandom = new RNG(engine.getSeedManager().getPost()); + int seaLevel = seaLevel(engine.getDimension()); + return switch (palette.kind()) { + case LAYER -> new SurfaceLayer(biome, palette.index(), layer, terrainRandom, data); + case SEA_LAYER -> new LegacyStackedLayer(layer, data, terrainRandom.nextParallelRNG(palette.index()), + biome.getLayerSeaHeightGenerators(terrainRandom, data), palette.index()); + case CAVE_CEILING_LAYER -> new LegacyStackedLayer(layer, data, carveRandom.nextParallelRNG(palette.index()), + biome.getLayerHeightGenerators(terrainRandom, data), palette.index()); + case WALL -> new SingleBlocks(layer, data, carveRandom, seaLevel); + case SLAB -> new SingleBlocks(layer, data, postRandom, seaLevel); + }; + } + + private static int seaLevel(IrisDimension dimension) { + return dimension == null ? 0 : dimension.getFluidHeight(); + } + + /** + * How many blocks of this palette are stacked at the spot. + */ + abstract int thickness(int blockX, int blockZ); + + abstract double noise(int blockX, int blockZ, int depth); + + /** + * What the preview simplifies about this palette, or null when it shows exactly what the world places. + */ + String simplification() { + return null; + } + + private static final class SurfaceLayer extends BiomePalettePlacement { + private final IrisBiome biome; + private final int index; + private final RNG random; + + private SurfaceLayer(IrisBiome biome, int index, IrisBiomePaletteLayer layer, RNG random, IrisData data) { + super(layer, data); + this.biome = biome; + this.index = index; + this.random = random; + } + + @Override + int thickness(int blockX, int blockZ) { + return biome.getLayerThickness(index, blockX, blockZ, random, data); + } + + @Override + double noise(int blockX, int blockZ, int depth) { + return biome.getLayerNoise(index, blockX, blockZ, depth, random, data); + } + } + + /** + * Sea and cave ceiling layers go through the older sampling path, which divides by the zoom twice and borrows + * its thickness noise from another list: a cave ceiling layer uses the surface layer at the same position's. + */ + private static final class LegacyStackedLayer extends BiomePalettePlacement { + private final RNG random; + private final CNG thicknessNoise; + + private LegacyStackedLayer(IrisBiomePaletteLayer layer, IrisData data, RNG random, KList thicknessNoises, + int index) { + super(layer, data); + this.random = random; + this.thicknessNoise = index < thicknessNoises.size() ? thicknessNoises.get(index) : null; + } + + @Override + int thickness(int blockX, int blockZ) { + if (thicknessNoise == null) { + return 0; + } + double zoom = layer.getZoom(); + return Math.max(0, thicknessNoise.fit(layer.getMinHeight(), layer.getMaxHeight(), blockX / zoom, blockZ / zoom)); + } + + @Override + double noise(int blockX, int blockZ, int depth) { + double zoom = layer.getZoom(); + return layer.getLayerGenerator(random, data) + .noise((blockX + depth) / zoom / zoom, depth / zoom, (blockZ - depth) / zoom / zoom); + } + + @Override + String simplification() { + return thicknessNoise == null + ? "The generator takes this layer's thickness from the surface layer at the same position, which doesn't exist, so it is never placed." + : null; + } + } + + /** + * Wall and slab blocks are picked one at a time with noise that also changes with height. + */ + private static final class SingleBlocks extends BiomePalettePlacement { + private final RNG random; + private final int sampleHeight; + + private SingleBlocks(IrisBiomePaletteLayer layer, IrisData data, RNG random, int sampleHeight) { + super(layer, data); + this.random = random; + this.sampleHeight = sampleHeight; + } + + @Override + int thickness(int blockX, int blockZ) { + return 1; + } + + @Override + double noise(int blockX, int blockZ, int depth) { + double zoom = layer.getZoom(); + return layer.getLayerGenerator(random, data).noise(blockX / zoom, sampleHeight / zoom, blockZ / zoom); + } + + @Override + String simplification() { + return "These blocks also change with height; the preview shows them at sea level (Y " + sampleHeight + ")."; + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java index 4b1afa6a8..3925308bb 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java +++ b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java @@ -175,7 +175,7 @@ public final class NoiseExplorerGUI extends JPanel { String customName, long initialSeed, PaletteExplorerStart paletteStart) { this.mode = mode; this.paletteStart = paletteStart; - this.paletteNavigator = new PaletteNavigator(this::selectPaletteLayer); + this.paletteNavigator = new PaletteNavigator(this::selectPalette); this.awaitingInitialPackSource = mode == ExplorerMode.PALETTES; this.requestedPaletteBiome = paletteStart == null ? null : paletteStart.biomeKey(); this.initialViewport = paletteStart == null @@ -414,8 +414,9 @@ public final class NoiseExplorerGUI extends JPanel { return; } requestedPaletteBiome = biomeKey; - if (!packSources.isEmpty()) { - selectPaletteLayer(biomeKey, 0); + SourceItem firstPalette = firstPaletteOf(biomeKey, packSources); + if (firstPalette != null && firstPalette != selectedSource) { + selectSource(firstPalette); } } @@ -612,16 +613,15 @@ public final class NoiseExplorerGUI extends JPanel { if (biome == null) { continue; } - for (int layer = 0; layer < biome.getLayers().size(); layer++) { - int selectedLayer = layer; + for (BiomePalette palette : BiomePalette.allOf(key, biome)) { sources.add(new SourceItem( - paletteSourceId(key, layer), - "Layer " + layer + " · " + PaletteCatalog.summarizeBlocks(biome.getLayers().get(layer)), + palette.id(), + palette.label() + " · " + PaletteCatalog.summarizeBlocks(palette.resolve(biome)), key, NoiseCategory.PACK_PALETTES, false, (sourceSeed, depth) -> { - PaletteLayerPreview preview = PaletteLayerPreview.load(engine, key, selectedLayer, depth, blockTextures); + PaletteLayerPreview preview = PaletteLayerPreview.load(engine, palette, depth, blockTextures); return new LoadedSource(preview::sample, preview); } )); @@ -630,19 +630,19 @@ public final class NoiseExplorerGUI extends JPanel { return List.copyOf(sources); } - private static String paletteSourceId(String biomeKey, int layer) { - return "palette:" + biomeKey + "#" + layer; + private static SourceItem firstPaletteOf(String biomeKey, List sources) { + return sources.stream().filter(source -> BiomePalette.belongsTo(source.id(), biomeKey)).findFirst().orElse(null); } - private void selectPaletteLayer(String biomeKey, int layer) { - SourceItem source = findSourceById(paletteSourceId(biomeKey, layer), packSources); + private void selectPalette(BiomePalette palette) { + SourceItem source = findSourceById(palette.id(), packSources); if (source != null && source != selectedSource) { selectSource(source); } } - private static int paletteLayerOf(SourceItem source) { - return Integer.parseInt(source.id().substring(source.id().lastIndexOf('#') + 1)); + private static BiomePalette paletteOf(SourceItem source) { + return BiomePalette.fromId(source.id()); } private void loadBlockTextures() { @@ -708,8 +708,7 @@ public final class NoiseExplorerGUI extends JPanel { } private void selectInitialPackSource(List sources) { - String initialSourceId = requestedPaletteBiome == null ? null : paletteSourceId(requestedPaletteBiome, 0); - SourceItem requested = initialSourceId == null ? null : findSourceById(initialSourceId, sources); + SourceItem requested = requestedPaletteBiome == null ? null : firstPaletteOf(requestedPaletteBiome, sources); SourceItem initial = requested == null ? sources.get(0) : requested; if (!selectSourceInList(initial.id())) { selectSource(initial); @@ -724,7 +723,7 @@ public final class NoiseExplorerGUI extends JPanel { return "Open a studio world to see its palettes"; } long biomes = sources.stream().map(SourceItem::rawName).distinct().count(); - return sources.size() + " palette layers in " + biomes + " biomes"; + return sources.size() + " palettes in " + biomes + " biomes"; } private void failRemovedPackSource(SourceItem removedSource) { @@ -810,7 +809,7 @@ public final class NoiseExplorerGUI extends JPanel { paletteDepth = 0; selectedLabel.setText(source.category() == NoiseCategory.PACK_PALETTES ? source.rawName() : source.text()); if (source.category() == NoiseCategory.PACK_PALETTES) { - paletteNavigator.markSelected(source.rawName(), paletteLayerOf(source)); + paletteNavigator.markSelected(paletteOf(source)); } loadSelectedSource(); } @@ -1298,7 +1297,7 @@ public final class NoiseExplorerGUI extends JPanel { private static String sourceTitle(SourceItem source) { return source.category() == NoiseCategory.PACK_PALETTES - ? source.rawName() + " · layer " + paletteLayerOf(source) + ? source.rawName() + " · " + paletteOf(source).lowercaseLabel() : source.text(); } diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java index 4a1b6cb16..088ee46ad 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java @@ -30,6 +30,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -74,7 +75,7 @@ record PaletteCatalog(List regions, List biomesOutsideRegio static String summarizeBlocks(IrisBiomePaletteLayer layer) { List names = layer.getPalette().stream() - .map(block -> block.getBlock().replace("minecraft:", "")) + .map(block -> block.getBlock().replace("minecraft:", "").toLowerCase(Locale.ROOT)) .distinct() .toList(); if (names.size() <= SUMMARIZED_BLOCK_COUNT) { @@ -86,21 +87,42 @@ record PaletteCatalog(List regions, List biomesOutsideRegio private static Map loadBiomes(IrisData data) { String[] keys = data.getBiomeLoader().getPossibleKeys(); - Map biomes = new LinkedHashMap<>(); + Map loaded = new LinkedHashMap<>(); if (keys == null) { - return biomes; + return Map.of(); } Arrays.sort(keys); for (String key : keys) { IrisBiome biome = data.getBiomeLoader().load(key); if (biome != null) { - List layers = biome.getLayers().stream().map(PaletteCatalog::summarizeBlocks).toList(); - biomes.put(key, new BiomeEntry(key, biome.getName(), layers, List.copyOf(biome.getChildren()))); + loaded.put(key, biome); } } + Map> parentsByChild = parentsByChild(loaded); + Map biomes = new LinkedHashMap<>(); + loaded.forEach((key, biome) -> biomes.put(key, new BiomeEntry( + key, + biome.getName(), + summarizePalettes(key, biome), + biome.getChildren().stream().filter(loaded::containsKey).distinct().toList(), + parentsByChild.getOrDefault(key, List.of()) + ))); return biomes; } + private static Map> parentsByChild(Map biomes) { + Map> parents = new LinkedHashMap<>(); + biomes.forEach((parentKey, parent) -> parent.getChildren().stream().distinct().forEach(childKey -> + parents.computeIfAbsent(childKey, key -> new ArrayList<>()).add(parentKey))); + return parents; + } + + private static List summarizePalettes(String key, IrisBiome biome) { + return BiomePalette.allOf(key, biome).stream() + .map(palette -> new PaletteSummary(palette, summarizeBlocks(palette.resolve(biome)))) + .toList(); + } + private static RegionEntry describeRegion(String key, IrisRegion region, Map biomes) { List groups = new ArrayList<>(); addGroup(groups, "Land", region.getLandBiomes(), biomes); @@ -134,12 +156,26 @@ record PaletteCatalog(List regions, List biomesOutsideRegio record BiomeGroup(String label, List biomeKeys) { } - record BiomeEntry(String key, String name, List layerSummaries, List childKeys) { + /** + * @param parentKeys the biomes that list this one as a child, which it can appear inside of + */ + record BiomeEntry(String key, String name, List palettes, List childKeys, + List parentKeys) { boolean matches(String filter) { if (key.toLowerCase().contains(filter) || name.toLowerCase().contains(filter)) { return true; } - return layerSummaries.stream().anyMatch(summary -> summary.toLowerCase().contains(filter)); + return palettes.stream().anyMatch(summary -> summary.text().toLowerCase().contains(filter)); + } + + BiomePalette firstPalette() { + return palettes.isEmpty() ? null : palettes.get(0).palette(); + } + } + + record PaletteSummary(BiomePalette palette, String blocks) { + String text() { + return palette.label() + " · " + blocks; } } } diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java index 306ed5809..1c538ef9b 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java @@ -68,9 +68,8 @@ final class PaletteInspectorPanel extends JPanel { } void showPreview(PaletteLayerPreview preview) { - titleLabel.setText(preview.biomeKey() + " · layer " + preview.layer()); - detailLabel.setText(thicknessText(preview) + " · zoom " + formatZoom(preview.zoom()) - + " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)"); + titleLabel.setText(preview.biomeKey() + " · " + preview.palette().lowercaseLabel()); + detailLabel.setText(placementText(preview)); depth = preview.depth(); deepestDepth = Math.max(0, preview.maxThickness() - 1); refreshDepthControls(); @@ -162,6 +161,15 @@ final class PaletteInspectorPanel extends JPanel { return label; } + // Stacked layers divide their coordinates by the zoom twice before sampling; wall and slab blocks only once. + private static String placementText(PaletteLayerPreview preview) { + if (!preview.palette().kind().isStacked()) { + return "Single blocks · zoom " + formatZoom(preview.zoom()); + } + return thicknessText(preview) + " · zoom " + formatZoom(preview.zoom()) + + " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)"; + } + private static String thicknessText(PaletteLayerPreview preview) { if (preview.minThickness() == preview.maxThickness()) { return preview.maxThickness() + (preview.maxThickness() == 1 ? " block thick" : " blocks thick"); @@ -408,6 +416,9 @@ final class PaletteInspectorPanel extends JPanel { notes.add(percent(preview.outOfRangeShare()) + " of the noise values fall outside 0 to 1." + " Those spots get the first block."); } + if (preview.simplification() != null) { + notes.add(preview.simplification()); + } if (preview.hasSlopeCondition()) { notes.add("This layer has a slope condition, which the preview ignores."); } diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java index bfe714198..a4e6a24ca 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java @@ -40,12 +40,10 @@ final class PaletteLayerPreview { private static final double FAR_FROM_WEIGHT_FACTOR = 2D; private static final int BLOCK_CACHE_SIZE = 1 << 16; - private final String biomeKey; - private final IrisBiome biome; + private final BiomePalette palette; private final IrisBiomePaletteLayer paletteLayer; - private final int layer; + private final BiomePalettePlacement placement; private final int depth; - private final RNG random; private final IrisData data; private final int[] entryBySlot; private final BiomeTintField biomeTints; @@ -58,18 +56,16 @@ final class PaletteLayerPreview { private final double absentShare; private final double outOfRangeShare; - private PaletteLayerPreview(String biomeKey, IrisBiome biome, int layer, int depth, RNG random, IrisData data, - BlockTextures textures, BiomeTintField biomeTints) { - this.biomeKey = biomeKey; - this.biome = biome; - this.paletteLayer = biome.getLayers().get(layer); - this.layer = layer; + private PaletteLayerPreview(BiomePalette palette, IrisBiomePaletteLayer paletteLayer, BiomePalettePlacement placement, + int depth, IrisData data, BlockTextures textures, BiomeTintField biomeTints) { + this.palette = palette; + this.paletteLayer = paletteLayer; + this.placement = placement; this.depth = depth; - this.random = random; this.data = data; this.entryBySlot = paletteLayer.getPaletteEntryPerSlot(data).stream().mapToInt(Integer::intValue).toArray(); if (entryBySlot.length == 0) { - throw new IllegalStateException("No palette block in " + biomeKey + " layer " + layer + " could be found"); + throw new IllegalStateException("No block of " + palette.biomeKey() + " " + palette.lowercaseLabel() + " could be found"); } this.biomeTints = biomeTints; this.textureByEntry = resolveEntryTextures(textures); @@ -83,20 +79,21 @@ final class PaletteLayerPreview { this.entries = describeEntries(statistics.sharesByEntry()); } - static PaletteLayerPreview load(Engine engine, String biomeKey, int layer, int depth, BlockTextures textures) { + static PaletteLayerPreview load(Engine engine, BiomePalette palette, int depth, BlockTextures textures) { if (engine == null || engine.isClosed()) { throw new IllegalStateException("Open a studio world to preview pack palettes"); } IrisData data = engine.getData(); - IrisBiome biome = data.getBiomeLoader().load(biomeKey); - if (biome == null || layer >= biome.getLayers().size()) { - throw new IllegalStateException("Palette layer removed during hotload: " + biomeKey + " layer " + layer); + IrisBiome biome = data.getBiomeLoader().load(palette.biomeKey()); + IrisBiomePaletteLayer paletteLayer = biome == null ? null : palette.resolve(biome); + if (paletteLayer == null) { + throw new IllegalStateException("Palette removed during hotload: " + palette.biomeKey() + " " + palette.lowercaseLabel()); } - RNG terrainRandom = new RNG(engine.getSeedManager().getTerrain()); + BiomePalettePlacement placement = BiomePalettePlacement.of(engine, biome, palette, paletteLayer); BiomeTintField biomeTints = textures == null ? null : new BiomeTintField(biome, new RNG(engine.getSeedManager().getBiome()), textures.biomeColors()); - return new PaletteLayerPreview(biomeKey, biome, layer, depth, terrainRandom, data, textures, biomeTints); + return new PaletteLayerPreview(palette, paletteLayer, placement, depth, data, textures, biomeTints); } // The world picks one block per whole coordinate, and zoomed in, many pixels fall on the same block. @@ -114,10 +111,10 @@ final class PaletteLayerPreview { } private double sampleBlock(int blockX, int blockZ) { - if (biome.getLayerThickness(layer, blockX, blockZ, random, data) <= depth) { + if (placement.thickness(blockX, blockZ) <= depth) { return Double.NaN; } - return biome.getLayerNoise(layer, blockX, blockZ, depth, random, data); + return placement.noise(blockX, blockZ, depth); } private static int blockCacheSlot(int blockX, int blockZ) { @@ -148,11 +145,15 @@ final class PaletteLayerPreview { } String biomeKey() { - return biomeKey; + return palette.biomeKey(); } - int layer() { - return layer; + BiomePalette palette() { + return palette; + } + + String simplification() { + return placement.simplification(); } int depth() { @@ -160,11 +161,11 @@ final class PaletteLayerPreview { } int minThickness() { - return paletteLayer.getMinHeight(); + return palette.kind().isStacked() ? paletteLayer.getMinHeight() : 1; } int maxThickness() { - return paletteLayer.getMaxHeight(); + return palette.kind().isStacked() ? paletteLayer.getMaxHeight() : 1; } double zoom() { diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java index f8b360d17..af4f8340d 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java @@ -48,6 +48,7 @@ import java.awt.GridLayout; import java.awt.Rectangle; import java.util.Enumeration; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Set; @@ -71,8 +72,7 @@ final class PaletteNavigator extends JPanel { private PaletteCatalog catalog; private PlayerLocation location; - private String selectedBiome; - private int selectedLayer; + private BiomePalette selectedPalette; private boolean updatingSelection; private boolean untracked; @@ -103,9 +103,8 @@ final class PaletteNavigator extends JPanel { refreshHere(); } - void markSelected(String biomeKey, int layer) { - selectedBiome = biomeKey; - selectedLayer = layer; + void markSelected(BiomePalette palette) { + selectedPalette = palette; updatingSelection = true; try { selectInHereList(); @@ -145,7 +144,7 @@ final class PaletteNavigator extends JPanel { hereList.addListSelectionListener(event -> { HereRow row = hereList.getSelectedValue(); if (!event.getValueIsAdjusting() && !updatingSelection && row != null && row.biomeKey() != null) { - chooseBiome(row.biomeKey(), Math.max(0, row.layer())); + choose(row.biomeKey(), row.palette()); } }); card.add(location, BorderLayout.NORTH); @@ -180,7 +179,7 @@ final class PaletteNavigator extends JPanel { overviewTree.setCellRenderer(new OverviewRenderer()); overviewTree.addTreeSelectionListener(event -> { if (!updatingSelection && nodeAt(event.getPath()) instanceof OverviewNode node && node.biomeKey() != null) { - chooseBiome(node.biomeKey(), Math.max(0, node.layer())); + choose(node.biomeKey(), node.palette()); } }); card.add(StudioToolbar.padded(searchField, 8, 8, 8, 8), BorderLayout.NORTH); @@ -188,12 +187,13 @@ final class PaletteNavigator extends JPanel { return card; } - private void chooseBiome(String biomeKey, int preferredLayer) { + // Choosing a biome rather than one of its palettes opens its first palette. + private void choose(String biomeKey, BiomePalette palette) { BiomeEntry biome = catalog == null ? null : catalog.biome(biomeKey); - if (biome == null || biome.layerSummaries().isEmpty()) { - return; + BiomePalette chosen = palette != null ? palette : biome == null ? null : biome.firstPalette(); + if (chosen != null) { + listener.onPaletteChosen(chosen); } - listener.onLayerChosen(biomeKey, Math.min(preferredLayer, biome.layerSummaries().size() - 1)); } private void refreshHere() { @@ -226,35 +226,54 @@ final class PaletteNavigator extends JPanel { private void showHereRows(PlayerLocation here) { RegionEntry region = catalog.region(here.regionKey()); regionLabel.setText(labeled("Region", region == null ? here.regionKey() : region.name(), here.regionKey())); - addBiomeLayers("Surface biome here", here.surfaceBiomeKey()); + addBiomePalettes("Surface biome here", here.surfaceBiomeKey()); if (here.isBelowSurfaceBiome()) { - addBiomeLayers("Underground biome at your height", here.biomeAtPositionKey()); + addBiomePalettes("Underground biome at your height", here.biomeAtPositionKey()); } if (region == null) { return; } - hereRows.addElement(HereRow.header("Other biomes in this region")); - for (String otherKey : region.allBiomeKeys()) { - BiomeEntry other = catalog.biome(otherKey); - boolean shownAbove = otherKey.equals(here.surfaceBiomeKey()) || otherKey.equals(here.biomeAtPositionKey()); - if (!shownAbove && other != null) { - hereRows.addElement(new HereRow(biomeText(other), otherKey, -1, false)); - } + hereRows.addElement(HereRow.header("Biomes in this region")); + for (String biomeKey : region.allBiomeKeys()) { + addRegionBiome(biomeKey, 0, new HashSet<>(), here); } } - private void addBiomeLayers(String title, String biomeKey) { + private void addBiomePalettes(String title, String biomeKey) { BiomeEntry biome = catalog.biome(biomeKey); if (biome == null) { return; } hereRows.addElement(HereRow.header(title)); - hereRows.addElement(new HereRow(biomeText(biome), biomeKey, -1, false)); - for (int layer = 0; layer < biome.layerSummaries().size(); layer++) { - hereRows.addElement(new HereRow(layerText(layer, biome), biomeKey, layer, false)); + hereRows.addElement(HereRow.biome(biomeText(biome, childOfText(biome)), biomeKey, 0)); + for (PaletteCatalog.PaletteSummary summary : biome.palettes()) { + hereRows.addElement(HereRow.palette(summary.text(), summary.palette())); } } + // Children sit under the biome they grow inside of, so a child biome is found where it actually appears. + private void addRegionBiome(String biomeKey, int nesting, Set ancestors, PlayerLocation here) { + BiomeEntry biome = catalog.biome(biomeKey); + if (biome == null || !ancestors.add(biomeKey)) { + return; + } + boolean standingHere = biomeKey.equals(here.surfaceBiomeKey()) || biomeKey.equals(here.biomeAtPositionKey()); + hereRows.addElement(HereRow.biome(biomeText(biome, standingHere ? "you are here" : null), biomeKey, nesting)); + for (String childKey : biome.childKeys()) { + addRegionBiome(childKey, nesting + 1, new HashSet<>(ancestors), here); + } + } + + private String childOfText(BiomeEntry biome) { + if (biome.parentKeys().isEmpty()) { + return null; + } + List parentNames = biome.parentKeys().stream() + .map(key -> catalog.biome(key) == null ? key : catalog.biome(key).name()) + .toList(); + return "child of " + String.join(", ", parentNames); + } + private void rebuildOverview() { if (catalog == null) { return; @@ -263,14 +282,14 @@ final class PaletteNavigator extends JPanel { DefaultMutableTreeNode root = new DefaultMutableTreeNode(); for (RegionEntry region : catalog.regions()) { boolean regionMatches = !filter.isEmpty() && region.name().toLowerCase(Locale.ROOT).contains(filter); - DefaultMutableTreeNode regionNode = new DefaultMutableTreeNode(new OverviewNode(region.name(), region.key(), null, -1)); + DefaultMutableTreeNode regionNode = new DefaultMutableTreeNode(new OverviewNode(region.name(), region.key(), null, null)); for (BiomeGroup group : region.groups()) { DefaultMutableTreeNode groupNode = new DefaultMutableTreeNode(); for (String biomeKey : group.biomeKeys()) { addBiomeNode(groupNode, biomeKey, regionMatches ? "" : filter, new HashSet<>()); } if (groupNode.getChildCount() > 0) { - groupNode.setUserObject(new OverviewNode(group.label(), groupNode.getChildCount() + " biomes", null, -1)); + groupNode.setUserObject(new OverviewNode(group.label(), groupNode.getChildCount() + " biomes", null, null)); regionNode.add(groupNode); } } @@ -283,7 +302,7 @@ final class PaletteNavigator extends JPanel { addBiomeNode(outside, biomeKey, filter, new HashSet<>()); } if (outside.getChildCount() > 0) { - outside.setUserObject(new OverviewNode("Not in any region", outside.getChildCount() + " biomes", null, -1)); + outside.setUserObject(new OverviewNode("Not in any region", outside.getChildCount() + " biomes", null, null)); root.add(outside); } updatingSelection = true; @@ -303,14 +322,14 @@ final class PaletteNavigator extends JPanel { if (biome == null || !ancestors.add(biomeKey)) { return; } - DefaultMutableTreeNode biomeNode = new DefaultMutableTreeNode(new OverviewNode(biome.name(), biomeKey, biomeKey, -1)); - for (int layer = 0; layer < biome.layerSummaries().size(); layer++) { - biomeNode.add(new DefaultMutableTreeNode(new OverviewNode(layerText(layer, biome), null, biomeKey, layer))); + DefaultMutableTreeNode biomeNode = new DefaultMutableTreeNode(new OverviewNode(biome.name(), biomeKey, biomeKey, null)); + for (PaletteCatalog.PaletteSummary summary : biome.palettes()) { + biomeNode.add(new DefaultMutableTreeNode(new OverviewNode(summary.text(), null, biomeKey, summary.palette()))); } for (String childKey : biome.childKeys()) { addBiomeNode(biomeNode, childKey, filter, new HashSet<>(ancestors)); } - boolean hasMatchingChild = biomeNode.getChildCount() > biome.layerSummaries().size(); + boolean hasMatchingChild = biomeNode.getChildCount() > biome.palettes().size(); if (filter.isEmpty() || biome.matches(filter) || hasMatchingChild) { parent.add(biomeNode); } @@ -346,9 +365,8 @@ final class PaletteNavigator extends JPanel { while (nodes.hasMoreElements()) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) nodes.nextElement(); if (node.getUserObject() instanceof OverviewNode overview - && overview.layer() == selectedLayer - && overview.biomeKey() != null - && overview.biomeKey().equals(selectedBiome)) { + && overview.palette() != null + && overview.palette().equals(selectedPalette)) { return node; } } @@ -358,7 +376,7 @@ final class PaletteNavigator extends JPanel { private void selectInHereList() { for (int index = 0; index < hereRows.size(); index++) { HereRow row = hereRows.get(index); - if (row.layer() == selectedLayer && row.biomeKey() != null && row.biomeKey().equals(selectedBiome)) { + if (row.palette() != null && row.palette().equals(selectedPalette)) { hereList.setSelectedIndex(index); return; } @@ -370,12 +388,10 @@ final class PaletteNavigator extends JPanel { return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); } - private static String layerText(int layer, BiomeEntry biome) { - return "Layer " + layer + " · " + biome.layerSummaries().get(layer); - } - - private static String biomeText(BiomeEntry biome) { - return "" + escape(biome.name()) + " " + escape(biome.key()) + ""; + private static String biomeText(BiomeEntry biome, String note) { + String noteText = note == null ? "" : " · " + escape(note); + return "" + escape(biome.name()) + noteText + " " + escape(biome.key()) + + ""; } private static String labeled(String title, String name, String key) { @@ -388,16 +404,27 @@ final class PaletteNavigator extends JPanel { } interface Listener { - void onLayerChosen(String biomeKey, int layer); + void onPaletteChosen(BiomePalette palette); } - private record HereRow(String text, String biomeKey, int layer, boolean header) { + /** + * @param nesting how many parent biomes deep a child biome sits + */ + private record HereRow(String text, String biomeKey, BiomePalette palette, int nesting, boolean header) { private static HereRow header(String text) { - return new HereRow(text, null, -1, true); + return new HereRow(text, null, null, 0, true); + } + + private static HereRow biome(String text, String biomeKey, int nesting) { + return new HereRow(text, biomeKey, null, nesting, false); + } + + private static HereRow palette(String text, BiomePalette palette) { + return new HereRow(text, palette.biomeKey(), palette, 0, false); } } - private record OverviewNode(String label, String detail, String biomeKey, int layer) { + private record OverviewNode(String label, String detail, String biomeKey, BiomePalette palette) { private String html() { if (detail == null) { return escape(label); @@ -410,7 +437,8 @@ final class PaletteNavigator extends JPanel { private static final long serialVersionUID = 2785341117408251320L; private static final int HEADER_INDENT = 11; private static final int BIOME_INDENT = 20; - private static final int LAYER_INDENT = 34; + private static final int PALETTE_INDENT = 34; + private static final int CHILD_INDENT = 16; @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean selected, boolean focus) { @@ -422,7 +450,9 @@ final class PaletteNavigator extends JPanel { if (row.header()) { setForeground(StudioTheme.MUTED_TEXT); } - int indent = row.header() ? HEADER_INDENT : row.layer() >= 0 ? LAYER_INDENT : BIOME_INDENT; + int indent = row.header() ? HEADER_INDENT + : row.palette() != null ? PALETTE_INDENT + : BIOME_INDENT + row.nesting() * CHILD_INDENT; setBorder(StudioTheme.emptyBorder(row.header() ? 11 : 4, indent, 4, 10)); return this; }