mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-23 07:10:51 +00:00
few more changes
This commit is contained in:
@@ -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());
|
||||
|
||||
-28
@@ -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();
|
||||
|
||||
-586
@@ -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);
|
||||
|
||||
+4
-4
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user