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;
}