From dc7615219b9bb7508c49af348ad54006c0251770 Mon Sep 17 00:00:00 2001 From: piexel Date: Tue, 20 Oct 2026 12:37:39 +0200 Subject: [PATCH] Give procedural objects their own tab Variant picker, grouped by type. --- .../com/volmit/iris/core/gui/MapView.java | 4 +- .../com/volmit/iris/core/gui/MapWindow.java | 44 +++--- .../volmit/iris/core/gui/ObjectBrowser.java | 16 +- .../volmit/iris/core/gui/ObjectExplorer.java | 102 +++++++------ .../volmit/iris/core/gui/ObjectSource.java | 38 +++++ .../iris/core/gui/ObjectVariantPicker.java | 137 ++++++++++++++++++ .../iris/core/gui/PackObjectSource.java | 45 ++++++ .../iris/core/gui/ProceduralObjectSource.java | 62 ++++++++ .../procedural/ProceduralObjectCatalog.java | 60 +++----- .../ResolvedProceduralPlacement.java | 14 +- .../resources/iris/icons/views/procedural.svg | 7 + .../iris/icons/views/procedural_dark.svg | 7 + 12 files changed, 420 insertions(+), 116 deletions(-) create mode 100644 core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java create mode 100644 core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java create mode 100644 core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java create mode 100644 core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java create mode 100644 core/src/main/resources/iris/icons/views/procedural.svg create mode 100644 core/src/main/resources/iris/icons/views/procedural_dark.svg diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapView.java b/core/src/main/java/com/volmit/iris/core/gui/MapView.java index f5f192430..ac130e3cc 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/MapView.java +++ b/core/src/main/java/com/volmit/iris/core/gui/MapView.java @@ -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; diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java b/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java index dcf244610..a701b8460 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java +++ b/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java @@ -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 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(); } diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectBrowser.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectBrowser.java index 8b487e0d9..6ceff2bca 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectBrowser.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectBrowser.java @@ -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 objectKeys = List.of(); private String chosenKey; private boolean updatingSelection; - ObjectBrowser(Consumer onObjectChosen) { + /** + * @param noun what one entry is called in the browser's texts, such as "object" + */ + ObjectBrowser(String noun, Consumer 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 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; } diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java index c9a0538b4..0ee1624a5 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java @@ -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 shownVariants = List.of(); private LoadedObject shown; private ObjectMesh wholeMesh; private Set 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 objectKeys() { - IrisData data = engine().getData(); - List keys = new ArrayList<>(Arrays.asList(data.getObjectLoader().getPossibleKeys())); - keys.addAll(data.getProceduralObjects().variantKeys()); - return keys; + return source.keys(engine().getData()); } private void showCatalog(List 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 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 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()) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java new file mode 100644 index 000000000..bbcd05a8d --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java @@ -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 . + */ +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 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 variants(IrisData data, String key); +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java new file mode 100644 index 000000000..87fd0350b --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectVariantPicker.java @@ -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 . + */ +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(); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java b/core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java new file mode 100644 index 000000000..e3f46b481 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java @@ -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 . + */ +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 keys(IrisData data) { + return Arrays.asList(data.getObjectLoader().getPossibleKeys()); + } + + @Override + public List variants(IrisData data, String key) { + IrisObject object = data.getObjectLoader().load(key); + return object == null ? List.of() : List.of(object); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java b/core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java new file mode 100644 index 000000000..a59e467cb --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java @@ -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 . + */ +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 FOLDER_BY_KIND = Map.of( + "tree", "Trees", + "formation", "Formations", + "crystal", "Crystals", + "coral", "Coral", + "fungus", "Fungi", + "ruin", "Ruins"); + + private Map definitionsByKey = Map.of(); + + @Override + public String noun() { + return "procedural object"; + } + + @Override + public List keys(IrisData data) { + Map 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 variants(IrisData data, String key) { + ResolvedProceduralPlacement definition = definitionsByKey.get(key); + return definition == null ? List.of() : definition.variants(); + } +} diff --git a/core/src/main/java/com/volmit/iris/engine/object/procedural/ProceduralObjectCatalog.java b/core/src/main/java/com/volmit/iris/engine/object/procedural/ProceduralObjectCatalog.java index cc97fdde8..36155d9da 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/procedural/ProceduralObjectCatalog.java +++ b/core/src/main/java/com/volmit/iris/engine/object/procedural/ProceduralObjectCatalog.java @@ -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 byDefinition = new KMap<>(); - private final KMap 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 definitions() { + Map 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 variantKeys() { - Set keys = new LinkedHashSet<>(); - for (Class type : DEFINITION_TYPES) { - snippetDefinitions(type).forEach(definition -> keys.addAll(placementOf(definition).variantKeys())); + private void addDefinitions(Map 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 byTypeAndName, ResolvedProceduralPlacement placement) { + byTypeAndName.putIfAbsent(placement.kind() + "/" + placement.name(), placement); } private List snippetDefinitions(Class type) { @@ -136,22 +140,4 @@ public final class ProceduralObjectCatalog { } } - private void addVariantKeys(Set 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/"); - } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/procedural/ResolvedProceduralPlacement.java b/core/src/main/java/com/volmit/iris/engine/object/procedural/ResolvedProceduralPlacement.java index d7bce1761..8ebf987fe 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/procedural/ResolvedProceduralPlacement.java +++ b/core/src/main/java/com/volmit/iris/engine/object/procedural/ResolvedProceduralPlacement.java @@ -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 bake(ObjectBuilder blocks) { diff --git a/core/src/main/resources/iris/icons/views/procedural.svg b/core/src/main/resources/iris/icons/views/procedural.svg new file mode 100644 index 000000000..5ad2cdb16 --- /dev/null +++ b/core/src/main/resources/iris/icons/views/procedural.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/src/main/resources/iris/icons/views/procedural_dark.svg b/core/src/main/resources/iris/icons/views/procedural_dark.svg new file mode 100644 index 000000000..3c5f7ed0d --- /dev/null +++ b/core/src/main/resources/iris/icons/views/procedural_dark.svg @@ -0,0 +1,7 @@ + + + + + + +