Show biome decorators in the studio palette explorer

Each decorator of a biome is now listed and previewed next to its layers,
walls and slabs. The preview shows where the decorator places, using the
same seed as the generator, and which block it picks there. Notes explain
what it leaves out: the part it decorates, competition with other
decorators, stacking and height.

Palettes that come from a snippet file show the snippet name, read from
the biome file since snippets are copied in when the pack loads.

Model used: Claude Opus 5.5
This commit is contained in:
piexel
2026-10-19 17:15:12 +02:00
parent b24c38896d
commit e4675ecab8
8 changed files with 318 additions and 70 deletions
@@ -20,31 +20,40 @@ package com.volmit.iris.core.gui;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
import com.volmit.iris.engine.object.IrisBlockData;
import com.volmit.iris.engine.object.IrisDecorator;
import com.volmit.iris.util.collection.KList;
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.
* One of the block palettes a biome places: a surface, sea or cave ceiling layer, its wall or slab blocks, or a
* decorator.
*/
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);
LAYER("Layer", "layers", true, true),
SEA_LAYER("Sea layer", "seaLayers", true, true),
CAVE_CEILING_LAYER("Cave ceiling layer", "caveCeilingLayers", true, true),
WALL("Wall", "wall", false, false),
SLAB("Slab", "slab", false, false),
DECORATOR("Decorator", "decorators", false, true);
private final String label;
private final String jsonKey;
private final boolean stacked;
private final boolean numbered;
Kind(String label, boolean stacked) {
Kind(String label, String jsonKey, boolean stacked, boolean numbered) {
this.label = label;
this.jsonKey = jsonKey;
this.stacked = stacked;
this.numbered = numbered;
}
/**
@@ -54,24 +63,42 @@ record BiomePalette(String biomeKey, Kind kind, int index) {
return stacked;
}
/**
* The biome file's key holding these palettes: a list of them, or a single one for walls and slabs.
*/
String jsonKey() {
return jsonKey;
}
// A biome without wall or slab blocks still has those palettes, just empty.
List<IrisBiomePaletteLayer> palettesOf(IrisBiome biome) {
List<IrisBiomePaletteLayer> layersOf(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());
case DECORATOR -> List.of();
};
}
// A decorator snippet that couldn't be found loads as an empty entry, which places nothing.
List<KList<IrisBlockData>> blockListsOf(IrisBiome biome) {
if (this != DECORATOR) {
return layersOf(biome).stream().map(IrisBiomePaletteLayer::getPalette).toList();
}
return biome.getDecorators().stream()
.map(decorator -> decorator == null ? new KList<IrisBlockData>() : decorator.getPalette())
.toList();
}
}
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()) {
List<KList<IrisBlockData>> blockLists = kind.blockListsOf(biome);
for (int index = 0; index < blockLists.size(); index++) {
if (!blockLists.get(index).isEmpty()) {
palettes.add(new BiomePalette(biomeKey, kind, index));
}
}
@@ -104,7 +131,7 @@ record BiomePalette(String biomeKey, Kind kind, int index) {
}
String label() {
return kind.isStacked() ? kind.label + " " + index : kind.label;
return kind.numbered ? kind.label + " " + index : kind.label;
}
String lowercaseLabel() {
@@ -112,10 +139,18 @@ record BiomePalette(String biomeKey, Kind kind, int index) {
}
/**
* The palette in the given copy of the biome, or null when a hotload removed it.
* The palette's blocks 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;
KList<IrisBlockData> blocksIn(IrisBiome biome) {
List<KList<IrisBlockData>> blockLists = kind.blockListsOf(biome);
return index < blockLists.size() ? blockLists.get(index) : null;
}
IrisBiomePaletteLayer layerIn(IrisBiome biome) {
return kind.layersOf(biome).get(index);
}
IrisDecorator decoratorIn(IrisBiome biome) {
return biome.getDecorators().get(index);
}
}
@@ -19,42 +19,49 @@
package com.volmit.iris.core.gui;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.decorator.IrisEngineDecorator;
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.IrisBlockData;
import com.volmit.iris.engine.object.IrisDecorationPart;
import com.volmit.iris.engine.object.IrisDecorator;
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;
import java.util.ArrayList;
import java.util.List;
/**
* 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.
* so these use the same random sources the terrain, carving, post and decorator passes use.
*/
abstract class BiomePalettePlacement {
protected final IrisBiomePaletteLayer layer;
protected final IrisData data;
private BiomePalettePlacement(IrisBiomePaletteLayer layer, IrisData data) {
this.layer = layer;
private BiomePalettePlacement(IrisData data) {
this.data = data;
}
static BiomePalettePlacement of(Engine engine, IrisBiome biome, BiomePalette palette, IrisBiomePaletteLayer layer) {
static BiomePalettePlacement of(Engine engine, IrisBiome biome, BiomePalette palette) {
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()),
case LAYER -> new SurfaceLayer(biome, palette.index(), palette.layerIn(biome), terrainRandom, data);
case SEA_LAYER -> new LegacyStackedLayer(palette.layerIn(biome), data, terrainRandom.nextParallelRNG(palette.index()),
biome.getLayerSeaHeightGenerators(terrainRandom, data), palette.index());
case CAVE_CEILING_LAYER -> new LegacyStackedLayer(layer, data, carveRandom.nextParallelRNG(palette.index()),
case CAVE_CEILING_LAYER -> new LegacyStackedLayer(palette.layerIn(biome), 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);
case WALL -> new SingleBlocks(palette.layerIn(biome), data, carveRandom, seaLevel);
case SLAB -> new SingleBlocks(palette.layerIn(biome), data, postRandom, seaLevel);
case DECORATOR -> new Decorations(biome, palette.decoratorIn(biome), engine.getSeedManager().getComponent(),
seaLevel, data);
};
}
@@ -62,6 +69,20 @@ abstract class BiomePalettePlacement {
return dimension == null ? 0 : dimension.getFluidHeight();
}
abstract KList<IrisBlockData> blocks();
abstract double zoom();
int minThickness() {
return 1;
}
int maxThickness() {
return 1;
}
abstract boolean hasSlopeCondition();
/**
* How many blocks of this palette are stacked at the spot.
*/
@@ -76,7 +97,41 @@ abstract class BiomePalettePlacement {
return null;
}
private static final class SurfaceLayer extends BiomePalettePlacement {
private abstract static class LayerPlacement extends BiomePalettePlacement {
protected final IrisBiomePaletteLayer layer;
private LayerPlacement(IrisBiomePaletteLayer layer, IrisData data) {
super(data);
this.layer = layer;
}
@Override
KList<IrisBlockData> blocks() {
return layer.getPalette();
}
@Override
double zoom() {
return layer.getZoom();
}
@Override
int minThickness() {
return layer.getMinHeight();
}
@Override
int maxThickness() {
return layer.getMaxHeight();
}
@Override
boolean hasSlopeCondition() {
return !layer.getSlopeCondition().isDefault();
}
}
private static final class SurfaceLayer extends LayerPlacement {
private final IrisBiome biome;
private final int index;
private final RNG random;
@@ -103,7 +158,7 @@ abstract class BiomePalettePlacement {
* 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 static final class LegacyStackedLayer extends LayerPlacement {
private final RNG random;
private final CNG thicknessNoise;
@@ -141,7 +196,7 @@ abstract class BiomePalettePlacement {
/**
* 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 static final class SingleBlocks extends LayerPlacement {
private final RNG random;
private final int sampleHeight;
@@ -151,6 +206,16 @@ abstract class BiomePalettePlacement {
this.sampleHeight = sampleHeight;
}
@Override
int minThickness() {
return 1;
}
@Override
int maxThickness() {
return 1;
}
@Override
int thickness(int blockX, int blockZ) {
return 1;
@@ -167,4 +232,85 @@ abstract class BiomePalettePlacement {
return "These blocks also change with height; the preview shows them at sea level (Y " + sampleHeight + ").";
}
}
/**
* A decorator places where its style noise stays under its chance, and its variance noise picks the block there.
* The generator seeds the variance noise from whichever block it decorates first, so the preview reuses that noise
* once the world has generated some decorations.
*/
private static final class Decorations extends BiomePalettePlacement {
private final IrisDecorator decorator;
private final CNG placementNoise;
private final CNG blockNoise;
private final int sampleHeight;
private final boolean sharesSpotsWithOtherDecorators;
private Decorations(IrisBiome biome, IrisDecorator decorator, long componentSeed, int sampleHeight, IrisData data) {
super(data);
this.decorator = decorator;
RNG partRandom = new RNG(IrisEngineDecorator.partSeed(componentSeed, decorator.getPartOf()));
this.placementNoise = decorator.getGenerator(partRandom, data);
this.blockNoise = decorator.getVarianceGenerator(partRandom, data);
this.sampleHeight = sampleHeight;
this.sharesSpotsWithOtherDecorators = biome.getDecorators().stream()
.anyMatch(other -> other != null && other != decorator && other.getPartOf() == decorator.getPartOf());
}
@Override
KList<IrisBlockData> blocks() {
return decorator.getPalette();
}
@Override
double zoom() {
return decorator.getStyle().getZoom();
}
@Override
boolean hasSlopeCondition() {
return !decorator.getSlopeCondition().isDefault();
}
@Override
int thickness(int blockX, int blockZ) {
double zoom = zoom();
return placementNoise.fitDouble(0D, 1D, blockX / zoom, blockZ / zoom) <= decorator.getChance() ? 1 : 0;
}
// The generator swaps X and Z when it picks the block.
@Override
double noise(int blockX, int blockZ, int depth) {
return blockNoise.noise(blockZ, sampleHeight, blockX);
}
@Override
String simplification() {
List<String> notes = new ArrayList<>();
String partNote = partNote(decorator.getPartOf());
if (partNote != null) {
notes.add(partNote);
}
if (sharesSpotsWithOtherDecorators) {
notes.add("Where other decorators of this biome could place too, the world picks one of them at random.");
}
if (decorator.isStacking()) {
notes.add("It stacks " + decorator.getStackMin() + "–" + decorator.getStackMax()
+ " blocks high; the preview shows the bottom block.");
}
if (blocks().size() > 1) {
notes.add("The block also changes with height; the preview picks it at sea level (Y " + sampleHeight + ").");
}
return notes.isEmpty() ? null : String.join(" ", notes);
}
private static String partNote(IrisDecorationPart part) {
return switch (part) {
case NONE -> null;
case SHORE_LINE -> "It only places along shore lines.";
case SEA_SURFACE -> "It only places on the sea surface.";
case SEA_FLOOR -> "It only places on the sea floor.";
case CEILING -> "It places on cave ceilings and the undersides of overhangs, not on the ground.";
};
}
}
}
@@ -613,10 +613,11 @@ public final class NoiseExplorerGUI extends JPanel {
if (biome == null) {
continue;
}
for (BiomePalette palette : BiomePalette.allOf(key, biome)) {
for (PaletteCatalog.PaletteSummary summary : PaletteCatalog.summarizePalettes(key, biome)) {
BiomePalette palette = summary.palette();
sources.add(new SourceItem(
palette.id(),
palette.label() + " · " + PaletteCatalog.summarizeBlocks(palette.resolve(biome)),
summary.text(),
key,
NoiseCategory.PACK_PALETTES,
false,
@@ -18,12 +18,21 @@
package com.volmit.iris.core.gui;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
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.IrisBlockData;
import com.volmit.iris.engine.object.IrisRegion;
import com.volmit.iris.util.collection.KList;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -73,8 +82,8 @@ record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegio
return regions.stream().filter(region -> region.key().equals(key)).findFirst().orElse(null);
}
static String summarizeBlocks(IrisBiomePaletteLayer layer) {
List<String> names = layer.getPalette().stream()
static String summarizeBlocks(KList<IrisBlockData> blocks) {
List<String> names = blocks.stream()
.map(block -> block.getBlock().replace("minecraft:", "").toLowerCase(Locale.ROOT))
.distinct()
.toList();
@@ -117,12 +126,40 @@ record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegio
return parents;
}
private static List<PaletteSummary> summarizePalettes(String key, IrisBiome biome) {
static List<PaletteSummary> summarizePalettes(String key, IrisBiome biome) {
JsonObject biomeSource = readSource(biome.getLoadFile());
return BiomePalette.allOf(key, biome).stream()
.map(palette -> new PaletteSummary(palette, summarizeBlocks(palette.resolve(biome))))
.map(palette -> new PaletteSummary(palette, snippetName(biomeSource, palette),
summarizeBlocks(palette.blocksIn(biome))))
.toList();
}
// Snippets are copied into the biome when it loads, so only its file still tells which palettes came from one.
private static String snippetName(JsonObject biomeSource, BiomePalette palette) {
JsonElement element = biomeSource == null ? null : biomeSource.get(palette.kind().jsonKey());
if (element instanceof JsonArray list) {
element = palette.index() < list.size() ? list.get(palette.index()) : null;
}
if (element == null || !element.isJsonPrimitive()) {
return null;
}
String reference = element.getAsString();
return reference.substring(reference.lastIndexOf('/') + 1);
}
private static JsonObject readSource(File file) {
if (file == null || !file.isFile()) {
return null;
}
try (JsonReader reader = new JsonReader(new FileReader(file))) {
reader.setLenient(true);
JsonElement source = JsonParser.parseReader(reader);
return source.isJsonObject() ? source.getAsJsonObject() : null;
} catch (IOException | RuntimeException e) {
return null;
}
}
private static RegionEntry describeRegion(String key, IrisRegion region, Map<String, BiomeEntry> biomes) {
List<BiomeGroup> groups = new ArrayList<>();
addGroup(groups, "Land", region.getLandBiomes(), biomes);
@@ -173,9 +210,14 @@ record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegio
}
}
record PaletteSummary(BiomePalette palette, String blocks) {
/**
* @param snippet the name of the snippet file the palette comes from, or null when the biome defines it itself
*/
record PaletteSummary(BiomePalette palette, String snippet, String blocks) {
String text() {
return palette.label() + " · " + blocks;
return snippet == null
? palette.label() + " · " + blocks
: palette.label() + " · " + snippet + " snippet · " + blocks;
}
}
}
@@ -409,8 +409,7 @@ final class PaletteInspectorPanel extends JPanel {
PaletteLayerPreview.STATISTICS_GRID_SIZE, PaletteLayerPreview.STATISTICS_GRID_SIZE,
PaletteLayerPreview.statisticsAreaBlocks()));
if (preview.absentShare() > 0D) {
notes.add("The layer doesn't reach this depth on " + percent(preview.absentShare())
+ " of the ground. That ground is dark on the map and left out of the shares.");
notes.add(absentText(preview) + " That ground is dark on the map and left out of the shares.");
}
if (preview.outOfRangeShare() > 0D) {
notes.add(percent(preview.outOfRangeShare()) + " of the noise values fall outside 0 to 1."
@@ -420,11 +419,26 @@ final class PaletteInspectorPanel extends JPanel {
notes.add(preview.simplification());
}
if (preview.hasSlopeCondition()) {
notes.add("This layer has a slope condition, which the preview ignores.");
notes.add("This " + paletteNoun(preview) + " has a slope condition, which the preview ignores.");
}
return notes;
}
private static String absentText(PaletteLayerPreview preview) {
if (isDecorator(preview)) {
return "The decorator leaves " + percent(preview.absentShare()) + " of the ground bare.";
}
return "The layer doesn't reach this depth on " + percent(preview.absentShare()) + " of the ground.";
}
private static String paletteNoun(PaletteLayerPreview preview) {
return isDecorator(preview) ? "decorator" : "layer";
}
private static boolean isDecorator(PaletteLayerPreview preview) {
return preview.palette().kind() == BiomePalette.Kind.DECORATOR;
}
private static String actualText(PaletteLayerPreview.Entry entry) {
if (entry.isFarBelowWeight()) {
return "▼ " + percent(entry.actualShare());
@@ -41,7 +41,7 @@ final class PaletteLayerPreview {
private static final int BLOCK_CACHE_SIZE = 1 << 16;
private final BiomePalette palette;
private final IrisBiomePaletteLayer paletteLayer;
private final KList<IrisBlockData> blocks;
private final BiomePalettePlacement placement;
private final int depth;
private final IrisData data;
@@ -56,14 +56,14 @@ final class PaletteLayerPreview {
private final double absentShare;
private final double outOfRangeShare;
private PaletteLayerPreview(BiomePalette palette, IrisBiomePaletteLayer paletteLayer, BiomePalettePlacement placement,
int depth, IrisData data, BlockTextures textures, BiomeTintField biomeTints) {
private PaletteLayerPreview(BiomePalette palette, BiomePalettePlacement placement, int depth, IrisData data,
BlockTextures textures, BiomeTintField biomeTints) {
this.palette = palette;
this.paletteLayer = paletteLayer;
this.blocks = placement.blocks();
this.placement = placement;
this.depth = depth;
this.data = data;
this.entryBySlot = paletteLayer.getPaletteEntryPerSlot(data).stream().mapToInt(Integer::intValue).toArray();
this.entryBySlot = IrisBiomePaletteLayer.paletteEntryPerSlot(blocks, data).stream().mapToInt(Integer::intValue).toArray();
if (entryBySlot.length == 0) {
throw new IllegalStateException("No block of " + palette.biomeKey() + " " + palette.lowercaseLabel() + " could be found");
}
@@ -85,15 +85,14 @@ final class PaletteLayerPreview {
}
IrisData data = engine.getData();
IrisBiome biome = data.getBiomeLoader().load(palette.biomeKey());
IrisBiomePaletteLayer paletteLayer = biome == null ? null : palette.resolve(biome);
if (paletteLayer == null) {
if (biome == null || palette.blocksIn(biome) == null) {
throw new IllegalStateException("Palette removed during hotload: " + palette.biomeKey() + " " + palette.lowercaseLabel());
}
BiomePalettePlacement placement = BiomePalettePlacement.of(engine, biome, palette, paletteLayer);
BiomePalettePlacement placement = BiomePalettePlacement.of(engine, biome, palette);
BiomeTintField biomeTints = textures == null
? null
: new BiomeTintField(biome, new RNG(engine.getSeedManager().getBiome()), textures.biomeColors());
return new PaletteLayerPreview(palette, paletteLayer, placement, depth, data, textures, biomeTints);
return new PaletteLayerPreview(palette, placement, depth, data, textures, biomeTints);
}
// The world picks one block per whole coordinate, and zoomed in, many pixels fall on the same block.
@@ -161,19 +160,19 @@ final class PaletteLayerPreview {
}
int minThickness() {
return palette.kind().isStacked() ? paletteLayer.getMinHeight() : 1;
return placement.minThickness();
}
int maxThickness() {
return palette.kind().isStacked() ? paletteLayer.getMaxHeight() : 1;
return placement.maxThickness();
}
double zoom() {
return paletteLayer.getZoom();
return placement.zoom();
}
boolean hasSlopeCondition() {
return !paletteLayer.getSlopeCondition().isDefault();
return placement.hasSlopeCondition();
}
List<Entry> entries() {
@@ -201,13 +200,12 @@ final class PaletteLayerPreview {
}
private BlockTexture[] resolveEntryTextures(BlockTextures textures) {
KList<IrisBlockData> palette = paletteLayer.getPalette();
BlockTexture[] resolved = new BlockTexture[palette.size()];
BlockTexture[] resolved = new BlockTexture[blocks.size()];
if (textures == null) {
return resolved;
}
for (int entry = 0; entry < palette.size(); entry++) {
BlockData block = palette.get(entry).getBlockData(data);
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
resolved[entry] = block == null ? null : textures.topTexture(block).orElse(null);
}
return resolved;
@@ -227,18 +225,17 @@ final class PaletteLayerPreview {
// Blocks without a texture, like custom blocks or everything before the textures load, fall back to map colours.
private int[] resolveLegendColors() {
KList<IrisBlockData> palette = paletteLayer.getPalette();
List<BlockData> untexturedBlocks = new ArrayList<>();
for (int entry = 0; entry < palette.size(); entry++) {
BlockData block = palette.get(entry).getBlockData(data);
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
if (block != null && textureByEntry[entry] == null) {
untexturedBlocks.add(block);
}
}
Map<String, Integer> mapColorsByBlock = BlockPreviewColors.colorsByBlock(untexturedBlocks);
int[] colors = new int[palette.size()];
for (int entry = 0; entry < palette.size(); entry++) {
BlockData block = palette.get(entry).getBlockData(data);
int[] colors = new int[blocks.size()];
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
if (block == null) {
colors[entry] = MISSING_BLOCK_COLOR;
} else if (legendTextureByEntry[entry] != null) {
@@ -252,7 +249,7 @@ final class PaletteLayerPreview {
private Statistics measure() {
long[] values = new long[HISTOGRAM_BINS];
long[] samplesByEntry = new long[paletteLayer.getPalette().size()];
long[] samplesByEntry = new long[blocks.size()];
long present = 0;
long absent = 0;
long outOfRange = 0;
@@ -281,11 +278,10 @@ final class PaletteLayerPreview {
}
private List<Entry> describeEntries(double[] sharesByEntry) {
KList<IrisBlockData> palette = paletteLayer.getPalette();
int slotCount = entryBySlot.length;
List<Entry> described = new ArrayList<>(palette.size());
for (int entry = 0; entry < palette.size(); entry++) {
IrisBlockData block = palette.get(entry);
List<Entry> described = new ArrayList<>(blocks.size());
for (int entry = 0; entry < blocks.size(); entry++) {
IrisBlockData block = blocks.get(entry);
int firstSlot = firstSlotOf(entry);
int lastSlot = lastSlotOf(entry);
boolean placed = firstSlot >= 0;
@@ -45,11 +45,18 @@ public abstract class IrisEngineDecorator extends EngineAssignedComponent implem
public IrisEngineDecorator(Engine engine, String name, IrisDecorationPart part) {
super(engine, name + " Decorator");
this.part = part;
this.seed = getSeed() + 29356788 - (part.ordinal() * 10439677L);
this.seed = partSeed(getSeed(), part);
this.modX = 29356788 ^ (part.ordinal() + 6);
this.modZ = 10439677 ^ (part.ordinal() + 1);
}
/**
* The seed each decorator of the given part decides where it places with.
*/
public static long partSeed(long componentSeed, IrisDecorationPart part) {
return componentSeed + 29356788 - (part.ordinal() * 10439677L);
}
@BlockCoordinates
protected RNG getRNG(int x, int z) {
return new RNG(x * modX + z * modZ + seed);
@@ -130,6 +130,13 @@ public class IrisBiomePaletteLayer {
}
public KList<Integer> getPaletteEntryPerSlot(IrisData data) {
return paletteEntryPerSlot(palette, data);
}
/**
* Each block fills as many slots of the noise range as its weight; blocks that can't be found get none.
*/
public static KList<Integer> paletteEntryPerSlot(KList<IrisBlockData> palette, IrisData data) {
KList<Integer> entries = new KList<>();
for (int entry = 0; entry < palette.size(); entry++) {
IrisBlockData block = palette.get(entry);