mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Explore region biomes by height
Preview, height chart; hybrid coastlines.
This commit is contained in:
1 parent
ea42141550
commit
29942fa1c2
18 files changed
+1491
-90
No files matched your search
@@ -21,6 +21,7 @@ package com.volmit.iris.core.gui;
|
|||||||
import com.volmit.iris.Iris;
|
import com.volmit.iris.Iris;
|
||||||
import com.volmit.iris.core.gui.components.IrisRenderer;
|
import com.volmit.iris.core.gui.components.IrisRenderer;
|
||||||
import com.volmit.iris.core.gui.components.MapLabels;
|
import com.volmit.iris.core.gui.components.MapLabels;
|
||||||
|
import com.volmit.iris.core.gui.components.MapLayer;
|
||||||
import com.volmit.iris.core.gui.components.MapStyle;
|
import com.volmit.iris.core.gui.components.MapStyle;
|
||||||
import com.volmit.iris.core.gui.components.RenderType;
|
import com.volmit.iris.core.gui.components.RenderType;
|
||||||
import com.volmit.iris.engine.framework.Engine;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
@@ -95,6 +96,7 @@ final class MapCanvas extends JPanel {
|
|||||||
private boolean tileLayerStale = true;
|
private boolean tileLayerStale = true;
|
||||||
private MapStyle style = MapStyle.PLAIN;
|
private MapStyle style = MapStyle.PLAIN;
|
||||||
private RenderType currentType = RenderType.BIOME;
|
private RenderType currentType = RenderType.BIOME;
|
||||||
|
private String previewRegionKey;
|
||||||
private String viewLabel = MapView.BIOMES.label();
|
private String viewLabel = MapView.BIOMES.label();
|
||||||
private List<GuiMarker> players = List.of();
|
private List<GuiMarker> players = List.of();
|
||||||
private List<GuiMarker> entities = List.of();
|
private List<GuiMarker> entities = List.of();
|
||||||
@@ -197,16 +199,41 @@ final class MapCanvas extends JPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
viewLabel = view.label();
|
viewLabel = view.label();
|
||||||
if (view.renderType() == currentType) {
|
RenderType nextType = view == MapView.REGIONS && previewRegionKey != null ? RenderType.REGION_PREVIEW : view.renderType();
|
||||||
|
if (nextType == currentType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentType = view.renderType();
|
currentType = nextType;
|
||||||
if (currentType != RenderType.HEIGHT) {
|
if (currentType != RenderType.HEIGHT) {
|
||||||
style = style.withHeightRange(null);
|
style = style.withHeightRange(null);
|
||||||
}
|
}
|
||||||
requestRender();
|
requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the given region's biomes everywhere in the region view, as if every spot belonged to it; null shows the
|
||||||
|
* regions themselves again.
|
||||||
|
*/
|
||||||
|
void previewRegion(String regionKey) {
|
||||||
|
if (Objects.equals(regionKey, previewRegionKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previewRegionKey = regionKey;
|
||||||
|
if (currentType == RenderType.REGION || currentType == RenderType.REGION_PREVIEW) {
|
||||||
|
currentType = regionKey == null ? RenderType.REGION : RenderType.REGION_PREVIEW;
|
||||||
|
requestRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MapCensus.ViewArea viewArea() {
|
||||||
|
return new MapCensus.ViewArea(screenToWorldX(0D), screenToWorldZ(0D), screenToWorldX(getWidth()),
|
||||||
|
screenToWorldZ(getHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private MapLayer currentLayer() {
|
||||||
|
return currentType == RenderType.REGION_PREVIEW ? MapLayer.regionPreview(previewRegionKey) : MapLayer.of(currentType);
|
||||||
|
}
|
||||||
|
|
||||||
String legendTitle() {
|
String legendTitle() {
|
||||||
return currentType == RenderType.REGION ? "Regions in view" : "Biomes in view";
|
return currentType == RenderType.REGION ? "Regions in view" : "Biomes in view";
|
||||||
}
|
}
|
||||||
@@ -233,6 +260,7 @@ final class MapCanvas extends JPanel {
|
|||||||
requestRender();
|
requestRender();
|
||||||
notifyUser("Refreshing");
|
notifyUser("Refreshing");
|
||||||
listener.onTogglesChanged();
|
listener.onTogglesChanged();
|
||||||
|
listener.onContentRefreshed();
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleShading() {
|
void toggleShading() {
|
||||||
@@ -922,8 +950,7 @@ final class MapCanvas extends JPanel {
|
|||||||
}
|
}
|
||||||
IrisRenderer legendRenderer = renderer;
|
IrisRenderer legendRenderer = renderer;
|
||||||
RenderType type = currentType;
|
RenderType type = currentType;
|
||||||
MapCensus.ViewArea area = new MapCensus.ViewArea(screenToWorldX(0D), screenToWorldZ(0D),
|
MapCensus.ViewArea area = viewArea();
|
||||||
screenToWorldX(getWidth()), screenToWorldZ(getHeight()));
|
|
||||||
List<MapCensus.Share> shares = MapCensus.count(renderFrame, area, label -> legendRenderer.legendColor(label, type));
|
List<MapCensus.Share> shares = MapCensus.count(renderFrame, area, label -> legendRenderer.legendColor(label, type));
|
||||||
listener.onLegendChanged(legendTitle(), shares, style.highlight());
|
listener.onLegendChanged(legendTitle(), shares, style.highlight());
|
||||||
if (type == RenderType.HEIGHT) {
|
if (type == RenderType.HEIGHT) {
|
||||||
@@ -952,7 +979,7 @@ final class MapCanvas extends JPanel {
|
|||||||
clearHover();
|
clearHover();
|
||||||
VisionRenderController.RenderSpec spec = new VisionRenderController.RenderSpec(
|
VisionRenderController.RenderSpec spec = new VisionRenderController.RenderSpec(
|
||||||
renderer,
|
renderer,
|
||||||
currentType,
|
currentLayer(),
|
||||||
contentRevision,
|
contentRevision,
|
||||||
centerX,
|
centerX,
|
||||||
centerZ,
|
centerZ,
|
||||||
@@ -1052,7 +1079,7 @@ final class MapCanvas extends JPanel {
|
|||||||
double worldZ = screenToWorldZ(point.y);
|
double worldZ = screenToWorldZ(point.y);
|
||||||
controller.submitProbe(
|
controller.submitProbe(
|
||||||
frame,
|
frame,
|
||||||
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ),
|
() -> MapHoverProbe.sample(probeEngine, worldX, worldZ, previewedRegion()),
|
||||||
details -> {
|
details -> {
|
||||||
QuickHover quick = quickHover;
|
QuickHover quick = quickHover;
|
||||||
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
|
if (quick != null && quick.worldX() == details.worldX() && quick.worldZ() == details.worldZ()) {
|
||||||
@@ -1063,6 +1090,10 @@ final class MapCanvas extends JPanel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String previewedRegion() {
|
||||||
|
return currentType == RenderType.REGION_PREVIEW ? previewRegionKey : null;
|
||||||
|
}
|
||||||
|
|
||||||
// The drawn tiles already know the biome and region under the cursor, so the card follows the mouse without waiting.
|
// The drawn tiles already know the biome and region under the cursor, so the card follows the mouse without waiting.
|
||||||
private void showQuickHover() {
|
private void showQuickHover() {
|
||||||
Point point = cursor;
|
Point point = cursor;
|
||||||
@@ -1161,10 +1192,10 @@ final class MapCanvas extends JPanel {
|
|||||||
}
|
}
|
||||||
double worldX = screenToWorldX(point.x);
|
double worldX = screenToWorldX(point.x);
|
||||||
double worldZ = screenToWorldZ(point.y);
|
double worldZ = screenToWorldZ(point.y);
|
||||||
RenderType type = currentType;
|
MapLayer layer = currentLayer();
|
||||||
controller.submitProbe(
|
controller.submitProbe(
|
||||||
renderFrame,
|
renderFrame,
|
||||||
() -> activeOverlay.openInEditor(worldX, worldZ, type),
|
() -> activeOverlay.openInEditor(worldX, worldZ, layer),
|
||||||
opened -> opened.ifPresent(fileName -> notifyUser("Opened " + fileName))
|
opened -> opened.ifPresent(fileName -> notifyUser("Opened " + fileName))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1187,6 +1218,8 @@ final class MapCanvas extends JPanel {
|
|||||||
interface Listener {
|
interface Listener {
|
||||||
void onTogglesChanged();
|
void onTogglesChanged();
|
||||||
|
|
||||||
|
void onContentRefreshed();
|
||||||
|
|
||||||
void onShowPalettesAt(double worldX, double worldZ);
|
void onShowPalettesAt(double worldX, double worldZ);
|
||||||
|
|
||||||
void onLegendChanged(String title, List<MapCensus.Share> shares, MapStyle.Highlight highlight);
|
void onLegendChanged(String title, List<MapCensus.Share> shares, MapStyle.Highlight highlight);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.volmit.iris.core.gui;
|
|||||||
import com.volmit.iris.core.gui.components.MapLabels;
|
import com.volmit.iris.core.gui.components.MapLabels;
|
||||||
import com.volmit.iris.core.gui.components.RenderType;
|
import com.volmit.iris.core.gui.components.RenderType;
|
||||||
import com.volmit.iris.core.loader.IrisRegistrant;
|
import com.volmit.iris.core.loader.IrisRegistrant;
|
||||||
|
import com.volmit.iris.engine.HybridBiomeSelector;
|
||||||
import com.volmit.iris.engine.object.IrisBiome;
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -91,6 +92,26 @@ final class MapHoverCard {
|
|||||||
if (details.vanillaBiome() != null) {
|
if (details.vanillaBiome() != null) {
|
||||||
lines.add("Minecraft biome " + details.vanillaBiome().replace('_', ' '));
|
lines.add("Minecraft biome " + details.vanillaBiome().replace('_', ' '));
|
||||||
}
|
}
|
||||||
|
if (details.placement() != null) {
|
||||||
|
lines.add(placementText(details.placement()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// How hybrid terrain chose the base biome; a child biome or a coast can still replace it afterwards.
|
||||||
|
private static String placementText(HybridBiomeSelector.Choice choice) {
|
||||||
|
String height = "Picked at " + heightAboveSea(choice.placementHeight()) + " · ";
|
||||||
|
if (choice.fallback()) {
|
||||||
|
return height + "no range covers it, closest range wins";
|
||||||
|
}
|
||||||
|
if (choice.rivalCount() == 0) {
|
||||||
|
return height + "the only biome in range";
|
||||||
|
}
|
||||||
|
return height + "beat " + choice.rivalCount() + (choice.rivalCount() == 1 ? " other biome" : " other biomes") + " in range";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String heightAboveSea(double height) {
|
||||||
|
long rounded = Math.round(height);
|
||||||
|
return rounded < 0 ? -rounded + " below sea" : rounded + " above sea";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addSource(List<String> lines, MapLabels.Label label) {
|
private static void addSource(List<String> lines, MapLabels.Label label) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.engine.HybridBiomeSelector;
|
||||||
import com.volmit.iris.engine.IrisComplex;
|
import com.volmit.iris.engine.IrisComplex;
|
||||||
import com.volmit.iris.engine.framework.Engine;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
import com.volmit.iris.engine.object.InferredType;
|
import com.volmit.iris.engine.object.InferredType;
|
||||||
@@ -38,11 +39,17 @@ final class MapHoverProbe {
|
|||||||
private MapHoverProbe() {
|
private MapHoverProbe() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Details sample(Engine engine, double worldX, double worldZ) {
|
/**
|
||||||
|
* @param previewRegionKey the region the region preview treats every spot as part of, or null for the real world
|
||||||
|
*/
|
||||||
|
static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey) {
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
IrisBiome surfaceBiome = complex.getBaseBiomeStream().get(worldX, worldZ);
|
IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey);
|
||||||
|
IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ);
|
||||||
|
IrisBiome surfaceBiome = previewRegion != null
|
||||||
|
? complex.surfaceBiomeAsIfIn(previewRegion, worldX, worldZ)
|
||||||
|
: complex.getBaseBiomeStream().get(worldX, worldZ);
|
||||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(worldX, worldZ);
|
IrisBiome caveBiome = complex.getCaveBiomeStream().get(worldX, worldZ);
|
||||||
IrisRegion region = complex.getRegionStream().get(worldX, worldZ);
|
|
||||||
InferredType terrainKind = complex.getBridgeStream().get(worldX, worldZ);
|
InferredType terrainKind = complex.getBridgeStream().get(worldX, worldZ);
|
||||||
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
|
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
|
||||||
double height = complex.getHeightStream().getDouble(worldX, worldZ);
|
double height = complex.getHeightStream().getDouble(worldX, worldZ);
|
||||||
@@ -58,7 +65,8 @@ final class MapHoverProbe {
|
|||||||
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
|
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
|
||||||
height,
|
height,
|
||||||
dimension == null ? 0D : dimension.getFluidHeight(),
|
dimension == null ? 0D : dimension.getFluidHeight(),
|
||||||
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS))
|
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
|
||||||
|
complex.explainHybridChoice(region, worldX, worldZ)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +92,8 @@ final class MapHoverProbe {
|
|||||||
String vanillaBiome,
|
String vanillaBiome,
|
||||||
double height,
|
double height,
|
||||||
double fluidHeight,
|
double fluidHeight,
|
||||||
double slopeDegrees
|
double slopeDegrees,
|
||||||
|
HybridBiomeSelector.Choice placement
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,10 @@ import java.awt.event.KeyEvent;
|
|||||||
*/
|
*/
|
||||||
enum MapView {
|
enum MapView {
|
||||||
BIOMES("Biomes", "The biome at each spot, in the biome's colour", Group.WORLD, RenderType.BIOME, KeyEvent.VK_1),
|
BIOMES("Biomes", "The biome at each spot, in the biome's colour", Group.WORLD, RenderType.BIOME, KeyEvent.VK_1),
|
||||||
REGIONS("Regions", "The region each spot belongs to", Group.WORLD, RenderType.REGION, KeyEvent.VK_2),
|
REGIONS("Regions", "The region each spot belongs to; pick one to see which biome covers which height",
|
||||||
|
Group.WORLD, RenderType.REGION, KeyEvent.VK_2),
|
||||||
HEIGHT("Height", "Terrain height, from deep water to snowy peaks", Group.WORLD, RenderType.HEIGHT, KeyEvent.VK_3),
|
HEIGHT("Height", "Terrain height, from deep water to snowy peaks", Group.WORLD, RenderType.HEIGHT, KeyEvent.VK_3),
|
||||||
CONTINENTS("Continents", "Land, coast or sea, judged by each biome's first height generator",
|
CONTINENTS("Continents", "Land, coast or sea: where hybrid terrain ends up, or each biome's first height generator otherwise",
|
||||||
Group.WORLD, RenderType.CONTINENT, KeyEvent.VK_4),
|
Group.WORLD, RenderType.CONTINENT, KeyEvent.VK_4),
|
||||||
LAND_BIOMES("Land biomes", "The land biome each spot would get, also where the sea is",
|
LAND_BIOMES("Land biomes", "The land biome each spot would get, also where the sea is",
|
||||||
Group.BIOME_STREAMS, RenderType.BIOME_LAND, KeyEvent.VK_5),
|
Group.BIOME_STREAMS, RenderType.BIOME_LAND, KeyEvent.VK_5),
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
private final JPanel inspectorSlot = new JPanel(new BorderLayout());
|
private final JPanel inspectorSlot = new JPanel(new BorderLayout());
|
||||||
private final JComponent viewListScroll;
|
private final JComponent viewListScroll;
|
||||||
private final JSplitPane explorerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
private final JSplitPane explorerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||||
|
private final JSplitPane regionInspectorSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||||
private final List<JComponent> mapControls = new ArrayList<>();
|
private final List<JComponent> mapControls = new ArrayList<>();
|
||||||
private final List<JComponent> explorerControls = new ArrayList<>();
|
private final List<JComponent> explorerControls = new ArrayList<>();
|
||||||
private final List<JComponent> noiseControls = new ArrayList<>();
|
private final List<JComponent> noiseControls = new ArrayList<>();
|
||||||
@@ -90,6 +91,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
private final JToggleButton shadingToggle = StudioToolbar.toggle("Shading", "Shade hills and valleys on every view (S)");
|
private final JToggleButton shadingToggle = StudioToolbar.toggle("Shading", "Shade hills and valleys on every view (S)");
|
||||||
private final JToggleButton bordersToggle = StudioToolbar.toggle("Borders", "Outline the regions on every view (B)");
|
private final JToggleButton bordersToggle = StudioToolbar.toggle("Borders", "Outline the regions on every view (B)");
|
||||||
private final MapLegendPanel legendPanel;
|
private final MapLegendPanel legendPanel;
|
||||||
|
private final RegionExplorer regionExplorer;
|
||||||
|
|
||||||
private NoiseExplorerGUI paletteExplorer;
|
private NoiseExplorerGUI paletteExplorer;
|
||||||
private final Map<MapView, ObjectExplorer> objectExplorers = new EnumMap<>(MapView.class);
|
private final Map<MapView, ObjectExplorer> objectExplorers = new EnumMap<>(MapView.class);
|
||||||
@@ -106,6 +108,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
this.viewList = new MapViewList(this::showView);
|
this.viewList = new MapViewList(this::showView);
|
||||||
this.viewListScroll = StudioToolbar.scroll(viewList);
|
this.viewListScroll = StudioToolbar.scroll(viewList);
|
||||||
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
|
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
|
||||||
|
this.regionExplorer = new RegionExplorer(canvas::engine, canvas::previewRegion);
|
||||||
canvas.centerOn(start.centerX(), start.centerZ());
|
canvas.centerOn(start.centerX(), start.centerZ());
|
||||||
buildFrame();
|
buildFrame();
|
||||||
}
|
}
|
||||||
@@ -132,6 +135,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
|
|
||||||
private void buildFrame() {
|
private void buildFrame() {
|
||||||
styleExplorerSplit();
|
styleExplorerSplit();
|
||||||
|
styleRegionInspectorSplit();
|
||||||
content.add(canvas, MAP_CARD);
|
content.add(canvas, MAP_CARD);
|
||||||
statusBars.add(canvas.statusBar(), MAP_CARD);
|
statusBars.add(canvas.statusBar(), MAP_CARD);
|
||||||
sidebar.add(viewListScroll, BorderLayout.CENTER);
|
sidebar.add(viewListScroll, BorderLayout.CENTER);
|
||||||
@@ -221,13 +225,38 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
canvas.showView(view);
|
canvas.showView(view);
|
||||||
canvas.setActive(true);
|
canvas.setActive(true);
|
||||||
showControls(mapControls);
|
showControls(mapControls);
|
||||||
useSidebar(viewListScroll);
|
|
||||||
inspectorSlot.removeAll();
|
inspectorSlot.removeAll();
|
||||||
inspectorSlot.add(legendPanel, BorderLayout.CENTER);
|
if (view == MapView.REGIONS) {
|
||||||
|
showRegionExplorer(previousView);
|
||||||
|
} else {
|
||||||
|
useSidebar(viewListScroll);
|
||||||
|
inspectorSlot.add(legendPanel, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
showCard(MAP_CARD);
|
showCard(MAP_CARD);
|
||||||
canvas.requestFocusInWindow();
|
canvas.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The region inspector sits above the legend, which then lists the chosen region's biomes on the map.
|
||||||
|
private void showRegionExplorer(MapView previousView) {
|
||||||
|
if (previousView != MapView.REGIONS) {
|
||||||
|
regionExplorer.loadRegions();
|
||||||
|
}
|
||||||
|
useSidebar(viewListAbove(regionExplorer.navigator()));
|
||||||
|
regionInspectorSplit.setTopComponent(regionExplorer.inspector());
|
||||||
|
regionInspectorSplit.setBottomComponent(legendPanel);
|
||||||
|
inspectorSlot.add(regionInspectorSplit, BorderLayout.CENTER);
|
||||||
|
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 showPalettes(MapView previousView) {
|
private void showPalettes(MapView previousView) {
|
||||||
boolean firstOpen = paletteExplorer == null;
|
boolean firstOpen = paletteExplorer == null;
|
||||||
if (firstOpen) {
|
if (firstOpen) {
|
||||||
@@ -389,6 +418,14 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
@Override
|
@Override
|
||||||
public void onLegendChanged(String title, List<MapCensus.Share> shares, MapStyle.Highlight highlight) {
|
public void onLegendChanged(String title, List<MapCensus.Share> shares, MapStyle.Highlight highlight) {
|
||||||
legendPanel.show(title, shares, highlight);
|
legendPanel.show(title, shares, highlight);
|
||||||
|
if (currentView == MapView.REGIONS) {
|
||||||
|
regionExplorer.measure(canvas.viewArea());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContentRefreshed() {
|
||||||
|
regionExplorer.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetExplorerView() {
|
private void resetExplorerView() {
|
||||||
@@ -486,6 +523,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
|
|
||||||
private void close() {
|
private void close() {
|
||||||
canvas.close();
|
canvas.close();
|
||||||
|
regionExplorer.close();
|
||||||
if (paletteExplorer != null) {
|
if (paletteExplorer != null) {
|
||||||
paletteExplorer.close();
|
paletteExplorer.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui.RegionHeightCoverage.BiomeRange;
|
||||||
|
import com.volmit.iris.core.gui.RegionNavigator.RegionRow;
|
||||||
|
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.List;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The region view's explorer: choosing a region fills the map with its biomes and measures which of them covers which
|
||||||
|
* ground height in view. Measuring runs off the Swing thread and only the newest request counts.
|
||||||
|
*/
|
||||||
|
final class RegionExplorer {
|
||||||
|
private static final int GROUND_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 = new RegionInspectorPanel();
|
||||||
|
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 String chosenRegionKey;
|
||||||
|
private MapCensus.ViewArea latestArea;
|
||||||
|
private MapCensus.ViewArea measuredArea;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onPreviewChanged receives the region to fill the map with, or null to show every region again
|
||||||
|
*/
|
||||||
|
RegionExplorer(Supplier<Engine> engineSource, Consumer<String> onPreviewChanged) {
|
||||||
|
this.engineSource = engineSource;
|
||||||
|
this.onPreviewChanged = onPreviewChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionNavigator navigator() {
|
||||||
|
return navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionInspectorPanel inspector() {
|
||||||
|
return inspector;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadRegions() {
|
||||||
|
Engine engine = engineSource.get();
|
||||||
|
if (engine == null || engine.getComplex() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<RegionRow> rows = new ArrayList<>();
|
||||||
|
for (IrisRegion region : engine.getData().getRegionLoader().loadAll(engine.getDimension().getRegions())) {
|
||||||
|
rows.add(new RegionRow(region.getLoadKey(), region.getName(),
|
||||||
|
region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF));
|
||||||
|
}
|
||||||
|
navigator.showRegions(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the chosen region against the ground in this area, unless that was already done.
|
||||||
|
*/
|
||||||
|
void measure(MapCensus.ViewArea area) {
|
||||||
|
latestArea = area;
|
||||||
|
if (chosenRegionKey == null || area == null || area.equals(measuredArea)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pack changed, so the regions and their biomes are read again.
|
||||||
|
*/
|
||||||
|
void refresh() {
|
||||||
|
loadRegions();
|
||||||
|
measuredArea = null;
|
||||||
|
measure(latestArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
measurer.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void choose(String regionKey) {
|
||||||
|
chosenRegionKey = regionKey;
|
||||||
|
measuredArea = null;
|
||||||
|
measurementGeneration.incrementAndGet();
|
||||||
|
onPreviewChanged.accept(regionKey);
|
||||||
|
if (regionKey == null) {
|
||||||
|
inspector.showNothingChosen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inspector.showMeasuring(regionName(regionKey));
|
||||||
|
measure(latestArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String regionName(String regionKey) {
|
||||||
|
Engine engine = engineSource.get();
|
||||||
|
IrisRegion region = engine == null ? null : engine.getData().getRegionLoader().load(regionKey);
|
||||||
|
return region == null ? regionKey : region.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void show(Measurement measurement) {
|
||||||
|
if (!Objects.equals(measurement.regionKey(), chosenRegionKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (measurement.coverage() == null) {
|
||||||
|
inspector.showBiomeTerrain(measurement.regionName());
|
||||||
|
} else {
|
||||||
|
inspector.showCoverage(measurement.regionName(), measurement.coverage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement measureSafely(String regionKey, MapCensus.ViewArea area) {
|
||||||
|
try {
|
||||||
|
return measureRegion(regionKey, area);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
Iris.reportError(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement measureRegion(String regionKey, MapCensus.ViewArea area) {
|
||||||
|
Engine engine = engineSource.get();
|
||||||
|
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||||
|
IrisRegion region = complex == null ? null : engine.getData().getRegionLoader().load(regionKey);
|
||||||
|
if (region == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!complex.isHybridTerrain()) {
|
||||||
|
return new Measurement(regionKey, region.getName(), null);
|
||||||
|
}
|
||||||
|
RegionHeightCoverage coverage = RegionHeightCoverage.measure(biomeRanges(engine, region), groundHeights(complex, area));
|
||||||
|
return new Measurement(regionKey, region.getName(), coverage);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In hybrid terrain the overlay noise is the ground's height above the fluid, before any biome adds its own terrain.
|
||||||
|
private static int[] groundHeights(IrisComplex complex, MapCensus.ViewArea area) {
|
||||||
|
int[] heights = new int[GROUND_SAMPLES_PER_SIDE * GROUND_SAMPLES_PER_SIDE];
|
||||||
|
double stepX = (area.maximumX() - area.minimumX()) / GROUND_SAMPLES_PER_SIDE;
|
||||||
|
double stepZ = (area.maximumZ() - area.minimumZ()) / GROUND_SAMPLES_PER_SIDE;
|
||||||
|
for (int row = 0; row < GROUND_SAMPLES_PER_SIDE; row++) {
|
||||||
|
for (int column = 0; column < GROUND_SAMPLES_PER_SIDE; column++) {
|
||||||
|
double x = area.minimumX() + (column + 0.5D) * stepX;
|
||||||
|
double z = area.minimumZ() + (row + 0.5D) * stepZ;
|
||||||
|
heights[row * GROUND_SAMPLES_PER_SIDE + column] = (int) Math.round(complex.getOverlayStream().get(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return heights;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Thread daemonThread(Runnable task) {
|
||||||
|
Thread thread = new Thread(task, "Iris Region Explorer");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param coverage how the region's biomes cover the ground in view, or null when the dimension uses biome terrain
|
||||||
|
*/
|
||||||
|
private record Measurement(String regionKey, String regionName, RegionHeightCoverage coverage) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
/*
|
||||||
|
* 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.RegionHeightCoverage.BiomeRange;
|
||||||
|
import com.volmit.iris.core.gui.RegionHeightCoverage.HeightBand;
|
||||||
|
import com.volmit.iris.engine.object.InferredType;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import java.awt.BasicStroke;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.Shape;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ground height runs left to right. On top, how much of the ground in view sits at each height; below, one bar per
|
||||||
|
* biome over the heights it allows. Heights no biome covers are red, heights several ranged biomes share are hatched.
|
||||||
|
*/
|
||||||
|
final class RegionHeightChart extends JComponent {
|
||||||
|
private static final long serialVersionUID = 6218894112903760545L;
|
||||||
|
private static final List<InferredType> ROW_GROUPS = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA);
|
||||||
|
private static final int CHART_WIDTH = 330;
|
||||||
|
private static final int PADDING = 12;
|
||||||
|
private static final int NAME_COLUMN = 104;
|
||||||
|
private static final int NAME_GAP = 8;
|
||||||
|
private static final int HISTOGRAM_HEIGHT = 54;
|
||||||
|
private static final int AXIS_HEIGHT = 20;
|
||||||
|
private static final int GROUP_HEADER_HEIGHT = 22;
|
||||||
|
private static final int ROW_HEIGHT = 18;
|
||||||
|
private static final int BAR_HEIGHT = 10;
|
||||||
|
private static final int MINIMUM_TICK_SPACING = 38;
|
||||||
|
private static final int[] TICK_STEPS = {5, 10, 20, 25, 50, 100, 200, 250, 500, 1000};
|
||||||
|
private static final int HATCH_SPACING = 5;
|
||||||
|
private static final double RANGE_PADDING_FRACTION = 0.04D;
|
||||||
|
private static final Color LAND_GROUND = new ThemeColor(0x9DA0A8, 0x6F737A);
|
||||||
|
private static final Color SEA_GROUND = new ThemeColor(0x7FA3D9, 0x4A6FA8);
|
||||||
|
private static final Color GAP_FILL = new ThemeColor(new Color(0xDB, 0x3B, 0x4B, 46), new Color(0xF7, 0x54, 0x64, 52));
|
||||||
|
private static final Color SHARED_HATCH = new ThemeColor(new Color(0, 0, 0, 70), new Color(255, 255, 255, 70));
|
||||||
|
private static final Color SEA_LEVEL = new ThemeColor(new Color(0x31, 0x5F, 0xBD, 150), new Color(0x54, 0x8A, 0xF7, 150));
|
||||||
|
|
||||||
|
private RegionHeightCoverage coverage;
|
||||||
|
private int lowestShown;
|
||||||
|
private int highestShown;
|
||||||
|
|
||||||
|
RegionHeightChart() {
|
||||||
|
setToolTipText("");
|
||||||
|
setOpaque(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show(RegionHeightCoverage nextCoverage) {
|
||||||
|
coverage = nextCoverage;
|
||||||
|
fitShownHeights();
|
||||||
|
revalidate();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chart spans the ground in view and every range edge, so a biome placed above or below the view still shows.
|
||||||
|
private void fitShownHeights() {
|
||||||
|
if (coverage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lowest = Math.min(0, coverage.lowestHeight());
|
||||||
|
int highest = Math.max(0, coverage.highestHeight());
|
||||||
|
for (InferredType type : ROW_GROUPS) {
|
||||||
|
for (BiomeRange range : coverage.ranges(type)) {
|
||||||
|
if (range.hasLowerBound()) {
|
||||||
|
lowest = Math.min(lowest, range.minHeight());
|
||||||
|
}
|
||||||
|
if (range.hasUpperBound()) {
|
||||||
|
highest = Math.max(highest, range.maxHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int padding = Math.max(2, (int) Math.round((highest - lowest) * RANGE_PADDING_FRACTION));
|
||||||
|
lowestShown = lowest - padding;
|
||||||
|
highestShown = highest + padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
int rows = 0;
|
||||||
|
int groups = 0;
|
||||||
|
if (coverage != null) {
|
||||||
|
for (InferredType type : ROW_GROUPS) {
|
||||||
|
int count = coverage.ranges(type).size();
|
||||||
|
rows += count;
|
||||||
|
groups += count > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int height = PADDING * 2 + HISTOGRAM_HEIGHT + AXIS_HEIGHT + groups * GROUP_HEADER_HEIGHT + rows * ROW_HEIGHT;
|
||||||
|
return StudioTheme.scaledSize(CHART_WIDTH, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getMaximumSize() {
|
||||||
|
return new Dimension(Integer.MAX_VALUE, getPreferredSize().height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
if (coverage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
int top = StudioTheme.scale(PADDING);
|
||||||
|
int rowsTop = top + StudioTheme.scale(HISTOGRAM_HEIGHT + AXIS_HEIGHT);
|
||||||
|
int bottom = getHeight() - StudioTheme.scale(PADDING);
|
||||||
|
drawBands(canvas, top, bottom);
|
||||||
|
drawGround(canvas, top);
|
||||||
|
drawAxis(canvas, top + StudioTheme.scale(HISTOGRAM_HEIGHT));
|
||||||
|
drawRows(canvas, rowsTop);
|
||||||
|
drawSeaLevel(canvas, top, bottom);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBands(Graphics2D canvas, int top, int bottom) {
|
||||||
|
int histogramBottom = top + StudioTheme.scale(HISTOGRAM_HEIGHT);
|
||||||
|
for (InferredType type : RegionHeightCoverage.MEASURED_TYPES) {
|
||||||
|
for (HeightBand band : coverage.bands(type)) {
|
||||||
|
int left = xOf(band.fromHeight());
|
||||||
|
int width = Math.max(1, xOf(band.toHeight() + 1) - left);
|
||||||
|
if (band.isGap()) {
|
||||||
|
canvas.setColor(GAP_FILL);
|
||||||
|
canvas.fillRect(left, top, width, bottom - top);
|
||||||
|
} else if (band.isShared() && band.biomes().stream().allMatch(BiomeRange::isBounded)) {
|
||||||
|
hatch(canvas, new Rectangle(left, top, width, histogramBottom - top));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void hatch(Graphics2D canvas, Rectangle area) {
|
||||||
|
Shape previousClip = canvas.getClip();
|
||||||
|
canvas.clip(area);
|
||||||
|
canvas.setColor(SHARED_HATCH);
|
||||||
|
int spacing = StudioTheme.scale(HATCH_SPACING);
|
||||||
|
for (int offset = -area.height; offset < area.width; offset += spacing) {
|
||||||
|
canvas.drawLine(area.x + offset, area.y + area.height, area.x + offset + area.height, area.y);
|
||||||
|
}
|
||||||
|
canvas.setClip(previousClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawGround(Graphics2D canvas, int top) {
|
||||||
|
int histogramHeight = StudioTheme.scale(HISTOGRAM_HEIGHT);
|
||||||
|
int plotLeft = plotLeft();
|
||||||
|
int plotRight = plotRight();
|
||||||
|
int[] columnCounts = new int[plotRight - plotLeft];
|
||||||
|
int highestColumn = 1;
|
||||||
|
for (int column = 0; column < columnCounts.length; column++) {
|
||||||
|
int fromHeight = heightAt(plotLeft + column);
|
||||||
|
int toHeight = Math.max(fromHeight, heightAt(plotLeft + column + 1) - 1);
|
||||||
|
for (int height = fromHeight; height <= toHeight; height++) {
|
||||||
|
columnCounts[column] += coverage.count(height);
|
||||||
|
}
|
||||||
|
highestColumn = Math.max(highestColumn, columnCounts[column]);
|
||||||
|
}
|
||||||
|
for (int column = 0; column < columnCounts.length; column++) {
|
||||||
|
if (columnCounts[column] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int barHeight = Math.max(1, (int) Math.round(columnCounts[column] / (double) highestColumn * histogramHeight));
|
||||||
|
canvas.setColor(heightAt(plotLeft + column) < 0 ? SEA_GROUND : LAND_GROUND);
|
||||||
|
canvas.fillRect(plotLeft + column, top + histogramHeight - barHeight, 1, barHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawAxis(Graphics2D canvas, int axisTop) {
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawLine(plotLeft(), axisTop, plotRight(), axisTop);
|
||||||
|
int step = tickStep();
|
||||||
|
int firstTick = Math.floorDiv(lowestShown, step) * step + step;
|
||||||
|
for (int height = firstTick; height < highestShown; height += step) {
|
||||||
|
int x = xOf(height);
|
||||||
|
String label = height > 0 ? "+" + height : String.valueOf(height);
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawLine(x, axisTop, x, axisTop + StudioTheme.scale(3));
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
canvas.drawString(label, x - metrics.stringWidth(label) / 2, axisTop + StudioTheme.scale(4) + metrics.getAscent());
|
||||||
|
}
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
canvas.drawString("Ground in view", StudioTheme.scale(PADDING), axisTop - StudioTheme.scale(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int tickStep() {
|
||||||
|
double pixelsPerBlock = (plotRight() - plotLeft()) / (double) Math.max(1, highestShown - lowestShown);
|
||||||
|
for (int step : TICK_STEPS) {
|
||||||
|
if (step * pixelsPerBlock >= StudioTheme.scale(MINIMUM_TICK_SPACING)) {
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TICK_STEPS[TICK_STEPS.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawRows(Graphics2D canvas, int rowsTop) {
|
||||||
|
int y = rowsTop;
|
||||||
|
for (InferredType type : ROW_GROUPS) {
|
||||||
|
List<BiomeRange> ranges = coverage.ranges(type);
|
||||||
|
if (ranges.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
canvas.setFont(StudioTheme.sectionFont());
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
canvas.drawString(groupTitle(type), StudioTheme.scale(PADDING), y + StudioTheme.scale(GROUP_HEADER_HEIGHT) - StudioTheme.scale(7));
|
||||||
|
y += StudioTheme.scale(GROUP_HEADER_HEIGHT);
|
||||||
|
for (BiomeRange range : ranges) {
|
||||||
|
drawRow(canvas, range, y);
|
||||||
|
y += StudioTheme.scale(ROW_HEIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawRow(Graphics2D canvas, BiomeRange range, int rowTop) {
|
||||||
|
int rowHeight = StudioTheme.scale(ROW_HEIGHT);
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
canvas.drawString(ellipsize(range.name(), metrics, StudioTheme.scale(NAME_COLUMN)), StudioTheme.scale(PADDING),
|
||||||
|
rowTop + (rowHeight + metrics.getAscent() - metrics.getDescent()) / 2);
|
||||||
|
int left = xOf(Math.max(range.minHeight(), lowestShown));
|
||||||
|
int right = xOf(Math.min(range.maxHeight(), highestShown) + 1);
|
||||||
|
int barHeight = StudioTheme.scale(BAR_HEIGHT);
|
||||||
|
int barTop = rowTop + (rowHeight - barHeight) / 2;
|
||||||
|
int arc = StudioTheme.scale(4);
|
||||||
|
canvas.setColor(new Color(range.color()));
|
||||||
|
canvas.fillRoundRect(left, barTop, Math.max(2, right - left), barHeight, arc, arc);
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawRoundRect(left, barTop, Math.max(2, right - left) - 1, barHeight - 1, arc, arc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawSeaLevel(Graphics2D canvas, int top, int bottom) {
|
||||||
|
int x = xOf(0);
|
||||||
|
canvas.setColor(SEA_LEVEL);
|
||||||
|
float dash = StudioTheme.scale(3);
|
||||||
|
canvas.setStroke(new BasicStroke(1F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10F, new float[]{dash, dash}, 0F));
|
||||||
|
canvas.drawLine(x, top, x, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getToolTipText(MouseEvent event) {
|
||||||
|
if (coverage == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BiomeRange row = rowAt(event.getY());
|
||||||
|
if (row != null) {
|
||||||
|
return row.name() + ": " + (row.isBounded() ? RegionHeightCoverage.heightText(row.minHeight(), row.maxHeight()) : "any height");
|
||||||
|
}
|
||||||
|
int height = heightAt(event.getX());
|
||||||
|
for (InferredType type : RegionHeightCoverage.MEASURED_TYPES) {
|
||||||
|
for (HeightBand band : coverage.bands(type)) {
|
||||||
|
if (band.isGap() && height >= band.fromHeight() && height <= band.toHeight()) {
|
||||||
|
return "No biome covers " + RegionHeightCoverage.heightText(band.fromHeight(), band.toHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeRange rowAt(int y) {
|
||||||
|
int rowTop = StudioTheme.scale(PADDING + HISTOGRAM_HEIGHT + AXIS_HEIGHT);
|
||||||
|
for (InferredType type : ROW_GROUPS) {
|
||||||
|
List<BiomeRange> ranges = coverage.ranges(type);
|
||||||
|
if (ranges.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rowTop += StudioTheme.scale(GROUP_HEADER_HEIGHT);
|
||||||
|
for (BiomeRange range : ranges) {
|
||||||
|
if (y >= rowTop && y < rowTop + StudioTheme.scale(ROW_HEIGHT)) {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
rowTop += StudioTheme.scale(ROW_HEIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int plotLeft() {
|
||||||
|
return StudioTheme.scale(PADDING + NAME_COLUMN + NAME_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int plotRight() {
|
||||||
|
return Math.max(plotLeft() + 1, getWidth() - StudioTheme.scale(PADDING));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int xOf(int height) {
|
||||||
|
double fraction = (height - lowestShown) / (double) Math.max(1, highestShown - lowestShown);
|
||||||
|
return plotLeft() + (int) Math.round(fraction * (plotRight() - plotLeft()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int heightAt(int x) {
|
||||||
|
double fraction = (x - plotLeft()) / (double) Math.max(1, plotRight() - plotLeft());
|
||||||
|
return (int) Math.floor(lowestShown + fraction * (highestShown - lowestShown));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String groupTitle(InferredType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case LAND -> "LAND";
|
||||||
|
case SHORE -> "SHORE";
|
||||||
|
default -> "SEA";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String ellipsize(String text, FontMetrics metrics, int maximumWidth) {
|
||||||
|
if (metrics.stringWidth(text) <= maximumWidth) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
String ellipsis = "…";
|
||||||
|
int end = text.length();
|
||||||
|
while (end > 0 && metrics.stringWidth(text.substring(0, end) + ellipsis) > maximumWidth) {
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
return text.substring(0, end) + ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.engine.object.InferredType;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which of a region's biomes allow which ground height in HYBRID terrain, measured against the ground heights the map
|
||||||
|
* shows, so the region view can point out heights no biome covers and heights several biomes share.
|
||||||
|
* Heights are in blocks above the fluid height, negative below it.
|
||||||
|
*/
|
||||||
|
final class RegionHeightCoverage {
|
||||||
|
static final List<InferredType> MEASURED_TYPES = List.of(InferredType.SEA, InferredType.LAND);
|
||||||
|
private static final int MAXIMUM_NOTES = 8;
|
||||||
|
|
||||||
|
private final List<BiomeRange> ranges;
|
||||||
|
private final int lowestHeight;
|
||||||
|
private final int highestHeight;
|
||||||
|
private final int[] countsFromLowest;
|
||||||
|
private final int highestCount;
|
||||||
|
|
||||||
|
private RegionHeightCoverage(List<BiomeRange> ranges, int lowestHeight, int[] countsFromLowest) {
|
||||||
|
this.ranges = List.copyOf(ranges);
|
||||||
|
this.lowestHeight = lowestHeight;
|
||||||
|
this.highestHeight = lowestHeight + countsFromLowest.length - 1;
|
||||||
|
this.countsFromLowest = countsFromLowest;
|
||||||
|
int highest = 0;
|
||||||
|
for (int count : countsFromLowest) {
|
||||||
|
highest = Math.max(highest, count);
|
||||||
|
}
|
||||||
|
this.highestCount = highest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param groundHeights the ground height of every sampled spot, rounded to whole blocks
|
||||||
|
*/
|
||||||
|
static RegionHeightCoverage measure(List<BiomeRange> ranges, int[] groundHeights) {
|
||||||
|
if (groundHeights.length == 0) {
|
||||||
|
return new RegionHeightCoverage(ranges, 0, new int[1]);
|
||||||
|
}
|
||||||
|
int lowest = Integer.MAX_VALUE;
|
||||||
|
int highest = Integer.MIN_VALUE;
|
||||||
|
for (int height : groundHeights) {
|
||||||
|
lowest = Math.min(lowest, height);
|
||||||
|
highest = Math.max(highest, height);
|
||||||
|
}
|
||||||
|
int[] counts = new int[highest - lowest + 1];
|
||||||
|
for (int height : groundHeights) {
|
||||||
|
counts[height - lowest]++;
|
||||||
|
}
|
||||||
|
return new RegionHeightCoverage(ranges, lowest, counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BiomeRange> ranges(InferredType type) {
|
||||||
|
return ranges.stream().filter(range -> range.type() == type).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
int lowestHeight() {
|
||||||
|
return lowestHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
int highestHeight() {
|
||||||
|
return highestHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count(int height) {
|
||||||
|
return height < lowestHeight || height > highestHeight ? 0 : countsFromLowest[height - lowestHeight];
|
||||||
|
}
|
||||||
|
|
||||||
|
int highestCount() {
|
||||||
|
return highestCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ground heights of one type split into runs that the same biomes allow, from low to high. Land is the ground
|
||||||
|
* at or above the fluid height, sea the ground below it.
|
||||||
|
*/
|
||||||
|
List<HeightBand> bands(InferredType type) {
|
||||||
|
int from = type == InferredType.SEA ? lowestHeight : Math.max(0, lowestHeight);
|
||||||
|
int to = type == InferredType.SEA ? Math.min(-1, highestHeight) : highestHeight;
|
||||||
|
int total = countBetween(from, to);
|
||||||
|
List<HeightBand> bands = new ArrayList<>();
|
||||||
|
if (total == 0) {
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
List<BiomeRange> typeRanges = ranges(type);
|
||||||
|
int bandStart = from;
|
||||||
|
List<BiomeRange> bandBiomes = coveringBiomes(typeRanges, from);
|
||||||
|
for (int height = from + 1; height <= to + 1; height++) {
|
||||||
|
List<BiomeRange> biomes = height <= to ? coveringBiomes(typeRanges, height) : null;
|
||||||
|
if (bandBiomes.equals(biomes)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bands.add(new HeightBand(type, bandStart, height - 1, bandBiomes, countBetween(bandStart, height - 1) / (double) total));
|
||||||
|
bandStart = height;
|
||||||
|
bandBiomes = biomes;
|
||||||
|
}
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> notes() {
|
||||||
|
List<String> notes = new ArrayList<>();
|
||||||
|
noteBiomesWithoutRange(notes);
|
||||||
|
for (InferredType type : MEASURED_TYPES) {
|
||||||
|
for (HeightBand band : bands(type)) {
|
||||||
|
if (band.share() == 0D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (band.isGap()) {
|
||||||
|
notes.add(gapNote(band));
|
||||||
|
} else if (band.isShared() && band.biomes().stream().allMatch(BiomeRange::isBounded)) {
|
||||||
|
notes.add(names(band.biomes()) + " share " + heightText(band.fromHeight(), band.toHeight())
|
||||||
|
+ ": their noise decides.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notes.size() > MAXIMUM_NOTES ? notes.subList(0, MAXIMUM_NOTES) : notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void noteBiomesWithoutRange(List<String> notes) {
|
||||||
|
List<BiomeRange> unbounded = ranges.stream()
|
||||||
|
.filter(range -> range.type() != InferredType.SHORE && !range.isBounded())
|
||||||
|
.toList();
|
||||||
|
if (unbounded.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean single = unbounded.size() == 1;
|
||||||
|
notes.add(names(unbounded) + (single ? " has no height range, so it" : " have no height range, so they")
|
||||||
|
+ " can show up at any height.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String gapNote(HeightBand gap) {
|
||||||
|
String ground = gap.type() == InferredType.SEA ? "sea floor" : "land";
|
||||||
|
String note = "Nothing covers " + heightText(gap.fromHeight(), gap.toHeight()) + " (" + percent(gap.share())
|
||||||
|
+ " of the " + ground + " in view)";
|
||||||
|
BiomeRange below = closestBelow(gap);
|
||||||
|
BiomeRange above = closestAbove(gap);
|
||||||
|
if (below != null && above != null) {
|
||||||
|
return note + ": " + below.name() + " and " + above.name() + ", the closest ranges, take it over.";
|
||||||
|
}
|
||||||
|
BiomeRange closest = below != null ? below : above;
|
||||||
|
return closest == null ? note + "." : note + ": " + closest.name() + ", the closest range, takes it over.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeRange closestBelow(HeightBand gap) {
|
||||||
|
return ranges(gap.type()).stream()
|
||||||
|
.filter(range -> range.maxHeight() < gap.fromHeight())
|
||||||
|
.max(Comparator.comparingInt(BiomeRange::maxHeight))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeRange closestAbove(HeightBand gap) {
|
||||||
|
return ranges(gap.type()).stream()
|
||||||
|
.filter(range -> range.minHeight() > gap.toHeight())
|
||||||
|
.min(Comparator.comparingInt(BiomeRange::minHeight))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<BiomeRange> coveringBiomes(List<BiomeRange> typeRanges, int height) {
|
||||||
|
return typeRanges.stream().filter(range -> range.covers(height)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countBetween(int from, int to) {
|
||||||
|
int total = 0;
|
||||||
|
for (int height = from; height <= to; height++) {
|
||||||
|
total += count(height);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String heightText(int from, int to) {
|
||||||
|
if (from >= 0) {
|
||||||
|
return from == to ? from + " above sea" : from + " to " + to + " above sea";
|
||||||
|
}
|
||||||
|
if (to < 0) {
|
||||||
|
return from == to ? -from + " below sea" : -to + " to " + -from + " below sea";
|
||||||
|
}
|
||||||
|
return -from + " below to " + to + " above sea";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String names(List<BiomeRange> biomes) {
|
||||||
|
List<String> names = biomes.stream().map(BiomeRange::name).toList();
|
||||||
|
if (names.size() == 1) {
|
||||||
|
return names.get(0);
|
||||||
|
}
|
||||||
|
return names.subList(0, names.size() - 1).stream().collect(Collectors.joining(", ")) + " and " + names.get(names.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String percent(double share) {
|
||||||
|
double percent = share * 100D;
|
||||||
|
return percent < 1D ? "<1%" : String.format(Locale.ROOT, "%.0f%%", percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The heights one of the region's biomes allows, from its placement.
|
||||||
|
*/
|
||||||
|
record BiomeRange(String key, String name, int color, InferredType type, int minHeight, int maxHeight) {
|
||||||
|
private static final int UNBOUNDED_LOW = -2032;
|
||||||
|
private static final int UNBOUNDED_HIGH = 2032;
|
||||||
|
|
||||||
|
boolean covers(int height) {
|
||||||
|
return height >= minHeight && height <= maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isBounded() {
|
||||||
|
return hasLowerBound() || hasUpperBound();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasLowerBound() {
|
||||||
|
return minHeight > UNBOUNDED_LOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasUpperBound() {
|
||||||
|
return maxHeight < UNBOUNDED_HIGH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A run of ground heights that the same biomes allow; no biomes makes it a gap.
|
||||||
|
*
|
||||||
|
* @param share how much of this type's ground in view lies in the run
|
||||||
|
*/
|
||||||
|
record HeightBand(InferredType type, int fromHeight, int toHeight, List<BiomeRange> biomes, double share) {
|
||||||
|
boolean isGap() {
|
||||||
|
return biomes.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isShared() {
|
||||||
|
return biomes.size() > 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextArea;
|
||||||
|
import javax.swing.Scrollable;
|
||||||
|
import javax.swing.SwingConstants;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.CardLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.GridLayout;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The region view's inspector: for the chosen region, which of its biomes covers which ground height, and what that
|
||||||
|
* means in words.
|
||||||
|
*/
|
||||||
|
final class RegionInspectorPanel extends JPanel {
|
||||||
|
private static final long serialVersionUID = -2874105263188240317L;
|
||||||
|
private static final String MESSAGE_CARD = "message";
|
||||||
|
private static final String COVERAGE_CARD = "coverage";
|
||||||
|
private static final int PANEL_WIDTH = 330;
|
||||||
|
private static final String PARAGRAPH_BREAK = "\n\n";
|
||||||
|
|
||||||
|
private final JLabel title = new JLabel();
|
||||||
|
private final JLabel detail = new JLabel();
|
||||||
|
private final CardLayout cards = new CardLayout();
|
||||||
|
private final JPanel cardPanel = new JPanel(cards);
|
||||||
|
private final JTextArea message = wrappingText();
|
||||||
|
private final RegionHeightChart chart = new RegionHeightChart();
|
||||||
|
private final JTextArea notes = wrappingText();
|
||||||
|
|
||||||
|
RegionInspectorPanel() {
|
||||||
|
super(new BorderLayout());
|
||||||
|
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||||
|
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
add(buildHeader(), BorderLayout.NORTH);
|
||||||
|
cardPanel.setOpaque(false);
|
||||||
|
cardPanel.add(buildMessageCard(), MESSAGE_CARD);
|
||||||
|
cardPanel.add(StudioToolbar.scroll(buildCoverageCard()), COVERAGE_CARD);
|
||||||
|
add(cardPanel, BorderLayout.CENTER);
|
||||||
|
showNothingChosen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showNothingChosen() {
|
||||||
|
showHeader("Regions", "Every region in its own colour");
|
||||||
|
showMessage("Pick a region on the left to fill the map with its biomes and see which biome covers which height.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMeasuring(String regionName) {
|
||||||
|
showHeader(regionName, "Measuring the ground in view…");
|
||||||
|
}
|
||||||
|
|
||||||
|
void showBiomeTerrain(String regionName) {
|
||||||
|
showHeader(regionName, "Biome terrain");
|
||||||
|
showMessage("This dimension uses BIOME terrain, where biomes are placed by noise and ignore height. "
|
||||||
|
+ "Set the dimension's terrainMode to HYBRID to choose biomes by the height of the ground.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void showCoverage(String regionName, RegionHeightCoverage coverage) {
|
||||||
|
showHeader(regionName, "Hybrid terrain · biomes by ground height");
|
||||||
|
chart.show(coverage);
|
||||||
|
notes.setText(notesText(coverage.notes()));
|
||||||
|
cards.show(cardPanel, COVERAGE_CARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildHeader() {
|
||||||
|
JPanel header = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(2)));
|
||||||
|
header.setOpaque(false);
|
||||||
|
header.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
|
||||||
|
StudioTheme.emptyBorder(10, 16, 9, 16)));
|
||||||
|
title.setFont(StudioTheme.titleFont());
|
||||||
|
detail.setFont(StudioTheme.smallFont());
|
||||||
|
detail.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
header.add(title);
|
||||||
|
header.add(detail);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildMessageCard() {
|
||||||
|
JPanel card = new JPanel(new BorderLayout());
|
||||||
|
card.setOpaque(false);
|
||||||
|
message.setBorder(StudioTheme.emptyBorder(12, 16, 12, 16));
|
||||||
|
card.add(message, BorderLayout.NORTH);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildCoverageCard() {
|
||||||
|
JPanel card = new ViewportWidthPanel();
|
||||||
|
card.setLayout(new BoxLayout(card, BoxLayout.Y_AXIS));
|
||||||
|
card.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
chart.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
notes.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
notes.setBorder(StudioTheme.emptyBorder(0, 16, 12, 16));
|
||||||
|
card.add(chart);
|
||||||
|
card.add(notes);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHeader(String regionName, String detailText) {
|
||||||
|
title.setText(regionName);
|
||||||
|
detail.setText(detailText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMessage(String text) {
|
||||||
|
message.setText(text);
|
||||||
|
cards.show(cardPanel, MESSAGE_CARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String notesText(List<String> lines) {
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
return "Every ground height in view has a biome, and no two ranged biomes share one.";
|
||||||
|
}
|
||||||
|
return String.join(PARAGRAPH_BREAK, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JTextArea wrappingText() {
|
||||||
|
JTextArea text = new JTextArea();
|
||||||
|
text.setEditable(false);
|
||||||
|
text.setFocusable(false);
|
||||||
|
text.setOpaque(false);
|
||||||
|
text.setLineWrap(true);
|
||||||
|
text.setWrapStyleWord(true);
|
||||||
|
text.setFont(StudioTheme.font());
|
||||||
|
text.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follows the scroll pane's width, so the chart stretches to it and the notes wrap inside it.
|
||||||
|
*/
|
||||||
|
private static final class ViewportWidthPanel extends JPanel implements Scrollable {
|
||||||
|
private static final long serialVersionUID = 6086542174870301352L;
|
||||||
|
private static final int SCROLL_STEP = 18;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredScrollableViewportSize() {
|
||||||
|
return getPreferredSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||||
|
return StudioTheme.scale(SCROLL_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||||
|
return orientation == SwingConstants.VERTICAL ? visibleRect.height : visibleRect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportWidth() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportHeight() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.DefaultListModel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.ListSelectionModel;
|
||||||
|
import javax.swing.event.DocumentEvent;
|
||||||
|
import javax.swing.event.DocumentListener;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The region view's sidebar: every region of the pack, or all of them at once, to fill the map with.
|
||||||
|
*/
|
||||||
|
final class RegionNavigator extends JPanel {
|
||||||
|
private static final long serialVersionUID = 3150735486722101466L;
|
||||||
|
private static final RegionRow ALL_REGIONS = new RegionRow(null, "All regions", -1);
|
||||||
|
private static final int ROW_HEIGHT = 24;
|
||||||
|
private static final int SWATCH_SIZE = 12;
|
||||||
|
private static final int SWATCH_GAP = 8;
|
||||||
|
private static final int ROW_PADDING = 10;
|
||||||
|
|
||||||
|
private final Consumer<String> onRegionChosen;
|
||||||
|
private final JTextField searchField = new JTextField();
|
||||||
|
private final DefaultListModel<RegionRow> rows = new DefaultListModel<>();
|
||||||
|
private final JList<RegionRow> list = new JList<>(rows);
|
||||||
|
private List<RegionRow> regions = List.of();
|
||||||
|
private String chosenKey;
|
||||||
|
private boolean updatingRows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onRegionChosen receives the chosen region's key, or null for all regions
|
||||||
|
*/
|
||||||
|
RegionNavigator(Consumer<String> onRegionChosen) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
this.onRegionChosen = onRegionChosen;
|
||||||
|
StudioToolbar.makeSearchField(searchField, "Search regions...");
|
||||||
|
searchField.getDocument().addDocumentListener(new DocumentListener() {
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(DocumentEvent event) {
|
||||||
|
showMatchingRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(DocumentEvent event) {
|
||||||
|
showMatchingRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(DocumentEvent event) {
|
||||||
|
showMatchingRows();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
list.setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
|
||||||
|
list.setCellRenderer(new RegionRowRenderer());
|
||||||
|
list.addListSelectionListener(event -> {
|
||||||
|
RegionRow row = list.getSelectedValue();
|
||||||
|
if (!event.getValueIsAdjusting() && !updatingRows && row != null && !Objects.equals(row.key(), chosenKey)) {
|
||||||
|
chosenKey = row.key();
|
||||||
|
onRegionChosen.accept(chosenKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(StudioToolbar.padded(searchField, 8, 8, 8, 8), BorderLayout.NORTH);
|
||||||
|
add(StudioToolbar.scroll(list), BorderLayout.CENTER);
|
||||||
|
showMatchingRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showRegions(List<RegionRow> nextRegions) {
|
||||||
|
regions = List.copyOf(nextRegions);
|
||||||
|
showMatchingRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMatchingRows() {
|
||||||
|
String filter = searchField.getText().trim().toLowerCase(Locale.ROOT);
|
||||||
|
updatingRows = true;
|
||||||
|
try {
|
||||||
|
rows.clear();
|
||||||
|
rows.addElement(ALL_REGIONS);
|
||||||
|
for (RegionRow region : regions) {
|
||||||
|
if (filter.isEmpty() || region.name().toLowerCase(Locale.ROOT).contains(filter)
|
||||||
|
|| region.key().toLowerCase(Locale.ROOT).contains(filter)) {
|
||||||
|
rows.addElement(region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectChosenRow();
|
||||||
|
} finally {
|
||||||
|
updatingRows = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectChosenRow() {
|
||||||
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
|
if (Objects.equals(rows.get(index).key(), chosenKey)) {
|
||||||
|
list.setSelectedIndex(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param color the region's map colour, or -1 for the all regions row
|
||||||
|
*/
|
||||||
|
record RegionRow(String key, String name, int color) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RegionRowRenderer extends DefaultListCellRenderer {
|
||||||
|
private static final long serialVersionUID = -6114522950207432516L;
|
||||||
|
private Color swatch;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> owner, Object value, int index, boolean selected, boolean focused) {
|
||||||
|
RegionRow row = (RegionRow) value;
|
||||||
|
super.getListCellRendererComponent(owner, row.name(), index, selected, false);
|
||||||
|
setFont(row.key() == null ? StudioTheme.boldFont() : StudioTheme.font());
|
||||||
|
setBorder(StudioTheme.emptyBorder(0, ROW_PADDING + SWATCH_SIZE + SWATCH_GAP, 0, ROW_PADDING));
|
||||||
|
setToolTipText(row.key());
|
||||||
|
swatch = row.color() < 0 ? null : new Color(row.color());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
super.paintComponent(graphics);
|
||||||
|
if (swatch == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
int size = StudioTheme.scale(SWATCH_SIZE);
|
||||||
|
int x = StudioTheme.scale(ROW_PADDING);
|
||||||
|
int y = (getHeight() - size) / 2;
|
||||||
|
int arc = StudioTheme.scale(4);
|
||||||
|
canvas.setColor(swatch);
|
||||||
|
canvas.fillRoundRect(x, y, size, size, arc, arc);
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawRoundRect(x, y, size - 1, size - 1, arc, arc);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,9 +19,10 @@
|
|||||||
package com.volmit.iris.core.gui;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
import com.volmit.iris.Iris;
|
import com.volmit.iris.Iris;
|
||||||
import com.volmit.iris.core.gui.components.RenderType;
|
import com.volmit.iris.core.gui.components.MapLayer;
|
||||||
import com.volmit.iris.engine.IrisComplex;
|
import com.volmit.iris.engine.IrisComplex;
|
||||||
import com.volmit.iris.engine.framework.Engine;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
import com.volmit.iris.engine.object.IrisWorld;
|
import com.volmit.iris.engine.object.IrisWorld;
|
||||||
import com.volmit.iris.util.format.Form;
|
import com.volmit.iris.util.format.Form;
|
||||||
import com.volmit.iris.util.scheduling.J;
|
import com.volmit.iris.util.scheduling.J;
|
||||||
@@ -65,20 +66,26 @@ final class VisionOverlay {
|
|||||||
runOnServerThread(() -> teleportOpener(blockX, blockZ));
|
runOnServerThread(() -> teleportOpener(blockX, blockZ));
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<String> openInEditor(double worldX, double worldZ, RenderType type) {
|
Optional<String> openInEditor(double worldX, double worldZ, MapLayer layer) {
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
File file = switch (type) {
|
File file = switch (layer.type()) {
|
||||||
case BIOME, HEIGHT ->
|
case BIOME, HEIGHT ->
|
||||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
||||||
|
case REGION_PREVIEW -> openPreviewedBiome(complex, layer.previewRegionKey(), worldX, worldZ);
|
||||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||||
case CONTINENT -> null;
|
case CONTINENT -> null;
|
||||||
};
|
};
|
||||||
return Optional.ofNullable(file).map(File::getName);
|
return Optional.ofNullable(file).map(File::getName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private File openPreviewedBiome(IrisComplex complex, String regionKey, double worldX, double worldZ) {
|
||||||
|
IrisRegion region = engine.getData().getRegionLoader().load(regionKey);
|
||||||
|
return region == null ? null : complex.surfaceBiomeAsIfIn(region, worldX, worldZ).openInVSCode();
|
||||||
|
}
|
||||||
|
|
||||||
private void queuePlayerRefresh() {
|
private void queuePlayerRefresh() {
|
||||||
if (!playerRefreshQueued.compareAndSet(false, true)) {
|
if (!playerRefreshQueued.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ package com.volmit.iris.core.gui;
|
|||||||
import com.volmit.iris.Iris;
|
import com.volmit.iris.Iris;
|
||||||
import com.volmit.iris.core.gui.components.IrisRenderer;
|
import com.volmit.iris.core.gui.components.IrisRenderer;
|
||||||
import com.volmit.iris.core.gui.components.MapLabels;
|
import com.volmit.iris.core.gui.components.MapLabels;
|
||||||
|
import com.volmit.iris.core.gui.components.MapLayer;
|
||||||
import com.volmit.iris.core.gui.components.MapTile;
|
import com.volmit.iris.core.gui.components.MapTile;
|
||||||
import com.volmit.iris.core.gui.components.RenderType;
|
|
||||||
|
|
||||||
import java.awt.EventQueue;
|
import java.awt.EventQueue;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -212,7 +212,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
for (long tileX = minimumX; tileX <= maximumX; tileX++) {
|
for (long tileX = minimumX; tileX <= maximumX; tileX++) {
|
||||||
double deltaX = tileX + 0.5D - centerTileX;
|
double deltaX = tileX + 0.5D - centerTileX;
|
||||||
double deltaZ = tileZ + 0.5D - centerTileZ;
|
double deltaZ = tileZ + 0.5D - centerTileZ;
|
||||||
TileKey key = new TileKey(spec.contentRevision(), spec.type(), spec.heights(), renderScale, tileX, tileZ);
|
TileKey key = new TileKey(spec.contentRevision(), spec.layer(), spec.heights(), renderScale, tileX, tileZ);
|
||||||
tiles.add(new VisibleTile(key, deltaX * deltaX + deltaZ * deltaZ));
|
tiles.add(new VisibleTile(key, deltaX * deltaX + deltaZ * deltaZ));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,7 +302,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
key.tileZ() * tileSpan,
|
key.tileZ() * tileSpan,
|
||||||
key.blocksPerPixel(),
|
key.blocksPerPixel(),
|
||||||
TILE_PIXELS,
|
TILE_PIXELS,
|
||||||
key.type(),
|
key.layer(),
|
||||||
key.heights(),
|
key.heights(),
|
||||||
() -> !isWanted(key)
|
() -> !isWanted(key)
|
||||||
);
|
);
|
||||||
@@ -337,7 +337,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
long parentX = Math.floorDiv(key.tileX(), scale);
|
long parentX = Math.floorDiv(key.tileX(), scale);
|
||||||
long parentZ = Math.floorDiv(key.tileZ(), scale);
|
long parentZ = Math.floorDiv(key.tileZ(), scale);
|
||||||
MapTile parent = cache.get(new TileKey(key.contentRevision(), key.type(), key.heights(), parentScale, parentX,
|
MapTile parent = cache.get(new TileKey(key.contentRevision(), key.layer(), key.heights(), parentScale, parentX,
|
||||||
parentZ));
|
parentZ));
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
int sourceX = (int) (key.tileX() - parentX * scale) * sourceSize;
|
int sourceX = (int) (key.tileX() - parentX * scale) * sourceSize;
|
||||||
@@ -357,7 +357,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
List<FallbackPiece> pieces = new ArrayList<>(4);
|
List<FallbackPiece> pieces = new ArrayList<>(4);
|
||||||
for (int childZ = 0; childZ < 2; childZ++) {
|
for (int childZ = 0; childZ < 2; childZ++) {
|
||||||
for (int childX = 0; childX < 2; childX++) {
|
for (int childX = 0; childX < 2; childX++) {
|
||||||
MapTile child = cache.get(new TileKey(key.contentRevision(), key.type(), key.heights(), childScale,
|
MapTile child = cache.get(new TileKey(key.contentRevision(), key.layer(), key.heights(), childScale,
|
||||||
key.tileX() * 2L + childX, key.tileZ() * 2L + childZ));
|
key.tileX() * 2L + childX, key.tileZ() * 2L + childZ));
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
pieces.add(new FallbackPiece(child, 0, 0, TILE_PIXELS, childX * 0.5D, childZ * 0.5D, 0.5D));
|
pieces.add(new FallbackPiece(child, 0, 0, TILE_PIXELS, childX * 0.5D, childZ * 0.5D, 0.5D));
|
||||||
@@ -420,7 +420,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
|
|
||||||
record RenderSpec(
|
record RenderSpec(
|
||||||
IrisRenderer renderer,
|
IrisRenderer renderer,
|
||||||
RenderType type,
|
MapLayer layer,
|
||||||
long contentRevision,
|
long contentRevision,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerZ,
|
double centerZ,
|
||||||
@@ -431,7 +431,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
) {
|
) {
|
||||||
RenderSpec {
|
RenderSpec {
|
||||||
Objects.requireNonNull(renderer, "renderer");
|
Objects.requireNonNull(renderer, "renderer");
|
||||||
Objects.requireNonNull(type, "type");
|
Objects.requireNonNull(layer, "layer");
|
||||||
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||||
throw new IllegalArgumentException("Vision center must be finite");
|
throw new IllegalArgumentException("Vision center must be finite");
|
||||||
}
|
}
|
||||||
@@ -492,7 +492,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
double tileSpan = TILE_PIXELS * renderBlocksPerPixel;
|
double tileSpan = TILE_PIXELS * renderBlocksPerPixel;
|
||||||
long tileX = (long) Math.floor(worldX / tileSpan);
|
long tileX = (long) Math.floor(worldX / tileSpan);
|
||||||
long tileZ = (long) Math.floor(worldZ / tileSpan);
|
long tileZ = (long) Math.floor(worldZ / tileSpan);
|
||||||
VisibleTile visible = tilesByKey.get(new TileKey(spec.contentRevision(), spec.type(), spec.heights(),
|
VisibleTile visible = tilesByKey.get(new TileKey(spec.contentRevision(), spec.layer(), spec.heights(),
|
||||||
renderBlocksPerPixel, tileX, tileZ));
|
renderBlocksPerPixel, tileX, tileZ));
|
||||||
MapTile tile = visible == null ? null : visible.tile();
|
MapTile tile = visible == null ? null : visible.tile();
|
||||||
if (tile == null) {
|
if (tile == null) {
|
||||||
@@ -563,16 +563,16 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
/**
|
/**
|
||||||
* @param heights whether the tile also carries terrain heights, for shading
|
* @param heights whether the tile also carries terrain heights, for shading
|
||||||
*/
|
*/
|
||||||
record TileKey(long contentRevision, RenderType type, boolean heights, double blocksPerPixel, long tileX, long tileZ) {
|
record TileKey(long contentRevision, MapLayer layer, boolean heights, double blocksPerPixel, long tileX, long tileZ) {
|
||||||
TileKey {
|
TileKey {
|
||||||
Objects.requireNonNull(type, "type");
|
Objects.requireNonNull(layer, "layer");
|
||||||
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||||
throw new IllegalArgumentException("Vision cache scale must be finite and positive");
|
throw new IllegalArgumentException("Vision cache scale must be finite and positive");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TileKey withHeights(boolean nextHeights) {
|
TileKey withHeights(boolean nextHeights) {
|
||||||
return new TileKey(contentRevision, type, nextHeights, blocksPerPixel, tileX, tileZ);
|
return new TileKey(contentRevision, layer, nextHeights, blocksPerPixel, tileX, tileZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public final class IrisRenderer {
|
|||||||
double startZ,
|
double startZ,
|
||||||
double blocksPerPixel,
|
double blocksPerPixel,
|
||||||
int size,
|
int size,
|
||||||
RenderType type,
|
MapLayer layer,
|
||||||
boolean withHeights,
|
boolean withHeights,
|
||||||
BooleanSupplier cancelled
|
BooleanSupplier cancelled
|
||||||
) {
|
) {
|
||||||
@@ -86,12 +86,13 @@ public final class IrisRenderer {
|
|||||||
if (size < 1) {
|
if (size < 1) {
|
||||||
throw new IllegalArgumentException("Map tile size must be positive");
|
throw new IllegalArgumentException("Map tile size must be positive");
|
||||||
}
|
}
|
||||||
Objects.requireNonNull(type, "type");
|
Objects.requireNonNull(layer, "layer");
|
||||||
Objects.requireNonNull(cancelled, "cancelled");
|
Objects.requireNonNull(cancelled, "cancelled");
|
||||||
|
RenderType type = layer.type();
|
||||||
checkCancelled(cancelled);
|
checkCancelled(cancelled);
|
||||||
|
|
||||||
TileGrid grid = new TileGrid(size + 2, startX - blocksPerPixel, startZ - blocksPerPixel, blocksPerPixel, cancelled);
|
TileGrid grid = new TileGrid(size + 2, startX - blocksPerPixel, startZ - blocksPerPixel, blocksPerPixel, cancelled);
|
||||||
int[] identities = sampleIdentities(grid, type);
|
int[] identities = sampleIdentities(grid, layer);
|
||||||
double[] heights = withHeights || type == RenderType.HEIGHT ? sampleHeights(grid) : null;
|
double[] heights = withHeights || type == RenderType.HEIGHT ? sampleHeights(grid) : null;
|
||||||
short[] subjects = new short[identities.length];
|
short[] subjects = new short[identities.length];
|
||||||
short[] regions = new short[identities.length];
|
short[] regions = new short[identities.length];
|
||||||
@@ -139,8 +140,8 @@ public final class IrisRenderer {
|
|||||||
return blend(ROCK, SNOW, (land - 0.78D) / 0.22D);
|
return blend(ROCK, SNOW, (land - 0.78D) / 0.22D);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] sampleIdentities(TileGrid grid, RenderType type) {
|
private int[] sampleIdentities(TileGrid grid, MapLayer layer) {
|
||||||
IdentitySampler sampler = new IdentitySampler(grid, identityShader(type));
|
IdentitySampler sampler = new IdentitySampler(grid, identityShader(layer));
|
||||||
sampler.renderAll(sampleBlockPixels(grid.step()));
|
sampler.renderAll(sampleBlockPixels(grid.step()));
|
||||||
return sampler.identities;
|
return sampler.identities;
|
||||||
}
|
}
|
||||||
@@ -155,26 +156,41 @@ public final class IrisRenderer {
|
|||||||
return Integer.highestOneBit(Math.max(1, Math.min(LARGEST_SAMPLE_BLOCK_PIXELS, largestPixels)));
|
return Integer.highestOneBit(Math.max(1, Math.min(LARGEST_SAMPLE_BLOCK_PIXELS, largestPixels)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IdentityShader identityShader(RenderType type) {
|
private IdentityShader identityShader(MapLayer layer) {
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
ProceduralStream<IrisRegion> regionStream = complex.getRegionStream();
|
ProceduralStream<IrisRegion> regionStream = complex.getRegionStream();
|
||||||
LabelLookup lookup = new LabelLookup(labels);
|
LabelLookup lookup = new LabelLookup(labels);
|
||||||
if (type == RenderType.REGION) {
|
if (layer.type() == RenderType.REGION) {
|
||||||
return (double x, double z) -> {
|
return (double x, double z) -> {
|
||||||
short region = lookup.idOf(regionStream.get(x, z));
|
short region = lookup.idOf(regionStream.get(x, z));
|
||||||
return identity(region, region);
|
return identity(region, region);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ProceduralStream<IrisBiome> biomeStream = subjectBiomeStream(complex, type);
|
if (layer.type() == RenderType.REGION_PREVIEW) {
|
||||||
|
return regionPreviewShader(complex, layer.previewRegionKey(), lookup);
|
||||||
|
}
|
||||||
|
ProceduralStream<IrisBiome> biomeStream = subjectBiomeStream(complex, layer.type());
|
||||||
return (double x, double z) -> identity(lookup.idOf(biomeStream.get(x, z)), lookup.idOf(regionStream.get(x, z)));
|
return (double x, double z) -> identity(lookup.idOf(biomeStream.get(x, z)), lookup.idOf(regionStream.get(x, z)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every spot is treated as part of the previewed region, so the tile shows the biomes that region places there.
|
||||||
|
private IdentityShader regionPreviewShader(IrisComplex complex, String regionKey, LabelLookup lookup) {
|
||||||
|
IrisRegion region = engine.getData().getRegionLoader().load(regionKey);
|
||||||
|
if (region == null) {
|
||||||
|
return (double x, double z) -> identity(MapLabels.NONE, MapLabels.NONE);
|
||||||
|
}
|
||||||
|
short regionId = lookup.idOf(region);
|
||||||
|
return (double x, double z) -> identity(lookup.idOf(complex.surfaceBiomeAsIfIn(region, x, z)), regionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hybrid terrain decides land, coast and sea by height after the biomes are chosen, so the final biome tells.
|
||||||
private static ProceduralStream<IrisBiome> subjectBiomeStream(IrisComplex complex, RenderType type) {
|
private static ProceduralStream<IrisBiome> subjectBiomeStream(IrisComplex complex, RenderType type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream();
|
case BIOME_LAND -> complex.getLandBiomeStream();
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream();
|
case BIOME_SEA -> complex.getSeaBiomeStream();
|
||||||
case CAVE_LAND -> complex.getCaveBiomeStream();
|
case CAVE_LAND -> complex.getCaveBiomeStream();
|
||||||
case BIOME, HEIGHT, CONTINENT, REGION -> complex.getBaseBiomeStream();
|
case CONTINENT -> complex.isHybridTerrain() ? complex.getTrueBiomeStream() : complex.getBaseBiomeStream();
|
||||||
|
case BIOME, HEIGHT, REGION, REGION_PREVIEW -> complex.getBaseBiomeStream();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +227,7 @@ public final class IrisRenderer {
|
|||||||
|
|
||||||
private int subjectColor(Object subject, RenderType type) {
|
private int subjectColor(Object subject, RenderType type) {
|
||||||
if (type == RenderType.CONTINENT && subject instanceof IrisBiome biome) {
|
if (type == RenderType.CONTINENT && subject instanceof IrisBiome biome) {
|
||||||
return continentColor(biome);
|
return engine.getComplex().isHybridTerrain() ? continentColorByPlacement(biome) : continentColor(biome);
|
||||||
}
|
}
|
||||||
if (subject instanceof IrisRegion region) {
|
if (subject instanceof IrisRegion region) {
|
||||||
return region.getColor(engine.getComplex(), type).getRGB() & 0xFFFFFF;
|
return region.getColor(engine.getComplex(), type).getRGB() & 0xFFFFFF;
|
||||||
@@ -237,6 +253,13 @@ public final class IrisRenderer {
|
|||||||
return GREEN;
|
return GREEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int continentColorByPlacement(IrisBiome biome) {
|
||||||
|
if (biome.isShore()) {
|
||||||
|
return YELLOW;
|
||||||
|
}
|
||||||
|
return biome.isSea() ? BLUE : GREEN;
|
||||||
|
}
|
||||||
|
|
||||||
private static int identity(short subject, short region) {
|
private static int identity(short subject, short region) {
|
||||||
return (subject << 16) | (region & 0xFFFF);
|
return (subject << 16) | (region & 0xFFFF);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.components;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a map tile shows: one of the render types, and for a region preview which region every spot is treated as.
|
||||||
|
*
|
||||||
|
* @param previewRegionKey the region a {@link RenderType#REGION_PREVIEW} tile shows everywhere, null for every other type
|
||||||
|
*/
|
||||||
|
public record MapLayer(RenderType type, String previewRegionKey) {
|
||||||
|
public MapLayer {
|
||||||
|
Objects.requireNonNull(type, "type");
|
||||||
|
if ((type == RenderType.REGION_PREVIEW) != (previewRegionKey != null)) {
|
||||||
|
throw new IllegalArgumentException("Only a region preview names a region");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MapLayer of(RenderType type) {
|
||||||
|
return new MapLayer(type, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MapLayer regionPreview(String regionKey) {
|
||||||
|
return new MapLayer(RenderType.REGION_PREVIEW, Objects.requireNonNull(regionKey, "regionKey"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,5 +19,5 @@
|
|||||||
package com.volmit.iris.core.gui.components;
|
package com.volmit.iris.core.gui.components;
|
||||||
|
|
||||||
public enum RenderType {
|
public enum RenderType {
|
||||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, CONTINENT
|
BIOME, BIOME_LAND, BIOME_SEA, REGION, REGION_PREVIEW, CAVE_LAND, HEIGHT, CONTINENT
|
||||||
}
|
}
|
||||||
@@ -41,20 +41,17 @@ public class HybridBiomeSelector {
|
|||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisBiome select(List<IrisBiome> candidates, double placementHeight, double x, double z) {
|
public Choice choose(List<IrisBiome> candidates, double placementHeight, double x, double z) {
|
||||||
IrisBiome strongest = strongestAllowedBiome(candidates, placementHeight, x, z);
|
|
||||||
return strongest != null ? strongest : closestBiomeByHeight(candidates, placementHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IrisBiome strongestAllowedBiome(List<IrisBiome> candidates, double placementHeight, double x, double z) {
|
|
||||||
IrisBiome strongest = null;
|
IrisBiome strongest = null;
|
||||||
double strongestScore = Double.NEGATIVE_INFINITY;
|
double strongestScore = Double.NEGATIVE_INFINITY;
|
||||||
|
int allowedCount = 0;
|
||||||
|
|
||||||
for (IrisBiome biome : candidates) {
|
for (IrisBiome biome : candidates) {
|
||||||
if (!biome.getPlacement().allowsHeight(placementHeight)) {
|
if (!biome.getPlacement().allowsHeight(placementHeight)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowedCount++;
|
||||||
double score = competitionScore(biome, x, z);
|
double score = competitionScore(biome, x, z);
|
||||||
|
|
||||||
if (score > strongestScore) {
|
if (score > strongestScore) {
|
||||||
@@ -63,7 +60,11 @@ public class HybridBiomeSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strongest;
|
if (strongest == null) {
|
||||||
|
return new Choice(closestBiomeByHeight(candidates, placementHeight), placementHeight, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Choice(strongest, placementHeight, allowedCount - 1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double competitionScore(IrisBiome biome, double x, double z) {
|
private double competitionScore(IrisBiome biome, double x, double z) {
|
||||||
@@ -91,4 +92,11 @@ public class HybridBiomeSelector {
|
|||||||
|
|
||||||
return closest;
|
return closest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param rivalCount how many other biomes allowed this height and lost the competition
|
||||||
|
* @param fallback whether no biome allowed this height, so the one with the closest range was taken
|
||||||
|
*/
|
||||||
|
public record Choice(IrisBiome biome, double placementHeight, int rivalCount, boolean fallback) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
private static final List<InferredType> HYBRID_PLACED_TYPES = List.of(InferredType.LAND, InferredType.SEA, InferredType.SHORE);
|
private static final List<InferredType> HYBRID_PLACED_TYPES = List.of(InferredType.LAND, InferredType.SEA, InferredType.SHORE);
|
||||||
private RNG rng;
|
private RNG rng;
|
||||||
private double fluidHeight;
|
private double fluidHeight;
|
||||||
|
private IrisDimension dimension;
|
||||||
|
private boolean hybridTerrain;
|
||||||
|
private boolean legacyRarity;
|
||||||
private IrisData data;
|
private IrisData data;
|
||||||
private Map<IrisInterpolator, Set<IrisGenerator>> generators;
|
private Map<IrisInterpolator, Set<IrisGenerator>> generators;
|
||||||
private ProceduralStream<IrisRegion> regionStream;
|
private ProceduralStream<IrisRegion> regionStream;
|
||||||
@@ -99,6 +102,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
this.data = engine.getData();
|
this.data = engine.getData();
|
||||||
double height = engine.getMaxHeight();
|
double height = engine.getMaxHeight();
|
||||||
fluidHeight = engine.getDimension().getFluidHeight();
|
fluidHeight = engine.getDimension().getFluidHeight();
|
||||||
|
dimension = engine.getDimension();
|
||||||
|
hybridTerrain = dimension.getTerrainMode() == IrisTerrainMode.HYBRID;
|
||||||
|
legacyRarity = dimension.isLegacyRarity();
|
||||||
generators = new HashMap<>();
|
generators = new HashMap<>();
|
||||||
focusBiome = engine.getFocus();
|
focusBiome = engine.getFocus();
|
||||||
focusRegion = engine.getFocusRegion();
|
focusRegion = engine.getFocusRegion();
|
||||||
@@ -135,7 +141,6 @@ public class IrisComplex implements DataProvider {
|
|||||||
: regionStyleStream
|
: regionStyleStream
|
||||||
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()), legacy)
|
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()), legacy)
|
||||||
.cache2D("regionStream", engine, cacheSize).waste("Region Stream");
|
.cache2D("regionStream", engine, cacheSize).waste("Region Stream");
|
||||||
boolean hybridTerrain = engine.getDimension().getTerrainMode() == IrisTerrainMode.HYBRID;
|
|
||||||
if (hybridTerrain) {
|
if (hybridTerrain) {
|
||||||
prepareHybridBiomePlacement(engine, cacheSize);
|
prepareHybridBiomePlacement(engine, cacheSize);
|
||||||
}
|
}
|
||||||
@@ -150,33 +155,11 @@ public class IrisComplex implements DataProvider {
|
|||||||
.onNull(emptyBiome)
|
.onNull(emptyBiome)
|
||||||
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
|
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
|
||||||
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
||||||
landBiomeStream = hybridTerrain ? createHybridBiomeStream(engine, InferredType.LAND, cacheSize) : regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
|
landBiomeStream = createRegionBiomeStream(engine, InferredType.LAND, cacheSize);
|
||||||
.convert((r)
|
|
||||||
-> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
|
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
|
||||||
.zoom(engine.getDimension().getLandZoom())
|
|
||||||
.zoom(r.getLandBiomeZoom())
|
|
||||||
.selectRarity(data.getBiomeLoader().loadAll(r.getLandBiomes(), (t) -> t.setInferredType(InferredType.LAND)), legacy)
|
|
||||||
).convertAware2D(ProceduralStream::get)
|
|
||||||
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
|
|
||||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||||
seaBiomeStream = hybridTerrain ? createHybridBiomeStream(engine, InferredType.SEA, cacheSize) : regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
|
seaBiomeStream = createRegionBiomeStream(engine, InferredType.SEA, cacheSize);
|
||||||
.convert((r)
|
|
||||||
-> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
|
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
|
||||||
.zoom(engine.getDimension().getSeaZoom())
|
|
||||||
.zoom(r.getSeaBiomeZoom())
|
|
||||||
.selectRarity(data.getBiomeLoader().loadAll(r.getSeaBiomes(), (t) -> t.setInferredType(InferredType.SEA)), legacy)
|
|
||||||
).convertAware2D(ProceduralStream::get)
|
|
||||||
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
|
|
||||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||||
shoreBiomeStream = hybridTerrain ? createHybridBiomeStream(engine, InferredType.SHORE, cacheSize) : regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
|
shoreBiomeStream = createRegionBiomeStream(engine, InferredType.SHORE, cacheSize);
|
||||||
.convert((r)
|
|
||||||
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
|
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
|
||||||
.zoom(r.getShoreBiomeZoom())
|
|
||||||
.selectRarity(data.getBiomeLoader().loadAll(r.getShoreBiomes(), (t) -> t.setInferredType(InferredType.SHORE)), legacy)
|
|
||||||
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream");
|
|
||||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||||
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
|
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
|
||||||
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
|
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
|
||||||
@@ -203,7 +186,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
||||||
.convertAware2D((h, x, z) ->
|
.convertAware2D((h, x, z) ->
|
||||||
fixBiomeType(h, baseBiomeStream.get(x, z),
|
fixBiomeType(h, baseBiomeStream.get(x, z),
|
||||||
regionStream.contextInjecting((c, xx, zz) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
|
regionStream.contextInjecting((c, xx, zz) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(xx, zz)).get(x, z), x, z,
|
||||||
|
(type, xx, zz) -> getBiomeStream(type).get(xx, zz)))
|
||||||
.cache2D("trueBiomeStream", engine, cacheSize).waste("True Biome Stream");
|
.cache2D("trueBiomeStream", engine, cacheSize).waste("True Biome Stream");
|
||||||
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
|
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getBiome().get(x, z))
|
||||||
.convert(IrisBiome::getDerivative).cache2D("trueBiomeDerivativeStream", engine, cacheSize).waste("True Biome Derivative Stream");
|
.convert(IrisBiome::getDerivative).cache2D("trueBiomeDerivativeStream", engine, cacheSize).waste("True Biome Derivative Stream");
|
||||||
@@ -274,17 +258,74 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProceduralStream<IrisBiome> createHybridBiomeStream(Engine engine, InferredType type, int cacheSize) {
|
private ProceduralStream<IrisBiome> createRegionBiomeStream(Engine engine, InferredType type, int cacheSize) {
|
||||||
String name = type.name().toLowerCase() + "BiomeStream";
|
String typeName = type.name().charAt(0) + type.name().substring(1).toLowerCase(Locale.ROOT);
|
||||||
return regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
|
return regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
|
||||||
.convertAware2D((region, x, z) -> {
|
.convertAware2D((region, x, z) -> biomeInRegion(region, type, x, z))
|
||||||
double zoom = hybridBiomeZoom(engine.getDimension(), region, type);
|
.cache2D(type.name().toLowerCase(Locale.ROOT) + "BiomeStream", engine, cacheSize)
|
||||||
return hybridBiomeSelector.select(region.getBiomes(this, type), placementHeightStream.get(x, z), x / zoom, z / zoom);
|
.waste(typeName + " Biome Stream");
|
||||||
})
|
|
||||||
.cache2D(name, engine, cacheSize).waste(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double hybridBiomeZoom(IrisDimension dimension, IrisRegion region, InferredType type) {
|
/**
|
||||||
|
* The surface biome a spot would get if it belonged to the given region, for previewing one region across the map.
|
||||||
|
*/
|
||||||
|
public IrisBiome surfaceBiomeAsIfIn(IrisRegion region, double x, double z) {
|
||||||
|
if (focusBiome != null) {
|
||||||
|
return focusBiome;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisBiome base = implode(biomeInRegion(region, bridgeStream.get(x, z), x, z), x, z);
|
||||||
|
return fixBiomeType(heightStream.get(x, z), base, region, x, z, (type, xx, zz) -> biomeInRegion(region, type, xx, zz));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Why HYBRID terrain gives a spot its base biome when the spot belongs to the given region; null for BIOME terrain.
|
||||||
|
*/
|
||||||
|
public HybridBiomeSelector.Choice explainHybridChoice(IrisRegion region, double x, double z) {
|
||||||
|
if (!hybridTerrain || focusBiome != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chooseHybridBiome(region, bridgeStream.get(x, z), x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisBiome biomeInRegion(IrisRegion region, InferredType type, double x, double z) {
|
||||||
|
if (hybridTerrain) {
|
||||||
|
return chooseHybridBiome(region, type, x, z).biome();
|
||||||
|
}
|
||||||
|
|
||||||
|
return noiseBiomeStream(region, type).get(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HybridBiomeSelector.Choice chooseHybridBiome(IrisRegion region, InferredType type, double x, double z) {
|
||||||
|
double zoom = hybridBiomeZoom(region, type);
|
||||||
|
return hybridBiomeSelector.choose(region.getBiomes(this, type), placementHeightStream.get(x, z), x / zoom, z / zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The zooms stay chained one by one as they always were, so BIOME terrain keeps placing exactly the same biomes.
|
||||||
|
private ProceduralStream<IrisBiome> noiseBiomeStream(IrisRegion region, InferredType type) {
|
||||||
|
ProceduralStream<Double> noise = dimension.getBiomeStyle(type).create(rng.nextParallelRNG(type.ordinal()), getData()).stream()
|
||||||
|
.zoom(dimension.getBiomeZoom());
|
||||||
|
if (type == InferredType.LAND) {
|
||||||
|
noise = noise.zoom(dimension.getLandZoom());
|
||||||
|
} else if (type == InferredType.SEA) {
|
||||||
|
noise = noise.zoom(dimension.getSeaZoom());
|
||||||
|
}
|
||||||
|
|
||||||
|
return noise.zoom(region.getBiomeZoom(type))
|
||||||
|
.selectRarity(data.getBiomeLoader().loadAll(biomeKeys(region, type), biome -> biome.setInferredType(type)), legacyRarity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KList<String> biomeKeys(IrisRegion region, InferredType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case LAND -> region.getLandBiomes();
|
||||||
|
case SEA -> region.getSeaBiomes();
|
||||||
|
case SHORE -> region.getShoreBiomes();
|
||||||
|
default -> region.getCaveBiomes();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private double hybridBiomeZoom(IrisRegion region, InferredType type) {
|
||||||
double typeZoom = switch (type) {
|
double typeZoom = switch (type) {
|
||||||
case LAND -> dimension.getLandZoom();
|
case LAND -> dimension.getLandZoom();
|
||||||
case SEA -> dimension.getSeaZoom();
|
case SEA -> dimension.getSeaZoom();
|
||||||
@@ -334,23 +375,23 @@ public class IrisComplex implements DataProvider {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
private IrisBiome fixBiomeType(double height, IrisBiome biome, IrisRegion region, double x, double z, BiomeOfType biomeOfType) {
|
||||||
double sh = region.getShoreHeight(x, z);
|
double sh = region.getShoreHeight(x, z);
|
||||||
|
|
||||||
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
|
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
|
||||||
return shoreBiomeStream.get(x, z);
|
return biomeOfType.at(InferredType.SHORE, x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height > fluidHeight + sh && !biome.isLand()) {
|
if (height > fluidHeight + sh && !biome.isLand()) {
|
||||||
return landBiomeStream.get(x, z);
|
return biomeOfType.at(InferredType.LAND, x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height < fluidHeight && !biome.isAquatic()) {
|
if (height < fluidHeight && !biome.isAquatic()) {
|
||||||
return seaBiomeStream.get(x, z);
|
return biomeOfType.at(InferredType.SEA, x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height == fluidHeight && !biome.isShore()) {
|
if (height == fluidHeight && !biome.isShore()) {
|
||||||
return shoreBiomeStream.get(x, z);
|
return biomeOfType.at(InferredType.SHORE, x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
return biome;
|
return biome;
|
||||||
@@ -464,4 +505,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
public void close() {
|
public void close() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface BiomeOfType {
|
||||||
|
IrisBiome at(InferredType type, double x, double z);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -666,7 +666,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
|
|
||||||
public Color getColor(Engine engine, RenderType type) {
|
public Color getColor(Engine engine, RenderType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> {
|
case BIOME, HEIGHT, CAVE_LAND, REGION, REGION_PREVIEW, BIOME_SEA, BIOME_LAND -> {
|
||||||
return this.cacheColor.aquire(() -> {
|
return this.cacheColor.aquire(() -> {
|
||||||
if (this.color == null) {
|
if (this.color == null) {
|
||||||
RandomColor randomColor = new RandomColor(getName().hashCode());
|
RandomColor randomColor = new RandomColor(getName().hashCode());
|
||||||
|
|||||||
Reference in new issue
Block a user