Make the palette explorer open on the palette you are standing in

The explorer read the persistent world instead of the studio world the
player was in, and left out the studio focus region, so it showed the
wrong palettes. It now uses the player's own world, lists the focus
region first and opens on the surface biome centred on the player.

Blocks are sampled once per whole block and drawn at full resolution
without smoothing, so textures look like they do in game. The view and
depth controls use styled buttons instead of the default Swing widgets.

Model used: Claude Opus 5.5
This commit is contained in:
piexel committed 2026-10-18 21:22:31 +02:00
1 parent 76860e94fa
commit 02956fd358
11 files changed
+294 -128

No files matched your search

@@ -22,6 +22,7 @@ import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.gui.GuiHost;
import com.volmit.iris.core.gui.NoiseExplorerGUI;
import com.volmit.iris.core.gui.PaletteExplorerStart;
import com.volmit.iris.core.gui.VisionGUI;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.project.IrisProject;
@@ -252,14 +253,25 @@ public class CommandStudio implements DecreeExecutor {
NoiseExplorerGUI.launch();
}
@Decree(description = "Preview biome palette layers (External GUI)", aliases = "pal")
public void palette(
@Param(description = "The biome to open on", contextual = true)
IrisBiome biome
) {
@Decree(description = "Preview the palettes around you (External GUI)", aliases = "pal", origin = DecreeOrigin.PLAYER)
public void palette() {
if (noGUI()) return;
Engine engine = engine();
if (engine == null) {
sender().sendMessage(C.RED + "Stand in an Iris world to preview its palettes!");
return;
}
Location location = player().getLocation();
IrisBiome surfaceBiome = engine.getSurfaceBiome(location.getBlockX(), location.getBlockZ());
sender().sendMessage(C.GREEN + "Opening Palette Explorer!");
NoiseExplorerGUI.launchPalettes(biome == null ? null : biome.getLoadKey(), sender().isPlayer() ? player().getUniqueId() : null);
NoiseExplorerGUI.launchPalettes(new PaletteExplorerStart(
engine,
location.getWorld().getUID(),
player().getUniqueId(),
surfaceBiome == null ? null : surfaceBiome.getLoadKey(),
location.getX(),
location.getZ()
));
}
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
@@ -0,0 +1,70 @@
/*
* 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 javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JToggleButton;
import static com.volmit.iris.core.gui.NoiseExplorerGUI.ACCENT;
import static com.volmit.iris.core.gui.NoiseExplorerGUI.BORDER;
import static com.volmit.iris.core.gui.NoiseExplorerGUI.FIELD_BACKGROUND;
import static com.volmit.iris.core.gui.NoiseExplorerGUI.ITEM_FONT;
import static com.volmit.iris.core.gui.NoiseExplorerGUI.PRIMARY_TEXT;
import static com.volmit.iris.core.gui.NoiseExplorerGUI.SECONDARY_TEXT;
final class ExplorerStyle {
private static final java.awt.Color SELECTED_BACKGROUND = ACCENT.darker().darker();
private ExplorerStyle() {
}
static JToggleButton toggleButton(String text) {
JToggleButton button = new JToggleButton(text);
applyFrame(button);
button.addItemListener(event -> paintToggleState(button));
paintToggleState(button);
return button;
}
static JButton button(String text) {
JButton button = new JButton(text);
applyFrame(button);
button.setBackground(FIELD_BACKGROUND);
button.setForeground(PRIMARY_TEXT);
return button;
}
private static void applyFrame(AbstractButton button) {
button.setFocusPainted(false);
button.setFont(ITEM_FONT);
button.setContentAreaFilled(false);
button.setOpaque(true);
button.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(BORDER),
BorderFactory.createEmptyBorder(5, 9, 5, 9)
));
}
private static void paintToggleState(JToggleButton button) {
button.setBackground(button.isSelected() ? SELECTED_BACKGROUND : FIELD_BACKGROUND);
button.setForeground(button.isSelected() ? PRIMARY_TEXT : SECONDARY_TEXT);
}
}
@@ -40,6 +40,7 @@ import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -84,6 +85,11 @@ public final class GuiHost {
return null;
}
public static Engine findEngine(UUID worldId) {
World world = Bukkit.getWorld(worldId);
return world == null ? null : engineOf(world);
}
public static void registerHotloadHook(Runnable onHotload) {
Listener listener = new HotloadListener(onHotload);
HOTLOAD_HOOKS.put(onHotload, listener);
@@ -72,7 +72,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
@@ -105,9 +104,11 @@ public final class NoiseExplorerGUI extends JPanel {
private static final int REFINEMENT_DELAY_MILLIS = 180;
private static final long DEFAULT_SEED = 12345L;
private static final double TEXTURE_PIXELS_MAX_BLOCKS_PER_PIXEL = 0.25D;
private static final double PALETTE_START_BLOCKS_PER_PIXEL = 1D / 16D;
private static final long COARSE_SAMPLE_BUDGET = 4_096L;
private static final long MAX_REFINED_SAMPLE_BUDGET = 48_000L;
private static final double REFINEMENT_TARGET_MILLIS = 250D;
private static final double PALETTE_REFINEMENT_TARGET_MILLIS = 2_000D;
private static final double WHEEL_ZOOM_EXPONENT = 0.08D;
private static final NoiseCategory[] CATEGORY_ORDER = {
NoiseCategory.CUSTOM,
@@ -131,7 +132,8 @@ public final class NoiseExplorerGUI extends JPanel {
};
private final ExplorerMode mode;
private final String initialPackSourceId;
private final PaletteExplorerStart paletteStart;
private final NoiseViewport initialViewport;
private volatile Engine activeEngine;
private final LongFunction<NoiseProvider> customFactory;
private final String customName;
@@ -154,12 +156,11 @@ public final class NoiseExplorerGUI extends JPanel {
private final JComboBox<NoisePalette> palettePicker = new JComboBox<>(NoisePalette.values());
private final PaletteInspectorPanel paletteInspector = new PaletteInspectorPanel(new PaletteInspectorListener());
private final PaletteNavigator paletteNavigator;
private final UUID trackedPlayerId;
private PlayerLocationTracker locationTracker;
private volatile BlockTextures blockTextures;
private List<SourceItem> packSources = List.of();
private NoiseViewport viewport = new NoiseViewport(0D, 0D, 1D);
private NoiseViewport viewport;
private NoisePalette palette;
private SourceItem selectedSource;
private NoiseProvider sampler;
@@ -179,12 +180,15 @@ public final class NoiseExplorerGUI extends JPanel {
private String errorMessage;
private NoiseExplorerGUI(ExplorerMode mode, Engine engine, String customGeneratorKey, IrisGenerator fallbackGenerator,
String customName, long initialSeed, String initialPackSourceId, UUID trackedPlayerId) {
String customName, long initialSeed, PaletteExplorerStart paletteStart) {
this.mode = mode;
this.trackedPlayerId = trackedPlayerId;
this.paletteNavigator = new PaletteNavigator(trackedPlayerId != null, this::selectPaletteLayer);
this.initialPackSourceId = initialPackSourceId;
this.paletteStart = paletteStart;
this.paletteNavigator = new PaletteNavigator(this::selectPaletteLayer);
this.awaitingInitialPackSource = mode == ExplorerMode.PALETTES;
this.initialViewport = paletteStart == null
? new NoiseViewport(0D, 0D, 1D)
: new NoiseViewport(paletteStart.centerX(), paletteStart.centerZ(), PALETTE_START_BLOCKS_PER_PIXEL);
this.viewport = initialViewport;
this.activeEngine = engine;
this.customFactory = fallbackGenerator == null
? null
@@ -192,7 +196,7 @@ public final class NoiseExplorerGUI extends JPanel {
this.customName = customName;
this.seed = initialSeed;
this.seedField.setText(Long.toString(initialSeed));
this.palette = IrisSettings.get().getGui().colorMode ? NoisePalette.TERRAIN : NoisePalette.GRAYSCALE;
this.palette = IrisSettings.get().getGui().colorMode && mode == ExplorerMode.NOISE ? NoisePalette.TERRAIN : NoisePalette.GRAYSCALE;
this.sourceExecutor = createExecutor("Iris Noise Source Loader");
this.catalogExecutor = createExecutor("Iris Noise Catalog Loader");
this.renderCoordinator = new NoiseRenderCoordinator(new RenderListener());
@@ -212,18 +216,15 @@ public final class NoiseExplorerGUI extends JPanel {
public static void launch() {
Engine engine = GuiHost.findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.NOISE, engine, null, null, null, DEFAULT_SEED, null, null);
NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.NOISE, engine, null, null, null, DEFAULT_SEED, null);
buildFrame("Noise Explorer", explorer);
});
}
public static void launchPalettes(String biomeKey, UUID trackedPlayerId) {
Engine engine = GuiHost.findActiveEngine();
String initialSourceId = biomeKey == null ? null : paletteSourceId(biomeKey, 0);
String title = biomeKey == null ? "Palette Explorer" : "Palette Explorer: " + biomeKey;
public static void launchPalettes(PaletteExplorerStart start) {
EventQueue.invokeLater(() -> {
NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.PALETTES, engine, null, null, null, DEFAULT_SEED, initialSourceId, trackedPlayerId);
buildFrame(title, explorer);
NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.PALETTES, start.engine(), null, null, null, DEFAULT_SEED, start);
buildFrame("Palette Explorer", explorer);
});
}
@@ -238,7 +239,6 @@ public final class NoiseExplorerGUI extends JPanel {
fallbackGenerator,
displayName,
initialSeed,
null,
null
);
buildFrame("Noise Explorer: " + displayName, explorer);
@@ -376,9 +376,17 @@ public final class NoiseExplorerGUI extends JPanel {
if (mode == ExplorerMode.NOISE) {
addSeedControls(controls);
addPaletteControls(controls);
}
JLabel paletteTitle = new JLabel(mode == ExplorerMode.PALETTES ? "Noise view colours" : "Palette");
JButton resetView = createButton("Reset view");
resetView.addActionListener(event -> resetViewport());
controls.add(resetView);
return controls;
}
private void addPaletteControls(JPanel controls) {
JLabel paletteTitle = new JLabel("Palette");
paletteTitle.setForeground(SECONDARY_TEXT);
paletteTitle.setFont(HEADER_FONT);
controls.add(paletteTitle);
@@ -393,11 +401,6 @@ public final class NoiseExplorerGUI extends JPanel {
}
});
controls.add(palettePicker);
JButton resetView = createButton("Reset view");
resetView.addActionListener(event -> resetViewport());
controls.add(resetView);
return controls;
}
private void addSeedControls(JPanel controls) {
@@ -443,8 +446,8 @@ public final class NoiseExplorerGUI extends JPanel {
loadCatalog();
if (mode == ExplorerMode.PALETTES) {
loadBlockTextures();
if (trackedPlayerId != null) {
locationTracker = PlayerLocationTracker.follow(trackedPlayerId, paletteNavigator::showLocation);
if (paletteStart != null) {
locationTracker = PlayerLocationTracker.follow(paletteStart.playerId(), paletteStart.worldId(), paletteNavigator::showLocation);
}
}
SourceItem initial = findInitialSource();
@@ -686,7 +689,8 @@ public final class NoiseExplorerGUI extends JPanel {
}
private void selectInitialPackSource(List<SourceItem> sources) {
SourceItem requested = initialPackSourceId == null ? null : findSourceById(initialPackSourceId, sources);
String initialSourceId = paletteStart == null || paletteStart.biomeKey() == null ? null : paletteSourceId(paletteStart.biomeKey(), 0);
SourceItem requested = initialSourceId == null ? null : findSourceById(initialSourceId, sources);
SourceItem initial = requested == null ? sources.get(0) : requested;
if (!selectSourceInList(initial.id())) {
selectSource(initial);
@@ -938,13 +942,18 @@ public final class NoiseExplorerGUI extends JPanel {
requestRender(sampleStep);
}
private static long refinedSampleBudget(NoiseRenderCoordinator.Result currentResult) {
// Smooth noise looks fine upscaled, but blocks and their textures only read correctly at full resolution.
private long refinedSampleBudget(NoiseRenderCoordinator.Result currentResult) {
boolean fullResolution = mode == ExplorerMode.PALETTES;
long maximumBudget = fullResolution
? Math.max(COARSE_SAMPLE_BUDGET, (long) currentResult.request().width() * currentResult.request().height())
: MAX_REFINED_SAMPLE_BUDGET;
return NoiseRenderCoordinator.timeBoundSampleBudget(
currentResult.samples(),
currentResult.milliseconds(),
REFINEMENT_TARGET_MILLIS,
fullResolution ? PALETTE_REFINEMENT_TARGET_MILLIS : REFINEMENT_TARGET_MILLIS,
COARSE_SAMPLE_BUDGET,
MAX_REFINED_SAMPLE_BUDGET
maximumBudget
);
}
@@ -1039,12 +1048,12 @@ public final class NoiseExplorerGUI extends JPanel {
}
private void resetViewport() {
viewport = new NoiseViewport(0D, 0D, 1D);
viewport = initialViewport;
requestPreview();
}
private void handleHotload() {
Engine refreshedEngine = GuiHost.findActiveEngine();
Engine refreshedEngine = paletteStart == null ? GuiHost.findActiveEngine() : GuiHost.findEngine(paletteStart.worldId());
EventQueue.invokeLater(() -> {
if (disposed) {
return;
@@ -1088,7 +1097,7 @@ public final class NoiseExplorerGUI extends JPanel {
if (renderedImage == null) {
drawEmptyState(canvas, getWidth(), renderHeight);
} else {
Object interpolation = renderedImage.getWidth() == getWidth()
Object interpolation = renderedImage.getWidth() == getWidth() || mode == ExplorerMode.PALETTES
? RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
: RenderingHints.VALUE_INTERPOLATION_BILINEAR;
canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation);
@@ -28,6 +28,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -40,7 +41,7 @@ record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegio
Map<String, BiomeEntry> biomes = loadBiomes(data);
List<RegionEntry> regions = new ArrayList<>();
Set<String> biomesInRegions = new HashSet<>();
for (String regionKey : engine.getDimension().getRegions()) {
for (String regionKey : regionKeysInUse(engine)) {
IrisRegion region = data.getRegionLoader().load(regionKey);
if (region == null) {
continue;
@@ -53,6 +54,16 @@ record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegio
return new PaletteCatalog(List.copyOf(regions), outside, Map.copyOf(biomes));
}
// A studio focus region is generated even when the dimension's region list doesn't include it.
private static Set<String> regionKeysInUse(Engine engine) {
Set<String> keys = new LinkedHashSet<>();
if (engine.getDimension().hasFocusRegion()) {
keys.add(engine.getDimension().getFocusRegion());
}
keys.addAll(engine.getDimension().getRegions());
return keys;
}
BiomeEntry biome(String key) {
return key == null ? null : biomes.get(key);
}
@@ -0,0 +1,26 @@
/*
* 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.framework.Engine;
import java.util.UUID;
public record PaletteExplorerStart(Engine engine, UUID worldId, UUID playerId, String biomeKey, double centerX, double centerZ) {
}
@@ -19,12 +19,14 @@
package com.volmit.iris.core.gui;
import javax.swing.BorderFactory;
import javax.swing.JComboBox;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
@@ -59,11 +61,14 @@ final class PaletteInspectorPanel extends JPanel {
private final Listener listener;
private final JLabel titleLabel = new JLabel();
private final JLabel detailLabel = new JLabel();
private final JComboBox<PaletteLayerScale.View> viewPicker = new JComboBox<>(PaletteLayerScale.View.values());
private final JLabel depthLabel = new JLabel();
private final JSlider depthSlider = new JSlider(0, 0, 0);
private final JLabel depthLabel = new JLabel("", SwingConstants.CENTER);
private final JButton shallowerButton = ExplorerStyle.button("‹");
private final JButton deeperButton = ExplorerStyle.button("›");
private final JPanel depthRow = new JPanel(new BorderLayout(6, 0));
private final StatisticsView statisticsView = new StatisticsView();
private boolean updatingControls;
private PaletteLayerScale.View selectedView = PaletteLayerScale.View.BLOCKS;
private int depth;
private int deepestDepth;
PaletteInspectorPanel(Listener listener) {
super(new BorderLayout());
@@ -80,19 +85,12 @@ final class PaletteInspectorPanel extends JPanel {
}
void showPreview(PaletteLayerPreview preview) {
updatingControls = true;
try {
titleLabel.setText(preview.biomeKey() + " · layer " + preview.layer());
detailLabel.setText(thicknessText(preview) + " · zoom " + formatZoom(preview.zoom())
+ " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)");
int deepestDepth = Math.max(0, preview.maxThickness() - 1);
depthSlider.setMaximum(deepestDepth);
depthSlider.setValue(preview.depth());
depthSlider.setEnabled(deepestDepth > 0);
depthLabel.setText(depthText(preview.depth(), deepestDepth));
} finally {
updatingControls = false;
}
titleLabel.setText(preview.biomeKey() + " · layer " + preview.layer());
detailLabel.setText(thicknessText(preview) + " · zoom " + formatZoom(preview.zoom())
+ " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)");
depth = preview.depth();
deepestDepth = Math.max(0, preview.maxThickness() - 1);
refreshDepthControls();
statisticsView.show(preview);
setVisible(true);
}
@@ -106,7 +104,7 @@ final class PaletteInspectorPanel extends JPanel {
}
PaletteLayerScale.View selectedView() {
return (PaletteLayerScale.View) viewPicker.getSelectedItem();
return selectedView;
}
private JPanel buildControls() {
@@ -123,26 +121,58 @@ final class PaletteInspectorPanel extends JPanel {
controls.add(titleLabel);
controls.add(detailLabel);
controls.add(sectionLabel("View"));
viewPicker.setBackground(FIELD_BACKGROUND);
viewPicker.setForeground(PRIMARY_TEXT);
viewPicker.addActionListener(event -> listener.onViewChanged(selectedView()));
controls.add(viewPicker);
depthLabel.setForeground(SECONDARY_TEXT);
depthLabel.setFont(HEADER_FONT);
controls.add(depthLabel);
depthSlider.setOpaque(false);
depthSlider.addChangeListener(event -> {
if (!updatingControls && !depthSlider.getValueIsAdjusting()) {
listener.onDepthChanged(depthSlider.getValue());
}
});
depthSlider.addChangeListener(event -> depthLabel.setText(depthText(depthSlider.getValue(), depthSlider.getMaximum())));
controls.add(depthSlider);
controls.add(sectionLabel("Show"));
controls.add(buildViewButtons());
controls.add(sectionLabel("Depth"));
controls.add(buildDepthRow());
return controls;
}
private JPanel buildViewButtons() {
JPanel row = new JPanel(new GridLayout(1, 0, 4, 0));
row.setOpaque(false);
ButtonGroup group = new ButtonGroup();
for (PaletteLayerScale.View view : PaletteLayerScale.View.values()) {
JToggleButton button = ExplorerStyle.toggleButton(view.toString());
button.setSelected(view == selectedView);
button.addActionListener(event -> {
selectedView = view;
listener.onViewChanged(view);
});
group.add(button);
row.add(button);
}
return row;
}
private JPanel buildDepthRow() {
depthRow.setOpaque(false);
depthLabel.setForeground(PRIMARY_TEXT);
depthLabel.setFont(ITEM_FONT);
shallowerButton.addActionListener(event -> changeDepth(-1));
deeperButton.addActionListener(event -> changeDepth(1));
depthRow.add(shallowerButton, BorderLayout.WEST);
depthRow.add(depthLabel, BorderLayout.CENTER);
depthRow.add(deeperButton, BorderLayout.EAST);
return depthRow;
}
private void changeDepth(int step) {
int nextDepth = Math.clamp(depth + step, 0, deepestDepth);
if (nextDepth == depth) {
return;
}
depth = nextDepth;
refreshDepthControls();
listener.onDepthChanged(depth);
}
private void refreshDepthControls() {
depthLabel.setText(depthText(depth, deepestDepth));
shallowerButton.setEnabled(depth > 0);
deeperButton.setEnabled(depth < deepestDepth);
}
private static JLabel sectionLabel(String text) {
JLabel label = new JLabel(text);
label.setForeground(SECONDARY_TEXT);
@@ -159,9 +189,9 @@ final class PaletteInspectorPanel extends JPanel {
private static String depthText(int depth, int deepestDepth) {
if (deepestDepth == 0) {
return "Depth: top block only";
return "Top block only";
}
return "Depth " + depth + (depth == 0 ? " (top)" : "") + " of " + deepestDepth;
return depth == 0 ? "Top block" : depth + (depth == 1 ? " block down" : " blocks down");
}
private static String formatZoom(double zoom) {
@@ -38,6 +38,7 @@ final class PaletteLayerPreview {
static final double STATISTICS_SPACING_BLOCKS = 37D;
private static final int MISSING_BLOCK_COLOR = 0x5A5F6B;
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;
@@ -50,6 +51,7 @@ final class PaletteLayerPreview {
private final int[] colorByEntry;
private final BlockTexture[] textureByEntry;
private final List<Entry> entries;
private final SampledBlock[] blockCache = new SampledBlock[BLOCK_CACHE_SIZE];
private final long[] histogram;
private final double absentShare;
private final double outOfRangeShare;
@@ -90,11 +92,30 @@ final class PaletteLayerPreview {
return new PaletteLayerPreview(biomeKey, biome, layer, depth, terrainRandom, data, textures);
}
// The world picks one block per whole coordinate, and zoomed in, many pixels fall on the same block.
double sample(double x, double z) {
if (biome.getLayerThickness(layer, x, z, random, data) <= depth) {
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
int cacheSlot = blockCacheSlot(blockX, blockZ);
SampledBlock cached = blockCache[cacheSlot];
if (cached != null && cached.x() == blockX && cached.z() == blockZ) {
return cached.noise();
}
double noise = sampleBlock(blockX, blockZ);
blockCache[cacheSlot] = new SampledBlock(blockX, blockZ, noise);
return noise;
}
private double sampleBlock(int blockX, int blockZ) {
if (biome.getLayerThickness(layer, blockX, blockZ, random, data) <= depth) {
return Double.NaN;
}
return biome.getLayerNoise(layer, x, z, depth, random, data);
return biome.getLayerNoise(layer, blockX, blockZ, depth, random, data);
}
private static int blockCacheSlot(int blockX, int blockZ) {
long hash = blockX * 0x9E3779B97F4A7C15L ^ blockZ * 0xC2B2AE3D27D4EB4FL;
return (int) ((hash ^ (hash >>> 29)) & (BLOCK_CACHE_SIZE - 1));
}
int colorForNoise(double noise) {
@@ -291,6 +312,9 @@ final class PaletteLayerPreview {
}
}
private record SampledBlock(int x, int z, double noise) {
}
private record Statistics(long[] histogram, double[] sharesByEntry, double absentShare, double outOfRangeShare) {
}
}
@@ -87,7 +87,7 @@ final class PaletteLayerScale implements NoiseColorScale {
enum View {
BLOCKS("Blocks"),
NOISE("Noise"),
BLOCKS_SHADED_BY_NOISE("Blocks + noise");
BLOCKS_SHADED_BY_NOISE("Both");
private final String label;
@@ -50,7 +50,6 @@ 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,11 +70,9 @@ final class PaletteNavigator extends JPanel {
private static final String DETAIL_COLOR = "#8890A0";
private final Listener listener;
private final boolean showsPlayerLocation;
private final CardLayout cards = new CardLayout();
private final JPanel cardPanel = new JPanel(cards);
private final JLabel regionLabel = new JLabel();
private final JLabel positionLabel = new JLabel();
private final DefaultListModel<HereRow> hereRows = new DefaultListModel<>();
private final JList<HereRow> hereList = new JList<>(hereRows);
private final JTextField searchField = new JTextField();
@@ -88,9 +85,8 @@ final class PaletteNavigator extends JPanel {
private int selectedLayer;
private boolean updatingSelection;
PaletteNavigator(boolean showsPlayerLocation, Listener listener) {
PaletteNavigator(Listener listener) {
super(new BorderLayout());
this.showsPlayerLocation = showsPlayerLocation;
this.listener = listener;
setBackground(SIDEBAR_BACKGROUND);
cardPanel.add(buildHereCard(), HERE_CARD);
@@ -132,32 +128,17 @@ final class PaletteNavigator extends JPanel {
JToggleButton overview = createTab("Overview", OVERVIEW_CARD, group);
bar.add(here);
bar.add(overview);
(showsPlayerLocation ? here : overview).doClick();
here.doClick();
return bar;
}
private JToggleButton createTab(String text, String card, ButtonGroup group) {
JToggleButton tab = new JToggleButton(text);
tab.setFocusPainted(false);
tab.setFont(ITEM_FONT);
tab.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(BORDER),
BorderFactory.createEmptyBorder(5, 9, 5, 9)
));
tab.addItemListener(event -> styleTab(tab));
JToggleButton tab = ExplorerStyle.toggleButton(text);
tab.addActionListener(event -> cards.show(cardPanel, card));
styleTab(tab);
group.add(tab);
return tab;
}
private static void styleTab(JToggleButton tab) {
tab.setBackground(tab.isSelected() ? ACCENT.darker().darker() : FIELD_BACKGROUND);
tab.setForeground(tab.isSelected() ? PRIMARY_TEXT : SECONDARY_TEXT);
tab.setContentAreaFilled(false);
tab.setOpaque(true);
}
private JPanel buildHereCard() {
JPanel card = new JPanel(new BorderLayout());
card.setBackground(SIDEBAR_BACKGROUND);
@@ -167,11 +148,9 @@ final class PaletteNavigator extends JPanel {
BorderFactory.createMatteBorder(1, 0, 1, 0, BORDER),
BorderFactory.createEmptyBorder(9, 12, 9, 12)
));
for (JLabel label : List.of(regionLabel, positionLabel)) {
label.setForeground(PRIMARY_TEXT);
label.setFont(ITEM_FONT);
location.add(label);
}
regionLabel.setForeground(PRIMARY_TEXT);
regionLabel.setFont(ITEM_FONT);
location.add(regionLabel);
hereList.setBackground(SIDEBAR_BACKGROUND);
hereList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@@ -254,17 +233,13 @@ final class PaletteNavigator extends JPanel {
updatingSelection = true;
try {
hereRows.clear();
if (!showsPlayerLocation) {
showHereMessage("Run /iris std pal in-game", "to see where you are standing.");
return;
}
if (location == null || catalog == null) {
showHereMessage("Finding where you are...", " ");
showHereMessage("Finding where you are...");
return;
}
switch (location.status()) {
case OFFLINE -> showHereMessage("You are offline.", " ");
case OUTSIDE_IRIS_WORLD -> showHereMessage("You are not in an Iris world.", " ");
case OFFLINE -> showHereMessage("You are offline.");
case IN_OTHER_WORLD -> showHereMessage("You left the world this window shows.");
case IN_REGION -> showHereRows(location);
}
selectInHereList();
@@ -273,18 +248,16 @@ final class PaletteNavigator extends JPanel {
}
}
private void showHereMessage(String firstLine, String secondLine) {
regionLabel.setText(firstLine);
positionLabel.setText(secondLine);
private void showHereMessage(String message) {
regionLabel.setText(message);
}
private void showHereRows(PlayerLocation here) {
RegionEntry region = catalog.region(here.regionKey());
regionLabel.setText(labeled("Region", region == null ? here.regionKey() : region.name(), here.regionKey()));
positionLabel.setText(here.isBelowSurfaceBiome() ? "Underground, below the surface biome" : "On the surface");
addBiomeLayers("Surface biome", here.surfaceBiomeKey());
addBiomeLayers("Surface biome here", here.surfaceBiomeKey());
if (here.isBelowSurfaceBiome()) {
addBiomeLayers("Biome at your position", here.biomeAtPositionKey());
addBiomeLayers("Underground biome at your height", here.biomeAtPositionKey());
}
if (region == null) {
return;
@@ -36,17 +36,19 @@ final class PlayerLocationTracker {
private static final long CHECK_INTERVAL_TICKS = 10L;
private final UUID playerId;
private final UUID worldId;
private final Listener listener;
private BukkitTask task;
private PlayerLocation lastLocation;
private PlayerLocationTracker(UUID playerId, Listener listener) {
private PlayerLocationTracker(UUID playerId, UUID worldId, Listener listener) {
this.playerId = playerId;
this.worldId = worldId;
this.listener = listener;
}
static PlayerLocationTracker follow(UUID playerId, Listener listener) {
PlayerLocationTracker tracker = new PlayerLocationTracker(playerId, listener);
static PlayerLocationTracker follow(UUID playerId, UUID worldId, Listener listener) {
PlayerLocationTracker tracker = new PlayerLocationTracker(playerId, worldId, listener);
tracker.task = Bukkit.getScheduler().runTaskTimer(Iris.instance, tracker::checkLocation, 0L, CHECK_INTERVAL_TICKS);
return tracker;
}
@@ -71,16 +73,19 @@ final class PlayerLocationTracker {
if (player == null) {
return PlayerLocation.OFFLINE;
}
if (player.getWorld() == null || !player.getWorld().getUID().equals(worldId)) {
return PlayerLocation.IN_OTHER_WORLD;
}
Engine engine = engineAt(player.getLocation());
if (engine == null) {
return PlayerLocation.OUTSIDE_IRIS_WORLD;
return PlayerLocation.IN_OTHER_WORLD;
}
Location position = player.getLocation();
IrisRegion region = engine.getRegion(position);
String surfaceBiome = keyOf(engine.getSurfaceBiome(position.getBlockX(), position.getBlockZ()));
String biomeAtPosition = keyOf(engine.getBiomeOrMantle(position));
if (region == null || region.getLoadKey() == null || surfaceBiome == null) {
return PlayerLocation.OUTSIDE_IRIS_WORLD;
return PlayerLocation.IN_OTHER_WORLD;
}
return new PlayerLocation(Status.IN_REGION, region.getLoadKey(), surfaceBiome, biomeAtPosition);
}
@@ -99,14 +104,14 @@ final class PlayerLocationTracker {
enum Status {
OFFLINE,
OUTSIDE_IRIS_WORLD,
IN_OTHER_WORLD,
IN_REGION
}
// Biomes are 3D: underground, the biome at the player's position can differ from the surface biome above it.
record PlayerLocation(Status status, String regionKey, String surfaceBiomeKey, String biomeAtPositionKey) {
static final PlayerLocation OFFLINE = new PlayerLocation(Status.OFFLINE, null, null, null);
static final PlayerLocation OUTSIDE_IRIS_WORLD = new PlayerLocation(Status.OUTSIDE_IRIS_WORLD, null, null, null);
static final PlayerLocation IN_OTHER_WORLD = new PlayerLocation(Status.IN_OTHER_WORLD, null, null, null);
boolean isBelowSurfaceBiome() {
return biomeAtPositionKey != null && !biomeAtPositionKey.equals(surfaceBiomeKey);