mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +00:00
Show biome picking noise in the biome view
Noise switch, per-region breakdown.
This commit is contained in:
1 parent
4ac268047e
commit
3123ab3753
11 files changed
+687
-223
No files matched your search
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.core.gui.PickMeasuring.TypedSpread;
|
||||
import com.volmit.iris.core.gui.components.PickDisplay;
|
||||
import com.volmit.iris.engine.IrisComplex;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.InferredType;
|
||||
import com.volmit.iris.engine.object.IrisRegion;
|
||||
import com.volmit.iris.util.stream.ProceduralStream;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* The biome view's explorer: every region picks the biomes on its ground from its own biome lists, so for each region
|
||||
* in view it shows how its noise, or in hybrid terrain the height, picks them.
|
||||
*/
|
||||
final class BiomeExplorer {
|
||||
private final Supplier<Engine> engineSource;
|
||||
private final PickInspectorPanel inspector;
|
||||
private final LatestMeasurement<Measurement> measurer = new LatestMeasurement<>("Iris Biome Explorer");
|
||||
private MapCensus.ViewArea latestArea;
|
||||
private MapCensus.ViewArea measuredArea;
|
||||
|
||||
/**
|
||||
* @param onDisplayChosen receives how the map should draw the picks
|
||||
*/
|
||||
BiomeExplorer(Supplier<Engine> engineSource, Consumer<PickDisplay> onDisplayChosen) {
|
||||
this.engineSource = engineSource;
|
||||
this.inspector = new PickInspectorPanel("Biomes", onDisplayChosen);
|
||||
}
|
||||
|
||||
PickInspectorPanel inspector() {
|
||||
return inspector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the regions in this area and how they pick their biomes, unless that was already done.
|
||||
*/
|
||||
void measure(MapCensus.ViewArea area) {
|
||||
latestArea = area;
|
||||
if (area == null || area.equals(measuredArea)) {
|
||||
return;
|
||||
}
|
||||
measuredArea = area;
|
||||
measurer.measure(() -> measurePicks(area), this::show);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pack changed, so the regions and their biomes are read again.
|
||||
*/
|
||||
void refresh() {
|
||||
measuredArea = null;
|
||||
measure(latestArea);
|
||||
}
|
||||
|
||||
void close() {
|
||||
measurer.close();
|
||||
}
|
||||
|
||||
private void show(Measurement measurement) {
|
||||
List<PickInspectorPanel.Section> sections = new ArrayList<>();
|
||||
for (RegionPicks region : measurement.regions()) {
|
||||
List<JComponent> charts = region.coverage() != null
|
||||
? List.of(PickInspectorPanel.heightChart(region.coverage()))
|
||||
: region.spreads().stream().map(spread -> (JComponent) new NoisePickChart(spread, "Biomes")).toList();
|
||||
sections.add(new PickInspectorPanel.Section(region.name(), region.color(), region.share(), charts, region.notes()));
|
||||
}
|
||||
String detail = measurement.hybrid()
|
||||
? "Hybrid terrain · each region picks biomes by height"
|
||||
: "Each region splits its own biome noise by rarity";
|
||||
inspector.showRegionSections(detail, sections, measurement.notes());
|
||||
}
|
||||
|
||||
private Measurement measurePicks(MapCensus.ViewArea area) {
|
||||
Engine engine = engineSource.get();
|
||||
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||
if (complex == null) {
|
||||
return null;
|
||||
}
|
||||
Map<IrisRegion, RegionSamples> samplesByRegion = new IdentityHashMap<>();
|
||||
int[] total = {0};
|
||||
PickMeasuring.forEachSample(area, (x, z) -> {
|
||||
IrisRegion region = complex.getRegionStream().get(x, z);
|
||||
if (region == null) {
|
||||
return;
|
||||
}
|
||||
total[0]++;
|
||||
samplesByRegion.computeIfAbsent(region, subject -> new RegionSamples(engine, complex, subject)).add(x, z);
|
||||
});
|
||||
List<RegionPicks> regions = samplesByRegion.values().stream()
|
||||
.sorted(Comparator.comparingInt(RegionSamples::count).reversed())
|
||||
.map(samples -> samples.picks(engine, total[0]))
|
||||
.toList();
|
||||
List<String> notes = new ArrayList<>();
|
||||
if (complex.isHybridTerrain()) {
|
||||
notes.add(PickMeasuring.hybridNoiseNote());
|
||||
} else {
|
||||
PickMeasuring.addRarityNotes(notes, complex, regions.stream().flatMap(region -> region.spreads().stream()).toList());
|
||||
}
|
||||
notes.add(PickMeasuring.measuredNote());
|
||||
return new Measurement(complex.isHybridTerrain(), regions, notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* The spots in view one region owns. The biome view shows each spot's base biome, which the region picks from the
|
||||
* biome list of the spot's continent type.
|
||||
*/
|
||||
private static final class RegionSamples {
|
||||
private final IrisComplex complex;
|
||||
private final IrisRegion region;
|
||||
private final List<TypedSpread> spreads;
|
||||
private final Map<InferredType, ProceduralStream<Double>> noiseByType = new EnumMap<>(InferredType.class);
|
||||
private final List<Integer> pickingHeights = new ArrayList<>();
|
||||
private int count;
|
||||
|
||||
private RegionSamples(Engine engine, IrisComplex complex, IrisRegion region) {
|
||||
this.complex = complex;
|
||||
this.region = region;
|
||||
this.spreads = complex.isHybridTerrain() ? List.of() : PickMeasuring.biomeSpreads(engine, complex, region);
|
||||
}
|
||||
|
||||
private void add(double x, double z) {
|
||||
count++;
|
||||
if (complex.isHybridTerrain()) {
|
||||
pickingHeights.add((int) Math.round(complex.getPlacementHeightStream().get(x, z)));
|
||||
return;
|
||||
}
|
||||
InferredType type = complex.getBridgeStream().get(x, z);
|
||||
for (TypedSpread typed : spreads) {
|
||||
if (typed.type() == type) {
|
||||
typed.spread().add(noiseByType.computeIfAbsent(type, shown -> complex.biomePickNoiseStream(region, shown)).get(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int count() {
|
||||
return count;
|
||||
}
|
||||
|
||||
private RegionPicks picks(Engine engine, int totalCount) {
|
||||
double share = totalCount == 0 ? 0D : count / (double) totalCount;
|
||||
String name = region.getName();
|
||||
int color = PickMeasuring.regionColor(engine, region);
|
||||
List<String> notes = new ArrayList<>();
|
||||
if (complex.isHybridTerrain()) {
|
||||
RegionHeightCoverage coverage = RegionHeightCoverage.measure(PickMeasuring.biomeRanges(engine, region),
|
||||
pickingHeights.stream().mapToInt(Integer::intValue).toArray());
|
||||
notes.addAll(coverage.notes());
|
||||
return new RegionPicks(name, color, share, List.of(), coverage, notes);
|
||||
}
|
||||
List<TypedSpread> inView = spreads.stream().filter(typed -> typed.spread().sampleCount() > 0).toList();
|
||||
PickMeasuring.addBiomeSpreadNotes(notes, inView);
|
||||
PickMeasuring.addChildBiomeNote(notes, complex, region);
|
||||
return new RegionPicks(name, color, share, inView.stream().map(TypedSpread::spread).toList(), null, notes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param coverage how the region's biomes cover the heights in view in hybrid terrain, or null in biome terrain
|
||||
*/
|
||||
private record RegionPicks(String name, int color, double share, List<NoisePickSpread> spreads,
|
||||
RegionHeightCoverage coverage, List<String> notes) {
|
||||
}
|
||||
|
||||
private record Measurement(boolean hybrid, List<RegionPicks> regions, List<String> notes) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.Iris;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Measures off the Swing thread, one measurement at a time, and only hands back the newest one asked for.
|
||||
*/
|
||||
final class LatestMeasurement<T> {
|
||||
private final ThreadPoolExecutor worker;
|
||||
private final AtomicLong generation = new AtomicLong();
|
||||
|
||||
LatestMeasurement(String threadName) {
|
||||
this.worker = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), task -> {
|
||||
Thread thread = new Thread(task, threadName);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}, new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the measurement and shows its result on the Swing thread, unless another was asked for meanwhile or it
|
||||
* measured nothing.
|
||||
*/
|
||||
void measure(Supplier<T> measurement, Consumer<T> show) {
|
||||
long requested = generation.incrementAndGet();
|
||||
worker.execute(() -> {
|
||||
T result = measureSafely(measurement);
|
||||
EventQueue.invokeLater(() -> {
|
||||
if (requested == generation.get() && result != null) {
|
||||
show.accept(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the measurement under way, whose result no longer fits.
|
||||
*/
|
||||
void discardPending() {
|
||||
generation.incrementAndGet();
|
||||
}
|
||||
|
||||
void close() {
|
||||
worker.shutdownNow();
|
||||
}
|
||||
|
||||
private static <T> T measureSafely(Supplier<T> measurement) {
|
||||
try {
|
||||
return measurement.get();
|
||||
} catch (Throwable error) {
|
||||
Iris.reportError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,15 +227,19 @@ final class MapCanvas extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
PickDisplay pickDisplay() {
|
||||
return pickDisplay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the region view's picks, the noise that picks them, or both.
|
||||
* Draws the region or biome view's picks, the noise that picks them, or both.
|
||||
*/
|
||||
void showPicksAs(PickDisplay display) {
|
||||
if (display == pickDisplay) {
|
||||
return;
|
||||
}
|
||||
pickDisplay = display;
|
||||
if (currentType.isRegionView()) {
|
||||
if (currentType.drawsPickNoise()) {
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
@@ -247,6 +251,7 @@ final class MapCanvas extends JPanel {
|
||||
|
||||
private MapLayer currentLayer() {
|
||||
return switch (currentType) {
|
||||
case BIOME -> MapLayer.biomes(pickDisplay);
|
||||
case REGION -> MapLayer.regions(pickDisplay);
|
||||
case REGION_PREVIEW -> MapLayer.regionPreview(previewRegionKey, pickDisplay);
|
||||
default -> MapLayer.of(currentType);
|
||||
@@ -1098,7 +1103,7 @@ final class MapCanvas extends JPanel {
|
||||
double worldZ = screenToWorldZ(point.y);
|
||||
controller.submitProbe(
|
||||
frame,
|
||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.isRegionView()),
|
||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion(), currentType.drawsPickNoise()),
|
||||
details -> {
|
||||
QuickHover quick = quickHover;
|
||||
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
|
||||
|
||||
@@ -68,17 +68,20 @@ final class MapHoverProbe {
|
||||
dimension == null ? 0D : dimension.getFluidHeight(),
|
||||
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
|
||||
complex.explainHybridChoice(region, worldX, worldZ),
|
||||
explainPicks ? pickNoise(complex, region, previewRegion == null, worldX, worldZ) : null
|
||||
explainPicks ? pickNoise(complex, region, previewRegion, worldX, worldZ) : null
|
||||
);
|
||||
}
|
||||
|
||||
// HYBRID terrain picks biomes by height, which the placement line already tells, so only the region noise is left.
|
||||
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, boolean regionPicked, double x, double z) {
|
||||
Double regionNoise = regionPicked ? complex.getRegionStyleStream().get(x, z) : null;
|
||||
// The preview shows surface biomes, which the height can move to another biome list; the real map shows base biomes.
|
||||
private static PickNoise pickNoise(IrisComplex complex, IrisRegion region, IrisRegion previewRegion, double x, double z) {
|
||||
Double regionNoise = previewRegion == null ? complex.getRegionStyleStream().get(x, z) : null;
|
||||
if (complex.isHybridTerrain() || region == null) {
|
||||
return new PickNoise(regionNoise, null, Double.NaN, null);
|
||||
}
|
||||
InferredType biomeType = complex.surfacePickAsIfIn(region, x, z).type();
|
||||
InferredType biomeType = previewRegion != null
|
||||
? complex.surfacePickAsIfIn(region, x, z).type()
|
||||
: complex.getBridgeStream().get(x, z);
|
||||
return new PickNoise(regionNoise, biomeType, complex.biomePickNoise(region, biomeType, x, z),
|
||||
complex.biomePickedByNoise(region, biomeType, x, z));
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
private final JPanel inspectorSlot = new JPanel(new BorderLayout());
|
||||
private final JComponent viewListScroll;
|
||||
private final JSplitPane explorerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
private final JSplitPane regionInspectorSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
private final JSplitPane pickInspectorSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
private final List<JComponent> mapControls = new ArrayList<>();
|
||||
private final List<JComponent> explorerControls = new ArrayList<>();
|
||||
private final List<JComponent> noiseControls = new ArrayList<>();
|
||||
@@ -92,6 +92,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
private final JToggleButton bordersToggle = StudioToolbar.toggle("Borders", "Outline the regions on every view (B)");
|
||||
private final MapLegendPanel legendPanel;
|
||||
private final RegionExplorer regionExplorer;
|
||||
private final BiomeExplorer biomeExplorer;
|
||||
|
||||
private NoiseExplorerGUI paletteExplorer;
|
||||
private final Map<MapView, ObjectExplorer> objectExplorers = new EnumMap<>(MapView.class);
|
||||
@@ -109,6 +110,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
this.viewListScroll = StudioToolbar.scroll(viewList);
|
||||
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
|
||||
this.regionExplorer = new RegionExplorer(canvas::engine, canvas::previewRegion, canvas::showPicksAs);
|
||||
this.biomeExplorer = new BiomeExplorer(canvas::engine, canvas::showPicksAs);
|
||||
canvas.centerOn(start.centerX(), start.centerZ());
|
||||
buildFrame();
|
||||
}
|
||||
@@ -135,7 +137,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
|
||||
private void buildFrame() {
|
||||
styleExplorerSplit();
|
||||
styleRegionInspectorSplit();
|
||||
stylePickInspectorSplit();
|
||||
content.add(canvas, MAP_CARD);
|
||||
statusBars.add(canvas.statusBar(), MAP_CARD);
|
||||
sidebar.add(viewListScroll, BorderLayout.CENTER);
|
||||
@@ -228,6 +230,8 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
inspectorSlot.removeAll();
|
||||
if (view == MapView.REGIONS) {
|
||||
showRegionExplorer(previousView);
|
||||
} else if (view == MapView.BIOMES) {
|
||||
showBiomeExplorer();
|
||||
} else {
|
||||
useSidebar(viewListScroll);
|
||||
inspectorSlot.add(legendPanel, BorderLayout.CENTER);
|
||||
@@ -242,19 +246,31 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
regionExplorer.loadRegions();
|
||||
}
|
||||
useSidebar(viewListAbove(regionExplorer.navigator()));
|
||||
regionInspectorSplit.setTopComponent(regionExplorer.inspector());
|
||||
regionInspectorSplit.setBottomComponent(legendPanel);
|
||||
inspectorSlot.add(regionInspectorSplit, BorderLayout.CENTER);
|
||||
showPickInspector(regionExplorer.inspector());
|
||||
EventQueue.invokeLater(() -> viewList.select(currentView));
|
||||
regionExplorer.measure(canvas.viewArea());
|
||||
}
|
||||
|
||||
private void styleRegionInspectorSplit() {
|
||||
regionInspectorSplit.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
|
||||
regionInspectorSplit.setResizeWeight(0.7D);
|
||||
regionInspectorSplit.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
regionInspectorSplit.setDividerSize(StudioTheme.scale(EXPLORER_DIVIDER_HEIGHT));
|
||||
regionInspectorSplit.putClientProperty("FlatLaf.style", "style: grip");
|
||||
private void showBiomeExplorer() {
|
||||
useSidebar(viewListScroll);
|
||||
showPickInspector(biomeExplorer.inspector());
|
||||
biomeExplorer.measure(canvas.viewArea());
|
||||
}
|
||||
|
||||
// The region and biome views share how the map draws its picks, so their switches show the same choice.
|
||||
private void showPickInspector(PickInspectorPanel inspector) {
|
||||
inspector.selectDisplay(canvas.pickDisplay());
|
||||
pickInspectorSplit.setTopComponent(inspector);
|
||||
pickInspectorSplit.setBottomComponent(legendPanel);
|
||||
inspectorSlot.add(pickInspectorSplit, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private void stylePickInspectorSplit() {
|
||||
pickInspectorSplit.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
|
||||
pickInspectorSplit.setResizeWeight(0.7D);
|
||||
pickInspectorSplit.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
pickInspectorSplit.setDividerSize(StudioTheme.scale(EXPLORER_DIVIDER_HEIGHT));
|
||||
pickInspectorSplit.putClientProperty("FlatLaf.style", "style: grip");
|
||||
}
|
||||
|
||||
private void showPalettes(MapView previousView) {
|
||||
@@ -420,12 +436,15 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
legendPanel.show(title, shares, highlight);
|
||||
if (currentView == MapView.REGIONS) {
|
||||
regionExplorer.measure(canvas.viewArea());
|
||||
} else if (currentView == MapView.BIOMES) {
|
||||
biomeExplorer.measure(canvas.viewArea());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentRefreshed() {
|
||||
regionExplorer.refresh();
|
||||
biomeExplorer.refresh();
|
||||
}
|
||||
|
||||
private void resetExplorerView() {
|
||||
@@ -524,6 +543,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
private void close() {
|
||||
canvas.close();
|
||||
regionExplorer.close();
|
||||
biomeExplorer.close();
|
||||
if (paletteExplorer != null) {
|
||||
paletteExplorer.close();
|
||||
}
|
||||
|
||||
@@ -33,9 +33,15 @@ import javax.swing.SwingConstants;
|
||||
import javax.swing.text.DefaultCaret;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.GridLayout;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -43,10 +49,10 @@ import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The region view's inspector: how the noise picks the regions, or for a chosen region how its biomes are picked,
|
||||
* measured against what is in view and put in words.
|
||||
* The inspector of the region and biome views: how noise picks the regions or biomes in view, measured against what
|
||||
* is in view and put in words, with a switch for how the map draws the picks.
|
||||
*/
|
||||
final class RegionInspectorPanel extends JPanel {
|
||||
final class PickInspectorPanel extends JPanel {
|
||||
private static final long serialVersionUID = -2874105263188240317L;
|
||||
private static final String MESSAGE_CARD = "message";
|
||||
private static final String DETAILS_CARD = "details";
|
||||
@@ -60,13 +66,15 @@ final class RegionInspectorPanel extends JPanel {
|
||||
private final JPanel cardPanel = new JPanel(cards);
|
||||
private final JTextArea message = wrappingText();
|
||||
private final JPanel details = new ViewportWidthPanel();
|
||||
private final RegionHeightChart heightChart = new RegionHeightChart();
|
||||
private final String picksName;
|
||||
|
||||
/**
|
||||
* @param picksName what the map shows when it draws the picks: "Regions" or "Biomes"
|
||||
* @param onDisplayChosen receives how the map should draw the picks
|
||||
*/
|
||||
RegionInspectorPanel(Consumer<PickDisplay> onDisplayChosen) {
|
||||
PickInspectorPanel(String picksName, Consumer<PickDisplay> onDisplayChosen) {
|
||||
super(new BorderLayout());
|
||||
this.picksName = picksName;
|
||||
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
add(buildHeader(onDisplayChosen), BorderLayout.NORTH);
|
||||
@@ -74,10 +82,13 @@ final class RegionInspectorPanel extends JPanel {
|
||||
cardPanel.add(buildMessageCard(), MESSAGE_CARD);
|
||||
details.setLayout(new BoxLayout(details, BoxLayout.Y_AXIS));
|
||||
details.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
heightChart.setAlignmentX(LEFT_ALIGNMENT);
|
||||
cardPanel.add(StudioToolbar.scroll(details), DETAILS_CARD);
|
||||
add(cardPanel, BorderLayout.CENTER);
|
||||
showMeasuring("Regions");
|
||||
showMeasuring(picksName);
|
||||
}
|
||||
|
||||
void selectDisplay(PickDisplay display) {
|
||||
displayButtons.get(display).setSelected(true);
|
||||
}
|
||||
|
||||
void showMeasuring(String name) {
|
||||
@@ -100,8 +111,31 @@ final class RegionInspectorPanel extends JPanel {
|
||||
void showCoverage(String regionName, RegionHeightCoverage coverage, List<String> notes) {
|
||||
showHeader(regionName, "Hybrid terrain · biomes by height");
|
||||
nameFirstDisplay("Biomes");
|
||||
heightChart.show(coverage);
|
||||
showDetails(List.of(heightChart), notes);
|
||||
showDetails(List.of(heightChart(coverage)), notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* One section per region in view, each with how it picks its biomes, then notes about all of them.
|
||||
*/
|
||||
void showRegionSections(String detailText, List<Section> sections, List<String> notes) {
|
||||
showHeader(picksName, detailText);
|
||||
nameFirstDisplay(picksName);
|
||||
List<JComponent> components = new ArrayList<>();
|
||||
for (Section section : sections) {
|
||||
components.add(new SectionHeader(section));
|
||||
components.addAll(section.charts());
|
||||
if (!section.notes().isEmpty()) {
|
||||
components.add(notesText(section.notes()));
|
||||
}
|
||||
}
|
||||
showDetails(components, notes);
|
||||
}
|
||||
|
||||
static RegionHeightChart heightChart(RegionHeightCoverage coverage) {
|
||||
RegionHeightChart chart = new RegionHeightChart();
|
||||
chart.setAlignmentX(LEFT_ALIGNMENT);
|
||||
chart.show(coverage);
|
||||
return chart;
|
||||
}
|
||||
|
||||
private JPanel buildHeader(Consumer<PickDisplay> onDisplayChosen) {
|
||||
@@ -136,9 +170,9 @@ final class RegionInspectorPanel extends JPanel {
|
||||
return row;
|
||||
}
|
||||
|
||||
private static String displayName(PickDisplay display) {
|
||||
private String displayName(PickDisplay display) {
|
||||
return switch (display) {
|
||||
case PICKS -> "Regions";
|
||||
case PICKS -> picksName;
|
||||
case NOISE -> "Noise";
|
||||
case BOTH -> "Both";
|
||||
};
|
||||
@@ -170,11 +204,7 @@ final class RegionInspectorPanel extends JPanel {
|
||||
details.removeAll();
|
||||
charts.forEach(details::add);
|
||||
if (!notes.isEmpty()) {
|
||||
JTextArea notesText = wrappingText();
|
||||
notesText.setAlignmentX(LEFT_ALIGNMENT);
|
||||
notesText.setBorder(StudioTheme.emptyBorder(4, 16, 12, 16));
|
||||
notesText.setText(String.join(PARAGRAPH_BREAK, notes));
|
||||
details.add(notesText);
|
||||
details.add(notesText(notes));
|
||||
}
|
||||
details.revalidate();
|
||||
details.repaint();
|
||||
@@ -182,6 +212,14 @@ final class RegionInspectorPanel extends JPanel {
|
||||
cards.show(cardPanel, DETAILS_CARD);
|
||||
}
|
||||
|
||||
private static JTextArea notesText(List<String> notes) {
|
||||
JTextArea text = wrappingText();
|
||||
text.setAlignmentX(LEFT_ALIGNMENT);
|
||||
text.setBorder(StudioTheme.emptyBorder(4, 16, 12, 16));
|
||||
text.setText(String.join(PARAGRAPH_BREAK, notes));
|
||||
return text;
|
||||
}
|
||||
|
||||
private static JLabel sectionLabel(String text) {
|
||||
JLabel label = new JLabel(text.toUpperCase(Locale.ROOT));
|
||||
label.setForeground(StudioTheme.MUTED_TEXT);
|
||||
@@ -202,6 +240,69 @@ final class RegionInspectorPanel extends JPanel {
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* One region of the biome view's inspector.
|
||||
*
|
||||
* @param share how much of the view the region covers
|
||||
*/
|
||||
record Section(String name, int color, double share, List<JComponent> charts, List<String> notes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A region's name in its colour above its charts, with how much of the view it covers.
|
||||
*/
|
||||
private static final class SectionHeader extends JComponent {
|
||||
private static final long serialVersionUID = -4730226930154581123L;
|
||||
private static final int HEIGHT = 34;
|
||||
private static final int MARGIN = 16;
|
||||
private static final int SWATCH_SIZE = 12;
|
||||
private static final int SWATCH_GAP = 8;
|
||||
|
||||
private final Section section;
|
||||
|
||||
private SectionHeader(Section section) {
|
||||
this.section = section;
|
||||
setAlignmentX(LEFT_ALIGNMENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
return StudioTheme.scaledSize(PANEL_WIDTH, HEIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return new Dimension(Integer.MAX_VALUE, getPreferredSize().height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics graphics) {
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
canvas.setColor(StudioTheme.SEPARATOR);
|
||||
canvas.fillRect(0, 0, getWidth(), 1);
|
||||
int margin = StudioTheme.scale(MARGIN);
|
||||
int swatch = StudioTheme.scale(SWATCH_SIZE);
|
||||
canvas.setColor(new Color(section.color()));
|
||||
int arc = StudioTheme.scale(4);
|
||||
canvas.fillRoundRect(margin, (getHeight() - swatch) / 2, swatch, swatch, arc, arc);
|
||||
canvas.setFont(StudioTheme.boldFont());
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
int baseline = (getHeight() + metrics.getAscent() - metrics.getDescent()) / 2;
|
||||
canvas.setColor(StudioTheme.TEXT);
|
||||
canvas.drawString(section.name(), margin + swatch + StudioTheme.scale(SWATCH_GAP), baseline);
|
||||
String share = NoisePickChart.percent(section.share()) + " of view";
|
||||
canvas.setFont(StudioTheme.smallFont());
|
||||
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||
canvas.drawString(share, getWidth() - margin - canvas.getFontMetrics().stringWidth(share), baseline);
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows the scroll pane's width, so the charts stretch to it and the notes wrap inside it.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.core.gui.NoisePickSpread.Option;
|
||||
import com.volmit.iris.core.gui.RegionHeightCoverage.BiomeRange;
|
||||
import com.volmit.iris.core.gui.components.RenderType;
|
||||
import com.volmit.iris.engine.IrisComplex;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.InferredType;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisRegion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Samples the map in view to measure how regions and biomes are picked, and puts what stands out in words; shared by
|
||||
* the region and biome views.
|
||||
*/
|
||||
final class PickMeasuring {
|
||||
static final List<InferredType> PLACED_TYPES = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA);
|
||||
private static final int SAMPLES_PER_SIDE = 96;
|
||||
|
||||
private PickMeasuring() {
|
||||
}
|
||||
|
||||
static void forEachSample(MapCensus.ViewArea area, SampleVisitor visitor) {
|
||||
double stepX = (area.maximumX() - area.minimumX()) / SAMPLES_PER_SIDE;
|
||||
double stepZ = (area.maximumZ() - area.minimumZ()) / SAMPLES_PER_SIDE;
|
||||
for (int row = 0; row < SAMPLES_PER_SIDE; row++) {
|
||||
for (int column = 0; column < SAMPLES_PER_SIDE; column++) {
|
||||
visitor.visit(area.minimumX() + (column + 0.5D) * stepX, area.minimumZ() + (row + 0.5D) * stepZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Option regionOption(Engine engine, IrisRegion region) {
|
||||
return new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity());
|
||||
}
|
||||
|
||||
static int regionColor(Engine engine, IrisRegion region) {
|
||||
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* One spread per biome list of the region that has biomes, in land, shore, sea order.
|
||||
*/
|
||||
static List<TypedSpread> biomeSpreads(Engine engine, IrisComplex complex, IrisRegion region) {
|
||||
List<TypedSpread> spreads = new ArrayList<>();
|
||||
for (InferredType type : PLACED_TYPES) {
|
||||
List<Option> options = complex.noisePickedBiomes(region, type).stream()
|
||||
.map(biome -> biomeOption(engine, biome))
|
||||
.toList();
|
||||
if (!options.isEmpty()) {
|
||||
spreads.add(new TypedSpread(type,
|
||||
NoisePickSpread.of(typeName(type) + " biome noise in view", options, complex.isLegacyRarity())));
|
||||
}
|
||||
}
|
||||
return spreads;
|
||||
}
|
||||
|
||||
static List<BiomeRange> biomeRanges(Engine engine, IrisRegion region) {
|
||||
List<BiomeRange> ranges = new ArrayList<>();
|
||||
for (InferredType type : PLACED_TYPES) {
|
||||
for (IrisBiome biome : region.getBiomes(engine.getComplex(), type)) {
|
||||
if (biome == null) {
|
||||
continue;
|
||||
}
|
||||
ranges.add(new BiomeRange(biome.getLoadKey(), biome.getName(),
|
||||
biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF, type,
|
||||
biome.getPlacement().getMinHeight(), biome.getPlacement().getMaxHeight()));
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
static void addSpreadNotes(List<String> notes, NoisePickSpread spread, String noun) {
|
||||
List<NoisePickSpread.Row> rows = spread.rows();
|
||||
if (spread.sampleCount() == 0) {
|
||||
notes.add("No " + noun + " is picked in view.");
|
||||
return;
|
||||
}
|
||||
if (rows.size() == 1) {
|
||||
notes.add(rows.getFirst().option().name() + " is the only " + noun + ", so the " + noun + " noise has nothing to pick.");
|
||||
return;
|
||||
}
|
||||
if (spread.belowZeroShare() > 0D) {
|
||||
notes.add(NoisePickChart.percent(spread.belowZeroShare()) + " of the " + noun + " noise in view is below 0, so "
|
||||
+ rows.getFirst().option().name() + " gets it.");
|
||||
}
|
||||
if (spread.aboveOneShare() > 0D) {
|
||||
notes.add(NoisePickChart.percent(spread.aboveOneShare()) + " of the " + noun + " noise in view is above 1, so "
|
||||
+ rows.getLast().option().name() + " gets it.");
|
||||
}
|
||||
List<String> starved = rows.stream().filter(NoisePickSpread.Row::isFarBelowWeight).map(row -> row.option().name()).toList();
|
||||
if (!starved.isEmpty()) {
|
||||
boolean single = starved.size() == 1;
|
||||
notes.add(String.join(", ", starved) + (single ? " gets" : " get") + " under half " + (single ? "its" : "their")
|
||||
+ " weight in view (▼): the " + noun + " noise rarely reaches " + (single ? "its band." : "their bands."));
|
||||
}
|
||||
}
|
||||
|
||||
static void addBiomeSpreadNotes(List<String> notes, List<TypedSpread> spreads) {
|
||||
for (TypedSpread typed : spreads) {
|
||||
addSpreadNotes(notes, typed.spread(), typeName(typed.type()).toLowerCase(Locale.ROOT) + " biome");
|
||||
}
|
||||
}
|
||||
|
||||
static void addChildBiomeNote(List<String> notes, IrisComplex complex, IrisRegion region) {
|
||||
String parents = PLACED_TYPES.stream()
|
||||
.flatMap(type -> complex.noisePickedBiomes(region, type).stream())
|
||||
.filter(biome -> !biome.getChildren().isEmpty())
|
||||
.map(IrisBiome::getName)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(", "));
|
||||
if (!parents.isEmpty()) {
|
||||
notes.add(parents + " then hand some spots to child biomes by a noise of their own.");
|
||||
}
|
||||
}
|
||||
|
||||
static void addRarityNotes(List<String> notes, IrisComplex complex, Collection<NoisePickSpread> spreads) {
|
||||
if (spreads.stream().allMatch(spread -> spread.bands().size() <= 1)) {
|
||||
return;
|
||||
}
|
||||
notes.add(complex.isLegacyRarity()
|
||||
? "Legacy rarity: each weight is the total rarity minus the option's own."
|
||||
: "Each weight is one over the rarity, so rarity 2 gets half the noise of rarity 1.");
|
||||
}
|
||||
|
||||
static String measuredNote() {
|
||||
return "Measured on a " + SAMPLES_PER_SIDE + " × " + SAMPLES_PER_SIDE + " grid over the map in view.";
|
||||
}
|
||||
|
||||
static String hybridNoiseNote() {
|
||||
return "Noise shows the height biomes are picked at: the overlay noise plus the dimension's biome placement "
|
||||
+ "jitter. Where ranges share a height, each biome's competition noise, divided by its rarity, decides.";
|
||||
}
|
||||
|
||||
static String typeName(InferredType type) {
|
||||
String name = type.name().toLowerCase(Locale.ROOT);
|
||||
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
}
|
||||
|
||||
private static Option biomeOption(Engine engine, IrisBiome biome) {
|
||||
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
|
||||
biome.getRarity());
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SampleVisitor {
|
||||
void visit(double x, double z);
|
||||
}
|
||||
|
||||
/**
|
||||
* The spread of the noise that picks from one of a region's biome lists.
|
||||
*/
|
||||
record TypedSpread(InferredType type, NoisePickSpread spread) {
|
||||
}
|
||||
}
|
||||
@@ -18,50 +18,30 @@
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.gui.NoisePickSpread.Option;
|
||||
import com.volmit.iris.core.gui.RegionHeightCoverage.BiomeRange;
|
||||
import com.volmit.iris.core.gui.PickMeasuring.TypedSpread;
|
||||
import com.volmit.iris.core.gui.RegionNavigator.RegionRow;
|
||||
import com.volmit.iris.core.gui.components.PickDisplay;
|
||||
import com.volmit.iris.core.gui.components.RenderType;
|
||||
import com.volmit.iris.engine.IrisComplex;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.InferredType;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisRegion;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The region view's explorer: shows how the region noise picks the regions, and for a chosen region fills the map with
|
||||
* its biomes and shows how they are picked. Measuring what is in view runs off the Swing thread and only the newest
|
||||
* request counts.
|
||||
* its biomes and shows how they are picked, measured against what is in view.
|
||||
*/
|
||||
final class RegionExplorer {
|
||||
private static final int SAMPLES_PER_SIDE = 96;
|
||||
private static final List<InferredType> PLACED_TYPES = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA);
|
||||
|
||||
private final Supplier<Engine> engineSource;
|
||||
private final Consumer<String> onPreviewChanged;
|
||||
private final RegionNavigator navigator = new RegionNavigator(this::choose);
|
||||
private final RegionInspectorPanel inspector;
|
||||
private final ThreadPoolExecutor measurer = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(1), RegionExplorer::daemonThread, new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||
private final AtomicLong measurementGeneration = new AtomicLong();
|
||||
private final PickInspectorPanel inspector;
|
||||
private final LatestMeasurement<Measurement> measurer = new LatestMeasurement<>("Iris Region Explorer");
|
||||
private String chosenRegionKey;
|
||||
private MapCensus.ViewArea latestArea;
|
||||
private MapCensus.ViewArea measuredArea;
|
||||
@@ -73,14 +53,14 @@ final class RegionExplorer {
|
||||
RegionExplorer(Supplier<Engine> engineSource, Consumer<String> onPreviewChanged, Consumer<PickDisplay> onDisplayChosen) {
|
||||
this.engineSource = engineSource;
|
||||
this.onPreviewChanged = onPreviewChanged;
|
||||
this.inspector = new RegionInspectorPanel(onDisplayChosen);
|
||||
this.inspector = new PickInspectorPanel("Regions", onDisplayChosen);
|
||||
}
|
||||
|
||||
RegionNavigator navigator() {
|
||||
return navigator;
|
||||
}
|
||||
|
||||
RegionInspectorPanel inspector() {
|
||||
PickInspectorPanel inspector() {
|
||||
return inspector;
|
||||
}
|
||||
|
||||
@@ -90,8 +70,8 @@ final class RegionExplorer {
|
||||
return;
|
||||
}
|
||||
List<RegionRow> rows = new ArrayList<>();
|
||||
for (IrisRegion region : engine.getData().getRegionLoader().loadAll(engine.getDimension().getRegions())) {
|
||||
rows.add(new RegionRow(region.getLoadKey(), region.getName(), regionColor(engine, region)));
|
||||
for (IrisRegion region : engine.getComplex().noisePickedRegions()) {
|
||||
rows.add(new RegionRow(region.getLoadKey(), region.getName(), PickMeasuring.regionColor(engine, region)));
|
||||
}
|
||||
navigator.showRegions(rows);
|
||||
}
|
||||
@@ -106,15 +86,7 @@ final class RegionExplorer {
|
||||
}
|
||||
measuredArea = area;
|
||||
String regionKey = chosenRegionKey;
|
||||
long generation = measurementGeneration.incrementAndGet();
|
||||
measurer.execute(() -> {
|
||||
Measurement measurement = measureSafely(regionKey, area);
|
||||
EventQueue.invokeLater(() -> {
|
||||
if (generation == measurementGeneration.get() && measurement != null) {
|
||||
show(measurement);
|
||||
}
|
||||
});
|
||||
});
|
||||
measurer.measure(() -> measurePicks(regionKey, area), this::show);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,13 +99,13 @@ final class RegionExplorer {
|
||||
}
|
||||
|
||||
void close() {
|
||||
measurer.shutdownNow();
|
||||
measurer.close();
|
||||
}
|
||||
|
||||
private void choose(String regionKey) {
|
||||
chosenRegionKey = regionKey;
|
||||
measuredArea = null;
|
||||
measurementGeneration.incrementAndGet();
|
||||
measurer.discardPending();
|
||||
onPreviewChanged.accept(regionKey);
|
||||
inspector.showMeasuring(regionKey == null ? "Regions" : regionName(regionKey));
|
||||
measure(latestArea);
|
||||
@@ -156,15 +128,6 @@ final class RegionExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
private Measurement measureSafely(String regionKey, MapCensus.ViewArea area) {
|
||||
try {
|
||||
return measurePicks(regionKey, area);
|
||||
} catch (Throwable error) {
|
||||
Iris.reportError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Measurement measurePicks(String regionKey, MapCensus.ViewArea area) {
|
||||
Engine engine = engineSource.get();
|
||||
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||
@@ -184,155 +147,47 @@ final class RegionExplorer {
|
||||
}
|
||||
|
||||
private static RegionPicks measureRegionPicks(Engine engine, IrisComplex complex, MapCensus.ViewArea area) {
|
||||
List<Option> options = complex.noisePickedRegions().stream()
|
||||
.map(region -> new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity()))
|
||||
List<NoisePickSpread.Option> options = complex.noisePickedRegions().stream()
|
||||
.map(region -> PickMeasuring.regionOption(engine, region))
|
||||
.toList();
|
||||
NoisePickSpread spread = NoisePickSpread.of("Region noise in view", options, complex.isLegacyRarity());
|
||||
forEachSample(area, (x, z) -> spread.add(complex.getRegionStyleStream().get(x, z)));
|
||||
PickMeasuring.forEachSample(area, (x, z) -> spread.add(complex.getRegionStyleStream().get(x, z)));
|
||||
List<String> notes = new ArrayList<>();
|
||||
addSpreadNotes(notes, spread, "region");
|
||||
addRarityNotes(notes, complex, List.of(spread));
|
||||
PickMeasuring.addSpreadNotes(notes, spread, "region");
|
||||
PickMeasuring.addRarityNotes(notes, complex, List.of(spread));
|
||||
notes.add(PickMeasuring.measuredNote());
|
||||
return new RegionPicks(spread, notes);
|
||||
}
|
||||
|
||||
// The preview treats every spot as part of the region, so each spot's biome list is the one its surface needs.
|
||||
private static BiomePicks measureBiomePicks(Engine engine, IrisComplex complex, IrisRegion region, MapCensus.ViewArea area) {
|
||||
Map<InferredType, NoisePickSpread> spreads = new LinkedHashMap<>();
|
||||
for (InferredType type : PLACED_TYPES) {
|
||||
List<Option> options = complex.noisePickedBiomes(region, type).stream()
|
||||
.map(biome -> biomeOption(engine, biome))
|
||||
.toList();
|
||||
if (!options.isEmpty()) {
|
||||
spreads.put(type, NoisePickSpread.of(typeName(type) + " biome noise in view", options, complex.isLegacyRarity()));
|
||||
}
|
||||
}
|
||||
forEachSample(area, (x, z) -> {
|
||||
IrisComplex.BiomePick pick = complex.surfacePickAsIfIn(region, x, z);
|
||||
NoisePickSpread spread = spreads.get(pick.type());
|
||||
if (spread != null) {
|
||||
spread.add(complex.biomePickNoise(region, pick.type(), x, z));
|
||||
}
|
||||
List<TypedSpread> spreads = PickMeasuring.biomeSpreads(engine, complex, region);
|
||||
PickMeasuring.forEachSample(area, (x, z) -> {
|
||||
InferredType type = complex.surfacePickAsIfIn(region, x, z).type();
|
||||
spreads.stream()
|
||||
.filter(typed -> typed.type() == type)
|
||||
.findFirst()
|
||||
.ifPresent(typed -> typed.spread().add(complex.biomePickNoise(region, type, x, z)));
|
||||
});
|
||||
List<String> notes = new ArrayList<>();
|
||||
spreads.forEach((type, spread) -> addSpreadNotes(notes, spread, typeName(type).toLowerCase(Locale.ROOT) + " biome"));
|
||||
addChildBiomeNote(notes, complex, region);
|
||||
addRarityNotes(notes, complex, spreads.values());
|
||||
return new BiomePicks(region.getLoadKey(), region.getName(), List.copyOf(spreads.values()), notes);
|
||||
PickMeasuring.addBiomeSpreadNotes(notes, spreads);
|
||||
PickMeasuring.addChildBiomeNote(notes, complex, region);
|
||||
List<NoisePickSpread> plainSpreads = spreads.stream().map(TypedSpread::spread).toList();
|
||||
PickMeasuring.addRarityNotes(notes, complex, plainSpreads);
|
||||
notes.add(PickMeasuring.measuredNote());
|
||||
return new BiomePicks(region.getLoadKey(), region.getName(), plainSpreads, notes);
|
||||
}
|
||||
|
||||
private static HeightPicks measureHeightPicks(Engine engine, IrisComplex complex, IrisRegion region, MapCensus.ViewArea area) {
|
||||
RegionHeightCoverage coverage = RegionHeightCoverage.measure(biomeRanges(engine, region), pickingHeights(complex, area));
|
||||
List<Integer> heights = new ArrayList<>();
|
||||
PickMeasuring.forEachSample(area, (x, z) -> heights.add((int) Math.round(complex.getPlacementHeightStream().get(x, z))));
|
||||
RegionHeightCoverage coverage = RegionHeightCoverage.measure(PickMeasuring.biomeRanges(engine, region),
|
||||
heights.stream().mapToInt(Integer::intValue).toArray());
|
||||
List<String> notes = new ArrayList<>(coverage.notes());
|
||||
notes.add("Noise shows the height biomes are picked at: the overlay noise plus the dimension's biome placement "
|
||||
+ "jitter. Where ranges share a height, each biome's competition noise, divided by its rarity, decides.");
|
||||
notes.add(PickMeasuring.hybridNoiseNote());
|
||||
return new HeightPicks(region.getLoadKey(), region.getName(), coverage, notes);
|
||||
}
|
||||
|
||||
private static void addSpreadNotes(List<String> notes, NoisePickSpread spread, String noun) {
|
||||
List<NoisePickSpread.Row> rows = spread.rows();
|
||||
if (spread.sampleCount() == 0) {
|
||||
notes.add("No " + noun + " is picked in view.");
|
||||
return;
|
||||
}
|
||||
if (rows.size() == 1) {
|
||||
notes.add(rows.getFirst().option().name() + " is the only " + noun + ", so the " + noun + " noise has nothing to pick.");
|
||||
return;
|
||||
}
|
||||
if (spread.belowZeroShare() > 0D) {
|
||||
notes.add(NoisePickChart.percent(spread.belowZeroShare()) + " of the " + noun + " noise in view is below 0, so "
|
||||
+ rows.getFirst().option().name() + " gets it.");
|
||||
}
|
||||
if (spread.aboveOneShare() > 0D) {
|
||||
notes.add(NoisePickChart.percent(spread.aboveOneShare()) + " of the " + noun + " noise in view is above 1, so "
|
||||
+ rows.getLast().option().name() + " gets it.");
|
||||
}
|
||||
List<String> starved = rows.stream().filter(NoisePickSpread.Row::isFarBelowWeight).map(row -> row.option().name()).toList();
|
||||
if (!starved.isEmpty()) {
|
||||
notes.add(String.join(", ", starved) + (starved.size() == 1 ? " gets" : " get")
|
||||
+ " under half " + (starved.size() == 1 ? "its" : "their") + " weight in view (▼): the "
|
||||
+ noun + " noise rarely reaches " + (starved.size() == 1 ? "its band." : "their bands."));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addChildBiomeNote(List<String> notes, IrisComplex complex, IrisRegion region) {
|
||||
String parents = PLACED_TYPES.stream()
|
||||
.flatMap(type -> complex.noisePickedBiomes(region, type).stream())
|
||||
.filter(biome -> !biome.getChildren().isEmpty())
|
||||
.map(IrisBiome::getName)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(", "));
|
||||
if (!parents.isEmpty()) {
|
||||
notes.add(parents + " then hand some spots to child biomes by a noise of their own.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void addRarityNotes(List<String> notes, IrisComplex complex, Collection<NoisePickSpread> spreads) {
|
||||
if (spreads.stream().allMatch(spread -> spread.bands().size() <= 1)) {
|
||||
return;
|
||||
}
|
||||
notes.add(complex.isLegacyRarity()
|
||||
? "Legacy rarity: each weight is the total rarity minus the option's own."
|
||||
: "Each weight is one over the rarity, so rarity 2 gets half the noise of rarity 1.");
|
||||
notes.add("Measured on a " + SAMPLES_PER_SIDE + " × " + SAMPLES_PER_SIDE + " grid over the map in view.");
|
||||
}
|
||||
|
||||
private static Option biomeOption(Engine engine, IrisBiome biome) {
|
||||
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
|
||||
biome.getRarity());
|
||||
}
|
||||
|
||||
private static int regionColor(Engine engine, IrisRegion region) {
|
||||
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
|
||||
}
|
||||
|
||||
private static String typeName(InferredType type) {
|
||||
String name = type.name().toLowerCase(Locale.ROOT);
|
||||
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
}
|
||||
|
||||
private static List<BiomeRange> biomeRanges(Engine engine, IrisRegion region) {
|
||||
List<BiomeRange> ranges = new ArrayList<>();
|
||||
for (InferredType type : PLACED_TYPES) {
|
||||
for (IrisBiome biome : region.getBiomes(engine.getComplex(), type)) {
|
||||
if (biome == null) {
|
||||
continue;
|
||||
}
|
||||
ranges.add(new BiomeRange(biome.getLoadKey(), biome.getName(),
|
||||
biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF, type,
|
||||
biome.getPlacement().getMinHeight(), biome.getPlacement().getMaxHeight()));
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// HYBRID terrain picks biomes by the overlay noise plus the placement jitter, in blocks above the fluid.
|
||||
private static int[] pickingHeights(IrisComplex complex, MapCensus.ViewArea area) {
|
||||
int[] heights = new int[SAMPLES_PER_SIDE * SAMPLES_PER_SIDE];
|
||||
int[] next = {0};
|
||||
forEachSample(area, (x, z) -> heights[next[0]++] = (int) Math.round(complex.getPlacementHeightStream().get(x, z)));
|
||||
return heights;
|
||||
}
|
||||
|
||||
private static void forEachSample(MapCensus.ViewArea area, SampleVisitor visitor) {
|
||||
double stepX = (area.maximumX() - area.minimumX()) / SAMPLES_PER_SIDE;
|
||||
double stepZ = (area.maximumZ() - area.minimumZ()) / SAMPLES_PER_SIDE;
|
||||
for (int row = 0; row < SAMPLES_PER_SIDE; row++) {
|
||||
for (int column = 0; column < SAMPLES_PER_SIDE; column++) {
|
||||
visitor.visit(area.minimumX() + (column + 0.5D) * stepX, area.minimumZ() + (row + 0.5D) * stepZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Thread daemonThread(Runnable task) {
|
||||
Thread thread = new Thread(task, "Iris Region Explorer");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SampleVisitor {
|
||||
void visit(double x, double z);
|
||||
}
|
||||
|
||||
/**
|
||||
* What was measured for the region view; the region key is null when every region was measured.
|
||||
*/
|
||||
|
||||
@@ -214,17 +214,33 @@ public final class IrisRenderer {
|
||||
ProceduralStream<Double> regionNoise = complex.getRegionStyleStream();
|
||||
return new UnitNoise((gridIndex, x, z) -> regionNoise.get(x, z));
|
||||
}
|
||||
IrisRegion region = engine.getData().getRegionLoader().load(layer.previewRegionKey());
|
||||
if (region == null) {
|
||||
return new UnitNoise((gridIndex, x, z) -> 0D);
|
||||
}
|
||||
if (complex.isHybridTerrain()) {
|
||||
ProceduralStream<Double> placementHeight = complex.getPlacementHeightStream();
|
||||
return new PlacementHeightNoise((gridIndex, x, z) -> placementHeight.get(x, z), fluidHeight(), defaultHeightRange());
|
||||
}
|
||||
if (layer.type() == RenderType.BIOME) {
|
||||
return new UnitNoise(baseBiomePickNoise(complex));
|
||||
}
|
||||
IrisRegion region = engine.getData().getRegionLoader().load(layer.previewRegionKey());
|
||||
if (region == null) {
|
||||
return new UnitNoise((gridIndex, x, z) -> 0D);
|
||||
}
|
||||
return new UnitNoise(biomePickNoise(complex, region, grid));
|
||||
}
|
||||
|
||||
// The biome view shows each spot's base biome, which its own region picks from the biome list of the spot's type.
|
||||
private static NoiseField baseBiomePickNoise(IrisComplex complex) {
|
||||
ProceduralStream<IrisRegion> regions = complex.getRegionStream();
|
||||
ProceduralStream<InferredType> types = complex.getBridgeStream();
|
||||
Map<IrisRegion, Map<InferredType, ProceduralStream<Double>>> noiseByRegion = new IdentityHashMap<>();
|
||||
return (gridIndex, x, z) -> {
|
||||
IrisRegion region = regions.get(x, z);
|
||||
return noiseByRegion.computeIfAbsent(region, subject -> new EnumMap<>(InferredType.class))
|
||||
.computeIfAbsent(types.get(x, z), type -> complex.biomePickNoiseStream(region, type))
|
||||
.get(x, z);
|
||||
};
|
||||
}
|
||||
|
||||
// Finding which biome list picked a spot is slow, so it is sampled in squares like the biomes; the noise is cheap.
|
||||
private static NoiseField biomePickNoise(IrisComplex complex, IrisRegion region, TileGrid grid) {
|
||||
IdentitySampler typeSampler = new IdentitySampler(grid, (x, z) -> complex.surfacePickAsIfIn(region, x, z).type().ordinal());
|
||||
|
||||
@@ -22,10 +22,10 @@ import java.util.Objects;
|
||||
|
||||
/**
|
||||
* What a map tile shows: one of the render types, for a region preview which region every spot is treated as, and in
|
||||
* the region view whether the noise that picks is drawn too.
|
||||
* the region and biome views whether the noise that picks is drawn too.
|
||||
*
|
||||
* @param previewRegionKey the region a {@link RenderType#REGION_PREVIEW} tile shows everywhere, null for every other type
|
||||
* @param display how the picking noise is drawn; only the region view draws anything but the picks
|
||||
* @param display how the picking noise is drawn; only the region and biome views draw anything but the picks
|
||||
*/
|
||||
public record MapLayer(RenderType type, String previewRegionKey, PickDisplay display) {
|
||||
public MapLayer {
|
||||
@@ -34,8 +34,8 @@ public record MapLayer(RenderType type, String previewRegionKey, PickDisplay dis
|
||||
if ((type == RenderType.REGION_PREVIEW) != (previewRegionKey != null)) {
|
||||
throw new IllegalArgumentException("Only a region preview names a region");
|
||||
}
|
||||
if (display != PickDisplay.PICKS && !type.isRegionView()) {
|
||||
throw new IllegalArgumentException("Only the region view draws its picking noise");
|
||||
if (display != PickDisplay.PICKS && !type.drawsPickNoise()) {
|
||||
throw new IllegalArgumentException(type + " has no picking noise to draw");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ public record MapLayer(RenderType type, String previewRegionKey, PickDisplay dis
|
||||
return new MapLayer(type, null, PickDisplay.PICKS);
|
||||
}
|
||||
|
||||
public static MapLayer biomes(PickDisplay display) {
|
||||
return new MapLayer(RenderType.BIOME, null, display);
|
||||
}
|
||||
|
||||
public static MapLayer regions(PickDisplay display) {
|
||||
return new MapLayer(RenderType.REGION, null, display);
|
||||
}
|
||||
|
||||
@@ -24,4 +24,11 @@ public enum RenderType {
|
||||
public boolean isRegionView() {
|
||||
return this == REGION || this == REGION_PREVIEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the view can draw the noise that picks what it shows: the regions, or the surface biomes.
|
||||
*/
|
||||
public boolean drawsPickNoise() {
|
||||
return isRegionView() || this == BIOME;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user