From 6ceab880ea768d15516791a26ec291ae0e411bab Mon Sep 17 00:00:00 2001 From: Pxx500 Date: Fri, 14 Aug 2026 13:05:22 +0200 Subject: [PATCH 1/6] render production OpenGL primitives accurately --- .../modularui/utils/GlStateManager.java | 6 +- .../modularui/preview/PreviewDrawContext.java | 179 +++++++++++++++++- .../preview/runtime/ProjectRuntime.java | 16 +- src/main/java/org/lwjgl/opengl/GL11.java | 37 +++- .../preview/PreviewDrawContextTest.java | 163 ++++++++++++++++ 5 files changed, 383 insertions(+), 18 deletions(-) create mode 100644 src/test/java/dev/modularui/preview/PreviewDrawContextTest.java diff --git a/src/main/java/com/cleanroommc/modularui/utils/GlStateManager.java b/src/main/java/com/cleanroommc/modularui/utils/GlStateManager.java index 6894d2f..e5b7f2e 100644 --- a/src/main/java/com/cleanroommc/modularui/utils/GlStateManager.java +++ b/src/main/java/com/cleanroommc/modularui/utils/GlStateManager.java @@ -1,8 +1,12 @@ package com.cleanroommc.modularui.utils; +import dev.modularui.preview.PreviewDrawContext; + public final class GlStateManager { private GlStateManager() {} - public static void color(float red, float green, float blue, float alpha) {} + public static void color(float red, float green, float blue, float alpha) { + PreviewDrawContext.color(red, green, blue, alpha); + } } diff --git a/src/main/java/dev/modularui/preview/PreviewDrawContext.java b/src/main/java/dev/modularui/preview/PreviewDrawContext.java index 2994707..97e340c 100644 --- a/src/main/java/dev/modularui/preview/PreviewDrawContext.java +++ b/src/main/java/dev/modularui/preview/PreviewDrawContext.java @@ -1,16 +1,23 @@ package dev.modularui.preview; import dev.modularui.preview.assets.AssetResolver; -import java.awt.Graphics2D; +import java.awt.BasicStroke; import java.awt.Color; +import java.awt.Graphics2D; import java.awt.Polygon; +import java.awt.RenderingHints; +import java.awt.Shape; import java.awt.image.BufferedImage; import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.FloatBuffer; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; import java.util.LinkedHashSet; import java.util.List; @@ -42,9 +49,16 @@ public static List run(Graphics2D graphics, AssetResolver assets, Runnab } public static void drawRect(int left, int top, int right, int bottom, int color) { + Polygon polygon = polygon(new double[] { + left, top, 0, + right, top, 0, + right, bottom, 0, + left, bottom, 0 + }, new int[] { 0, 1, 2, 3 }); + if (captureStencil(polygon)) return; Graphics2D graphics = requireGraphics(); graphics.setColor(new Color(color, true)); - graphics.fillRect(left, top, Math.max(0, right - left), Math.max(0, bottom - top)); + graphics.fillPolygon(polygon); } public static int stringWidth(String text) { @@ -105,6 +119,67 @@ public static int currentColor() { return requireState().color.getRGB(); } + public static void enable(int capability) { + State state = requireState(); + if (capability == org.lwjgl.opengl.GL11.GL_STENCIL_TEST) state.stencilEnabled = true; + if (capability == org.lwjgl.opengl.GL11.GL_LINE_SMOOTH) state.smoothLines = true; + } + + public static void disable(int capability) { + State state = requireState(); + if (capability == org.lwjgl.opengl.GL11.GL_LINE_SMOOTH) { + state.smoothLines = false; + return; + } + if (capability != org.lwjgl.opengl.GL11.GL_STENCIL_TEST) return; + state.stencilEnabled = false; + state.stencilClips.clear(); + state.graphics.setClip(state.originalClip); + } + + public static void colorMask(boolean red, boolean green, boolean blue, boolean alpha) { + State state = requireState(); + boolean drawingColors = red || green || blue || alpha; + if (!drawingColors && state.stencilEnabled) { + state.capturedStencil = new Area(); + return; + } + if (drawingColors && state.capturedStencil != null) finishStencilCapture(state); + } + + public static void stencilOperation(int depthPass) { + requireState().stencilDepthPass = depthPass; + } + + public static void lineWidth(float width) { + requireState().lineWidth = Math.max(1F, width); + } + + public static void begin(int mode) { + State state = requireState(); + state.immediateMode = mode; + state.immediateVertices.clear(); + } + + public static void vertex(double x, double y) { + State state = requireState(); + state.immediateVertices.add(x); + state.immediateVertices.add(y); + state.immediateVertices.add(0.0); + } + + public static void end() { + State state = requireState(); + if (state.immediateMode < 0) return; + double[] positions = state.immediateVertices.stream().mapToDouble(Double::doubleValue).toArray(); + int count = positions.length / 3; + int[] colors = new int[count]; + java.util.Arrays.fill(colors, state.color.getRGB()); + drawVertices(state.immediateMode, positions, null, colors, count); + state.immediateMode = -1; + state.immediateVertices.clear(); + } + public static void bindTexture(ResourceLocation location) { State state = requireState(); if (state.assets == null) throw new IllegalStateException("No asset resolver is active for " + location); @@ -146,9 +221,16 @@ public static void drawVertices(int mode, double[] positions, double[] textureCo return; } if (mode == org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP) { - for (int index = 0; index + 2 < count; index++) { - drawPolygon(positions, colors, new int[] { index, index + 1, index + 2 }); - } + drawTriangleStrip(positions, colors, count); + return; + } + if (mode == org.lwjgl.opengl.GL11.GL_LINES) { + for (int index = 0; index + 1 < count; index += 2) drawLine(positions, index, index + 1); + return; + } + if (mode == org.lwjgl.opengl.GL11.GL_LINE_STRIP || mode == org.lwjgl.opengl.GL11.GL_LINE_LOOP) { + for (int index = 0; index + 1 < count; index++) drawLine(positions, index, index + 1); + if (mode == org.lwjgl.opengl.GL11.GL_LINE_LOOP && count > 2) drawLine(positions, count - 1, 0); } } @@ -186,14 +268,87 @@ private static void drawPolygon(double[] positions, int[] colors, int offset, in } private static void drawPolygon(double[] positions, int[] colors, int[] indices) { + Polygon polygon = polygon(positions, indices); + fillShape(polygon, colors, indices[0]); + } + + private static void drawTriangleStrip(double[] positions, int[] colors, int count) { + Area strip = new Area(); + for (int index = 0; index + 2 < count; index++) { + int first = index % 2 == 0 ? index : index + 1; + int second = index % 2 == 0 ? index + 1 : index; + strip.add(new Area(triangle(positions, first, second, index + 2))); + } + fillShape(strip, colors, 0); + } + + private static Shape triangle(double[] positions, int first, int second, int third) { + Path2D.Double triangle = new Path2D.Double(); + Point2D firstPoint = transform(positions[first * 3], positions[first * 3 + 1]); + Point2D secondPoint = transform(positions[second * 3], positions[second * 3 + 1]); + Point2D thirdPoint = transform(positions[third * 3], positions[third * 3 + 1]); + triangle.moveTo(firstPoint.getX(), firstPoint.getY()); + triangle.lineTo(secondPoint.getX(), secondPoint.getY()); + triangle.lineTo(thirdPoint.getX(), thirdPoint.getY()); + triangle.closePath(); + return triangle; + } + + private static void fillShape(Shape shape, int[] colors, int colorIndex) { + if (captureStencil(shape)) return; + State state = requireState(); + Color drawColor = colors.length > colorIndex ? new Color(colors[colorIndex], true) : state.color; + state.graphics.setColor(drawColor); + state.graphics.fill(shape); + } + + private static Polygon polygon(double[] positions, int[] indices) { Polygon polygon = new Polygon(); for (int index : indices) { Point2D point = transform(positions[index * 3], positions[index * 3 + 1]); polygon.addPoint((int) Math.round(point.getX()), (int) Math.round(point.getY())); } + return polygon; + } + + private static boolean captureStencil(Shape shape) { State state = requireState(); - state.graphics.setColor(colors.length > indices[0] ? new Color(colors[indices[0]], true) : state.color); - state.graphics.fillPolygon(polygon); + if (state.capturedStencil == null) return false; + state.capturedStencil.add(new Area(shape)); + return true; + } + + private static void finishStencilCapture(State state) { + if (state.stencilDepthPass == org.lwjgl.opengl.GL11.GL_DECR) { + if (!state.stencilClips.isEmpty()) state.stencilClips.pop(); + } else if (state.stencilDepthPass == org.lwjgl.opengl.GL11.GL_INCR) { + Area clip = new Area(state.capturedStencil); + if (!state.stencilClips.isEmpty()) clip.intersect(new Area(state.stencilClips.peek())); + state.stencilClips.push(clip); + } + state.capturedStencil = null; + if (state.stencilClips.isEmpty()) state.graphics.setClip(state.originalClip); + else state.graphics.setClip(state.stencilClips.peek()); + } + + private static void drawLine(double[] positions, int first, int second) { + Point2D from = transform(positions[first * 3], positions[first * 3 + 1]); + Point2D to = transform(positions[second * 3], positions[second * 3 + 1]); + State state = requireState(); + state.graphics.setColor(state.color); + Object previousAntialiasing = state.graphics.getRenderingHint(RenderingHints.KEY_ANTIALIASING); + java.awt.Stroke previousStroke = state.graphics.getStroke(); + double framebufferScale = Math.sqrt(Math.abs(state.graphics.getTransform().getDeterminant())); + float logicalWidth = (float) (state.lineWidth / Math.max(1D, framebufferScale)); + state.graphics.setStroke(new BasicStroke(logicalWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); + state.graphics.setRenderingHint( + RenderingHints.KEY_ANTIALIASING, + state.smoothLines ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); + state.graphics.draw(new Line2D.Double(from, to)); + state.graphics.setStroke(previousStroke); + if (previousAntialiasing != null) { + state.graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, previousAntialiasing); + } } private static void drawTexturedQuad(double[] positions, double[] textureCoordinates, int offset) { @@ -258,15 +413,25 @@ private static final class State { private final Graphics2D graphics; private final AssetResolver assets; + private final Shape originalClip; private final Deque matrices = new ArrayDeque<>(); + private final Deque stencilClips = new ArrayDeque<>(); private final Set assetSources = new LinkedHashSet<>(); private AffineTransform matrix = new AffineTransform(); private Color color = Color.WHITE; private BufferedImage texture; + private int immediateMode = -1; + private final List immediateVertices = new ArrayList<>(); + private boolean stencilEnabled; + private boolean smoothLines; + private float lineWidth = 1F; + private int stencilDepthPass = org.lwjgl.opengl.GL11.GL_KEEP; + private Area capturedStencil; private State(Graphics2D graphics, AssetResolver assets) { this.graphics = graphics; this.assets = assets; + this.originalClip = graphics.getClip(); } } } diff --git a/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java b/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java index b96306e..467b325 100644 --- a/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java +++ b/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java @@ -169,13 +169,17 @@ public boolean scroll(ScrollDirection direction, int amount) { private PreviewResult render(Class screenClass, Object screen, Object panel, Bounds panelBounds, PreviewScreen previewScreen, ScreenLayout layout, AssetResolver assets, AssetResolver.Translations translations) { - BufferedImage logicalImage = new BufferedImage( - layout.logicalWidth(), - layout.logicalHeight(), + BufferedImage framebuffer = new BufferedImage( + layout.screenWidth(), + layout.screenHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = logicalImage.createGraphics(); + Graphics2D graphics = framebuffer.createGraphics(); + graphics.setRenderingHint( + java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + graphics.scale(layout.guiScale(), layout.guiScale()); graphics.setColor(new Color(previewScreen.backgroundColor(), true)); - graphics.fillRect(0, 0, logicalImage.getWidth(), logicalImage.getHeight()); + graphics.fillRect(0, 0, layout.logicalWidth(), layout.logicalHeight()); List renderedAssets = new ArrayList<>(translations.sources()); try { StatCollector.installTranslations(translations.values()); @@ -190,7 +194,7 @@ private PreviewResult render(Class screenClass, Object screen, Object panel, graphics.dispose(); } List widgets = captureWidgets(panel, panelBounds, layout); - return new PreviewResult(layout.toFramebuffer(logicalImage), layout, widgets, List.of(), renderedAssets); + return new PreviewResult(framebuffer, layout, widgets, List.of(), renderedAssets); } private boolean dispatchMouse(Class screenClass, Object screen, int button, boolean pressed) { diff --git a/src/main/java/org/lwjgl/opengl/GL11.java b/src/main/java/org/lwjgl/opengl/GL11.java index 5b790ed..41f4f43 100644 --- a/src/main/java/org/lwjgl/opengl/GL11.java +++ b/src/main/java/org/lwjgl/opengl/GL11.java @@ -11,6 +11,7 @@ public final class GL11 { public static final int GL_LINES = 1; public static final int GL_LINE_LOOP = 2; public static final int GL_LINE_STRIP = 3; + public static final int GL_LINE_SMOOTH = 0x0B20; public static final int GL_TRIANGLES = 4; public static final int GL_TRIANGLE_STRIP = 5; public static final int GL_TRIANGLE_FAN = 6; @@ -66,6 +67,26 @@ public static void glLoadMatrix(FloatBuffer matrix) { PreviewDrawContext.loadMatrix(matrix); } + public static void glLineWidth(float width) { + PreviewDrawContext.lineWidth(width); + } + + public static void glBegin(int mode) { + PreviewDrawContext.begin(mode); + } + + public static void glEnd() { + PreviewDrawContext.end(); + } + + public static void glVertex2f(float x, float y) { + PreviewDrawContext.vertex(x, y); + } + + public static void glVertex2d(double x, double y) { + PreviewDrawContext.vertex(x, y); + } + public static void glMultMatrix(FloatBuffer matrix) { PreviewDrawContext.multiplyMatrix(matrix); } @@ -106,9 +127,13 @@ public static void glGetInteger(int name, IntBuffer target) { while (target.hasRemaining()) target.put(0); } - public static void glDisable(int capability) {} + public static void glDisable(int capability) { + PreviewDrawContext.disable(capability); + } - public static void glEnable(int capability) {} + public static void glEnable(int capability) { + PreviewDrawContext.enable(capability); + } public static void glBlendFunc(int source, int destination) {} @@ -120,7 +145,9 @@ public static void glClearDepth(double depth) {} public static void glClearStencil(int value) {} - public static void glColorMask(boolean red, boolean green, boolean blue, boolean alpha) {} + public static void glColorMask(boolean red, boolean green, boolean blue, boolean alpha) { + PreviewDrawContext.colorMask(red, green, blue, alpha); + } public static void glDepthMask(boolean enabled) {} @@ -146,7 +173,9 @@ public static void glStencilFunc(int function, int reference, int mask) {} public static void glStencilMask(int mask) {} - public static void glStencilOp(int fail, int depthFail, int depthPass) {} + public static void glStencilOp(int fail, int depthFail, int depthPass) { + PreviewDrawContext.stencilOperation(depthPass); + } public static void glViewport(int x, int y, int width, int height) {} } diff --git a/src/test/java/dev/modularui/preview/PreviewDrawContextTest.java b/src/test/java/dev/modularui/preview/PreviewDrawContextTest.java new file mode 100644 index 0000000..d66705e --- /dev/null +++ b/src/test/java/dev/modularui/preview/PreviewDrawContextTest.java @@ -0,0 +1,163 @@ +package dev.modularui.preview; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import org.lwjgl.opengl.GL11; + +class PreviewDrawContextTest { + + @Test + void clipsDrawCallsToAnActiveStencilRectangle() { + BufferedImage image = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + try { + PreviewDrawContext.run(graphics, () -> { + GL11.glEnable(GL11.GL_STENCIL_TEST); + GL11.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_INCR); + GL11.glColorMask(false, false, false, false); + drawQuad(5, 5, 15, 15, Color.WHITE.getRGB()); + GL11.glColorMask(true, true, true, true); + + PreviewDrawContext.drawRect(0, 0, 20, 20, Color.RED.getRGB()); + + GL11.glDisable(GL11.GL_STENCIL_TEST); + PreviewDrawContext.drawRect(0, 0, 2, 2, Color.BLUE.getRGB()); + }); + } finally { + graphics.dispose(); + } + + assertEquals(Color.BLUE.getRGB(), image.getRGB(1, 1)); + assertEquals(0, image.getRGB(3, 3)); + assertEquals(Color.RED.getRGB(), image.getRGB(10, 10)); + assertEquals(0, image.getRGB(17, 17)); + } + + @Test + void fillsAConnectedTriangleStripWithoutCrossingItsInterior() { + BufferedImage image = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + try { + PreviewDrawContext.run(graphics, () -> PreviewDrawContext.drawVertices( + GL11.GL_TRIANGLE_STRIP, + new double[] { + 2, 2, 0, + 2, 8, 0, + 8, 2, 0, + 8, 8, 0, + 14, 2, 0, + 14, 8, 0 + }, + null, + new int[] { Color.WHITE.getRGB(), Color.WHITE.getRGB(), Color.WHITE.getRGB(), + Color.WHITE.getRGB(), Color.WHITE.getRGB(), Color.WHITE.getRGB() }, + 6)); + } finally { + graphics.dispose(); + } + + assertNotEquals(0, image.getRGB(4, 5)); + assertNotEquals(0, image.getRGB(10, 5)); + assertEquals(0, image.getRGB(17, 5)); + } + + @Test + void blendsAnAnnularTriangleStripOnceWithoutFillingItsCenter() { + BufferedImage image = new BufferedImage(40, 40, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + int color = new Color(110, 116, 128, 34).getRGB(); + try { + PreviewDrawContext.run(graphics, () -> PreviewDrawContext.drawVertices( + GL11.GL_TRIANGLE_STRIP, + annularStrip(20, 20, 16, 10, 12), + null, + IntStream.generate(() -> color).limit(26).toArray(), + 26)); + } finally { + graphics.dispose(); + } + + assertEquals(0, alphaAt(image, 20, 20)); + assertEquals(34, alphaAt(image, 33, 20)); + assertEquals(34, alphaAt(image, 31, 27)); + } + + @Test + void appliesTheModularUiColorStateToImmediatePrimitives() { + BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + try { + PreviewDrawContext.run(graphics, () -> { + com.cleanroommc.modularui.utils.GlStateManager.color(0.25F, 0.5F, 0.75F, 0.5F); + GL11.glBegin(GL11.GL_TRIANGLE_FAN); + GL11.glVertex2f(1, 1); + GL11.glVertex2f(8, 1); + GL11.glVertex2f(8, 8); + GL11.glVertex2f(1, 8); + GL11.glEnd(); + }); + } finally { + graphics.dispose(); + } + + assertEquals(new Color(64, 128, 191, 128).getRGB(), image.getRGB(5, 5)); + } + + @Test + void keepsOpenGlLineWidthInFramebufferPixelsWhenGuiCoordinatesAreScaled() { + BufferedImage image = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + graphics.scale(2, 2); + try { + PreviewDrawContext.run(graphics, () -> { + GL11.glEnable(GL11.GL_LINE_SMOOTH); + GL11.glLineWidth(1F); + GL11.glBegin(GL11.GL_LINES); + GL11.glVertex2d(1, 5.25); + GL11.glVertex2d(9, 5.25); + GL11.glEnd(); + }); + } finally { + graphics.dispose(); + } + + long paintedRows = IntStream.range(0, image.getHeight()) + .filter(y -> IntStream.range(0, image.getWidth()).anyMatch(x -> (image.getRGB(x, y) >>> 24) != 0)) + .count(); + assertEquals(1, paintedRows); + } + + private static void drawQuad(int left, int top, int right, int bottom, int color) { + PreviewDrawContext.drawVertices( + GL11.GL_QUADS, + new double[] { left, top, 0, right, top, 0, right, bottom, 0, left, bottom, 0 }, + null, + new int[] { color, color, color, color }, + 4); + } + + private static double[] annularStrip( + double centerX, double centerY, double outerRadius, double innerRadius, int segments) { + double[] positions = new double[(segments + 1) * 6]; + for (int segment = 0; segment <= segments; segment++) { + double angle = segment * Math.PI * 2 / segments; + int offset = segment * 6; + positions[offset] = centerX + outerRadius * Math.cos(angle); + positions[offset + 1] = centerY + outerRadius * Math.sin(angle); + positions[offset + 3] = centerX + innerRadius * Math.cos(angle); + positions[offset + 4] = centerY + innerRadius * Math.sin(angle); + } + return positions; + } + + private static int alphaAt(BufferedImage image, int x, int y) { + return image.getRGB(x, y) >>> 24; + } +} + From c89c184a9ae4d3838c11263f56e9c4fc30b71d5d Mon Sep 17 00:00:00 2001 From: Pxx500 Date: Fri, 14 Aug 2026 13:06:48 +0200 Subject: [PATCH 2/6] share preview translations with production classes --- .../preview/runtime/ProjectRuntime.java | 15 ++++- .../preview/PreviewDrawContextTest.java | 1 - .../modularui/preview/PreviewEngineTest.java | 64 ++++++++++++++++--- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java b/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java index 467b325..2ab26a4 100644 --- a/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java +++ b/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java @@ -447,10 +447,19 @@ private static final class IsolatedClassLoader extends URLClassLoader { "jdk.", "sun.", "dev.modularui.preview.", - "net.minecraft.", - "net.minecraftforge.", - "cpw.mods.", + "net.minecraft.client.", + "net.minecraft.client.renderer.GlStateManager", + "net.minecraft.entity.", + "net.minecraft.inventory.", + "net.minecraft.item.", + "net.minecraft.util.ResourceLocation", + "net.minecraft.util.StatCollector", + "net.minecraft.util.StringTranslate", + "cpw.mods.fml.common.Loader", + "cpw.mods.fml.common.ModContainer", + "cpw.mods.fml.relauncher.", "org.lwjgl.", + "org.lwjgl.opengl.GL11", "org.apache.logging.log4j.", "com.cleanroommc.modularui.core.mixins.early.minecraft."); diff --git a/src/test/java/dev/modularui/preview/PreviewDrawContextTest.java b/src/test/java/dev/modularui/preview/PreviewDrawContextTest.java index d66705e..1625dda 100644 --- a/src/test/java/dev/modularui/preview/PreviewDrawContextTest.java +++ b/src/test/java/dev/modularui/preview/PreviewDrawContextTest.java @@ -160,4 +160,3 @@ private static int alphaAt(BufferedImage image, int x, int y) { return image.getRGB(x, y) >>> 24; } } - diff --git a/src/test/java/dev/modularui/preview/PreviewEngineTest.java b/src/test/java/dev/modularui/preview/PreviewEngineTest.java index ff7726f..ea32393 100644 --- a/src/test/java/dev/modularui/preview/PreviewEngineTest.java +++ b/src/test/java/dev/modularui/preview/PreviewEngineTest.java @@ -173,6 +173,49 @@ void opensARealModularUiPanelWithALinkedSyncHandlerAndUsesItsLayoutAndArtifact() } } + @Test + void sharesInstalledTranslationsWithProductionRuntimeClasses() throws Exception { + Path projectRoot = Files.createDirectories(temporaryDirectory.resolve("translated-panel-preview")); + Path libraries = Files.createDirectories(projectRoot.resolve("libs")); + writeClassJar( + libraries.resolve("production-boundaries.jar"), + net.minecraft.util.StatCollector.class, + com.cleanroommc.modularui.utils.GlStateManager.class, + org.lwjgl.opengl.GL11.class); + Path languageFile = projectRoot.resolve("src/preview/resources/assets/example/lang/en_US.lang"); + Files.createDirectories(languageFile.getParent()); + Files.writeString(languageFile, "example.preview.label=Translated label\n"); + Path source = projectRoot.resolve("src/preview/java/example/TranslatedPanelPreview.java"); + Files.createDirectories(source.getParent()); + Files.writeString( + source, + """ + package example; + + import com.cleanroommc.modularui.screen.ModularPanel; + import dev.modularui.preview.PreviewEntrypoint; + import net.minecraft.util.StatCollector; + + public final class TranslatedPanelPreview implements PreviewEntrypoint { + @Override + public Object createPanel(PreviewEntrypoint.Context context) { + String label = StatCollector.translateToLocal("example.preview.label"); + if (!"Translated label".equals(label)) { + throw new IllegalStateException("Production runtime did not receive preview translations: " + label); + } + return ModularPanel.defaultPanel("translated_panel", 176, 220); + } + } + """); + + try (PreviewSession ignored = PreviewEngine.open( + projectRoot, + "example.TranslatedPanelPreview", + new PreviewScreen(800, 600, 1))) { + // Opening the production panel proves that its StatCollector sees the installed language map. + } + } + @Test void routesLocalMouseClicksThroughTheRealModularUiScreen() throws Exception { Path projectRoot = Files.createDirectories(temporaryDirectory.resolve("interactive-panel-preview")); @@ -452,15 +495,18 @@ private static void writeEntries(JarOutputStream jar, String... entries) throws } } - private static void writeClassJar(Path file, Class type) throws Exception { - String entryName = type.getName() - .replace('.', '/') + ".class"; - try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(file)); - InputStream bytes = type.getClassLoader() - .getResourceAsStream(entryName)) { - jar.putNextEntry(new JarEntry(entryName)); - jar.write(bytes.readAllBytes()); - jar.closeEntry(); + private static void writeClassJar(Path file, Class... types) throws Exception { + try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(file))) { + for (Class type : types) { + String entryName = type.getName() + .replace('.', '/') + ".class"; + try (InputStream bytes = type.getClassLoader() + .getResourceAsStream(entryName)) { + jar.putNextEntry(new JarEntry(entryName)); + jar.write(bytes.readAllBytes()); + jar.closeEntry(); + } + } } } From 2564cc420ca165dca4cbbdd376ff02648497b36b Mon Sep 17 00:00:00 2001 From: Pxx500 Date: Fri, 14 Aug 2026 13:07:42 +0200 Subject: [PATCH 3/6] support production Minecraft and Forge client ABI --- src/main/java/cpw/mods/fml/common/Loader.java | 24 +++++++++-- .../java/net/minecraft/client/Minecraft.java | 8 ++++ .../client/multiplayer/WorldClient.java | 3 ++ .../client/renderer/GlStateManager.java | 41 +++++++++++++++++++ .../client/renderer/Tessellator.java | 5 +++ .../java/net/minecraft/entity/Entity.java | 6 ++- .../minecraft/entity/player/EntityPlayer.java | 7 ++++ .../entity/player/PlayerCapabilities.java | 6 +++ src/main/java/org/lwjgl/input/Mouse.java | 4 ++ 9 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/minecraft/client/multiplayer/WorldClient.java create mode 100644 src/main/java/net/minecraft/client/renderer/GlStateManager.java create mode 100644 src/main/java/net/minecraft/entity/player/PlayerCapabilities.java diff --git a/src/main/java/cpw/mods/fml/common/Loader.java b/src/main/java/cpw/mods/fml/common/Loader.java index 9c9ae00..a870046 100644 --- a/src/main/java/cpw/mods/fml/common/Loader.java +++ b/src/main/java/cpw/mods/fml/common/Loader.java @@ -5,13 +5,21 @@ /** Minimal mod lookup used by ModularUI2 while running outside Forge. */ public final class Loader { - private static final Loader INSTANCE = new Loader(); + private static Loader instance = new Loader(); private static final ModContainer ACTIVE_MOD_CONTAINER = () -> "preview"; + private static final ModContainer MINECRAFT_CONTAINER = () -> "minecraft"; + private static boolean bootstrappingVanilla; + @SuppressWarnings("unused") + private java.util.List mods = java.util.List.of(); + @SuppressWarnings("unused") + private Map namedMods = Map.of(); + @SuppressWarnings("unused") + private Object modController; private Loader() {} public static Loader instance() { - return INSTANCE; + return instance; } public static boolean isModLoaded(String modId) { @@ -19,10 +27,18 @@ public static boolean isModLoaded(String modId) { } public Map getIndexedModList() { - return Map.of(); + return namedMods; } public ModContainer activeModContainer() { - return ACTIVE_MOD_CONTAINER; + return bootstrappingVanilla ? MINECRAFT_CONTAINER : ACTIVE_MOD_CONTAINER; + } + + public static void beginVanillaBootstrap() { + bootstrappingVanilla = true; + } + + public static void endVanillaBootstrap() { + bootstrappingVanilla = false; } } diff --git a/src/main/java/net/minecraft/client/Minecraft.java b/src/main/java/net/minecraft/client/Minecraft.java index d12f8b7..97d1d2c 100644 --- a/src/main/java/net/minecraft/client/Minecraft.java +++ b/src/main/java/net/minecraft/client/Minecraft.java @@ -1,9 +1,11 @@ package net.minecraft.client; import java.io.FileNotFoundException; +import java.io.File; import net.minecraft.client.audio.SoundHandler; import net.minecraft.client.entity.EntityClientPlayerMP; import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.multiplayer.WorldClient; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.client.resources.IResourceManager; @@ -14,6 +16,8 @@ public class Minecraft { public final FontRenderer fontRenderer = new FontRenderer(); public final TextureManager renderEngine = new TextureManager(); public final EntityClientPlayerMP thePlayer = new EntityClientPlayerMP(); + public final WorldClient theWorld = null; + public final File mcDataDir = new File(System.getProperty("java.io.tmpdir"), "modularui2-preview"); private final SoundHandler soundHandler = new SoundHandler(); private final IResourceManager resourceManager = location -> { throw new FileNotFoundException(location.toString()); @@ -31,6 +35,10 @@ public SoundHandler getSoundHandler() { return soundHandler; } + public TextureManager getTextureManager() { + return renderEngine; + } + public static long getSystemTime() { return System.currentTimeMillis(); } diff --git a/src/main/java/net/minecraft/client/multiplayer/WorldClient.java b/src/main/java/net/minecraft/client/multiplayer/WorldClient.java new file mode 100644 index 0000000..926a3e0 --- /dev/null +++ b/src/main/java/net/minecraft/client/multiplayer/WorldClient.java @@ -0,0 +1,3 @@ +package net.minecraft.client.multiplayer; + +public class WorldClient {} diff --git a/src/main/java/net/minecraft/client/renderer/GlStateManager.java b/src/main/java/net/minecraft/client/renderer/GlStateManager.java new file mode 100644 index 0000000..f16b229 --- /dev/null +++ b/src/main/java/net/minecraft/client/renderer/GlStateManager.java @@ -0,0 +1,41 @@ +package net.minecraft.client.renderer; + +import org.lwjgl.opengl.GL11; + +/** Minecraft 1.7 render-state bridge used by production UI code. */ +public final class GlStateManager { + + private GlStateManager() {} + + public static void color(float red, float green, float blue, float alpha) { + GL11.glColor4f(red, green, blue, alpha); + } + + public static void enableBlend() { + GL11.glEnable(GL11.GL_BLEND); + } + + public static void disableBlend() { + GL11.glDisable(GL11.GL_BLEND); + } + + public static void blendFunc(int source, int destination) { + GL11.glBlendFunc(source, destination); + } + + public static void enableTexture2D() { + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + public static void disableTexture2D() { + GL11.glDisable(GL11.GL_TEXTURE_2D); + } + + public static void pushMatrix() { + GL11.glPushMatrix(); + } + + public static void popMatrix() { + GL11.glPopMatrix(); + } +} diff --git a/src/main/java/net/minecraft/client/renderer/Tessellator.java b/src/main/java/net/minecraft/client/renderer/Tessellator.java index 3630e59..5b9028e 100644 --- a/src/main/java/net/minecraft/client/renderer/Tessellator.java +++ b/src/main/java/net/minecraft/client/renderer/Tessellator.java @@ -52,6 +52,11 @@ public void addVertex(double x, double y, double z) { nextVertexTextured = false; } + public void addVertexWithUV(double x, double y, double z, double u, double v) { + setTextureUV(u, v); + addVertex(x, y, z); + } + public int draw() { double[] positions = new double[vertices.size() * 3]; double[] textureCoordinates = new double[vertices.size() * 2]; diff --git a/src/main/java/net/minecraft/entity/Entity.java b/src/main/java/net/minecraft/entity/Entity.java index f74c9c1..ad56f40 100644 --- a/src/main/java/net/minecraft/entity/Entity.java +++ b/src/main/java/net/minecraft/entity/Entity.java @@ -1,3 +1,7 @@ package net.minecraft.entity; -public class Entity {} +public class Entity { + + /** Current dimension id, matching the public field exposed by Minecraft 1.7.10. */ + public int dimension; +} diff --git a/src/main/java/net/minecraft/entity/player/EntityPlayer.java b/src/main/java/net/minecraft/entity/player/EntityPlayer.java index 455cb86..3d54b4f 100644 --- a/src/main/java/net/minecraft/entity/player/EntityPlayer.java +++ b/src/main/java/net/minecraft/entity/player/EntityPlayer.java @@ -1,9 +1,16 @@ package net.minecraft.entity.player; +import java.util.UUID; + import net.minecraft.entity.EntityLivingBase; /** Marker type used only to preserve ModularUI2's GuiData constructor descriptor. */ public class EntityPlayer extends EntityLivingBase { public final InventoryPlayer inventory = new InventoryPlayer(this); + public final PlayerCapabilities capabilities = new PlayerCapabilities(); + + public UUID getUniqueID() { + return new UUID(0L, 1L); + } } diff --git a/src/main/java/net/minecraft/entity/player/PlayerCapabilities.java b/src/main/java/net/minecraft/entity/player/PlayerCapabilities.java new file mode 100644 index 0000000..82d0e69 --- /dev/null +++ b/src/main/java/net/minecraft/entity/player/PlayerCapabilities.java @@ -0,0 +1,6 @@ +package net.minecraft.entity.player; + +public class PlayerCapabilities { + + public boolean isCreativeMode; +} diff --git a/src/main/java/org/lwjgl/input/Mouse.java b/src/main/java/org/lwjgl/input/Mouse.java index dd5477e..b9f4c5c 100644 --- a/src/main/java/org/lwjgl/input/Mouse.java +++ b/src/main/java/org/lwjgl/input/Mouse.java @@ -9,4 +9,8 @@ private Mouse() {} public static Cursor setNativeCursor(Cursor cursor) throws LWJGLException { return cursor; } + + public static boolean isButtonDown(int button) { + return false; + } } From a6d1df5d2cbb5b8fa1c66655195510ce5ea5587c Mon Sep 17 00:00:00 2001 From: Pxx500 Date: Fri, 14 Aug 2026 13:28:21 +0200 Subject: [PATCH 4/6] bootstrap local Forge client interactions --- .../cpw/mods/fml/common/ICrashCallable.java | 9 ++++ src/main/java/cpw/mods/fml/common/Loader.java | 15 +++++++ .../preview/runtime/ProjectRuntime.java | 43 +++++++++++++++++++ .../java/cpw/mods/fml/common/LoaderTest.java | 20 +++++++++ 4 files changed, 87 insertions(+) create mode 100644 src/main/java/cpw/mods/fml/common/ICrashCallable.java create mode 100644 src/test/java/cpw/mods/fml/common/LoaderTest.java diff --git a/src/main/java/cpw/mods/fml/common/ICrashCallable.java b/src/main/java/cpw/mods/fml/common/ICrashCallable.java new file mode 100644 index 0000000..abcbc58 --- /dev/null +++ b/src/main/java/cpw/mods/fml/common/ICrashCallable.java @@ -0,0 +1,9 @@ +package cpw.mods.fml.common; + +/** Minimal Forge crash-report callback contract used by the preview runtime. */ +public interface ICrashCallable { + + String call() throws Exception; + + String getLabel(); +} diff --git a/src/main/java/cpw/mods/fml/common/Loader.java b/src/main/java/cpw/mods/fml/common/Loader.java index a870046..a8676a2 100644 --- a/src/main/java/cpw/mods/fml/common/Loader.java +++ b/src/main/java/cpw/mods/fml/common/Loader.java @@ -34,6 +34,21 @@ public ModContainer activeModContainer() { return bootstrappingVanilla ? MINECRAFT_CONTAINER : ACTIVE_MOD_CONTAINER; } + public ICrashCallable getCallableCrashInformation() { + return new ICrashCallable() { + + @Override + public String call() { + return ""; + } + + @Override + public String getLabel() { + return "FML"; + } + }; + } + public static void beginVanillaBootstrap() { bootstrappingVanilla = true; } diff --git a/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java b/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java index 2ab26a4..ecfdda4 100644 --- a/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java +++ b/src/main/java/dev/modularui/preview/runtime/ProjectRuntime.java @@ -16,8 +16,11 @@ import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; +import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; @@ -47,6 +50,7 @@ Class loadClass(String className) throws ClassNotFoundException { public static PreviewSession openSession(PreviewProject project, String entrypointName, PreviewScreen previewScreen) { ProjectRuntime runtime = open(project.runtimeArtifacts()); try { + runtime.initialiseForgeClientSide(); return runtime.createSession(project, entrypointName, previewScreen); } catch (RuntimeException | LinkageError exception) { try { @@ -58,6 +62,44 @@ public static PreviewSession openSession(PreviewProject project, String entrypoi } } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void initialiseForgeClientSide() { + try { + Class commonHandlerClass = classLoader.loadClass("cpw.mods.fml.common.FMLCommonHandler"); + Class sidedHandlerClass = classLoader.loadClass("cpw.mods.fml.common.IFMLSidedHandler"); + Class sideClass = classLoader.loadClass("cpw.mods.fml.relauncher.Side") + .asSubclass(Enum.class); + Object clientSide = Enum.valueOf(sideClass, "CLIENT"); + Object sidedHandler = Proxy.newProxyInstance( + classLoader, + new Class[] { sidedHandlerClass }, + (proxy, method, arguments) -> previewSidedHandlerValue(proxy, method, arguments, clientSide)); + Object commonHandler = invoke(commonHandlerClass, null, "instance", new Class[0]); + Field sidedDelegate = commonHandlerClass.getDeclaredField("sidedDelegate"); + sidedDelegate.setAccessible(true); + sidedDelegate.set(commonHandler, sidedHandler); + } catch (ClassNotFoundException ignored) { + // Portable previews do not need the Forge client bootstrap. + } catch (ReflectiveOperationException exception) { + throw reflectionFailure("Could not initialise the Forge client side", exception); + } + } + + private static Object previewSidedHandlerValue(Object proxy, Method method, Object[] arguments, Object clientSide) { + return switch (method.getName()) { + case "getSide" -> clientSide; + case "getCurrentLanguage" -> "en_US"; + case "toString" -> "Preview client sided handler"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> arguments != null && arguments.length == 1 && proxy == arguments[0]; + default -> primitiveDefault(method.getReturnType()); + }; + } + + private static Object primitiveDefault(Class type) { + return type.isPrimitive() && type != void.class ? Array.get(Array.newInstance(type, 1), 0) : null; + } + private PreviewSession createSession(PreviewProject project, String entrypointName, PreviewScreen previewScreen) { Thread thread = Thread.currentThread(); ClassLoader previous = thread.getContextClassLoader(); @@ -455,6 +497,7 @@ private static final class IsolatedClassLoader extends URLClassLoader { "net.minecraft.util.ResourceLocation", "net.minecraft.util.StatCollector", "net.minecraft.util.StringTranslate", + "cpw.mods.fml.common.ICrashCallable", "cpw.mods.fml.common.Loader", "cpw.mods.fml.common.ModContainer", "cpw.mods.fml.relauncher.", diff --git a/src/test/java/cpw/mods/fml/common/LoaderTest.java b/src/test/java/cpw/mods/fml/common/LoaderTest.java new file mode 100644 index 0000000..bee6196 --- /dev/null +++ b/src/test/java/cpw/mods/fml/common/LoaderTest.java @@ -0,0 +1,20 @@ +package cpw.mods.fml.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +class LoaderTest { + + @Test + void exposesTheCrashCallableExpectedByForge() throws Exception { + Method method = Loader.class.getMethod("getCallableCrashInformation"); + Object callable = method.invoke(Loader.instance()); + + assertEquals("cpw.mods.fml.common.ICrashCallable", method.getReturnType().getName()); + assertEquals("FML", callable.getClass().getMethod("getLabel").invoke(callable)); + assertEquals("", callable.getClass().getMethod("call").invoke(callable)); + } +} From a1dab67d7ed38648d007985b19a595c8c708c322 Mon Sep 17 00:00:00 2001 From: Pxx500 Date: Fri, 14 Aug 2026 13:29:55 +0200 Subject: [PATCH 5/6] require Java 25 for production classpaths --- .github/workflows/release.yml | 8 ++++---- .github/workflows/verify.yml | 4 ++-- README.md | 4 ++-- build.gradle.kts | 2 +- preview.bat | 6 +++--- preview.sh | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da306a3..d8fc2ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,11 +26,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: temurin - java-version: "21" + java-version: "25" - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 @@ -80,11 +80,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: temurin - java-version: "21" + java-version: "25" - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 8ed5bc2..58c834a 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -25,11 +25,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: temurin - java-version: "21" + java-version: "25" - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 diff --git a/README.md b/README.md index 036111e..f12deb8 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ The preview project uses normal ModularUI2 classes and widget APIs. There is no ## Requirements - Windows, Linux, or macOS; -- JDK 21; +- JDK 25; - internet access on the first run so the Gradle Wrapper can download build dependencies. Gradle does not need to be installed separately. ## Install a release -Download the ZIP on Windows or the `tar.gz` archive on Linux and macOS, extract it, and run `preview.bat help` or `./preview.sh help`. Release archives already contain the compiled previewer and do not require Gradle. They still require JDK 21 because preview projects compile Java source code on every build. +Download the ZIP on Windows or the `tar.gz` archive on Linux and macOS, extract it, and run `preview.bat help` or `./preview.sh help`. Release archives already contain the compiled previewer and do not require Gradle. They still require JDK 25 because preview projects compile Java source code on every build. The launchers use `JAVA_HOME` when it is set, otherwise they use `java` and `javac` from `PATH`. A JRE without `javac` is not sufficient. diff --git a/build.gradle.kts b/build.gradle.kts index 26bd050..50ecda7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ repositories { java { toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) + languageVersion.set(JavaLanguageVersion.of(25)) } } diff --git a/preview.bat b/preview.bat index 63c8efb..d6ace72 100644 --- a/preview.bat +++ b/preview.bat @@ -23,7 +23,7 @@ if defined JAVA_HOME ( set "PREVIEW_JAVAC=%JAVA_HOME%\bin\javac.exe" ) else ( where java.exe >nul 2>nul || ( - echo ModularUI2 Preview requires JDK 21. Set JAVA_HOME or add java and javac to PATH. 1>&2 + echo ModularUI2 Preview requires JDK 25. Set JAVA_HOME or add java and javac to PATH. 1>&2 exit /b 2 ) where javac.exe >nul 2>nul || ( @@ -51,8 +51,8 @@ for /f "tokens=1,2,3" %%A in ('"%PREVIEW_JAVA%" -version 2^>^&1') do if not defi for /f "tokens=1 delims=." %%V in ("%PREVIEW_JAVA_VERSION%") do set "PREVIEW_JAVA_MAJOR=%%V" set "PREVIEW_JAVA_MAJOR_NUMBER=0" set /a PREVIEW_JAVA_MAJOR_NUMBER=%PREVIEW_JAVA_MAJOR% >nul 2>nul -if %PREVIEW_JAVA_MAJOR_NUMBER% LSS 21 ( - echo ModularUI2 Preview requires JDK 21 or newer; found %PREVIEW_JAVA_VERSION%. 1>&2 +if %PREVIEW_JAVA_MAJOR_NUMBER% LSS 25 ( + echo ModularUI2 Preview requires JDK 25 or newer; found %PREVIEW_JAVA_VERSION%. 1>&2 exit /b 2 ) exit /b 0 diff --git a/preview.sh b/preview.sh index d4e01d2..cea1b73 100644 --- a/preview.sh +++ b/preview.sh @@ -12,7 +12,7 @@ else fi if [ -z "$preview_java" ] || [ ! -x "$preview_java" ]; then - echo "ModularUI2 Preview requires JDK 21. Set JAVA_HOME or add java and javac to PATH." >&2 + echo "ModularUI2 Preview requires JDK 25. Set JAVA_HOME or add java and javac to PATH." >&2 exit 2 fi if [ -z "$preview_javac" ] || [ ! -x "$preview_javac" ]; then @@ -35,8 +35,8 @@ case "$java_major" in exit 2 ;; esac -if [ "$java_major" -lt 21 ]; then - echo "ModularUI2 Preview requires JDK 21 or newer; found $java_version." >&2 +if [ "$java_major" -lt 25 ]; then + echo "ModularUI2 Preview requires JDK 25 or newer; found $java_version." >&2 exit 2 fi From b694ccbf720ea02eb9b3d66549928ec9722ed2cd Mon Sep 17 00:00:00 2001 From: Pxx500 Date: Fri, 14 Aug 2026 13:31:00 +0200 Subject: [PATCH 6/6] add Galaxia Starmap production integration --- integrations/galaxia-starmap/actions.txt | 4 + .../galaxia-starmap/preview.properties | 5 ++ .../runtime-classpath.example.txt | 3 + .../java/example/GalaxiaStarmapPreview.java | 81 +++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 integrations/galaxia-starmap/actions.txt create mode 100644 integrations/galaxia-starmap/preview.properties create mode 100644 integrations/galaxia-starmap/runtime-classpath.example.txt create mode 100644 integrations/galaxia-starmap/src/preview/java/example/GalaxiaStarmapPreview.java diff --git a/integrations/galaxia-starmap/actions.txt b/integrations/galaxia-starmap/actions.txt new file mode 100644 index 0000000..6787323 --- /dev/null +++ b/integrations/galaxia-starmap/actions.txt @@ -0,0 +1,4 @@ +capture orbital-overview +move 28 235 +click left +capture mars-expanded diff --git a/integrations/galaxia-starmap/preview.properties b/integrations/galaxia-starmap/preview.properties new file mode 100644 index 0000000..417c706 --- /dev/null +++ b/integrations/galaxia-starmap/preview.properties @@ -0,0 +1,5 @@ +preview.entrypoint=example.GalaxiaStarmapPreview +screen.width=1920 +screen.height=1080 +gui.scale=2 +screen.background=#000000 diff --git a/integrations/galaxia-starmap/runtime-classpath.example.txt b/integrations/galaxia-starmap/runtime-classpath.example.txt new file mode 100644 index 0000000..727d395 --- /dev/null +++ b/integrations/galaxia-starmap/runtime-classpath.example.txt @@ -0,0 +1,3 @@ +# Generate runtime-classpath.txt from Galaxia's runtime classpath. +# Include Galaxia's compiled classes and resources, but omit ModularUI and ModularUI2; +# the previewer supplies the versions it was built and verified against. diff --git a/integrations/galaxia-starmap/src/preview/java/example/GalaxiaStarmapPreview.java b/integrations/galaxia-starmap/src/preview/java/example/GalaxiaStarmapPreview.java new file mode 100644 index 0000000..acae1c6 --- /dev/null +++ b/integrations/galaxia-starmap/src/preview/java/example/GalaxiaStarmapPreview.java @@ -0,0 +1,81 @@ +package example; + +import com.cleanroommc.modularui.value.sync.PanelSyncManager; +import com.gtnewhorizons.galaxia.client.gui.orbitalGUI.GalacticChartGui; +import com.gtnewhorizons.galaxia.registry.celestial.CelestialRegistry; +import com.gtnewhorizons.galaxia.registry.celestial.asteroid.AsteroidFieldOrbitResolver; +import com.gtnewhorizons.galaxia.registry.orbital.OrbitalMechanics; + +import dev.modularui.preview.PreviewEntrypoint; +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityClientPlayerMP; + +import gregtech.api.enums.Materials; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidRegistry; + +/** Runs Galaxia's production Starmap builder against representative local client state. */ +public final class GalaxiaStarmapPreview implements PreviewEntrypoint { + + @Override + public String owner() { + return "galaxia"; + } + + @Override + public Class previewedClass() { + return GalacticChartGui.class; + } + + @Override + public Object createPanel(Context context) { + initializeFmlSide(); + setVanillaBootstrap(true); + try { + if (Block.blockRegistry.getObject("water") == null) { + Block.registerBlocks(); + } + Class.forName("net.minecraft.init.Blocks", true, getClass().getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Failed to initialize vanilla block constants", e); + } finally { + setVanillaBootstrap(false); + } + if (Materials.Air.getGas(1L) == null) { + Fluid air = new Fluid("air"); + FluidRegistry.registerFluid(air); + Materials.Air.mGas = air; + } + OrbitalMechanics.registerMinorBodyResolver(AsteroidFieldOrbitResolver.INSTANCE); + CelestialRegistry.freezeAndBake(); + + EntityClientPlayerMP player = Minecraft.getMinecraft().thePlayer; + player.dimension = 0; + + return new GalacticChartGui().build((PanelSyncManager) context.panelSyncManager(), player); + } + + private static void setVanillaBootstrap(boolean active) { + try { + Class loader = Class.forName("cpw.mods.fml.common.Loader"); + loader.getMethod(active ? "beginVanillaBootstrap" : "endVanillaBootstrap").invoke(null); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Preview Loader does not support vanilla registry bootstrapping", e); + } + } + + private static void initializeFmlSide() { + try { + Class log = Class.forName("cpw.mods.fml.relauncher.FMLRelaunchLog"); + java.lang.reflect.Field field = log.getDeclaredField("side"); + field.setAccessible(true); + if (field.get(null) == null) { + field.set(null, cpw.mods.fml.relauncher.Side.CLIENT); + } + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to initialize the local FML side", e); + } + } + +}