diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java b/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java index bec6d1dc3..18d2c6237 100644 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java +++ b/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java @@ -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) diff --git a/core/src/main/java/com/volmit/iris/core/gui/ExplorerStyle.java b/core/src/main/java/com/volmit/iris/core/gui/ExplorerStyle.java new file mode 100644 index 000000000..36f84db44 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/ExplorerStyle.java @@ -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 . + */ + +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); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java b/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java index e574db837..aac0f7df2 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java +++ b/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java @@ -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); diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java index 96a12765c..179f17de5 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java +++ b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java @@ -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 customFactory; private final String customName; @@ -154,12 +156,11 @@ public final class NoiseExplorerGUI extends JPanel { private final JComboBox 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 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 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); diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java index a4d19cf6a..4a1b6cb16 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteCatalog.java @@ -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 regions, List biomesOutsideRegio Map biomes = loadBiomes(data); List regions = new ArrayList<>(); Set 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 regions, List 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 regionKeysInUse(Engine engine) { + Set 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); } diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteExplorerStart.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteExplorerStart.java new file mode 100644 index 000000000..4ec630a51 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteExplorerStart.java @@ -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 . + */ + +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) { +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java index 62d81cf23..6e87dd690 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteInspectorPanel.java @@ -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 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) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java index 0ce1191c6..46acd59eb 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerPreview.java @@ -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 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) { } } diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerScale.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerScale.java index a9d715847..12a0cfffd 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerScale.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteLayerScale.java @@ -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; diff --git a/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java b/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java index 51112f2cc..df3d2808e 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PaletteNavigator.java @@ -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 hereRows = new DefaultListModel<>(); private final JList 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; diff --git a/core/src/main/java/com/volmit/iris/core/gui/PlayerLocationTracker.java b/core/src/main/java/com/volmit/iris/core/gui/PlayerLocationTracker.java index 00eb1cc88..f9f99ffd6 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PlayerLocationTracker.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PlayerLocationTracker.java @@ -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);