few more changes

This commit is contained in:
Brian Neumann-Fopiano
2026-06-17 19:18:44 -04:00
parent 8fb627f9c1
commit f777ea6b18
77 changed files with 4270 additions and 1369 deletions
@@ -150,11 +150,6 @@ public class IrisSettings {
public int maxResidentTectonicPlates = 96;
public int mantleBackpressureWaitMs = 25;
public int mantleBackpressureTimeoutMs = 60_000;
public boolean enableSpigotDirectPregen = false;
public boolean isEnableSpigotDirectPregen() {
return enableSpigotDirectPregen;
}
public int getChunkLoadTimeoutSeconds() {
return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120));
@@ -0,0 +1,61 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.Engine;
import java.awt.GraphicsEnvironment;
public final class GuiHost {
private static volatile Provider provider = new Provider() {
};
private GuiHost() {
}
public interface Provider {
default Engine findActiveEngine() {
return null;
}
default void registerHotloadHook(Runnable onHotload) {
}
default void unregisterHotloadHook(Runnable onHotload) {
}
default GuiOverlay overlayFor(Engine engine) {
return null;
}
}
public static void set(Provider boundProvider) {
if (boundProvider != null) {
provider = boundProvider;
}
}
public static Provider get() {
return provider;
}
public static boolean isAvailable() {
return !GraphicsEnvironment.isHeadless();
}
}
@@ -0,0 +1,29 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 art.arcane.iris.core.gui;
public record GuiMarker(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
public static GuiMarker player(String name, double worldX, double worldZ) {
return new GuiMarker(name, worldX, 0, worldZ, 0, 0);
}
public static GuiMarker entity(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
return new GuiMarker(label, worldX, worldY, worldZ, health, maxHealth);
}
}
@@ -0,0 +1,34 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.render.RenderType;
import java.util.List;
import java.util.function.Consumer;
public interface GuiOverlay {
List<GuiMarker> players();
void requestEntities(Consumer<List<GuiMarker>> sink);
void teleport(double worldX, double worldZ);
String openInEditor(double worldX, double worldZ, RenderType type);
}
@@ -0,0 +1,557 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 art.arcane.iris.core.gui;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
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.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener {
private static final long serialVersionUID = 2094606939770332040L;
private static final Color BG = new Color(24, 24, 28);
private static final Color SIDEBAR_BG = new Color(20, 20, 24);
private static final Color SIDEBAR_SELECTED = new Color(40, 50, 70);
private static final Color SIDEBAR_ITEM_COLOR = new Color(170, 170, 185);
private static final Color SEARCH_BG = new Color(30, 30, 38);
private static final Color SEARCH_FG = new Color(180, 180, 190);
private static final Color STATUS_BG = new Color(32, 32, 38, 230);
private static final Color STATUS_TEXT = new Color(180, 180, 190);
private static final Color ACCENT = new Color(90, 140, 255);
private static final Color SEPARATOR = new Color(40, 40, 50);
private static final Font STATUS_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12);
private static final Font SIDEBAR_HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
private static final Font SIDEBAR_ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
private static final Font SEARCH_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
private static final int SIDEBAR_WIDTH = 240;
private static final int[] HSB_LUT = new int[256];
private static final String[] CATEGORY_ORDER = {
"Pack Generators", "Simplex", "Perlin", "Cellular", "Iris", "Clover",
"Hexagon", "Vascular", "Globe", "Cubic", "Fractal", "Static",
"Nowhere", "Sierpinski", "Utility", "Other"
};
static {
for (int i = 0; i < 256; i++) {
float n = i / 255f;
HSB_LUT[i] = Color.HSBtoRGB(0.666f - n * 0.666f, 1f, 1f - n * 0.8f);
}
}
private final RollingSequence fpsHistory = new RollingSequence(60);
private final boolean colorMode = IrisSettings.get().getGui().colorMode;
private final MultiBurst gx = MultiBurst.burst;
private final Runnable hotloadHook = this::refreshGenerator;
private double scale = 1;
private double animScale = 10;
private double ox = 0;
private double oz = 0;
private double animOx = 0;
private double animOz = 0;
private double lastMouseX = Double.MAX_VALUE;
private double lastMouseZ = Double.MAX_VALUE;
private double time = 0;
private double animTime = 0;
private int imgWidth = 0;
private int imgHeight = 0;
private BufferedImage img;
private CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong()));
private Function2<Double, Double, Double> generator;
private Supplier<Function2<Double, Double, Double>> loader;
private String currentName = "STATIC";
public NoiseExplorerGUI() {
GuiHost.get().registerHotloadHook(hotloadHook);
setBackground(BG);
addMouseWheelListener(this);
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
Point cp = e.getPoint();
lastMouseX = cp.getX();
lastMouseZ = cp.getY();
}
@Override
public void mouseDragged(MouseEvent e) {
Point cp = e.getPoint();
ox += (lastMouseX - cp.getX()) * scale;
oz += (lastMouseZ - cp.getY()) * scale;
lastMouseX = cp.getX();
lastMouseZ = cp.getY();
}
});
}
public static void launch() {
Engine engine = GuiHost.get().findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
buildFrame("Noise Explorer", nv, engine, null, null);
});
}
public static void launch(Supplier<Function2<Double, Double, Double>> gen, String genName) {
Engine engine = GuiHost.get().findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
nv.loader = gen;
nv.generator = gen.get();
nv.currentName = genName;
buildFrame("Noise Explorer: " + genName, nv, engine, gen, genName);
});
}
private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.getContentPane().setBackground(BG);
frame.setLayout(new BorderLayout());
JPanel sidebar = buildSidebar(nv, engine, customGen, customName);
frame.add(sidebar, BorderLayout.WEST);
frame.add(nv, BorderLayout.CENTER);
frame.setSize(1440, 820);
frame.setMinimumSize(new Dimension(640, 480));
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
GuiHost.get().unregisterHotloadHook(nv.hotloadHook);
}
});
return frame;
}
private static JPanel buildSidebar(NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
JPanel sidebar = new JPanel(new BorderLayout());
sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0));
sidebar.setBackground(SIDEBAR_BG);
sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, SEPARATOR));
JTextField search = new JTextField();
search.setBackground(SEARCH_BG);
search.setForeground(SEARCH_FG);
search.setCaretColor(SEARCH_FG);
search.setFont(SEARCH_FONT);
search.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, SEPARATOR),
BorderFactory.createEmptyBorder(8, 10, 8, 10)
));
search.putClientProperty("JTextField.placeholderText", "Search...");
LinkedHashMap<String, List<ListItem>> categories = buildCategoryMap(nv, engine, customGen, customName);
DefaultListModel<ListItem> model = new DefaultListModel<>();
populateModel(model, categories, "");
JList<ListItem> list = new JList<>(model);
list.setBackground(SIDEBAR_BG);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setCellRenderer(new SidebarCellRenderer());
list.setFixedCellHeight(-1);
list.addListSelectionListener(e -> {
if (e.getValueIsAdjusting()) return;
ListItem selected = list.getSelectedValue();
if (selected != null && !selected.header && selected.action != null) {
selected.action.run();
}
});
search.getDocument().addDocumentListener(new DocumentListener() {
private void filter() {
String text = search.getText().trim();
populateModel(model, categories, text);
}
public void insertUpdate(DocumentEvent e) { filter(); }
public void removeUpdate(DocumentEvent e) { filter(); }
public void changedUpdate(DocumentEvent e) { filter(); }
});
JScrollPane scrollPane = new JScrollPane(list);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.getVerticalScrollBar().setUnitIncrement(16);
scrollPane.getVerticalScrollBar().setBackground(SIDEBAR_BG);
sidebar.add(search, BorderLayout.NORTH);
sidebar.add(scrollPane, BorderLayout.CENTER);
return sidebar;
}
private static void populateModel(DefaultListModel<ListItem> model, LinkedHashMap<String, List<ListItem>> categories, String filter) {
model.clear();
String lower = filter.toLowerCase();
for (Map.Entry<String, List<ListItem>> entry : categories.entrySet()) {
List<ListItem> matching = new ArrayList<>();
for (ListItem item : entry.getValue()) {
if (lower.isEmpty() || item.text.toLowerCase().contains(lower) || item.rawName.toLowerCase().contains(lower)) {
matching.add(item);
}
}
if (!matching.isEmpty()) {
model.addElement(new ListItem(entry.getKey(), entry.getKey(), true, null));
for (ListItem item : matching) {
model.addElement(item);
}
}
}
}
private static LinkedHashMap<String, List<ListItem>> buildCategoryMap(NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
LinkedHashMap<String, List<ListItem>> categories = new LinkedHashMap<>();
if (customGen != null && customName != null) {
List<ListItem> custom = new ArrayList<>();
custom.add(new ListItem(customName, customName, false, () -> {
nv.generator = customGen.get();
nv.loader = customGen;
nv.currentName = customName;
}));
categories.put("Custom", custom);
}
Map<String, List<NoiseStyle>> styleGroups = new LinkedHashMap<>();
for (NoiseStyle style : NoiseStyle.values()) {
String cat = categorize(style);
styleGroups.computeIfAbsent(cat, k -> new ArrayList<>()).add(style);
}
if (engine != null && !engine.isClosed()) {
List<ListItem> genItems = new ArrayList<>();
try {
IrisData data = engine.getData();
String[] keys = data.getGeneratorLoader().getPossibleKeys();
Arrays.sort(keys);
for (String key : keys) {
IrisGenerator gen = data.getGeneratorLoader().load(key);
if (gen != null) {
long seed = new RNG(12345).nextParallelRNG(3245).lmax();
genItems.add(new ListItem(formatName(key), key, false, () -> {
nv.generator = (x, z) -> gen.getHeight(x, z, seed);
nv.loader = null;
nv.currentName = key;
}));
}
}
} catch (Throwable ignored) {}
if (!genItems.isEmpty()) {
categories.put("Pack Generators", genItems);
}
}
for (String cat : CATEGORY_ORDER) {
if ("Pack Generators".equals(cat)) continue;
List<NoiseStyle> styles = styleGroups.get(cat);
if (styles != null && !styles.isEmpty()) {
List<ListItem> items = new ArrayList<>();
for (NoiseStyle style : styles) {
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
nv.generator = null;
nv.loader = null;
nv.currentName = style.name();
}));
}
categories.put(cat, items);
}
}
for (Map.Entry<String, List<NoiseStyle>> entry : styleGroups.entrySet()) {
if (!categories.containsKey(entry.getKey())) {
List<ListItem> items = new ArrayList<>();
for (NoiseStyle style : entry.getValue()) {
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
nv.generator = null;
nv.loader = null;
nv.currentName = style.name();
}));
}
categories.put(entry.getKey(), items);
}
}
return categories;
}
private static String categorize(NoiseStyle style) {
String n = style.name();
if (n.startsWith("STATIC")) return "Static";
if (n.startsWith("IRIS")) return "Iris";
if (n.startsWith("CLOVER")) return "Clover";
if (n.startsWith("VASCULAR")) return "Vascular";
if (n.equals("FLAT")) return "Utility";
if (n.startsWith("CELLULAR")) return "Cellular";
if (n.startsWith("HEX") || n.equals("HEXAGON")) return "Hexagon";
if (n.startsWith("SIERPINSKI")) return "Sierpinski";
if (n.startsWith("NOWHERE")) return "Nowhere";
if (n.startsWith("GLOB")) return "Globe";
if (n.startsWith("PERLIN")) return "Perlin";
if (n.startsWith("CUBIC") || (n.startsWith("FRACTAL") && n.contains("CUBIC"))) return "Cubic";
if (n.contains("SIMPLEX") && !n.startsWith("FRACTAL")) return "Simplex";
if (n.startsWith("FRACTAL")) return "Fractal";
return "Other";
}
private static String formatName(String enumName) {
String lower = enumName.toLowerCase().replace('_', ' ');
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
}
private void refreshGenerator() {
if (generator != null && loader != null) {
generator = loader.get();
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
if (e.isControlDown()) {
time = time + ((0.0025 * time) * notches);
return;
}
scale = scale + ((0.044 * scale) * notches);
scale = Math.max(scale, 0.00001);
}
private double lerp(double current, double target, double speed) {
double diff = target - current;
if (Math.abs(diff) < 0.001) return target;
return current + diff * speed;
}
@Override
public void paint(Graphics g) {
animScale = lerp(animScale, scale, 0.16);
animTime = lerp(animTime, time, 0.29);
animOx = lerp(animOx, ox, 0.16);
animOz = lerp(animOz, oz, 0.16);
PrecisionStopwatch p = PrecisionStopwatch.start();
if (g instanceof Graphics2D gg) {
gg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
int pw = getWidth();
int ph = getHeight();
if (pw != imgWidth || ph != imgHeight || img == null) {
imgWidth = pw;
imgHeight = ph;
img = null;
}
int accuracy = M.clip((fpsHistory.getAverage() / 14D), 1D, 64D).intValue();
int rw = Math.max(1, pw / accuracy);
int rh = Math.max(1, ph / accuracy);
if (img == null || img.getWidth() != rw || img.getHeight() != rh) {
img = new BufferedImage(rw, rh, BufferedImage.TYPE_INT_RGB);
}
int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
BurstExecutor burst = gx.burst(rw);
for (int x = 0; x < rw; x++) {
int xx = x;
burst.queue(() -> {
for (int z = 0; z < rh; z++) {
double worldX = (xx * accuracy * animScale) + animOx;
double worldZ = (z * accuracy * animScale) + animOz;
double n = generator != null
? generator.apply(worldX, worldZ)
: cng.noise(worldX, worldZ);
n = Math.max(0, Math.min(1, n));
int rgb;
if (colorMode) {
rgb = HSB_LUT[(int) (n * 255)];
} else {
int v = (int) (n * 255);
rgb = (v << 16) | (v << 8) | v;
}
pixels[z * rw + xx] = rgb;
}
});
}
burst.complete();
gg.setColor(BG);
gg.fillRect(0, 0, pw, ph);
gg.drawImage(img, 0, 0, pw, ph, null);
renderStatusBar(gg, pw, ph, p.getMilliseconds());
renderCrosshair(gg, pw, ph);
}
p.end();
time += 1D;
fpsHistory.put(p.getMilliseconds());
if (!isVisible() || !getParent().isVisible()) {
return;
}
long sleepMs = Math.max(1, 16 - (long) p.getMilliseconds());
EventQueue.invokeLater(() -> {
J.sleep(sleepMs);
repaint();
});
}
private void renderCrosshair(Graphics2D g, int w, int h) {
int cx = w / 2;
int cy = h / 2;
g.setColor(new Color(255, 255, 255, 40));
g.drawLine(cx - 8, cy, cx + 8, cy);
g.drawLine(cx, cy - 8, cx, cy + 8);
}
private void renderStatusBar(Graphics2D g, int w, int h, double frameMs) {
int barHeight = 28;
int y = h - barHeight;
g.setColor(STATUS_BG);
g.fillRect(0, y, w, barHeight);
g.setColor(new Color(50, 50, 60));
g.drawLine(0, y, w, y);
g.setFont(STATUS_FONT);
g.setColor(STATUS_TEXT);
double worldX = (w / 2.0 * animScale) + animOx;
double worldZ = (h / 2.0 * animScale) + animOz;
double noiseVal = generator != null
? generator.apply(worldX, worldZ)
: cng.noise(worldX, worldZ);
noiseVal = Math.max(0, Math.min(1, noiseVal));
int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0;
String status = String.format(" %s | X: %.1f Z: %.1f | Zoom: %.4f | Value: %.4f | %d FPS",
currentName, worldX, worldZ, animScale, noiseVal, fps);
g.drawString(status, 8, y + 18);
int barW = 60;
int barX = w - barW - 12;
int barY = y + 6;
int barH = barHeight - 12;
g.setColor(new Color(40, 40, 48));
g.fillRoundRect(barX, barY, barW, barH, 4, 4);
int fillW = (int) (noiseVal * (barW - 2));
g.setColor(ACCENT);
g.fillRoundRect(barX + 1, barY + 1, fillW, barH - 2, 3, 3);
}
private static final class ListItem {
final String text;
final String rawName;
final boolean header;
final Runnable action;
ListItem(String text, String rawName, boolean header, Runnable action) {
this.text = text;
this.rawName = rawName;
this.header = header;
this.action = action;
}
@Override
public String toString() {
return text;
}
}
private static final class SidebarCellRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focus) {
ListItem item = (ListItem) value;
super.getListCellRendererComponent(list, item.text, index, !item.header && selected, false);
setOpaque(true);
if (item.header) {
setFont(SIDEBAR_HEADER_FONT);
setForeground(ACCENT);
setBackground(SIDEBAR_BG);
setBorder(BorderFactory.createEmptyBorder(10, 10, 4, 10));
} else {
setFont(SIDEBAR_ITEM_FONT);
setForeground(selected ? Color.WHITE : SIDEBAR_ITEM_COLOR);
setBackground(selected ? SIDEBAR_SELECTED : SIDEBAR_BG);
setBorder(BorderFactory.createEmptyBorder(3, 20, 3, 10));
}
return this;
}
}
}
@@ -0,0 +1,31 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 art.arcane.iris.core.gui;
import art.arcane.volmlib.util.math.Position2;
public interface PregenRenderSource {
Position2 min();
Position2 max();
String[] progress();
boolean paused();
}
@@ -0,0 +1,158 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 art.arcane.iris.core.gui;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.function.Consumer2;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.iris.util.common.scheduling.J;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.concurrent.locks.ReentrantLock;
public final class PregenRenderer extends JPanel implements KeyListener {
private static final long serialVersionUID = 2094606939770332040L;
private final KList<Runnable> order = new KList<>();
private final ReentrantLock lock = new ReentrantLock();
private final int res = 512;
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
private final PregenRenderSource source;
private final Runnable onPause;
private Graphics2D bg;
private JFrame frame;
private PregenRenderer(PregenRenderSource source, Runnable onPause) {
this.source = source;
this.onPause = onPause;
}
public static PregenRenderer open(String title, PregenRenderSource source, Runnable onPause) {
PregenRenderer renderer = new PregenRenderer(source, onPause);
JFrame frame = new JFrame(title);
renderer.frame = frame;
frame.addKeyListener(renderer);
frame.add(renderer);
frame.setSize(1000, 1000);
frame.setVisible(true);
return renderer;
}
public Consumer2<Position2, Color> drawFunction() {
return (Position2 c, Color color) -> {
lock.lock();
try {
order.add(() -> draw(c, color, bg));
} finally {
lock.unlock();
}
};
}
public void submit(int x, int z, Color color) {
drawFunction().accept(new Position2(x, z), color);
}
public boolean isVisibleFrame() {
return frame != null && frame.isVisible();
}
public void close() {
if (frame != null) {
frame.setVisible(false);
}
}
@Override
public void paint(Graphics gx) {
Graphics2D g = (Graphics2D) gx;
bg = (Graphics2D) image.getGraphics();
lock.lock();
try {
while (order.isNotEmpty()) {
try {
order.pop().run();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
} finally {
lock.unlock();
}
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
g.setColor(Color.WHITE);
g.setFont(new Font("Hevetica", Font.BOLD, 13));
String[] prog = source.progress();
int h = g.getFontMetrics().getHeight() + 5;
int hh = 20;
if (source.paused()) {
g.drawString("PAUSED", 20, hh += h);
g.drawString("Press P to Resume", 20, hh += h);
} else {
for (String i : prog) {
g.drawString(i, 20, hh += h);
}
g.drawString("Press P to Pause", 20, hh += h);
}
J.sleep(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250);
repaint();
}
private void draw(Position2 p, Color c, Graphics2D bg) {
double pw = M.lerpInverse(source.min().getX(), source.max().getX(), p.getX());
double ph = M.lerpInverse(source.min().getZ(), source.max().getZ(), p.getZ());
double pwa = M.lerpInverse(source.min().getX(), source.max().getX(), p.getX() + 1);
double pha = M.lerpInverse(source.min().getZ(), source.max().getZ(), p.getZ() + 1);
int x = (int) M.lerp(0, res, pw);
int z = (int) M.lerp(0, res, ph);
int xa = (int) M.lerp(0, res, pwa);
int za = (int) M.lerp(0, res, pha);
bg.setColor(c);
bg.fillRect(x, z, xa - x, za - z);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_P && onPause != null) {
onPause.run();
}
}
}
@@ -30,25 +30,19 @@ import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.format.MemoryMonitor;
import art.arcane.volmlib.util.function.Consumer2;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.awt.Color;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
public class PregeneratorJob implements PregenListener {
public class PregeneratorJob implements PregenListener, PregenRenderSource {
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
private static final Color COLOR_MANTLE = parseColor("#3c2773");
@@ -69,8 +63,8 @@ public class PregeneratorJob implements PregenListener {
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
private final Engine engine;
private final ExecutorService service;
private JFrame frame;
private PregenRenderer renderer;
private Consumer2<Position2, Color> drawFunction;
private int rgc = 0;
private String[] info;
private volatile double lastChunksPerSecond = 0D;
@@ -212,8 +206,8 @@ public class PregeneratorJob implements PregenListener {
public void draw(int x, int z, Color color) {
try {
if (renderer != null && frame != null && frame.isVisible()) {
renderer.func.accept(new Position2(x, z), color);
if (renderer != null && drawFunction != null && renderer.isVisibleFrame()) {
drawFunction.accept(new Position2(x, z), color);
}
} catch (Throwable ignored) {
IrisLogging.error("Failed to draw pregen");
@@ -232,11 +226,11 @@ public class PregeneratorJob implements PregenListener {
J.a(() -> {
try {
monitor.close();
if (frame == null) {
if (renderer == null) {
return;
}
J.sleep(3000);
frame.setVisible(false);
renderer.close();
} catch (Throwable ignored) {
IrisLogging.error("Error closing pregen gui");
}
@@ -246,20 +240,8 @@ public class PregeneratorJob implements PregenListener {
public void open() {
J.a(() -> {
try {
frame = new JFrame("Pregen View");
renderer = new PregenRenderer();
frame.addKeyListener(renderer);
renderer.l = new ReentrantLock();
renderer.frame = frame;
renderer.job = this;
renderer.func = (c, b) -> {
renderer.l.lock();
renderer.order.add(() -> renderer.draw(c, b, renderer.bg));
renderer.l.unlock();
};
frame.add(renderer);
frame.setSize(1000, 1000);
frame.setVisible(true);
renderer = PregenRenderer.open("Pregen View", this, PregeneratorJob::pauseResume);
drawFunction = renderer.drawFunction();
} catch (Throwable ignored) {
IrisLogging.error("Error opening pregen gui");
}
@@ -292,7 +274,7 @@ public class PregeneratorJob implements PregenListener {
@Override
public void onChunkGenerated(int x, int z, boolean cached) {
if (renderer == null || frame == null || !frame.isVisible()) return;
if (renderer == null || !renderer.isVisibleFrame()) return;
service.submit(() -> {
if (engine != null) {
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
@@ -378,112 +360,23 @@ public class PregeneratorJob implements PregenListener {
draw(x, z, COLOR_EXISTS);
}
private Position2 getMax() {
@Override
public Position2 max() {
return max;
}
private Position2 getMin() {
@Override
public Position2 min() {
return min;
}
private boolean paused() {
@Override
public boolean paused() {
return pregenerator.paused();
}
private String[] getProgress() {
@Override
public String[] progress() {
return info;
}
public static class PregenRenderer extends JPanel implements KeyListener {
private static final long serialVersionUID = 2094606939770332040L;
private final KList<Runnable> order = new KList<>();
private final int res = 512;
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
Graphics2D bg;
private PregeneratorJob job;
private ReentrantLock l;
private Consumer2<Position2, Color> func;
private JFrame frame;
public PregenRenderer() {
}
public void paint(int x, int z, Color c) {
func.accept(new Position2(x, z), c);
}
@Override
public void paint(Graphics gx) {
Graphics2D g = (Graphics2D) gx;
bg = (Graphics2D) image.getGraphics();
l.lock();
while (order.isNotEmpty()) {
try {
order.pop().run();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
l.unlock();
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
g.setColor(Color.WHITE);
g.setFont(new Font("Hevetica", Font.BOLD, 13));
String[] prog = job.getProgress();
int h = g.getFontMetrics().getHeight() + 5;
int hh = 20;
if (job.paused()) {
g.drawString("PAUSED", 20, hh += h);
g.drawString("Press P to Resume", 20, hh += h);
} else {
for (String i : prog) {
g.drawString(i, 20, hh += h);
}
g.drawString("Press P to Pause", 20, hh += h);
}
J.sleep(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250);
repaint();
}
private void draw(Position2 p, Color c, Graphics2D bg) {
double pw = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX());
double ph = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ());
double pwa = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX() + 1);
double pha = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ() + 1);
int x = (int) M.lerp(0, res, pw);
int z = (int) M.lerp(0, res, ph);
int xa = (int) M.lerp(0, res, pwa);
int za = (int) M.lerp(0, res, pha);
bg.setColor(c);
bg.fillRect(x, z, xa - x, za - z);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_P) {
PregeneratorJob.pauseResume();
}
}
public void close() {
frame.setVisible(false);
}
}
}
@@ -0,0 +1,917 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.render.IrisRenderer;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import javax.swing.event.MouseInputListener;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
private static final long serialVersionUID = 2094606939770332040L;
private static final Color BG = new Color(18, 18, 22);
private static final Color CARD_BG = new Color(28, 28, 36, 220);
private static final Color CARD_BORDER = new Color(60, 60, 75, 180);
private static final Color TEXT_PRIMARY = new Color(220, 220, 230);
private static final Color TEXT_SECONDARY = new Color(140, 140, 155);
private static final Color ACCENT = new Color(90, 140, 255);
private static final Color PLAYER_COLOR = new Color(80, 200, 120);
private static final Color MOB_COLOR = new Color(220, 80, 80);
private static final Color STATUS_BG = new Color(24, 24, 30, 240);
private static final Color GRID_COLOR = new Color(255, 255, 255, 12);
private static final Font FONT_STATUS = new Font(Font.MONOSPACED, Font.PLAIN, 12);
private static final Font FONT_CARD_TITLE = new Font(Font.SANS_SERIF, Font.BOLD, 13);
private static final Font FONT_CARD_BODY = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
private static final Font FONT_HELP_KEY = new Font(Font.MONOSPACED, Font.BOLD, 12);
private static final Font FONT_NOTIFICATION = new Font(Font.SANS_SERIF, Font.BOLD, 14);
private static final int CARD_RADIUS = 8;
private static final int CARD_PAD = 12;
private static final int STATUS_HEIGHT = 26;
private final KList<GuiMarker> lastEntities = new KList<>();
private final KMap<String, Long> notifications = new KMap<>();
private final ChronoLatch centities = new ChronoLatch(1000);
private final RollingSequence rs = new RollingSequence(512);
private final O<Integer> m = new O<>();
private final KMap<BlockPosition, BufferedImage> positions = new KMap<>();
private final KMap<BlockPosition, BufferedImage> fastpositions = new KMap<>();
private final KSet<BlockPosition> working = new KSet<>();
private final KSet<BlockPosition> workingfast = new KSet<>();
private RenderType currentType = RenderType.BIOME;
private boolean help = true;
private boolean helpIgnored = false;
private boolean shift = false;
private boolean debug = false;
private boolean control = false;
private boolean eco = false;
private boolean lowtile = false;
private boolean follow = false;
private boolean alt = false;
private boolean grid = false;
private GuiMarker followMarker = null;
private IrisRenderer renderer;
private GuiOverlay overlay;
private double velocity = 0;
private int lowq = 12;
private double scale = 128;
private double mscale = 4D;
private int w = 0;
private int h = 0;
private double lx = 0;
private double lz = 0;
private double ox = 0;
private double oz = 0;
private double hx = 0;
private double hz = 0;
private double oxp = 0;
private double ozp = 0;
private Engine engine;
private int tid = 0;
private Map<RenderType, JToggleButton> modeButtons;
private final ExecutorService e = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
tid++;
Thread t = new Thread(r);
t.setName("Iris HD Renderer " + tid);
t.setPriority(Thread.MIN_PRIORITY);
t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace());
return t;
});
private final ExecutorService eh = Executors.newFixedThreadPool(3, r -> {
tid++;
Thread t = new Thread(r);
t.setName("Iris Renderer " + tid);
t.setPriority(Thread.NORM_PRIORITY);
t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace());
return t;
});
public VisionGUI(JFrame frame) {
m.set(8);
rs.put(1);
setBackground(BG);
addMouseWheelListener(this);
addMouseMotionListener(this);
addMouseListener(this);
frame.addKeyListener(this);
J.a(() -> {
J.sleep(10000);
if (!helpIgnored && help) {
help = false;
}
});
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent windowEvent) {
e.shutdown();
eh.shutdown();
}
});
}
public static void launch(Engine g) {
J.a(() -> createAndShowGUI(g));
}
private static void createAndShowGUI(Engine r) {
JFrame frame = new JFrame("Iris Vision");
VisionGUI nv = new VisionGUI(frame);
nv.engine = r;
nv.overlay = GuiHost.get().overlayFor(r);
nv.renderer = new IrisRenderer(r);
frame.getContentPane().setBackground(BG);
frame.setLayout(new BorderLayout());
frame.add(buildToolbar(nv), BorderLayout.NORTH);
frame.add(nv, BorderLayout.CENTER);
frame.setSize(1440, 820);
frame.setMinimumSize(new Dimension(640, 480));
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static JPanel buildToolbar(VisionGUI nv) {
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 3));
toolbar.setBackground(new Color(22, 22, 28));
toolbar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(45, 45, 55)));
JLabel modeLabel = new JLabel("View:");
modeLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 11));
modeLabel.setForeground(TEXT_SECONDARY);
modeLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 2));
toolbar.add(modeLabel);
ButtonGroup modeGroup = new ButtonGroup();
Map<RenderType, JToggleButton> modeButtons = new LinkedHashMap<>();
for (RenderType type : RenderType.values()) {
JToggleButton btn = createToolbarToggle(modeName(type), type == nv.currentType);
btn.addActionListener(e -> {
nv.setRenderType(type);
for (Map.Entry<RenderType, JToggleButton> entry : modeButtons.entrySet()) {
entry.getValue().setSelected(entry.getKey() == type);
}
});
modeGroup.add(btn);
modeButtons.put(type, btn);
toolbar.add(btn);
}
nv.modeButtons = modeButtons;
toolbar.add(createToolbarSeparator());
JToggleButton gridBtn = createToolbarToggle("Grid", nv.grid);
gridBtn.addActionListener(e -> { nv.toggleGrid(); gridBtn.setSelected(nv.grid); });
toolbar.add(gridBtn);
JToggleButton followBtn = createToolbarToggle("Follow", nv.follow);
followBtn.addActionListener(e -> { nv.toggleFollow(); followBtn.setSelected(nv.follow); });
toolbar.add(followBtn);
JToggleButton qualityBtn = createToolbarToggle("LQ", nv.lowtile);
qualityBtn.addActionListener(e -> { nv.toggleQuality(); qualityBtn.setSelected(nv.lowtile); });
toolbar.add(qualityBtn);
return toolbar;
}
private static JToggleButton createToolbarToggle(String text, boolean selected) {
JToggleButton btn = new JToggleButton(text, selected);
btn.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 11));
btn.setFocusable(false);
btn.setForeground(new Color(170, 170, 185));
btn.setBackground(new Color(32, 32, 40));
btn.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(50, 50, 60)),
BorderFactory.createEmptyBorder(3, 8, 3, 8)
));
btn.setOpaque(true);
btn.addChangeListener(e -> {
if (btn.isSelected()) {
btn.setBackground(new Color(50, 60, 85));
btn.setForeground(Color.WHITE);
} else {
btn.setBackground(new Color(32, 32, 40));
btn.setForeground(new Color(170, 170, 185));
}
});
return btn;
}
private static JSeparator createToolbarSeparator() {
JSeparator sep = new JSeparator(SwingConstants.VERTICAL);
sep.setPreferredSize(new Dimension(1, 24));
sep.setForeground(new Color(50, 50, 60));
sep.setBackground(new Color(22, 22, 28));
return sep;
}
public boolean updateEngine() {
if (engine.isClosed()) {
try {
Engine reacquired = GuiHost.get().findActiveEngine();
if (reacquired != null && !reacquired.isClosed()) {
engine = reacquired;
overlay = GuiHost.get().overlayFor(reacquired);
renderer = new IrisRenderer(reacquired);
return true;
}
} catch (Throwable ignored) {
}
}
return false;
}
@Override
public void mouseMoved(MouseEvent e) {
Point cp = e.getPoint();
lx = cp.getX();
lz = cp.getY();
}
@Override
public void mouseDragged(MouseEvent e) {
Point cp = e.getPoint();
ox += (lx - cp.getX()) * scale;
oz += (lz - cp.getY()) * scale;
lx = cp.getX();
lz = cp.getY();
}
public void notify(String s) {
notifications.put(s, M.ms() + 2500);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = true;
if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = true;
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = true;
if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = true; helpIgnored = true; }
if (e.getKeyCode() == KeyEvent.VK_ALT) alt = true;
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = false;
if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = false;
if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = false;
if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = false; helpIgnored = true; }
if (e.getKeyCode() == KeyEvent.VK_ALT) alt = false;
if (e.getKeyCode() == KeyEvent.VK_F) { toggleFollow(); return; }
if (e.getKeyCode() == KeyEvent.VK_R) { dump(); notify("Refreshing"); return; }
if (e.getKeyCode() == KeyEvent.VK_P) { toggleQuality(); return; }
if (e.getKeyCode() == KeyEvent.VK_E) { eco = !eco; dump(); notify((eco ? "30" : "60") + " FPS"); return; }
if (e.getKeyCode() == KeyEvent.VK_G) { toggleGrid(); return; }
if (e.getKeyCode() == KeyEvent.VK_EQUALS) {
mscale = mscale + ((0.044 * mscale) * -3);
mscale = Math.max(mscale, 0.00001);
dump();
return;
}
if (e.getKeyCode() == KeyEvent.VK_MINUS) {
mscale = mscale + ((0.044 * mscale) * 3);
mscale = Math.max(mscale, 0.00001);
dump();
return;
}
if (e.getKeyCode() == KeyEvent.VK_BACK_SLASH) {
mscale = 1D;
dump();
notify("Zoom Reset");
return;
}
int currentMode = currentType.ordinal();
for (RenderType i : RenderType.values()) {
if (e.getKeyChar() == String.valueOf(i.ordinal() + 1).charAt(0)) {
if (i.ordinal() != currentMode) {
setRenderType(i);
syncModeButtons();
return;
}
}
}
if (e.getKeyCode() == KeyEvent.VK_M) {
setRenderType(RenderType.values()[(currentMode + 1) % RenderType.values().length]);
syncModeButtons();
}
}
private static String modeName(RenderType type) {
return Form.capitalizeWords(type.name().toLowerCase().replaceAll("\\Q_\\E", " "));
}
void setRenderType(RenderType type) {
currentType = type;
dump();
notify(modeName(type));
}
void toggleGrid() {
grid = !grid;
notify("Grid " + (grid ? "On" : "Off"));
}
void toggleFollow() {
follow = !follow;
if (followMarker != null && follow) {
notify("Following " + followMarker.label());
} else if (follow) {
notify("No player in world");
follow = false;
} else {
notify("Follow disabled");
}
}
void toggleQuality() {
lowtile = !lowtile;
dump();
notify((lowtile ? "Low" : "High") + " Quality");
}
private void syncModeButtons() {
if (modeButtons == null) return;
for (Map.Entry<RenderType, JToggleButton> entry : modeButtons.entrySet()) {
entry.getValue().setSelected(entry.getKey() == currentType);
}
}
private void dump() {
positions.clear();
fastpositions.clear();
}
public BufferedImage getTile(KSet<BlockPosition> fg, int div, int x, int z, O<Integer> m) {
BlockPosition key = new BlockPosition((int) mscale, Math.floorDiv(x, div), Math.floorDiv(z, div));
fg.add(key);
if (positions.containsKey(key)) {
return positions.get(key);
}
if (fastpositions.containsKey(key)) {
if (!working.contains(key) && working.size() < 9) {
m.set(m.get() - 1);
if (m.get() >= 0 && velocity < 50) {
working.add(key);
double mk = mscale;
double mkd = scale;
e.submit(() -> {
PrecisionStopwatch ps = PrecisionStopwatch.start();
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType);
rs.put(ps.getMilliseconds());
working.remove(key);
if (mk == mscale && mkd == scale) {
positions.put(key, b);
}
});
}
}
return fastpositions.get(key);
}
if (workingfast.contains(key) || workingfast.size() > Runtime.getRuntime().availableProcessors()) {
return null;
}
workingfast.add(key);
double mk = mscale;
double mkd = scale;
eh.submit(() -> {
PrecisionStopwatch ps = PrecisionStopwatch.start();
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType);
rs.put(ps.getMilliseconds());
workingfast.remove(key);
if (mk == mscale && mkd == scale) {
fastpositions.put(key, b);
}
});
return null;
}
private double getWorldX(double screenX) {
return (mscale * screenX) + ((oxp / scale) * mscale);
}
private double getWorldZ(double screenZ) {
return (mscale * screenZ) + ((ozp / scale) * mscale);
}
private double getScreenX(double x) {
return (x / mscale) - (oxp / scale);
}
private double getScreenZ(double z) {
return (z / mscale) - (ozp / scale);
}
private double lerp(double current, double target, double speed) {
double diff = target - current;
if (Math.abs(diff) < 0.5) return target;
return current + diff * speed;
}
@Override
public void paint(Graphics gx) {
if (engine.isClosed() && !updateEngine()) {
EventQueue.invokeLater(() -> {
try { setVisible(false); } catch (Throwable ignored) { }
});
return;
}
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
oxp = lerp(oxp, ox, 0.36);
ozp = lerp(ozp, oz, 0.36);
hx = lerp(hx, lx, 0.36);
hz = lerp(hz, lz, 0.36);
if (centities.flip() && overlay != null) {
overlay.requestEntities((List<GuiMarker> next) -> {
synchronized (lastEntities) {
lastEntities.clear();
if (next != null) {
lastEntities.addAll(next);
}
}
});
}
lowq = Math.max(Math.min((int) M.lerp(8, 28, velocity / 1000D), 28), 8);
PrecisionStopwatch p = PrecisionStopwatch.start();
Graphics2D g = (Graphics2D) gx;
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
w = getWidth();
h = getHeight();
double vscale = scale;
scale = w / 12D;
if (scale != vscale) {
positions.clear();
}
KSet<BlockPosition> gg = new KSet<>();
int iscale = (int) scale;
g.setColor(BG);
g.fillRect(0, 0, w, h);
double offsetX = oxp / scale;
double offsetZ = ozp / scale;
m.set(3);
for (int r = 0; r < Math.max(w, h); r += iscale) {
for (int i = -iscale; i < w + iscale; i += iscale) {
for (int j = -iscale; j < h + iscale; j += iscale) {
int a = i - (w / 2);
int b = j - (h / 2);
if (a * a + b * b <= r * r) {
int tx = (int) (Math.floor((offsetX + i) / iscale) * iscale);
int tz = (int) (Math.floor((offsetZ + j) / iscale) * iscale);
BufferedImage t = getTile(gg, iscale, tx, tz, m);
if (t != null) {
int rx = Math.floorMod((int) Math.floor(offsetX), iscale);
int rz = Math.floorMod((int) Math.floor(offsetZ), iscale);
g.drawImage(t, i - rx, j - rz, iscale, iscale, null);
}
}
}
}
}
if (grid) {
renderGrid(g, iscale, offsetX, offsetZ);
}
p.end();
for (BlockPosition i : positions.k()) {
if (!gg.contains(i)) {
positions.remove(i);
}
}
handleFollow();
renderOverlays(g, p.getMilliseconds());
if (!isVisible() || !getParent().isVisible()) {
return;
}
long targetMs = eco ? 32 : 16;
long sleepMs = Math.max(1, targetMs - (long) p.getMilliseconds());
J.a(() -> {
J.sleep(sleepMs);
repaint();
});
}
private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) {
g.setColor(GRID_COLOR);
int rx = Math.floorMod((int) Math.floor(offsetX), tileSize);
int rz = Math.floorMod((int) Math.floor(offsetZ), tileSize);
for (int i = -tileSize; i < w + tileSize; i += tileSize) {
g.drawLine(i - rx, 0, i - rx, h);
}
for (int j = -tileSize; j < h + tileSize; j += tileSize) {
g.drawLine(0, j - rz, w, j - rz);
}
}
private void handleFollow() {
if (follow && followMarker != null) {
animateTo(followMarker.worldX(), followMarker.worldZ());
}
}
private void renderOverlays(Graphics2D g, double frameMs) {
renderEntities(g);
if (help) {
renderOverlayHelp(g);
} else if (debug) {
renderOverlayDebug(g);
}
renderStatusBar(g, frameMs);
renderHoverOverlay(g, shift);
if (!notifications.isEmpty()) {
renderNotification(g);
}
}
private void renderStatusBar(Graphics2D g, double frameMs) {
int y = h - STATUS_HEIGHT;
g.setColor(STATUS_BG);
g.fillRect(0, y, w, STATUS_HEIGHT);
g.setColor(CARD_BORDER);
g.drawLine(0, y, w, y);
g.setFont(FONT_STATUS);
g.setColor(TEXT_SECONDARY);
double wx = getWorldX(w / 2.0);
double wz = getWorldZ(h / 2.0);
int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0;
String left = String.format(" %s | %.1f bpp | %s x %s blocks",
modeName(currentType), mscale,
Form.f((int) (mscale * w)), Form.f((int) (mscale * h)));
g.drawString(left, 8, y + 17);
String right = String.format("X: %s Z: %s | %d FPS ",
Form.f((int) wx), Form.f((int) wz), fps);
int rw = g.getFontMetrics().stringWidth(right);
g.drawString(right, w - rw - 8, y + 17);
g.setColor(ACCENT);
g.fillRect(0, y + 1, 3, STATUS_HEIGHT - 1);
}
private void renderEntities(Graphics2D g) {
GuiMarker firstPlayer = null;
List<GuiMarker> players = overlay == null ? List.of() : overlay.players();
if (players != null) {
for (GuiMarker i : players) {
firstPlayer = i;
renderPlayerMarker(g, i.worldX(), i.worldZ(), i.label());
}
}
synchronized (lastEntities) {
double dist = Double.MAX_VALUE;
GuiMarker nearest = null;
for (GuiMarker i : lastEntities) {
renderMobMarker(g, i.worldX(), i.worldZ());
if (shift) {
double dx = i.worldX() - getWorldX(hx);
double dz = i.worldZ() - getWorldZ(hz);
double d = dx * dx + dz * dz;
if (d < dist) {
dist = d;
nearest = i;
}
}
}
if (nearest != null && shift) {
double sx = getScreenX(nearest.worldX());
double sz = getScreenZ(nearest.worldZ());
g.setColor(MOB_COLOR);
g.fillOval((int) sx - 6, (int) sz - 6, 12, 12);
g.setColor(new Color(220, 80, 80, 60));
g.fillOval((int) sx - 10, (int) sz - 10, 20, 20);
KList<String> k = new KList<>();
k.add(nearest.label());
k.add("Pos: " + (int) nearest.worldX() + ", " + (int) nearest.worldY() + ", " + (int) nearest.worldZ());
if (nearest.maxHealth() > 0) {
k.add("HP: " + Form.f(nearest.health(), 1) + " / " + Form.f(nearest.maxHealth(), 1));
}
drawCard(w - CARD_PAD, CARD_PAD, 1, 0, g, k);
}
}
followMarker = firstPlayer;
}
private void renderPlayerMarker(Graphics2D g, double x, double z, String name) {
int sx = (int) getScreenX(x);
int sz = (int) getScreenZ(z);
g.setColor(new Color(80, 200, 120, 40));
g.fillOval(sx - 12, sz - 12, 24, 24);
g.setColor(PLAYER_COLOR);
g.fillOval(sx - 5, sz - 5, 10, 10);
g.setColor(new Color(40, 160, 80));
g.drawOval(sx - 5, sz - 5, 10, 10);
g.setFont(FONT_CARD_BODY);
g.setColor(TEXT_PRIMARY);
int nw = g.getFontMetrics().stringWidth(name);
g.drawString(name, sx - nw / 2, sz - 14);
}
private void renderMobMarker(Graphics2D g, double x, double z) {
int sx = (int) getScreenX(x);
int sz = (int) getScreenZ(z);
g.setColor(MOB_COLOR);
g.fillRect(sx - 2, sz - 2, 4, 4);
}
private void animateTo(double wx, double wz) {
double cx = getWorldX(getWidth() / 2.0);
double cz = getWorldZ(getHeight() / 2.0);
ox += ((wx - cx) / mscale) * scale;
oz += ((wz - cz) / mscale) * scale;
}
private void renderHoverOverlay(Graphics2D g, boolean detailed) {
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz));
IrisRegion region = engine.getComplex().getRegionStream().get(getWorldX(hx), getWorldZ(hz));
KList<String> l = new KList<>();
l.add(biome.getName());
l.add(region.getName());
l.add("Block " + (int) getWorldX(hx) + ", " + (int) getWorldZ(hz));
if (detailed) {
l.add("Chunk " + ((int) getWorldX(hx) >> 4) + ", " + ((int) getWorldZ(hz) >> 4));
l.add("Region " + (((int) getWorldX(hx) >> 4) >> 5) + ", " + (((int) getWorldZ(hz) >> 4) >> 5));
l.add("Key: " + biome.getLoadKey());
l.add("File: " + biome.getLoadFile());
}
drawCard((float) hx + 16, (float) hz, 0, 0, g, l);
}
private void renderOverlayDebug(Graphics2D g) {
KList<String> l = new KList<>();
l.add("Velocity: " + (int) velocity);
l.add("Tiles: " + positions.size() + " HD / " + fastpositions.size() + " LQ");
l.add("Workers: " + working.size() + " HD / " + workingfast.size() + " LQ");
l.add("Center: " + Form.f((int) getWorldX(getWidth() / 2.0)) + ", " + Form.f((int) getWorldZ(getHeight() / 2.0)));
drawCard(CARD_PAD, h - STATUS_HEIGHT - CARD_PAD, 0, 1, g, l);
}
private void renderOverlayHelp(Graphics2D g) {
KList<String> keys = new KList<>();
KList<String> descs = new KList<>();
keys.add("/"); descs.add("Toggle help");
keys.add("R"); descs.add("Refresh tiles");
keys.add("F"); descs.add("Follow player");
keys.add("+/-"); descs.add("Zoom in/out");
keys.add("\\"); descs.add("Reset zoom");
keys.add("M"); descs.add("Cycle render mode");
keys.add("P"); descs.add("Toggle tile quality");
keys.add("E"); descs.add("Toggle 30/60 FPS");
keys.add("G"); descs.add("Toggle grid");
int ff = 0;
for (RenderType i : RenderType.values()) {
ff++;
keys.add(String.valueOf(ff));
descs.add(modeName(i));
}
keys.add("Shift"); descs.add("Detailed biome info");
keys.add("Ctrl+Click"); descs.add("Teleport to cursor");
keys.add("Alt+Click"); descs.add("Open biome in editor");
int maxKeyW = 0;
g.setFont(FONT_HELP_KEY);
for (String k : keys) {
maxKeyW = Math.max(maxKeyW, g.getFontMetrics().stringWidth(k));
}
int lineH = 20;
int totalH = keys.size() * lineH + CARD_PAD * 2 + 4;
int totalW = maxKeyW + 180 + CARD_PAD * 2;
drawCardBackground(g, CARD_PAD, CARD_PAD, totalW, totalH);
for (int i = 0; i < keys.size(); i++) {
int y = CARD_PAD + 16 + i * lineH;
g.setFont(FONT_HELP_KEY);
g.setColor(ACCENT);
g.drawString(keys.get(i), CARD_PAD * 2, y);
g.setFont(FONT_CARD_BODY);
g.setColor(TEXT_SECONDARY);
g.drawString(descs.get(i), CARD_PAD * 2 + maxKeyW + 16, y);
}
}
private void renderNotification(Graphics2D g) {
int y = h - STATUS_HEIGHT - 50;
g.setFont(FONT_NOTIFICATION);
KList<String> active = new KList<>();
for (String i : notifications.k()) {
if (M.ms() > notifications.get(i)) {
notifications.remove(i);
} else {
active.add(i);
}
}
if (active.isEmpty()) return;
String text = String.join(" | ", active);
int tw = g.getFontMetrics().stringWidth(text);
int th = g.getFontMetrics().getHeight();
int px = (w - tw) / 2 - 16;
int py = y - th / 2 - 8;
int bw = tw + 32;
int bh = th + 16;
drawCardBackground(g, px, py, bw, bh);
g.setColor(TEXT_PRIMARY);
g.drawString(text, px + 16, py + th + 4);
}
private void drawCardBackground(Graphics2D g, int x, int y, int w, int h) {
RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, w, h, CARD_RADIUS, CARD_RADIUS);
g.setColor(CARD_BG);
g.fill(rect);
g.setColor(CARD_BORDER);
g.draw(rect);
}
private void drawCard(float x, float y, double pushX, double pushZ, Graphics2D g, KList<String> text) {
g.setFont(FONT_CARD_BODY);
int lineH = g.getFontMetrics().getHeight();
int cardW = 0;
for (String i : text) {
cardW = Math.max(cardW, g.getFontMetrics().stringWidth(i));
}
cardW += CARD_PAD * 2;
int cardH = text.size() * lineH + CARD_PAD * 2 - 4;
int cx = (int) (x - cardW * pushX);
int cy = (int) (y - cardH * pushZ);
drawCardBackground(g, cx, cy, cardW, cardH);
int ty = cy + CARD_PAD + lineH - 4;
for (int i = 0; i < text.size(); i++) {
g.setColor(i == 0 ? TEXT_PRIMARY : TEXT_SECONDARY);
g.setFont(i == 0 ? FONT_CARD_TITLE : FONT_CARD_BODY);
g.drawString(text.get(i), cx + CARD_PAD, ty + i * lineH);
}
}
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
if (e.isControlDown()) return;
double m0 = mscale;
double m1 = m0 + ((0.25 * m0) * notches);
m1 = Math.max(m1, 0.00001);
if (m1 == m0) return;
positions.clear();
fastpositions.clear();
Point p = e.getPoint();
double sx = p.getX();
double sz = p.getY();
double newOxp = scale * ((m0 / m1) * (sx + (oxp / scale)) - sx);
double newOzp = scale * ((m0 / m1) * (sz + (ozp / scale)) - sz);
mscale = m1;
oxp = newOxp;
ozp = newOzp;
ox = oxp;
oz = ozp;
}
@Override
public void mouseClicked(MouseEvent e) {
if (control) teleport();
else if (alt) open();
}
@Override public void mousePressed(MouseEvent e) { }
@Override public void mouseReleased(MouseEvent e) { }
@Override public void mouseEntered(MouseEvent e) { }
@Override public void mouseExited(MouseEvent e) { }
private void open() {
if (overlay == null) {
return;
}
String opened = overlay.openInEditor(getWorldX(hx), getWorldZ(hz), currentType);
if (opened != null) {
notify("Opened " + opened);
}
}
private void teleport() {
if (overlay == null) {
notify("No player in world");
return;
}
int xx = (int) getWorldX(hx);
int zz = (int) getWorldZ(hz);
overlay.teleport(xx, zz);
notify("Teleporting to " + xx + ", " + zz);
}
}
@@ -27,9 +27,7 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -182,10 +180,6 @@ public interface INMSBinding {
return false;
}
default boolean writeChunkNbtDirect(NBTWorld nbtWorld, int chunkX, int chunkZ, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes) {
return false;
}
default boolean forceEvictChunk(World world, int chunkX, int chunkZ) {
return false;
}
@@ -5,11 +5,10 @@ import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.EntityType;
import java.util.Locale;
@@ -23,7 +22,7 @@ public class DataFixerV1206 extends DataFixerV1192 {
json.remove("creature_spawn_probability");
}
var spawns = biome.getSpawns();
KList<IrisBiomeCustomSpawn> spawns = biome.getSpawns();
if (spawns != null && spawns.isNotEmpty()) {
JSONObject spawners = new JSONObject();
KMap<IrisBiomeCustomSpawnType, JSONArray> groups = new KMap<>();
@@ -32,20 +31,15 @@ public class DataFixerV1206 extends DataFixerV1192 {
if (i == null) {
continue;
}
EntityType type = i.getType();
if (type == null) {
String key = i.getTypeKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + biome.getId());
continue;
}
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
JSONObject o = new JSONObject();
NamespacedKey key = type.getKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with unresolved entity key in biome " + biome.getId());
continue;
}
o.put("type", key.toString());
o.put("type", key);
o.put("weight", i.getWeight());
o.put("minCount", i.getMinCount());
o.put("maxCount", i.getMaxCount());
@@ -18,13 +18,10 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.World;
public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
@@ -33,36 +30,11 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
public AsyncOrMedievalPregenMethod(World world, int threads) {
if (PaperLib.isPaper()) {
method = new AsyncPregenMethod(world, threads);
} else if (shouldUseSpigotDirect(world)) {
method = new SpigotDirectPregenMethod(world, threads);
} else {
method = new MedievalPregenMethod(world);
}
}
private static boolean shouldUseSpigotDirect(World world) {
if (!IrisSettings.get().getPregen().isEnableSpigotDirectPregen()) {
return false;
}
if (IrisToolbelt.isIrisStudioWorld(world)) {
return false;
}
if (!world.isAutoSave()) {
return false;
}
try {
String name = Bukkit.getServer().getName();
if (name != null) {
String lower = name.toLowerCase();
if (lower.contains("spigot") || lower.contains("craftbukkit")) {
return true;
}
}
} catch (Throwable ignored) {
}
return false;
}
@Override
public void init() {
method.init();
@@ -1,586 +0,0 @@
/*
* 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 art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.hunk.storage.AtomicHunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class SpigotDirectPregenMethod implements PregeneratorMethod {
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long PERMIT_WAIT_NOTIFY_MS = 500L;
private final World world;
private final int threads;
private final int timeoutSeconds;
private final int timeoutWarnIntervalMs;
private final int maxResidentTectonicPlates;
private final int mantleBackpressureWaitMs;
private final int mantleBackpressureTimeoutMs;
private final Semaphore semaphore;
private final AtomicInteger adaptiveInFlightLimit;
private final int adaptiveMinInFlightLimit;
private final AtomicInteger timeoutStreak = new AtomicInteger();
private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L);
private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger();
private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L);
private final AtomicInteger inFlight = new AtomicInteger();
private final AtomicLong submitted = new AtomicLong();
private final AtomicLong completed = new AtomicLong();
private final AtomicLong failed = new AtomicLong();
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
private final Object permitMonitor = new Object();
private final Set<Long> liveLoadedChunkKeys;
private final ConcurrentHashMap<Long, Boolean> chunksWrittenDirect;
private final AtomicBoolean writePathDisabled;
private volatile Engine cachedEngine;
private volatile Mantle cachedMantle;
private volatile NBTWorld nbtWorld;
private volatile MedievalPregenMethod fallback;
public SpigotDirectPregenMethod(World world, int threads) {
this.world = world;
int configured = Math.max(1, threads);
this.threads = Math.max(8, Math.min(32, configured));
this.semaphore = new Semaphore(this.threads, true);
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates();
this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs();
this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs();
this.liveLoadedChunkKeys = ConcurrentHashMap.newKeySet();
this.chunksWrittenDirect = new ConcurrentHashMap<>();
this.writePathDisabled = new AtomicBoolean(false);
}
@Override
public void init() {
try {
this.nbtWorld = new NBTWorld(world.getWorldFolder());
} catch (Throwable e) {
IrisLogging.error("SpigotDirect pregen could not open NBTWorld for " + world.getName() + "; disabling direct write path.");
IrisLogging.reportError(e);
writePathDisabled.set(true);
}
snapshotLoadedChunks();
IrisLogging.info("SpigotDirect pregen init: world=" + world.getName()
+ ", threads=" + threads
+ ", adaptiveLimit=" + adaptiveInFlightLimit.get()
+ ", initialLoadedChunks=" + liveLoadedChunkKeys.size()
+ ", maxResidentTectonicPlates=" + maxResidentTectonicPlates
+ ", timeout=" + timeoutSeconds + "s");
}
@Override
public void close() {
semaphore.acquireUninterruptibly(threads);
if (nbtWorld != null) {
try {
nbtWorld.flushNow();
nbtWorld.close();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
evictWrittenChunksFromServer();
if (fallback != null) {
try {
fallback.close();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
@Override
public void save() {
if (nbtWorld != null) {
try {
nbtWorld.save();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
if (fallback != null) {
try {
fallback.save();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
throw new UnsupportedOperationException();
}
@Override
public String getMethod(int x, int z) {
return "SpigotDirect";
}
@Override
public Mantle getMantle() {
if (IrisToolbelt.isIrisWorld(world)) {
return IrisToolbelt.access(world).getEngine().getMantle().getMantle();
}
return null;
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
enforceMantleBudget();
long key = chunkKey(x, z);
if (writePathDisabled.get() || nbtWorld == null || liveLoadedChunkKeys.contains(key) || world.isChunkLoaded(x, z)) {
ensureFallback().generateChunk(x, z, listener);
return;
}
Engine engine = resolveEngine();
if (engine == null || !engine.getDimension().isUseMantle()) {
ensureFallback().generateChunk(x, z, listener);
return;
}
try {
synchronized (permitMonitor) {
while (inFlight.get() >= adaptiveInFlightLimit.get()) {
permitMonitor.wait(PERMIT_WAIT_NOTIFY_MS);
}
}
while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
markSubmitted();
MultiBurst.burst.lazy(() -> runDirect(engine, x, z, listener));
}
private void runDirect(Engine engine, int x, int z, PregenListener listener) {
boolean success = false;
try {
int worldMinY = world.getMinHeight();
int worldMaxY = world.getMaxHeight();
int height = worldMaxY - worldMinY;
PlatformBlockState air = IrisPlatforms.get().registries().air();
Hunk<PlatformBlockState> blocks = new AirDefaultAtomicHunk(16, height, 16, air);
Hunk<PlatformBiome> biomes = Hunk.newSynchronizedArrayHunk(16, height, 16);
try {
engine.generate(x << 4, z << 4, blocks, biomes, false);
} catch (Throwable e) {
handleFailure(x, z, e);
return;
}
boolean wrote;
try {
wrote = INMS.get().writeChunkNbtDirect(nbtWorld, x, z, blocks, biomes);
} catch (Throwable e) {
handleFailure(x, z, e);
return;
}
if (!wrote) {
if (writePathDisabled.compareAndSet(false, true)) {
IrisLogging.warn("SpigotDirect NBT write returned false at chunk " + x + "," + z
+ "; disabling direct path. Subsequent chunks will route through MedievalPregenMethod on the iterator thread.");
}
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
success = true;
return;
}
chunksWrittenDirect.put(chunkKey(x, z), Boolean.TRUE);
try {
cleanupMantleChunk(x, z);
} catch (Throwable ignored) {
}
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
success = true;
} catch (Throwable e) {
handleFailure(x, z, e);
} finally {
markFinished(success);
semaphore.release();
}
}
private void handleFailure(int x, int z, Throwable error) {
IrisLogging.warn("SpigotDirect pregen failure at chunk " + x + "," + z + ". " + metricsSnapshot());
IrisLogging.reportError(error);
if (error != null) {
error.printStackTrace(System.err);
}
if (writePathDisabled.compareAndSet(false, true)) {
IrisLogging.warn("SpigotDirect: disabling direct write path after first failure. Subsequent chunks route through MedievalPregenMethod on the iterator thread.");
}
onTimeout(x, z);
}
private MedievalPregenMethod ensureFallback() {
MedievalPregenMethod existing = fallback;
if (existing != null) {
return existing;
}
synchronized (this) {
existing = fallback;
if (existing == null) {
existing = new MedievalPregenMethod(world);
existing.init();
fallback = existing;
}
}
return existing;
}
private void snapshotLoadedChunks() {
try {
Set<Long> loaded = J.sfut(() -> {
Set<Long> keys = new HashSet<>();
if (world == null) {
return keys;
}
for (Chunk loadedChunk : world.getLoadedChunks()) {
keys.add(chunkKey(loadedChunk.getX(), loadedChunk.getZ()));
}
return keys;
}).get();
if (loaded != null) {
liveLoadedChunkKeys.addAll(loaded);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void evictWrittenChunksFromServer() {
if (chunksWrittenDirect.isEmpty()) {
return;
}
try {
J.sfut(() -> {
int evicted = 0;
for (Long key : chunksWrittenDirect.keySet()) {
int x = keyX(key);
int z = keyZ(key);
if (!world.isChunkLoaded(x, z)) {
continue;
}
if (INMS.get().forceEvictChunk(world, x, z)) {
evicted++;
}
}
if (evicted > 0) {
IrisLogging.info("SpigotDirect: force-evicted " + evicted + " chunks loaded mid-pregen so the server reloads them from disk.");
}
}).get();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) | (z & 0xffffffffL);
}
private static int keyX(long key) {
return (int) (key >> 32);
}
private static int keyZ(long key) {
return (int) (key & 0xffffffffL);
}
private void onTimeout(int x, int z) {
int streak = timeoutStreak.incrementAndGet();
if (streak % ADAPTIVE_TIMEOUT_STEP == 0) {
lowerAdaptiveInFlightLimit();
}
long now = M.ms();
long last = lastTimeoutLogAt.get();
if (now - last < timeoutWarnIntervalMs || !lastTimeoutLogAt.compareAndSet(last, now)) {
suppressedTimeoutLogs.incrementAndGet();
return;
}
int suppressed = suppressedTimeoutLogs.getAndSet(0);
String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed;
IrisLogging.warn("SpigotDirect pregen failure cluster at " + x + "," + z
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ suppressedText + " " + metricsSnapshot());
}
private void onSuccess() {
int streak = timeoutStreak.get();
if (streak > 0) {
int newStreak = Math.max(0, streak - 2);
timeoutStreak.compareAndSet(streak, newStreak);
if (newStreak > 0) {
return;
}
}
if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) {
raiseAdaptiveInFlightLimit();
}
}
private void lowerAdaptiveInFlightLimit() {
while (true) {
int current = adaptiveInFlightLimit.get();
if (current <= adaptiveMinInFlightLimit) {
return;
}
int next = Math.max(adaptiveMinInFlightLimit, current - 1);
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("decrease", next);
notifyPermitWaiters();
return;
}
}
}
private void raiseAdaptiveInFlightLimit() {
while (true) {
int current = adaptiveInFlightLimit.get();
if (current >= threads) {
return;
}
int deficit = threads - current;
int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1;
int next = Math.min(threads, current + step);
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("increase", next);
notifyPermitWaiters();
return;
}
}
}
private void logAdaptiveLimit(String mode, int value) {
long now = M.ms();
long last = lastAdaptiveLogAt.get();
if (now - last < 5000L) {
return;
}
if (lastAdaptiveLogAt.compareAndSet(last, now)) {
IrisLogging.info("SpigotDirect pregen adaptive limit " + mode + " -> " + value + " (" + metricsSnapshot() + ")");
}
}
private String metricsSnapshot() {
long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get());
return "world=" + world.getName()
+ " permits=" + semaphore.availablePermits() + "/" + threads
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ " inFlight=" + inFlight.get()
+ " submitted=" + submitted.get()
+ " completed=" + completed.get()
+ " failed=" + failed.get()
+ " stalledForMs=" + stalledFor;
}
private void markSubmitted() {
submitted.incrementAndGet();
inFlight.incrementAndGet();
}
private void markFinished(boolean success) {
if (success) {
completed.incrementAndGet();
onSuccess();
} else {
failed.incrementAndGet();
}
lastProgressAt.set(M.ms());
int after = inFlight.decrementAndGet();
if (after < 0) {
inFlight.compareAndSet(after, 0);
}
notifyPermitWaiters();
}
private void notifyPermitWaiters() {
synchronized (permitMonitor) {
permitMonitor.notifyAll();
}
}
private void cleanupMantleChunk(int x, int z) {
Engine engine = resolveEngine();
if (engine != null) {
try {
engine.getMantle().forceCleanupChunk(x, z);
} catch (Throwable ignored) {
}
}
}
private Engine resolveEngine() {
Engine cached = cachedEngine;
if (cached != null) {
return cached;
}
if (!IrisToolbelt.isIrisWorld(world)) {
return null;
}
try {
Engine resolved = IrisToolbelt.access(world).getEngine();
if (resolved != null) {
cachedEngine = resolved;
}
return resolved;
} catch (Throwable ignored) {
return null;
}
}
private Mantle resolveMantle() {
Mantle cached = cachedMantle;
if (cached != null) {
return cached;
}
Mantle resolved = getMantle();
if (resolved != null) {
cachedMantle = resolved;
}
return resolved;
}
private void enforceMantleBudget() {
int cap = maxResidentTectonicPlates;
if (cap <= 0) {
return;
}
Mantle mantle = resolveMantle();
if (mantle == null) {
return;
}
int hardCap = cap * 2;
if (mantle.getLoadedRegionCount() <= hardCap) {
return;
}
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > hardCap) {
mantle.trim(0L, 0);
int freed = mantle.unloadTectonicPlate(0);
int resident = mantle.getLoadedRegionCount();
if (resident <= hardCap) {
break;
}
long elapsed = M.ms() - waitStart;
if (elapsed >= mantleBackpressureTimeoutMs) {
IrisLogging.warn("SpigotDirect mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident
+ " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. "
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot());
return;
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("SpigotDirect mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
}
try {
Thread.sleep(mantleBackpressureWaitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
private static final class AirDefaultAtomicHunk extends AtomicHunk<PlatformBlockState> {
private final PlatformBlockState airDefault;
AirDefaultAtomicHunk(int w, int h, int d, PlatformBlockState airDefault) {
super(w, h, d);
this.airDefault = airDefault;
}
@Override
public PlatformBlockState getRaw(int x, int y, int z) {
PlatformBlockState v = super.getRaw(x, y, z);
return v != null ? v : airDefault;
}
}
}
@@ -29,8 +29,6 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.EntityType;
import java.awt.*;
import java.util.Locale;
@@ -147,20 +145,15 @@ public class IrisBiomeCustom {
if (i == null) {
continue;
}
EntityType type = i.getType();
if (type == null) {
String key = i.getTypeKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + getId());
continue;
}
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
JSONObject o = new JSONObject();
NamespacedKey key = type.getKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with unresolved entity key in biome " + getId());
continue;
}
o.put("type", key.toString());
o.put("type", key);
o.put("weight", i.getWeight());
o.put("minCount", i.getMinCount());
o.put("maxCount", i.getMaxCount());
@@ -50,6 +50,13 @@ public class IrisBiomeCustomSpawn {
});
}
public String getTypeKey() {
if (type == null || type.isBlank()) {
return null;
}
return type.contains(":") ? type : "minecraft:" + type;
}
@MinNumber(1)
@Desc("The min to spawn")
private int minCount = 2;
@@ -38,11 +38,14 @@ import art.arcane.volmlib.util.math.Vector3d;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.ItemStack;
@@ -291,6 +294,35 @@ public final class BukkitPlatform implements IrisPlatform {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command);
}
@Override
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
if (!(world instanceof World bukkitWorld) || entityKey == null) {
return false;
}
NamespacedKey namespacedKey = NamespacedKey.fromString(entityKey);
if (namespacedKey == null) {
return false;
}
EntityType type = Registry.ENTITY_TYPE.get(namespacedKey);
if (type == null) {
return false;
}
Entity entity = spawnEntity(new Location(bukkitWorld, x, y, z), type, CreatureSpawnEvent.SpawnReason.COMMAND);
return entity != null;
}
@Override
public boolean giveItem(Object player, String itemKey, int amount) {
if (!(player instanceof Player bukkitPlayer) || itemKey == null || amount <= 0) {
return false;
}
Material material = Material.matchMaterial(itemKey);
if (material == null) {
return false;
}
return bukkitPlayer.getInventory().addItem(new ItemStack(material, amount)).isEmpty();
}
@Override
public void log(LogLevel level, String message) {
bridge().logSink().accept(level, message);
@@ -8,8 +8,8 @@ public class AsyncPregenMethodConcurrencyCapTest {
@Test
public void paperLikeRecommendedCapTracksWorkerThreads() {
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(1));
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(4));
assertEquals(48, AsyncPregenMethod.computePaperLikeRecommendedCap(12));
assertEquals(32, AsyncPregenMethod.computePaperLikeRecommendedCap(4));
assertEquals(96, AsyncPregenMethod.computePaperLikeRecommendedCap(12));
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(80));
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(128));
}
@@ -17,8 +17,8 @@ public class AsyncPregenMethodConcurrencyCapTest {
@Test
public void foliaRecommendedCapTracksWorkerThreads() {
assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(1));
assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(12));
assertEquals(80, AsyncPregenMethod.computeFoliaRecommendedCap(20));
assertEquals(96, AsyncPregenMethod.computeFoliaRecommendedCap(12));
assertEquals(160, AsyncPregenMethod.computeFoliaRecommendedCap(20));
assertEquals(192, AsyncPregenMethod.computeFoliaRecommendedCap(80));
}