mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 07:24:16 +00:00
Show every palette a biome places, and where child biomes grow
Palettes listed only a biome's surface layers. They now include sea layers, cave ceiling layers, and wall and slab blocks, each previewed with the noise and random source the generator uses for that kind. Wall and slab blocks also change with height, so their preview is drawn at sea level and says so. The Here tab names the parent of a child biome and lists the region's biomes with their children nested under them, marking where you stand. Model used: Claude Opus 5.5
This commit is contained in:
1 parent
19bbca0e4b
commit
5363cbbc02
7 files changed
+469
-101
No files matched your search
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<IrisBiomePaletteLayer> 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<BiomePalette> allOf(String biomeKey, IrisBiome biome) {
|
||||
List<BiomePalette> palettes = new ArrayList<>();
|
||||
for (Kind kind : Kind.values()) {
|
||||
List<IrisBiomePaletteLayer> 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<IrisBiomePaletteLayer> layers = kind.palettesOf(biome);
|
||||
return index < layers.size() ? layers.get(index) : null;
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<CNG> 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 + ").";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SourceItem> 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<SourceItem> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RegionEntry> regions, List<String> biomesOutsideRegio
|
||||
|
||||
static String summarizeBlocks(IrisBiomePaletteLayer layer) {
|
||||
List<String> 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<RegionEntry> regions, List<String> biomesOutsideRegio
|
||||
|
||||
private static Map<String, BiomeEntry> loadBiomes(IrisData data) {
|
||||
String[] keys = data.getBiomeLoader().getPossibleKeys();
|
||||
Map<String, BiomeEntry> biomes = new LinkedHashMap<>();
|
||||
Map<String, IrisBiome> 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<String> 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<String, List<String>> parentsByChild = parentsByChild(loaded);
|
||||
Map<String, BiomeEntry> 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<String, List<String>> parentsByChild(Map<String, IrisBiome> biomes) {
|
||||
Map<String, List<String>> 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<PaletteSummary> 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<String, BiomeEntry> biomes) {
|
||||
List<BiomeGroup> groups = new ArrayList<>();
|
||||
addGroup(groups, "Land", region.getLandBiomes(), biomes);
|
||||
@@ -134,12 +156,26 @@ record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegio
|
||||
record BiomeGroup(String label, List<String> biomeKeys) {
|
||||
}
|
||||
|
||||
record BiomeEntry(String key, String name, List<String> layerSummaries, List<String> 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<PaletteSummary> palettes, List<String> childKeys,
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<String> 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<String> 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 "<html>" + escape(biome.name()) + " <font color='" + DETAIL_COLOR + "'>" + escape(biome.key()) + "</font></html>";
|
||||
private static String biomeText(BiomeEntry biome, String note) {
|
||||
String noteText = note == null ? "" : " · " + escape(note);
|
||||
return "<html>" + escape(biome.name()) + noteText + " <font color='" + DETAIL_COLOR + "'>" + escape(biome.key())
|
||||
+ "</font></html>";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user