Give procedural objects their own tab

Variant picker, grouped by type.
This commit is contained in:
piexel committed 2026-10-20 12:37:39 +02:00
1 parent 8d12019246
commit dc7615219b
12 files changed
+420 -116

No files matched your search

@@ -40,6 +40,8 @@ enum MapView {
Group.BLOCKS, null, KeyEvent.VK_P),
OBJECTS("Objects", "Every object in the pack in 3D, with the blocks it is made of",
Group.BLOCKS, null, KeyEvent.VK_O),
PROCEDURAL("Procedural", "Trees, rock formations and more that Iris generates from settings, every variant in 3D",
Group.BLOCKS, null, KeyEvent.VK_T),
NOISE("Noise", "The pack's noise generators and Iris' built-in noise styles",
Group.GENERATORS, null, KeyEvent.VK_N);
@@ -70,7 +72,7 @@ enum MapView {
}
/**
* The tile renderer's mode for this view, or null for the palette, object and noise views, which draw themselves.
* The tile renderer's mode for this view, or null for the palette, object, procedural and noise views, which draw themselves.
*/
RenderType renderType() {
return renderType;
@@ -46,7 +46,9 @@ import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.function.IntConsumer;
/**
@@ -56,7 +58,6 @@ import java.util.function.IntConsumer;
public final class MapWindow implements MapCanvas.Listener {
private static final String MAP_CARD = "map";
private static final String PALETTES_CARD = "palettes";
private static final String OBJECTS_CARD = "objects";
private static final String NOISE_CARD = "noise";
private static final int SIDEBAR_WIDTH = 270;
private static final int SIDEBAR_MINIMUM_WIDTH = 200;
@@ -91,7 +92,7 @@ public final class MapWindow implements MapCanvas.Listener {
private final MapLegendPanel legendPanel;
private NoiseExplorerGUI paletteExplorer;
private ObjectExplorer objectExplorer;
private final Map<MapView, ObjectExplorer> objectExplorers = new EnumMap<>(MapView.class);
private NoiseExplorerGUI noiseExplorer;
private JComponent noiseSidebar;
private MapView currentView;
@@ -204,8 +205,8 @@ public final class MapWindow implements MapCanvas.Listener {
showMap(previousView, view);
} else if (view == MapView.PALETTES) {
showPalettes(previousView);
} else if (view == MapView.OBJECTS) {
showObjects();
} else if (view == MapView.OBJECTS || view == MapView.PROCEDURAL) {
showObjectExplorer(view);
} else {
showNoise();
}
@@ -246,20 +247,18 @@ public final class MapWindow implements MapCanvas.Listener {
}
}
private void showObjects() {
boolean firstOpen = objectExplorer == null;
if (firstOpen) {
createObjectExplorer();
}
private void showObjectExplorer(MapView view) {
boolean firstOpen = !objectExplorers.containsKey(view);
ObjectExplorer explorer = objectExplorers.computeIfAbsent(view, this::createObjectExplorer);
canvas.setActive(false);
showControls(explorerControls);
useSidebar(viewListAbove(objectExplorer.browser()));
useSidebar(viewListAbove(explorer.browser()));
inspectorSlot.removeAll();
inspectorSlot.add(objectExplorer.overview(), BorderLayout.CENTER);
showCard(OBJECTS_CARD);
inspectorSlot.add(explorer.overview(), BorderLayout.CENTER);
showCard(view.name());
EventQueue.invokeLater(() -> viewList.select(currentView));
if (firstOpen) {
objectExplorer.open();
explorer.open();
}
}
@@ -273,11 +272,13 @@ public final class MapWindow implements MapCanvas.Listener {
});
}
private void createObjectExplorer() {
objectExplorer = new ObjectExplorer(start.engine(), start.worldId());
keepClearOfIsland(objectExplorer.viewport()::setCoveredLeftWidth);
content.add(objectExplorer.view(), OBJECTS_CARD);
statusBars.add(objectExplorer.statusBar(), OBJECTS_CARD);
private ObjectExplorer createObjectExplorer(MapView view) {
ObjectSource source = view == MapView.PROCEDURAL ? new ProceduralObjectSource() : new PackObjectSource();
ObjectExplorer explorer = new ObjectExplorer(start.engine(), start.worldId(), source);
keepClearOfIsland(explorer::setCoveredLeftWidth);
content.add(explorer.view(), view.name());
statusBars.add(explorer.statusBar(), view.name());
return explorer;
}
private void showNoise() {
@@ -391,7 +392,8 @@ public final class MapWindow implements MapCanvas.Listener {
}
private void resetExplorerView() {
if (currentView == MapView.OBJECTS) {
ObjectExplorer objectExplorer = objectExplorers.get(currentView);
if (objectExplorer != null) {
objectExplorer.resetCamera();
return;
}
@@ -487,9 +489,7 @@ public final class MapWindow implements MapCanvas.Listener {
if (paletteExplorer != null) {
paletteExplorer.close();
}
if (objectExplorer != null) {
objectExplorer.close();
}
objectExplorers.values().forEach(ObjectExplorer::close);
if (noiseExplorer != null) {
noiseExplorer.close();
}
@@ -51,15 +51,21 @@ final class ObjectBrowser extends JPanel {
private final JTextField searchField = new JTextField();
private final DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
private final JTree tree = new JTree(treeModel);
private final JLabel countLabel = new JLabel("Loading objects...");
private final JLabel countLabel = new JLabel();
private final String noun;
private List<String> objectKeys = List.of();
private String chosenKey;
private boolean updatingSelection;
ObjectBrowser(Consumer<String> onObjectChosen) {
/**
* @param noun what one entry is called in the browser's texts, such as "object"
*/
ObjectBrowser(String noun, Consumer<String> onObjectChosen) {
super(new BorderLayout());
this.noun = noun;
this.onObjectChosen = onObjectChosen;
StudioToolbar.makeSearchField(searchField, "Search objects...");
countLabel.setText("Loading " + noun + "s...");
StudioToolbar.makeSearchField(searchField, "Search " + noun + "s...");
searchField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent event) {
@@ -97,7 +103,7 @@ final class ObjectBrowser extends JPanel {
void showObjects(List<String> keys) {
objectKeys = keys.stream().sorted(Comparator.comparing(key -> key.toLowerCase(Locale.ROOT))).toList();
countLabel.setText(objectKeys.size() == 1 ? "1 object" : objectKeys.size() + " objects");
countLabel.setText(objectKeys.size() == 1 ? "1 " + noun : objectKeys.size() + " " + noun + "s");
rebuildTree();
}
@@ -128,7 +134,7 @@ final class ObjectBrowser extends JPanel {
}
}
if (root.getChildCount() == 0) {
root.add(new DefaultMutableTreeNode(Entry.note("No matching objects")));
root.add(new DefaultMutableTreeNode(Entry.note("No matching " + noun + "s")));
}
return root;
}
@@ -19,10 +19,8 @@
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisObject;
import com.volmit.iris.engine.object.procedural.ProceduralObjectCatalog;
import org.bukkit.NamespacedKey;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector;
@@ -31,8 +29,6 @@ import javax.swing.JComponent;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -45,8 +41,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
/**
* The map window's Objects view: every object in the pack in a folder tree, the chosen one in 3D, and what it is made
* of. Inspired by the Procedural Objects workbench.
* The map window's Objects and Procedural views: everything its source offers in a folder tree, the chosen one in 3D,
* and what it is made of. Inspired by the Procedural Objects workbench.
*/
final class ObjectExplorer {
private static final NamespacedKey TINT_BIOME = NamespacedKey.minecraft("plains");
@@ -55,10 +51,12 @@ final class ObjectExplorer {
private final Engine startEngine;
private final UUID worldId;
private final ObjectBrowser browser = new ObjectBrowser(this::showObject);
private final ObjectSource source;
private final ObjectBrowser browser;
private final ObjectViewport viewport = new ObjectViewport();
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut());
private final JComponent viewWithLayerCut = layerCut.floatingOver(viewport);
private final ObjectVariantPicker variantPicker = new ObjectVariantPicker(this::showVariant);
private final JComponent viewWithOverlays = variantPicker.floatingOver(layerCut.floatingOver(viewport));
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
private final StudioStatusBar statusBar = new StudioStatusBar();
private final ExecutorService loader = Executors.newSingleThreadExecutor(runnable -> {
@@ -74,33 +72,39 @@ final class ObjectExplorer {
private volatile BlockTextures textures;
private volatile BiomeTintColors tintColors;
private String shownKey;
private List<IrisObject> shownVariants = List.of();
private LoadedObject shown;
private ObjectMesh wholeMesh;
private Set<String> seeThroughTypes = Set.of();
ObjectExplorer(Engine engine, UUID worldId) {
ObjectExplorer(Engine engine, UUID worldId, ObjectSource source) {
this.startEngine = engine;
this.worldId = worldId;
this.source = source;
this.browser = new ObjectBrowser(source.noun(), this::showObject);
GuiHost.interruptOnPluginShutdown(loader);
GuiHost.registerHotloadHook(hotloadHook);
statusBar.showPosition(CAMERA_HINT);
viewport.bindKey(KeyEvent.VK_PAGE_UP, "show-layer-above", () -> layerCut.stepBy(1));
viewport.bindKey(KeyEvent.VK_PAGE_DOWN, "hide-top-layer", () -> layerCut.stepBy(-1));
viewport.bindKey(KeyEvent.VK_OPEN_BRACKET, "previous-variant", () -> variantPicker.stepBy(-1));
viewport.bindKey(KeyEvent.VK_CLOSE_BRACKET, "next-variant", () -> variantPicker.stepBy(1));
}
JComponent browser() {
return browser;
}
ObjectViewport viewport() {
return viewport;
}
/**
* The 3D view with the slider that cuts away its top layers.
* The 3D view with the slider that cuts away its top layers and the bar that steps through variants.
*/
JComponent view() {
return viewWithLayerCut;
return viewWithOverlays;
}
void setCoveredLeftWidth(int width) {
viewport.setCoveredLeftWidth(width);
variantPicker.setCoveredLeftWidth(width);
}
JComponent overview() {
@@ -143,10 +147,7 @@ final class ObjectExplorer {
}
private List<String> objectKeys() {
IrisData data = engine().getData();
List<String> keys = new ArrayList<>(Arrays.asList(data.getObjectLoader().getPossibleKeys()));
keys.addAll(data.getProceduralObjects().variantKeys());
return keys;
return source.keys(engine().getData());
}
private void showCatalog(List<String> keys) {
@@ -154,9 +155,10 @@ final class ObjectExplorer {
browser.showObjects(keys);
String key = shownKey != null && keys.contains(shownKey) ? shownKey : browser.firstObject();
if (key == null) {
viewport.showMessage("This pack has no objects");
viewport.showMessage("This pack has no " + source.noun() + "s");
overview.clear();
layerCut.showHeight(1);
variantPicker.showVariants(0);
return;
}
browser.select(key);
@@ -165,31 +167,54 @@ final class ObjectExplorer {
private void showObject(String key) {
shownKey = key;
shown = null;
wholeMesh = null;
meshRevision.incrementAndGet();
long revision = objectRevision.incrementAndGet();
viewport.showMessage("Loading " + key + "...");
statusBar.showActivity("Loading " + key + "...");
shownVariants = List.of();
variantPicker.showVariants(0);
long revision = startLoading(key);
loader.execute(() -> {
if (revision != objectRevision.get()) {
return;
}
try {
LoadedObject loaded = load(key);
EventQueue.invokeLater(() -> showLoaded(revision, loaded));
List<IrisObject> variants = source.variants(engine().getData(), key);
if (variants.isEmpty()) {
throw new IllegalStateException("Nothing could be loaded for " + key);
}
LoadedObject loaded = load(variants.get(0));
EventQueue.invokeLater(() -> {
if (revision == objectRevision.get()) {
shownVariants = variants;
variantPicker.showVariants(variants.size());
}
showLoaded(revision, loaded);
});
} catch (RuntimeException error) {
EventQueue.invokeLater(() -> showFailure(revision, key, error));
}
});
}
private LoadedObject load(String key) {
IrisObject object = objectFor(key);
if (object == null) {
throw new IllegalStateException(ProceduralObjectCatalog.isVariantKey(key)
? "This variant has no blocks" : "The object file could not be read");
}
private void showVariant(int index) {
IrisObject variant = shownVariants.get(index);
long revision = startLoading(variant.getLoadKey());
loader.execute(() -> {
if (revision != objectRevision.get()) {
return;
}
LoadedObject loaded = load(variant);
EventQueue.invokeLater(() -> showLoaded(revision, loaded));
});
}
private long startLoading(String key) {
shown = null;
wholeMesh = null;
meshRevision.incrementAndGet();
viewport.showMessage("Loading " + key + "...");
statusBar.showActivity("Loading " + key + "...");
return objectRevision.incrementAndGet();
}
private LoadedObject load(IrisObject object) {
Map<BlockVector, BlockData> blocks = new HashMap<>();
object.getBlocks().forEach((position, block) -> {
if (!block.getMaterial().isAir()) {
@@ -197,17 +222,10 @@ final class ObjectExplorer {
}
});
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
ObjectSummary summary = ObjectSummary.of(key, mesh.bounds(), blocks.values(), this::iconOf);
ObjectSummary summary = ObjectSummary.of(object.getLoadKey(), mesh.bounds(), blocks.values(), this::iconOf);
return new LoadedObject(blocks, mesh, summary);
}
private IrisObject objectFor(String key) {
IrisData data = engine().getData();
if (ProceduralObjectCatalog.isVariantKey(key)) {
return data.getProceduralObjects().variant(key);
}
return data.getObjectLoader().load(key);
}
private void showLoaded(long revision, LoadedObject loaded) {
if (revision != objectRevision.get()) {
@@ -0,0 +1,38 @@
/*
* 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.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import java.util.List;
/**
* Where an object explorer gets what it shows: the pack's object files, or the objects Iris generates from settings.
* Both are called on the explorer's loader thread only.
*/
interface ObjectSource {
/** What one entry is called, such as "object", for the browser's texts. */
String noun();
/** The key of every entry, where a slash separates folders. */
List<String> keys(IrisData data);
/** The objects of the entry, one for an object file and one per variant for a procedural object; empty when none load. */
List<IrisObject> variants(IrisData data, String key);
}
@@ -0,0 +1,137 @@
/*
* 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.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
import java.util.function.IntConsumer;
/**
* A small bar floating at the bottom of the 3D view that steps through the variants of a procedural object. It hides
* itself for anything with a single variant.
*/
final class ObjectVariantPicker extends JPanel {
private static final long serialVersionUID = -2716301839488217455L;
private static final int BOTTOM_MARGIN = 14;
private static final int LABEL_WIDTH = 110;
private static final String SHORTCUT_HINT = " · [ and ]";
private final JLabel shownVariant = new JLabel(" ", SwingConstants.CENTER);
private final IntConsumer onVariantChosen;
private int variantCount;
private int shownIndex;
private int coveredLeftWidth;
/**
* @param onVariantChosen told the index of the variant to show whenever the user steps to another one
*/
ObjectVariantPicker(IntConsumer onVariantChosen) {
super(new BorderLayout());
this.onVariantChosen = onVariantChosen;
setOpaque(false);
setBorder(StudioTheme.emptyBorder(3, 4, 3, 4));
shownVariant.setFont(StudioTheme.smallFont());
shownVariant.setForeground(StudioTheme.TEXT);
shownVariant.setPreferredSize(new Dimension(StudioTheme.scale(LABEL_WIDTH), shownVariant.getPreferredSize().height));
add(StudioToolbar.button("‹", "Previous variant" + SHORTCUT_HINT, () -> stepBy(-1)), BorderLayout.WEST);
add(shownVariant, BorderLayout.CENTER);
add(StudioToolbar.button("›", "Next variant" + SHORTCUT_HINT, () -> stepBy(1)), BorderLayout.EAST);
showVariants(0);
}
/**
* The view with this bar floating over its bottom edge, centred on the part the left island leaves uncovered.
*/
JComponent floatingOver(JComponent view) {
ObjectVariantPicker picker = this;
JPanel stack = new JPanel(null) {
private static final long serialVersionUID = 1609842937565014832L;
// The bar overlaps the view, so the view may not repaint without the bar on top of it.
@Override
public boolean isOptimizedDrawingEnabled() {
return false;
}
@Override
public void doLayout() {
view.setBounds(0, 0, getWidth(), getHeight());
Dimension size = picker.getPreferredSize();
int centerX = coveredLeftWidth + (getWidth() - coveredLeftWidth) / 2;
picker.setBounds(centerX - size.width / 2, getHeight() - size.height - StudioTheme.scale(BOTTOM_MARGIN), size.width, size.height);
}
};
stack.add(picker);
stack.add(view);
return stack;
}
void setCoveredLeftWidth(int width) {
coveredLeftWidth = width;
if (getParent() != null) {
getParent().revalidate();
}
}
/**
* Starts again at the first of {@code count} variants without telling anyone, since the first is what loads anyway.
*/
void showVariants(int count) {
variantCount = count;
shownIndex = 0;
showLabel();
setVisible(count > 1);
}
void stepBy(int step) {
if (variantCount < 2) {
return;
}
shownIndex = Math.floorMod(shownIndex + step, variantCount);
showLabel();
onVariantChosen.accept(shownIndex);
}
private void showLabel() {
shownVariant.setText("Variant " + (shownIndex + 1) + " of " + variantCount);
}
@Override
protected void paintComponent(Graphics graphics) {
Graphics2D canvas = (Graphics2D) graphics.create();
try {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
double arc = StudioTheme.scale(StudioTheme.OVERLAY_ARC);
RoundRectangle2D shape = new RoundRectangle2D.Double(0.5, 0.5, getWidth() - 1D, getHeight() - 1D, arc, arc);
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
canvas.fill(shape);
canvas.setColor(StudioTheme.OVERLAY_BORDER);
canvas.draw(shape);
} finally {
canvas.dispose();
}
}
}
@@ -0,0 +1,45 @@
/*
* 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.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import java.util.Arrays;
import java.util.List;
/**
* The pack's object files, by their load keys.
*/
final class PackObjectSource implements ObjectSource {
@Override
public String noun() {
return "object";
}
@Override
public List<String> keys(IrisData data) {
return Arrays.asList(data.getObjectLoader().getPossibleKeys());
}
@Override
public List<IrisObject> variants(IrisData data, String key) {
IrisObject object = data.getObjectLoader().load(key);
return object == null ? List.of() : List.of(object);
}
}
@@ -0,0 +1,62 @@
/*
* 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.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* The objects the pack's procedural settings generate, one entry per definition in a folder per type.
*/
final class ProceduralObjectSource implements ObjectSource {
private static final Map<String, String> FOLDER_BY_KIND = Map.of(
"tree", "Trees",
"formation", "Formations",
"crystal", "Crystals",
"coral", "Coral",
"fungus", "Fungi",
"ruin", "Ruins");
private Map<String, ResolvedProceduralPlacement> definitionsByKey = Map.of();
@Override
public String noun() {
return "procedural object";
}
@Override
public List<String> keys(IrisData data) {
Map<String, ResolvedProceduralPlacement> byKey = new LinkedHashMap<>();
for (ResolvedProceduralPlacement definition : data.getProceduralObjects().definitions()) {
byKey.put(FOLDER_BY_KIND.getOrDefault(definition.kind(), definition.kind()) + "/" + definition.name(), definition);
}
definitionsByKey = byKey;
return List.copyOf(byKey.keySet());
}
@Override
public List<IrisObject> variants(IrisData data, String key) {
ResolvedProceduralPlacement definition = definitionsByKey.get(key);
return definition == null ? List.of() : definition.variants();
}
}
@@ -20,7 +20,6 @@ package com.volmit.iris.engine.object.procedural;
import com.google.gson.Gson;
import com.volmit.iris.Iris;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.object.IrisObject;
import com.volmit.iris.engine.object.annotations.Snippet;
import com.volmit.iris.engine.object.procedural.coral.Coral;
import com.volmit.iris.engine.object.procedural.coral.ResolvedCoral;
@@ -41,15 +40,14 @@ import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.stream.Stream;
/**
* The procedural objects of one pack: those its regions and biomes declare, and those saved on their own as snippets,
* such as {@code snippet/procedural-tree/oak.json}. A definition several places share is baked once, and every variant
* can be found again by its key.
* such as {@code snippet/procedural-tree/oak.json}. A definition several places share is baked once.
*/
public final class ProceduralObjectCatalog {
private static final Gson DEFINITION_JSON = new Gson();
@@ -57,7 +55,6 @@ public final class ProceduralObjectCatalog {
private final IrisData data;
private final KMap<String, ResolvedProceduralPlacement> byDefinition = new KMap<>();
private final KMap<String, ResolvedProceduralPlacement> byVariantKey = new KMap<>();
public ProceduralObjectCatalog(IrisData data) {
this.data = data;
@@ -74,7 +71,7 @@ public final class ProceduralObjectCatalog {
private ResolvedProceduralPlacement placementOf(Object definition) {
String identity = definition.getClass().getName() + DEFINITION_JSON.toJson(definition);
return byDefinition.computeIfAbsent(identity, ignored -> register(resolvedFor(definition)));
return byDefinition.computeIfAbsent(identity, ignored -> resolvedFor(definition));
}
private ResolvedProceduralPlacement resolvedFor(Object definition) {
@@ -89,22 +86,29 @@ public final class ProceduralObjectCatalog {
};
}
private ResolvedProceduralPlacement register(ResolvedProceduralPlacement placement) {
for (String key : placement.variantKeys()) {
byVariantKey.putIfAbsent(key, placement);
/**
* Every procedural object the pack defines, in snippets or in regions and biomes, whether or not its dimensions
* reach them. Where two definitions share a type and name, the first one found stands for both.
*/
public List<ResolvedProceduralPlacement> definitions() {
Map<String, ResolvedProceduralPlacement> byTypeAndName = new LinkedHashMap<>();
for (Class<?> type : DEFINITION_TYPES) {
snippetDefinitions(type).forEach(definition -> addDefinition(byTypeAndName, placementOf(definition)));
}
return placement;
data.getRegionLoader().streamAll().forEach(region -> addDefinitions(byTypeAndName, region.getProceduralObjects()));
data.getBiomeLoader().streamAll().forEach(biome -> addDefinitions(byTypeAndName, biome.getProceduralObjects()));
return List.copyOf(byTypeAndName.values());
}
/** The key of every variant the pack defines, in snippets or in regions and biomes, whether or not its dimensions reach them. */
public List<String> variantKeys() {
Set<String> keys = new LinkedHashSet<>();
for (Class<?> type : DEFINITION_TYPES) {
snippetDefinitions(type).forEach(definition -> keys.addAll(placementOf(definition).variantKeys()));
private void addDefinitions(Map<String, ResolvedProceduralPlacement> byTypeAndName, ProceduralObjectSettings settings) {
if (settings == null) {
return;
}
data.getRegionLoader().streamAll().forEach(region -> addVariantKeys(keys, region.getProceduralObjects()));
data.getBiomeLoader().streamAll().forEach(biome -> addVariantKeys(keys, biome.getProceduralObjects()));
return List.copyOf(keys);
settings.resolved(data).forEach(placement -> addDefinition(byTypeAndName, placement));
}
private static void addDefinition(Map<String, ResolvedProceduralPlacement> byTypeAndName, ResolvedProceduralPlacement placement) {
byTypeAndName.putIfAbsent(placement.kind() + "/" + placement.name(), placement);
}
private List<Object> snippetDefinitions(Class<?> type) {
@@ -136,22 +140,4 @@ public final class ProceduralObjectCatalog {
}
}
private void addVariantKeys(Set<String> keys, ProceduralObjectSettings settings) {
if (settings == null) {
return;
}
for (ResolvedProceduralPlacement placement : settings.resolved(data)) {
keys.addAll(placement.variantKeys());
}
}
/** The baked variant loaded under {@code key}; null when no definition resolved so far has it, or it baked no blocks. */
public IrisObject variant(String key) {
ResolvedProceduralPlacement placement = byVariantKey.get(key);
return placement == null ? null : placement.variant(key);
}
public static boolean isVariantKey(String key) {
return key.startsWith("procedural/");
}
}
@@ -68,6 +68,11 @@ public abstract class ResolvedProceduralPlacement {
return shape.name();
}
/** The type of object, as the variant keys name it: tree, ruin, formation, coral, fungus or crystal. */
public String kind() {
return shape.kind();
}
/**
* Whether the world generator can place it. A place mode this version does not know, such as v4's CEILING_HANG,
* reads as null, and placing such an object on the surface would put it in the wrong place.
@@ -130,15 +135,6 @@ public abstract class ResolvedProceduralPlacement {
return baked.get(rng.i(baked.size()));
}
/** The baked variant loaded under {@code key}; null when it is not one of these, or baked no blocks. */
public IrisObject variant(String key) {
for (IrisObject variant : variants()) {
if (variant.getLoadKey().equals(key)) {
return variant;
}
}
return null;
}
/** Bakes every variant; the default draws variant {@code i} from {@code seed + i * stride}. */
protected List<IrisObject> bake(ObjectBuilder blocks) {
@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.79289 13.7929L2.20711 13.2071C1.81658 12.8166 1.81658 12.1834 2.20711 11.7929L8.79289 5.20711C9.18342 4.81658 9.81658 4.81658 10.2071 5.20711L10.7929 5.79289C11.1834 6.18342 11.1834 6.81658 10.7929 7.20711L4.20711 13.7929C3.81658 14.1834 3.18342 14.1834 2.79289 13.7929Z" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 8.5L7.5 6.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 2.5V1.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 5.5H14.5M11.5 9.5L12.5 10.5M6.5 4.5L5.5 3.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 889 B

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.79289 13.7929L2.20711 13.2071C1.81658 12.8166 1.81658 12.1834 2.20711 11.7929L8.79289 5.20711C9.18342 4.81658 9.81658 4.81658 10.2071 5.20711L10.7929 5.79289C11.1834 6.18342 11.1834 6.81658 10.7929 7.20711L4.20711 13.7929C3.81658 14.1834 3.18342 14.1834 2.79289 13.7929Z" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 8.5L7.5 6.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 2.5V1.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 5.5H14.5M11.5 9.5L12.5 10.5M6.5 4.5L5.5 3.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 889 B