diff --git a/README.md b/README.md index cd126de..6e7f4ff 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ A full Java interop library that wraps Minecraft classes which allows you to write code for multiple versions at the same time. Built using ReplayMod's [Preprocessor](https://github.com/ReplayMod/preprocessor). + +It also features a "standalone" edition, which can run GUIs without Minecraft so long as they only depend on +UniversalCraft and not Minecraft directly. +This can allow for a faster development loop (no need to wait a minute for Minecraft to start), +automated testing without having to bootstrap a full Minecraft environment, +and even development of completely standalone applications using the same toolkit (e.g. [Elementa]) as one is already +familiar with from Minecraft development. +See the `standalone/example/` folder for a fully functional example. + ## Dependency It's recommended that you include [Essential](link eventually) instead of adding it yourself. @@ -58,6 +67,13 @@ done mcPlatform buildNumber + + standalone + N/A + + standalone + + 1.21fabric1.21-fabric 1.20.6fabric1.20.6-fabric 1.20.4forge1.20.4-forge @@ -154,4 +170,6 @@ tasks.shadowJar { tasks.reobfJar { dependsOn(tasks.shadowJar) } ``` - \ No newline at end of file + + +[Elementa]: https://github.com/EssentialGG/Elementa diff --git a/build.gradle.kts b/build.gradle.kts index 6ad2cc2..7411add 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,3 +24,8 @@ tasks.withType { apiVersion = "1.6" } } + +preprocess { + vars.put("STANDALONE", 0) + vars.put("!STANDALONE", 1) +} diff --git a/root.gradle.kts b/root.gradle.kts index 8898226..3f4e42b 100644 --- a/root.gradle.kts +++ b/root.gradle.kts @@ -62,3 +62,7 @@ preprocess { forge11602.link(forge11202, file("versions/1.16.2-1.12.2.txt")) forge11202.link(forge10809) } + +apiValidation { + ignoredProjects += listOf("standalone", "example") +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b65bfdf..d190bc0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,9 @@ pluginManagement { rootProject.name = "UniversalCraft" rootProject.buildFileName = "root.gradle.kts" +include(":standalone") +include(":standalone:example") + listOf( "1.8.9-forge", "1.12.2-forge", diff --git a/src/main/java/gg/essential/universal/UGraphics.java b/src/main/java/gg/essential/universal/UGraphics.java index 6a6f882..cef41da 100644 --- a/src/main/java/gg/essential/universal/UGraphics.java +++ b/src/main/java/gg/essential/universal/UGraphics.java @@ -1,5 +1,8 @@ package gg.essential.universal; +import gg.essential.universal.utils.ReleasedDynamicTexture; +import gg.essential.universal.vertex.UVertexConsumer; + import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; @@ -9,8 +12,21 @@ import java.util.List; import java.util.regex.Pattern; -import gg.essential.universal.utils.ReleasedDynamicTexture; -import gg.essential.universal.vertex.UVertexConsumer; +//#if STANDALONE +//$$ import gg.essential.universal.standalone.nanovg.NvgContext; +//$$ import gg.essential.universal.standalone.nanovg.NvgFont; +//$$ import gg.essential.universal.standalone.nanovg.NvgFontFace; +//$$ import gg.essential.universal.standalone.render.BufferBuilder; +//$$ import gg.essential.universal.standalone.render.DefaultShader; +//$$ import gg.essential.universal.standalone.render.DefaultVertexFormats; +//$$ import gg.essential.universal.standalone.render.Gl2Renderer; +//$$ import gg.essential.universal.standalone.render.VertexFormat; +//$$ import org.lwjgl.opengl.GL11; +//$$ import org.lwjgl.opengl.GL20; +//$$ import java.net.URL; +//$$ import static kotlin.io.TextStreamsKt.readBytes; +//$$ import static org.lwjgl.opengl.GL14.*; +//#else import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.Tessellator; @@ -85,16 +101,43 @@ import net.minecraft.client.renderer.texture.ITextureObject; //#endif +//#endif + @SuppressWarnings("deprecation") // lots of MC methods are deprecated on some versions but only replaced on the next one public class UGraphics { private static final Pattern formattingCodePattern = Pattern.compile("(?i)\u00a7[0-9A-FK-OR]"); + private static UMatrixStack UNIT_STACK = UMatrixStack.UNIT; + + //#if STANDALONE + //$$ public static void init() { /* triggers static initializer */ } + //$$ private static final Gl2Renderer RENDERER = new Gl2Renderer(); + //$$ private static final NvgFont MC_FONT; + //$$ static { + //$$ URL fontUrl = UGraphics.class.getResource("/fonts/Minecraft-Regular.otf"); + //$$ assert fontUrl != null; + //$$ MC_FONT = new NvgFont(new NvgFontFace(new NvgContext(), readBytes(fontUrl)), 10, 7f, 1f); + //$$ + //$$ // TODO should probably put this in a unit test, but lwjgl makes those a bit tricky to set up + //$$ assert UGraphics.getStringWidth("a") == 6; + //$$ assert UGraphics.getStringWidth("aa") == 12; + //$$ assert UGraphics.getStringWidth(" ") == 4; + //$$ assert UGraphics.getStringWidth(" ") == 8; + //$$ assert UGraphics.getStringWidth("a ") == 10; + //$$ assert UGraphics.getStringWidth("a ") == 14; + //$$ } + //$$ + //$$ private BufferBuilder instance; + //$$ private DrawMode drawMode; + //$$ private CommonVertexFormats vertexFormat; + //$$ private DefaultShader shader; + //#else + //#if MC>=12100 //$$ public static Style EMPTY_WITH_FONT_ID = Style.EMPTY.withFont(Identifier.of("minecraft", "alt")); //#elseif MC>=11602 //$$ public static Style EMPTY_WITH_FONT_ID = Style.EMPTY.setFontId(new ResourceLocation("minecraft", "alt")); //#endif - private static UMatrixStack UNIT_STACK = UMatrixStack.UNIT; public static int ZERO_TEXT_ALPHA = 10; private WorldRenderer instance; private VertexFormat vertexFormat; @@ -116,13 +159,20 @@ public class UGraphics { public UGraphics(WorldRenderer instance) { this.instance = instance; } + //#endif public UVertexConsumer asUVertexConsumer() { + //#if STANDALONE + //$$ return instance; + //#else return UVertexConsumer.of(instance); + //#endif } public static UGraphics getFromTessellator() { - //#if MC>=12100 + //#if STANDALONE + //$$ return new UGraphics(); + //#elseif MC>=12100 //$$ return new UGraphics(null); //#else return new UGraphics(getTessellator().getWorldRenderer()); @@ -166,9 +216,11 @@ public static void scale(double x, double y, double z) { } //#endif + //#if !STANDALONE public static Tessellator getTessellator() { return Tessellator.getInstance(); } + //#endif //#if MC>=12100 //$$ // No possible alternative on 1.21. A compile time error here is better than a run time one. @@ -240,7 +292,11 @@ public static void enableLight(int mode) { } public static void enableBlend() { + //#if STANDALONE + //$$ glEnable(GL_BLEND); + //#else GlStateManager.enableBlend(); + //#endif } /** @@ -275,7 +331,7 @@ public static void shadeModel(int mode) { } public static void blendEquation(int equation) { - //#if MC>=10900 + //#if MC>=10900 && !STANDALONE //$$ GlStateManager.glBlendEquation(equation); //#else org.lwjgl.opengl.GL14.glBlendEquation(equation); @@ -283,14 +339,18 @@ public static void blendEquation(int equation) { } public static void tryBlendFuncSeparate(int srcFactor, int dstFactor, int srcFactorAlpha, int dstFactorAlpha) { + //#if STANDALONE + //$$ glBlendFuncSeparate(srcFactor, dstFactor, srcFactorAlpha, dstFactorAlpha); + //#else GlStateManager.tryBlendFuncSeparate(srcFactor, dstFactor, srcFactorAlpha, dstFactorAlpha); + //#endif } /** * @deprecated Relies on the global {@link #setActiveTexture(int) activeTexture} state.
* Instead of manually managing TEXTURE_2D state, prefer using - * {@link #beginWithDefaultShader(DrawMode, VertexFormat)} or any of the other non-deprecated begin methods as - * these will set (and restore) the appropriate state for the given {@link VertexFormat} right before/after + * {@link #beginWithDefaultShader(DrawMode, CommonVertexFormats)} or any of the other non-deprecated begin methods as + * these will set (and restore) the appropriate state for the given {@link CommonVertexFormats} right before/after * rendering.
* Also incompatible with OpenGL Core / MC 1.17. */ @@ -304,11 +364,19 @@ public static void enableTexture2D() { } public static void disableBlend() { + //#if STANDALONE + //$$ glDisable(GL_BLEND); + //#else GlStateManager.disableBlend(); + //#endif } public static void deleteTexture(int glTextureId) { + //#if STANDALONE + //$$ glDeleteTextures(GL_BLEND); + //#else GlStateManager.deleteTexture(glTextureId); + //#endif } public static void enableAlpha() { @@ -319,11 +387,11 @@ public static void enableAlpha() { public static void configureTexture(int glTextureId, Runnable block) { int prevTextureBinding = GL11.glGetInteger(GL_TEXTURE_BINDING_2D); - GlStateManager.bindTexture(glTextureId); + bindTexture(glTextureId); block.run(); - GlStateManager.bindTexture(prevTextureBinding); + bindTexture(prevTextureBinding); } public static void configureTextureUnit(int index, Runnable block) { @@ -353,7 +421,9 @@ public static int getActiveTexture() { } public static void setActiveTexture(int glId) { - //#if MC>=11700 + //#if STANDALONE + //$$ glActiveTexture(glId); + //#elseif MC>=11700 //$$ GlStateManager._activeTexture(glId); //#elseif MC>=11400 //$$ GlStateManager.activeTexture(glId); @@ -368,13 +438,16 @@ public static void setActiveTexture(int glId) { */ @Deprecated public static void bindTexture(int glTextureId) { - //#if MC>=11700 + //#if STANDALONE + //$$ glBindTexture(GL_TEXTURE_2D, glTextureId); + //#elseif MC>=11700 //$$ RenderSystem.setShaderTexture(GlStateManager._getActiveTexture() - GL_TEXTURE0, glTextureId); //#else GlStateManager.bindTexture(glTextureId); //#endif } + //#if !STANDALONE /** * @deprecated Relies on the global {@link #setActiveTexture(int) activeTexture} state.
* Prefer {@link #bindTexture(int, ResourceLocation)} instead. @@ -383,15 +456,19 @@ public static void bindTexture(int glTextureId) { public static void bindTexture(ResourceLocation resourceLocation) { bindTexture(getOrLoadTextureId(resourceLocation)); } + //#endif public static void bindTexture(int index, int glTextureId) { - //#if MC>=11700 + //#if STANDALONE + //$$ configureTextureUnit(index, () -> glBindTexture(GL_TEXTURE_2D, glTextureId)); + //#elseif MC>=11700 //$$ RenderSystem.setShaderTexture(index, glTextureId); //#else configureTextureUnit(index, () -> GlStateManager.bindTexture(glTextureId)); //#endif } + //#if !STANDALONE public static void bindTexture(int index, ResourceLocation resourceLocation) { bindTexture(index, getOrLoadTextureId(resourceLocation)); } @@ -409,13 +486,22 @@ private static int getOrLoadTextureId(ResourceLocation resourceLocation) { } return texture.getGlTextureId(); } + //#endif public static int getStringWidth(String in) { + //#if STANDALONE + //$$ return (int) MC_FONT.getStringWidth(in, 10f); + //#else return UMinecraft.getFontRenderer().getStringWidth(in); + //#endif } public static int getFontHeight() { + //#if STANDALONE + //$$ return 9; + //#else return UMinecraft.getFontRenderer().FONT_HEIGHT; + //#endif } @Deprecated // Pass UMatrixStack as first arg, required for 1.17+ @@ -425,6 +511,9 @@ public static void drawString(String text, float x, float y, int color, boolean public static void drawString(UMatrixStack stack, String text, float x, float y, int color, boolean shadow) { if ((color >> 24 & 255) <= 10) return; + //#if STANDALONE + //$$ MC_FONT.drawString(stack, text, new Color(color), x, y, 10f, 1f, shadow, null); + //#else //#if MC>=11602 //#if MC>=12100 //$$ VertexConsumerProvider.Immediate irendertypebuffer$impl = UMinecraft.getMinecraft().getBufferBuilders().getEntityVertexConsumers(); @@ -447,6 +536,7 @@ public static void drawString(UMatrixStack stack, String text, float x, float y, //#endif if (stack != UNIT_STACK) GL.popMatrix(); //#endif + //#endif } @Deprecated // Pass UMatrixStack as first arg, required for 1.17+ @@ -457,6 +547,10 @@ public static void drawString(String text, float x, float y, int color, int shad public static void drawString(UMatrixStack stack, String text, float x, float y, int color, int shadowColor) { if ((color >> 24 & 255) <= 10) return; String shadowText = ChatColor.Companion.stripColorCodes(text); + //#if STANDALONE + //$$ MC_FONT.drawString(stack, shadowText, new Color(shadowColor), x + 1f, y + 1f, 10f, 1f, false, null); + //$$ MC_FONT.drawString(stack, text, new Color(color), x, y, 10f, 1f, false, null); + //#else //#if MC>=11602 //#if MC>=12100 //$$ VertexConsumerProvider.Immediate irendertypebuffer$impl = UMinecraft.getMinecraft().getBufferBuilders().getEntityVertexConsumers(); @@ -478,6 +572,7 @@ public static void drawString(UMatrixStack stack, String text, float x, float y, //#endif if (stack != UNIT_STACK) GL.popMatrix(); //#endif + //#endif } public static List listFormattedStringToWidth(String str, int wrapWidth) { @@ -493,6 +588,9 @@ public static List listFormattedStringToWidth(String str, int wrapWidth, wrapWidth = Math.max(max, wrapWidth); } + //#if STANDALONE + //$$ throw new UnsupportedOperationException("not implemented"); + //#else //#if MC>=11602 //$$ // TODO: Validate this code //$$ List strings = new ArrayList<>(); @@ -512,10 +610,13 @@ public static List listFormattedStringToWidth(String str, int wrapWidth, //#else return UMinecraft.getFontRenderer().listFormattedStringToWidth(str, wrapWidth); //#endif + //#endif } public static float getCharWidth(char character) { - //#if MC>=11602 + //#if STANDALONE + //$$ return MC_FONT.getStringWidth(String.valueOf(character), 10f); + //#elseif MC>=11602 //$$ return getStringWidth(String.valueOf(character)); //#else return UMinecraft.getFontRenderer().getCharWidth(character); // float because its a float in 1.15+ @@ -532,7 +633,7 @@ public static void glClearStencil(int mode) { public static ReleasedDynamicTexture getTexture(InputStream stream) { try { - //#if MC>=11502 + //#if MC>=11502 && !STANDALONE //$$ return new ReleasedDynamicTexture(NativeImage.read(stream)); //#else return new ReleasedDynamicTexture(ImageIO.read(stream)); @@ -544,7 +645,7 @@ public static ReleasedDynamicTexture getTexture(InputStream stream) { } public static ReleasedDynamicTexture getTexture(BufferedImage img) { - //#if MC>=11502 + //#if MC>=11502 && !STANDALONE //$$ try { //$$ ByteArrayOutputStream baos = new ByteArrayOutputStream(); //$$ ImageIO.write(img, "png", baos ); @@ -563,7 +664,9 @@ public static ReleasedDynamicTexture getEmptyTexture() { } public static void glUseProgram(int program) { - //#if MC>=11502 + //#if STANDALONE + //$$ GL20.glUseProgram(program); + //#elseif MC>=11502 //$$ GlStateManager.useProgram(program); //#else OpenGlHelper.glUseProgram(program); @@ -587,7 +690,9 @@ public static boolean areShadersSupported() { } public static int glCreateProgram() { - //#if MC>=11502 + //#if STANDALONE + //$$ return GL20.glCreateProgram(); + //#elseif MC>=11502 //$$ return GlStateManager.createProgram(); //#else return OpenGlHelper.glCreateProgram(); @@ -595,7 +700,9 @@ public static int glCreateProgram() { } public static int glCreateShader(int type) { - //#if MC>=11502 + //#if STANDALONE + //$$ return GL20.glCreateShader(type); + //#elseif MC>=11502 //$$ return GlStateManager.createShader(type); //#else return OpenGlHelper.glCreateShader(type); @@ -603,7 +710,9 @@ public static int glCreateShader(int type) { } public static void glCompileShader(int shaderIn) { - //#if MC>=11502 + //#if STANDALONE + //$$ GL20.glCompileShader(shaderIn); + //#elseif MC>=11502 //$$ GlStateManager.compileShader(shaderIn); //#else OpenGlHelper.glCompileShader(shaderIn); @@ -611,7 +720,9 @@ public static void glCompileShader(int shaderIn) { } public static int glGetShaderi(int shaderIn, int pname) { - //#if MC>=11502 + //#if STANDALONE + //$$ return GL20.glGetShaderi(shaderIn, pname); + //#elseif MC>=11502 //$$ return GlStateManager.getShader(shaderIn,pname); //#else return OpenGlHelper.glGetShaderi(shaderIn, pname); @@ -619,7 +730,9 @@ public static int glGetShaderi(int shaderIn, int pname) { } public static String glGetShaderInfoLog(int shader, int maxLen) { - //#if MC>=11502 + //#if STANDALONE + //$$ return GL20.glGetShaderInfoLog(shader, maxLen); + //#elseif MC>=11502 //$$ return GlStateManager.getShaderInfoLog( shader,maxLen); //#else return OpenGlHelper.glGetShaderInfoLog(shader, maxLen); @@ -627,7 +740,9 @@ public static String glGetShaderInfoLog(int shader, int maxLen) { } public static void glAttachShader(int program, int shaderIn) { - //#if MC>=11502 + //#if STANDALONE + //$$ GL20.glAttachShader(program, shaderIn); + //#elseif MC>=11502 //$$ GlStateManager.attachShader(program,shaderIn); //#else OpenGlHelper.glAttachShader(program, shaderIn); @@ -635,7 +750,9 @@ public static void glAttachShader(int program, int shaderIn) { } public static void glLinkProgram(int program) { - //#if MC>=11502 + //#if STANDALONE + //$$ GL20.glLinkProgram(program); + //#elseif MC>=11502 //$$ GlStateManager.linkProgram(program); //#else OpenGlHelper.glLinkProgram(program); @@ -643,7 +760,9 @@ public static void glLinkProgram(int program) { } public static int glGetProgrami(int program, int pname) { - //#if MC>=11502 + //#if STANDALONE + //$$ return GL20.glGetProgrami(program, pname); + //#elseif MC>=11502 //$$ return GlStateManager.getProgram(program,pname); //#else return OpenGlHelper.glGetProgrami(program, pname); @@ -651,7 +770,9 @@ public static int glGetProgrami(int program, int pname) { } public static String glGetProgramInfoLog(int program, int maxLen) { - //#if MC>=11502 + //#if STANDALONE + //$$ return GL20.glGetProgramInfoLog(program, maxLen); + //#elseif MC>=11502 //$$ return GlStateManager.getProgramInfoLog(program, maxLen); //#else return OpenGlHelper.glGetProgramInfoLog(program, maxLen); @@ -659,7 +780,11 @@ public static String glGetProgramInfoLog(int program, int maxLen) { } public static void color4f(float red, float green, float blue, float alpha) { + //#if STANDALONE + //$$ throw new UnsupportedOperationException(); + //#else GlStateManager.color(red, green, blue, alpha); + //#endif } public static void directColor3f(float red, float green, float blue) { @@ -671,22 +796,38 @@ public static void directColor3f(float red, float green, float blue) { } public static void enableDepth() { + //#if STANDALONE + //$$ glEnable(GL_DEPTH_TEST); + //#else GlStateManager.enableDepth(); + //#endif } public static void depthFunc(int mode) { + //#if STANDALONE + //$$ glDepthFunc(mode); + //#else GlStateManager.depthFunc(mode); + //#endif } public static void depthMask(boolean flag) { + //#if STANDALONE + //$$ glDepthMask(flag); + //#else GlStateManager.depthMask(flag); + //#endif } public static void disableDepth() { + //#if STANDALONE + //$$ glDisable(GL_DEPTH_TEST); + //#else GlStateManager.disableDepth(); + //#endif } - //#if MC>=11700 + //#if MC>=11700 && !STANDALONE //$$ public static void setShader(Supplier shader) { //$$ RenderSystem.setShader(shader); //$$ } @@ -702,22 +843,26 @@ public enum DrawMode { ; private final int glMode; + //#if !STANDALONE //#if MC>=11700 //$$ private final VertexFormat.DrawMode mcMode; //#else private final int mcMode; //#endif + //#endif DrawMode(int glMode) { this.glMode = glMode; + //#if !STANDALONE //#if MC>=11700 //$$ this.mcMode = glToMcDrawMode(glMode); //#else this.mcMode = glMode; //#endif + //#endif } - //#if MC>=11700 + //#if MC>=11700 && !STANDALONE //$$ private static VertexFormat.DrawMode glToMcDrawMode(int glMode) { //$$ switch (glMode) { //$$ case GL11.GL_LINES: return VertexFormat.DrawMode.LINES; @@ -755,7 +900,7 @@ public static DrawMode fromGl(int glMode) { } } - //#if MC>=11600 + //#if MC>=11600 && !STANDALONE //$$ public static DrawMode fromRenderLayer(RenderType renderLayer) { //#if MC>=11700 //$$ return fromMc(renderLayer.getDrawMode()); @@ -795,9 +940,18 @@ public enum CommonVertexFormats { } public UGraphics beginWithActiveShader(DrawMode mode, CommonVertexFormats format) { + //#if STANDALONE + //$$ vertexFormat = format; + //$$ drawMode = mode; + //$$ instance = new BufferBuilder(format.mc.getParts()); + //$$ shader = null; + //$$ return this; + //#else return beginWithActiveShader(mode, format.mc); + //#endif } + //#if !STANDALONE public UGraphics beginWithActiveShader(DrawMode mode, VertexFormat format) { vertexFormat = format; //#if MC>=12100 @@ -807,8 +961,9 @@ public UGraphics beginWithActiveShader(DrawMode mode, VertexFormat format) { //#endif return this; } + //#endif - //#if MC>=11700 + //#if MC>=11700 && !STANDALONE //$$ // Note: Needs to be an Identity hash map because VertexFormat's equals method is broken (compares via its //$$ // component Map but order very much matters for VertexFormat) as of 1.17 //$$ private static final Map> DEFAULT_SHADERS = new IdentityHashMap<>(); @@ -836,9 +991,16 @@ public UGraphics beginWithActiveShader(DrawMode mode, VertexFormat format) { //#endif public UGraphics beginWithDefaultShader(DrawMode mode, CommonVertexFormats format) { + //#if STANDALONE + //$$ beginWithActiveShader(mode, format); + //$$ shader = DefaultShader.Companion.get(format.mc.getParts()); + //$$ return this; + //#else return beginWithDefaultShader(mode, format.mc); + //#endif } + //#if !STANDALONE public UGraphics beginWithDefaultShader(DrawMode mode, VertexFormat format) { //#if MC>=11700 //$$ Supplier supplier = DEFAULT_SHADERS.get(format); @@ -873,8 +1035,12 @@ public UGraphics begin(int glMode, VertexFormat format) { //#endif return this; } + //#endif public void drawDirect() { + //#if STANDALONE + //$$ doDraw(); + //#else //#if MC>=12100 //$$ BuiltBuffer builtBuffer = instance.end(); //#endif @@ -895,9 +1061,14 @@ public void drawDirect() { //$$ builtBuffer //#endif ); + //#endif } public void drawSorted(int cameraX, int cameraY, int cameraZ) { + //#if STANDALONE + //$$ // TODO sorting, if we ever need it + //$$ doDraw(); + //#else //#if MC>=12100 //$$ BuiltBuffer builtBuffer = instance.end(); //$$ builtBuffer.sortQuads(SORTED_QUADS_ALLOCATOR, RenderSystem.getVertexSorting()); @@ -928,6 +1099,7 @@ public void drawSorted(int cameraX, int cameraY, int cameraZ) { //$$ builtBuffer //#endif ); + //#endif } //#if MC<11700 @@ -948,6 +1120,15 @@ private static boolean[] getDesiredTextureUnitState(VertexFormat vertexFormat) { } //#endif + //#if STANDALONE + //$$ private void doDraw() { + //$$ CommonVertexFormats vertexFormat = this.vertexFormat; + //$$ if (vertexFormat == null) { + //$$ throw new IllegalStateException("Must call `begin` before `draw`."); + //$$ } + //$$ RENDERER.draw(instance, drawMode, shader); + //$$ } + //#else private void doDraw( //#if MC>=12100 //$$ BuiltBuffer builtBuffer @@ -1001,6 +1182,7 @@ private void doDraw( } //#endif } + //#endif @Deprecated // Pass UMatrixStack as first arg, required for 1.17+ public UGraphics pos(double x, double y, double z) { @@ -1008,6 +1190,9 @@ public UGraphics pos(double x, double y, double z) { } public UGraphics pos(UMatrixStack stack, double x, double y, double z) { + //#if STANDALONE + //$$ asUVertexConsumer().pos(stack, x, y, z); + //#else if (stack == UNIT_STACK) { //#if MC>=12100 //$$ instance.vertex((float) x, (float) y, (float) z); @@ -1027,6 +1212,7 @@ public UGraphics pos(UMatrixStack stack, double x, double y, double z) { instance.pos(vec.getX(), vec.getY(), vec.getZ()); //#endif } + //#endif return this; } @@ -1036,6 +1222,9 @@ public UGraphics norm(float x, float y, float z) { } public UGraphics norm(UMatrixStack stack, float x, float y, float z) { + //#if STANDALONE + //$$ asUVertexConsumer().norm(stack, x, y, z); + //#else if (stack == UNIT_STACK) { instance.normal(x, y, z); } else { @@ -1054,6 +1243,7 @@ public UGraphics norm(UMatrixStack stack, float x, float y, float z) { instance.normal(vec.getX(), vec.getY(), vec.getZ()); //#endif } + //#endif return this; } @@ -1062,7 +1252,11 @@ public UGraphics color(int red, int green, int blue, int alpha) { } public UGraphics color(float red, float green, float blue, float alpha) { + //#if STANDALONE + //$$ asUVertexConsumer().color(red, green, blue, alpha); + //#else instance.color(red, green, blue, alpha); + //#endif return this; } @@ -1071,14 +1265,16 @@ public UGraphics color(Color color) { } public UGraphics endVertex() { - //#if MC<12100 + //#if STANDALONE + //$$ instance.endVertex(); + //#elseif MC<12100 instance.endVertex(); //#endif return this; } public UGraphics tex(double u, double v) { - //#if MC>=11502 + //#if MC>=11502 && !STANDALONE //$$ instance.tex((float)u,(float)v); //#else instance.tex(u, v); @@ -1087,7 +1283,7 @@ public UGraphics tex(double u, double v) { } public UGraphics overlay(int u, int v) { - //#if MC>=11502 + //#if MC>=11502 && !STANDALONE //$$ instance.overlay(u, v); //#else instance.tex(u, v); @@ -1096,7 +1292,11 @@ public UGraphics overlay(int u, int v) { } public UGraphics light(int u, int v) { + //#if STANDALONE + //$$ asUVertexConsumer().light(u, v); + //#else instance.lightmap(u, v); + //#endif return this; } diff --git a/src/main/kotlin/gg/essential/universal/UChat.kt b/src/main/kotlin/gg/essential/universal/UChat.kt index fe5a4da..dd062d7 100644 --- a/src/main/kotlin/gg/essential/universal/UChat.kt +++ b/src/main/kotlin/gg/essential/universal/UChat.kt @@ -1,8 +1,10 @@ package gg.essential.universal +//#if !STANDALONE import gg.essential.universal.wrappers.UPlayer import gg.essential.universal.wrappers.message.UMessage import gg.essential.universal.wrappers.message.UTextComponent +//#endif import java.util.regex.Pattern object UChat { @@ -17,6 +19,9 @@ object UChat { */ @JvmStatic fun chat(obj: Any) { + //#if STANDALONE + //$$ println(obj) + //#else if (obj is String || obj is UTextComponent) { UMessage(obj).chat() } else { @@ -27,6 +32,7 @@ object UChat { UMessage(obj.toString()).chat() } } + //#endif } /** @@ -38,6 +44,9 @@ object UChat { */ @JvmStatic fun actionBar(obj: Any) { + //#if STANDALONE + //$$ throw UnsupportedOperationException("actionBar($obj)") + //#else if (obj is String || obj is UTextComponent) { UMessage(obj).actionBar() } else { @@ -48,6 +57,7 @@ object UChat { UMessage(obj.toString()).actionBar() } } + //#endif } /** @@ -55,6 +65,9 @@ object UChat { */ @JvmStatic fun say(text: String) { + //#if STANDALONE + //$$ throw UnsupportedOperationException("say($text)") + //#else //#if MC>=11903 //$$ UPlayer.getPlayer()!!.networkHandler.sendChatMessage(text) //#elseif MC>=11901 @@ -62,6 +75,7 @@ object UChat { //#else UPlayer.getPlayer()!!.sendChatMessage(text) //#endif + //#endif } /** diff --git a/src/main/kotlin/gg/essential/universal/UDesktop.kt b/src/main/kotlin/gg/essential/universal/UDesktop.kt index 998e5ce..b5e952a 100644 --- a/src/main/kotlin/gg/essential/universal/UDesktop.kt +++ b/src/main/kotlin/gg/essential/universal/UDesktop.kt @@ -6,7 +6,16 @@ import java.io.IOException import java.net.URI import java.util.concurrent.TimeUnit -//#if MC>=11400 +//#if STANDALONE +//$$ import gg.essential.universal.standalone.glfw.Glfw +//$$ import gg.essential.universal.standalone.glfw.GlfwWindow +//$$ import kotlinx.coroutines.Dispatchers +//$$ import kotlinx.coroutines.runBlocking +//$$ import kotlinx.coroutines.withContext +//$$ import org.lwjgl.glfw.GLFW +//$$ import org.lwjgl.glfw.GLFWErrorCallback +//$$ import org.lwjgl.system.MemoryUtil +//#elseif MC>=11400 //#else import net.minecraft.client.gui.GuiScreen //#endif @@ -136,6 +145,43 @@ object UDesktop { } } + //#if STANDALONE + //$$ internal lateinit var glfwWindow: GlfwWindow + //$$ + //$$ @JvmStatic + //$$ fun getClipboardString(): String = runBlocking { + //$$ withContext(Dispatchers.Glfw.immediate) { + //$$ var oldCallback: GLFWErrorCallback? = null + //$$ oldCallback = GLFW.glfwSetErrorCallback { error, description -> + //$$ if (error == GLFW.GLFW_FORMAT_UNAVAILABLE) return@glfwSetErrorCallback + //$$ oldCallback?.invoke(error, description) + //$$ } + //$$ try { + //$$ GLFW.glfwGetClipboardString(glfwWindow.glfwId) ?: "" + //$$ } finally { + //$$ GLFW.glfwSetErrorCallback(oldCallback)?.free() + //$$ } + //$$ } + //$$ } + //$$ + //$$ @JvmStatic + //$$ fun setClipboardString(str: String) = runBlocking { + //$$ withContext(Dispatchers.Glfw.immediate) { + //$$ // If we pass a string, LWJGL allocates a temporary buffer for it on the stack, which is small by default (64kb). + //$$ // So for large strings, we need to explicitly allocate it on the heap. + //$$ if (str.length < 512) { + //$$ GLFW.glfwSetClipboardString(glfwWindow.glfwId, str) + //$$ } else { + //$$ val buffer = MemoryUtil.memUTF8(str) + //$$ try { + //$$ GLFW.glfwSetClipboardString(glfwWindow.glfwId, buffer) + //$$ } finally { + //$$ MemoryUtil.memFree(buffer) + //$$ } + //$$ } + //$$ } + //$$ } + //#else @JvmStatic fun getClipboardString(): String = //#if MC>=11400 @@ -152,4 +198,5 @@ object UDesktop { GuiScreen.setClipboardString(str) //#endif } + //#endif } diff --git a/src/main/kotlin/gg/essential/universal/UI18n.kt b/src/main/kotlin/gg/essential/universal/UI18n.kt index d48166a..603de19 100644 --- a/src/main/kotlin/gg/essential/universal/UI18n.kt +++ b/src/main/kotlin/gg/essential/universal/UI18n.kt @@ -1,9 +1,33 @@ package gg.essential.universal +//#if STANDALONE +//$$ import java.util.ServiceLoader +//$$ import java.util.IllegalFormatException +//#else import net.minecraft.client.resources.I18n +//#endif object UI18n { fun i18n(key: String, vararg arguments: Any?): String { + //#if STANDALONE + //$$ return theProvider.i18n(key, *arguments) + //#else return I18n.format(key, *arguments) + //#endif } + + //#if STANDALONE + //$$ private val theProvider = ServiceLoader.load(Provider::class.java).firstOrNull() ?: FallbackProvider + //$$ interface Provider { + //$$ fun i18n(key: String, vararg arguments: Any?): String + //$$ } + //$$ private object FallbackProvider : Provider { + //$$ override fun i18n(key: String, vararg arguments: Any?): String = try { + //$$ String.format(key, *arguments) + //$$ } catch (e: IllegalFormatException) { + //$$ e.printStackTrace() + //$$ "[format error $key]" + //$$ } + //$$ } + //#endif } diff --git a/src/main/kotlin/gg/essential/universal/UImage.kt b/src/main/kotlin/gg/essential/universal/UImage.kt index a2aa939..e4e05d8 100644 --- a/src/main/kotlin/gg/essential/universal/UImage.kt +++ b/src/main/kotlin/gg/essential/universal/UImage.kt @@ -1,12 +1,12 @@ package gg.essential.universal -//#if MC<11600 +//#if MC<11600 || STANDALONE import java.awt.image.BufferedImage //#else //$$ import net.minecraft.client.renderer.texture.NativeImage //#endif -//#if MC>=11600 +//#if MC>=11600 && !STANDALONE //$$ class UImage(val nativeImage: NativeImage) { //#else class UImage(val nativeImage: BufferedImage) { @@ -14,7 +14,7 @@ class UImage(val nativeImage: BufferedImage) { fun copyFrom(other: UImage) { val otherNative = other.nativeImage - //#if MC>=11600 + //#if MC>=11600 && !STANDALONE //$$ nativeImage.copyImageData(otherNative) //#else nativeImage.graphics.drawImage(otherNative, 0, 0, otherNative.width, otherNative.height, null) @@ -22,7 +22,7 @@ class UImage(val nativeImage: BufferedImage) { } fun copy(): UImage { - //#if MC>=11600 + //#if MC>=11600 && !STANDALONE //$$ return UImage(NativeImage(getWidth(), getHeight(), false)).also { it.copyFrom(this) } //#else return UImage(BufferedImage(getWidth(), getHeight(), nativeImage.type)).also { it.copyFrom(this) } @@ -30,7 +30,7 @@ class UImage(val nativeImage: BufferedImage) { } fun getPixelRGBA(x: Int, y: Int): Int { - //#if MC>=11600 + //#if MC>=11600 && !STANDALONE //$$ // Convert ABGR to RGBA //$$ val abgr = nativeImage.getPixelRGBA(x, y) // mappings are incorrect, this returns ABGR //$$ val a = abgr shr 24 and 0xFF @@ -44,7 +44,7 @@ class UImage(val nativeImage: BufferedImage) { } fun setPixelRGBA(x: Int, y: Int, color: Int) { - //#if MC>=11600 + //#if MC>=11600 && !STANDALONE //$$ // Convert RGBA to ABGR //$$ val r = color shr 24 and 0xFF //$$ val g = color shr 16 and 0xFF @@ -64,7 +64,7 @@ class UImage(val nativeImage: BufferedImage) { @JvmStatic @JvmOverloads fun ofSize(width: Int, height: Int, clear: Boolean = true): UImage { - //#if MC>=11600 + //#if MC>=11600 && !STANDALONE //$$ return UImage(NativeImage(width, height, clear)) //#else @Suppress("UNUSED_EXPRESSION") clear // not yet using native memory, so it'll be cleared by the jvm diff --git a/src/main/kotlin/gg/essential/universal/UKeyboard.kt b/src/main/kotlin/gg/essential/universal/UKeyboard.kt index 9c17d82..5b222c1 100644 --- a/src/main/kotlin/gg/essential/universal/UKeyboard.kt +++ b/src/main/kotlin/gg/essential/universal/UKeyboard.kt @@ -1,5 +1,11 @@ package gg.essential.universal +//#if STANDALONE +//$$ import gg.essential.universal.standalone.glfw.Glfw +//$$ import kotlinx.coroutines.Dispatchers +//$$ import kotlinx.coroutines.runBlocking +//$$ import org.lwjgl.glfw.GLFW +//#else import net.minecraft.client.settings.KeyBinding //#if MC>=11600 @@ -13,12 +19,16 @@ import net.minecraft.client.settings.KeyBinding //#else import org.lwjgl.input.Keyboard import org.lwjgl.input.Mouse - +//#endif //#endif object UKeyboard { //#if MC>=11502 + //#if STANDALONE + //$$ @JvmField val KEY_NONE: Int = noInline { -1 } + //#else //$$ @JvmField val KEY_NONE: Int = noInline { InputMappings.INPUT_INVALID.keyCode } + //#endif //$$ @JvmField val KEY_ESCAPE: Int = noInline { GLFW.GLFW_KEY_ESCAPE } //$$ @JvmField val KEY_LMETA: Int = noInline { GLFW.GLFW_KEY_LEFT_SUPER } // TODO: Correct? //$$ @JvmField val KEY_RMETA: Int = noInline { GLFW.GLFW_KEY_RIGHT_SUPER } // TODO: Correct? @@ -293,6 +303,14 @@ object UKeyboard { @JvmStatic fun isKeyComboCtrlShiftZ(key: Int): Boolean = key == KEY_Z && isCtrlKeyDown() && isShiftKeyDown() && !isAltKeyDown() + //#if STANDALONE + //$$ internal val keysDown = mutableSetOf() + //$$ + //$$ @JvmStatic + //$$ fun isKeyDown(key: Int): Boolean { + //$$ return key in keysDown + //$$ } + //#else @JvmStatic fun isKeyDown(key: Int): Boolean { if (key == KEY_NONE) return false @@ -310,7 +328,9 @@ object UKeyboard { } //#endif } + //#endif + //#if !STANDALONE /** * Returns the name of the key assigned to the specified binding as appropriate for display to the user. * @@ -338,12 +358,18 @@ object UKeyboard { //#endif //#endif } + //#endif @Deprecated("Does not work for mouse bindings", replaceWith = ReplaceWith("getKeyName(keyBinding)")) @JvmStatic fun getKeyName(keyCode: Int, scanCode: Int): String? { //#if MC>=11502 - //$$ return GLFW.glfwGetKeyName(keyCode, scanCode)?.let { + //#if STANDALONE + //$$ val glfwName = runBlocking(Dispatchers.Glfw.immediate) { GLFW.glfwGetKeyName(keyCode, scanCode) } + //#else + //$$ val glfwName = GLFW.glfwGetKeyName(keyCode, scanCode) + //#endif + //$$ return glfwName?.let { //$$ // If it's a single character, GLFW will give us a lowercase version but that's very weird and //$$ // inconsistent with old versions, so we uppercase it. Longer ones are already fine (e.g. "Space"). //$$ if (it.length == 1) it.uppercase() else it diff --git a/src/main/kotlin/gg/essential/universal/UMinecraft.kt b/src/main/kotlin/gg/essential/universal/UMinecraft.kt index 176a412..10d5fee 100644 --- a/src/main/kotlin/gg/essential/universal/UMinecraft.kt +++ b/src/main/kotlin/gg/essential/universal/UMinecraft.kt @@ -1,5 +1,10 @@ package gg.essential.universal +//#if STANDALONE +//$$ import kotlin.coroutines.EmptyCoroutineContext +//$$ import kotlinx.coroutines.Dispatchers +//$$ import org.lwjgl.glfw.GLFW +//#else import net.minecraft.client.Minecraft import net.minecraft.client.entity.EntityPlayerSP import net.minecraft.client.gui.FontRenderer @@ -12,8 +17,13 @@ import net.minecraft.client.settings.GameSettings //#if MC>=11502 //$$ import net.minecraft.client.util.NativeUtil //#endif +//#endif object UMinecraft { + //#if STANDALONE + //$$ @JvmStatic + //$$ var guiScale: Int = 1 + //#else //#if MC>=11900 //$$ private var guiScaleValue: Int //$$ get() = getSettings().guiScale.value @@ -36,11 +46,17 @@ object UMinecraft { //$$ window.setGuiScale(scaleFactor.toDouble()) //#endif } + //#endif @JvmField val isRunningOnMac: Boolean = + //#if STANDALONE + //$$ UDesktop.isMac + //#else Minecraft.isRunningOnMac + //#endif + //#if !STANDALONE @JvmStatic fun getMinecraft(): Minecraft { return Minecraft.getMinecraft() @@ -65,16 +81,20 @@ object UMinecraft { fun getFontRenderer(): FontRenderer { return getMinecraft().fontRendererObj } + //#endif @JvmStatic fun getTime(): Long { - //#if MC>=11502 + //#if STANDALONE + //$$ return (GLFW.glfwGetTime() * 1000).toLong() + //#elseif MC>=11502 //$$ return (NativeUtil.getTime() * 1000).toLong() //#else return Minecraft.getSystemTime() //#endif } + //#if !STANDALONE @JvmStatic //#if FORGE @Suppress("UNNECESSARY_SAFE_CALL") // Forge adds inappropriate NonNullByDefault @@ -83,9 +103,14 @@ object UMinecraft { @JvmStatic fun getSettings(): GameSettings = getMinecraft().gameSettings + //#endif @JvmStatic var currentScreenObj: Any? + //#if STANDALONE + //$$ get() = UScreen.currentScreen + //$$ set(value) = UScreen.displayScreen(value as UScreen?) + //#else get() = getMinecraft().currentScreen set(value) { //#if MC<11200 @@ -93,10 +118,14 @@ object UMinecraft { //#endif getMinecraft().displayGuiScreen(value as GuiScreen?) } + //#endif + @JvmStatic fun isCallingFromMinecraftThread(): Boolean { - //#if MC>=11400 + //#if STANDALONE + //$$ return !Dispatchers.Main.immediate.isDispatchNeeded(EmptyCoroutineContext) + //#elseif MC>=11400 //$$ return Minecraft.getInstance().isOnExecutionThread //#else return Minecraft.getMinecraft().isCallingFromMinecraftThread diff --git a/src/main/kotlin/gg/essential/universal/UMouse.kt b/src/main/kotlin/gg/essential/universal/UMouse.kt index 56fa3bd..092d572 100644 --- a/src/main/kotlin/gg/essential/universal/UMouse.kt +++ b/src/main/kotlin/gg/essential/universal/UMouse.kt @@ -8,6 +8,14 @@ import kotlin.math.max object UMouse { object Raw { + //#if STANDALONE + //$$ @JvmStatic + //$$ var x: Double = 0.0 + //$$ internal set + //$$ @JvmStatic + //$$ var y: Double = 0.0 + //$$ internal set + //#else @JvmStatic val x: Double get() { @@ -27,6 +35,7 @@ object UMouse { return UResolution.windowHeight - Mouse.getY().toDouble() - 1 //#endif } + //#endif } object Scaled { @@ -43,7 +52,7 @@ object UMouse { @Deprecated("Orientation is different between Minecraft versions.", replaceWith = ReplaceWith("UMouse.Raw.x")) fun getTrueX(): Double { //#if MC>=11502 - //$$ return UMinecraft.getMinecraft().mouseHelper.mouseX + //$$ return Raw.x //#else return Mouse.getX().toDouble() //#endif @@ -60,7 +69,7 @@ object UMouse { @Deprecated("Orientation is different between Minecraft versions.", replaceWith = ReplaceWith("UMouse.Raw.y")) fun getTrueY(): Double { //#if MC>=11502 - //$$ return UMinecraft.getMinecraft().mouseHelper.mouseY + //$$ return Raw.y //#else return Mouse.getY().toDouble() //#endif diff --git a/src/main/kotlin/gg/essential/universal/UResolution.kt b/src/main/kotlin/gg/essential/universal/UResolution.kt index ef78c30..2a65195 100644 --- a/src/main/kotlin/gg/essential/universal/UResolution.kt +++ b/src/main/kotlin/gg/essential/universal/UResolution.kt @@ -11,6 +11,20 @@ object UResolution { private var cachedScaledResolutionInputs: ScaledResolutionInputs? = null //#endif + //#if STANDALONE + //$$ @JvmStatic + //$$ var windowWidth: Int = 1 + //$$ internal set + //$$ @JvmStatic + //$$ var windowHeight: Int = 1 + //$$ internal set + //$$ @JvmStatic + //$$ var viewportWidth: Int = 1 + //$$ internal set + //$$ @JvmStatic + //$$ var viewportHeight: Int = 1 + //$$ internal set + //#else @JvmStatic val windowWidth: Int get() { @@ -50,6 +64,7 @@ object UResolution { return UMinecraft.getMinecraft().displayHeight //#endif } + //#endif //#if MC<=11202 private fun get(): ScaledResolution { @@ -66,7 +81,9 @@ object UResolution { @JvmStatic val scaledWidth: Int get() { - //#if MC>=11502 + //#if STANDALONE + //$$ return (viewportWidth / scaleFactor).toInt() + //#elseif MC>=11502 //$$ return UMinecraft.getMinecraft().mainWindow.scaledWidth //#else return get().scaledWidth @@ -76,7 +93,9 @@ object UResolution { @JvmStatic val scaledHeight: Int get() { - //#if MC>=11502 + //#if STANDALONE + //$$ return (viewportHeight / scaleFactor).toInt() + //#elseif MC>=11502 //$$ return UMinecraft.getMinecraft().mainWindow.scaledHeight //#else return get().scaledHeight @@ -86,7 +105,9 @@ object UResolution { @JvmStatic val scaleFactor: Double get() { - //#if MC>=11502 + //#if STANDALONE + //$$ return UMinecraft.guiScale.toDouble() + //#elseif MC>=11502 //$$ return UMinecraft.getMinecraft().mainWindow.guiScaleFactor //#else return get().scaleFactor.toDouble() diff --git a/src/main/kotlin/gg/essential/universal/USound.kt b/src/main/kotlin/gg/essential/universal/USound.kt index f8ecb84..3d584c5 100644 --- a/src/main/kotlin/gg/essential/universal/USound.kt +++ b/src/main/kotlin/gg/essential/universal/USound.kt @@ -1,5 +1,8 @@ package gg.essential.universal +//#if STANDALONE +//#else + //#if MC>=11903 //$$ import net.minecraft.registry.entry.RegistryEntry //#endif @@ -12,7 +15,11 @@ package gg.essential.universal import net.minecraft.util.ResourceLocation //#endif +//#endif + object USound { + //#if STANDALONE + //#else //#if MC>10809 //$$ fun playSoundStatic(event: SoundEvent, volume: Float, pitch: Float) { //#else @@ -31,10 +38,14 @@ object USound { //$$ playSoundStatic(registryEntry.value(), volume, pitch) //$$ } //#endif + //#endif @JvmOverloads fun playButtonPress(volume: Float = 0.25f) { - //#if MC>10809 + //#if STANDALONE + //$$ @Suppress("UNUSED_EXPRESSION") volume + //$$ // TODO + //#elseif MC>10809 //$$ playSoundStatic(SoundEvents.UI_BUTTON_CLICK, volume, 1.0f) //#else playSoundStatic(ResourceLocation("gui.button.press"), volume, 1.0F); @@ -42,7 +53,9 @@ object USound { } fun playExpSound() { - //#if MC>10809 + //#if STANDALONE + //$$ TODO() + //#elseif MC>10809 //$$ playSoundStatic(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 0.25F, 1.0f) //#else playSoundStatic(ResourceLocation("random.orb"), 0.25F, 1.0F); @@ -50,7 +63,9 @@ object USound { } fun playLevelupSound() { - //#if MC>10809 + //#if STANDALONE + //$$ TODO() + //#elseif MC>10809 //$$ playSoundStatic(SoundEvents.ENTITY_PLAYER_LEVELUP, 0.25F, 1.0f) //#else playSoundStatic(ResourceLocation("random.levelup"), 0.25F, 1.0F); @@ -58,7 +73,9 @@ object USound { } fun playPlingSound() { - //#if MC>10809 + //#if STANDALONE + //$$ TODO() + //#elseif MC>10809 //$$ playSoundStatic(SoundEvents.BLOCK_NOTE_PLING, 0.25F, 1.0f) //#else playSoundStatic(ResourceLocation("note.pling"), 0.25F, 1.0F); diff --git a/src/main/kotlin/gg/essential/universal/shader/BlendState.kt b/src/main/kotlin/gg/essential/universal/shader/BlendState.kt index d12b2a6..0652e29 100644 --- a/src/main/kotlin/gg/essential/universal/shader/BlendState.kt +++ b/src/main/kotlin/gg/essential/universal/shader/BlendState.kt @@ -4,7 +4,7 @@ import gg.essential.universal.UGraphics import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL14 -//#if MC>=11700 +//#if MC>=11700 && !STANDALONE //$$ import net.minecraft.client.gl.GlBlendState //#endif @@ -22,7 +22,7 @@ data class BlendState( ) { val separate = srcRgb != srcAlpha || dstRgb != dstAlpha - //#if MC>=11700 + //#if MC>=11700 && !STANDALONE //$$ private inner class McBlendState : GlBlendState { //$$ constructor() : super() //$$ constructor(srcRgb: Int, dstRgb: Int, func: Int) : super(srcRgb, dstRgb, func) diff --git a/src/main/kotlin/gg/essential/universal/shader/GlShader.kt b/src/main/kotlin/gg/essential/universal/shader/GlShader.kt index f05f5db..d265ef4 100644 --- a/src/main/kotlin/gg/essential/universal/shader/GlShader.kt +++ b/src/main/kotlin/gg/essential/universal/shader/GlShader.kt @@ -1,7 +1,6 @@ package gg.essential.universal.shader import gg.essential.universal.UGraphics -import net.minecraft.client.renderer.GlStateManager import org.lwjgl.opengl.ARBShaderObjects import org.lwjgl.opengl.ARBShaderObjects.glGetUniformLocationARB import org.lwjgl.opengl.ARBShaderObjects.glShaderSourceARB @@ -18,10 +17,19 @@ import org.lwjgl.opengl.GL20.glValidateProgram import java.nio.ByteBuffer import java.nio.ByteOrder +//#if STANDALONE +//$$ import gg.essential.universal.standalone.render.createOrthoProjectionMatrix +//$$ import gg.essential.universal.UMatrixStack +//$$ import gg.essential.universal.standalone.utils.toColumnMajor +//$$ import org.lwjgl.opengl.GL +//$$ import org.lwjgl.opengl.GL30C +//#endif + internal class GlShader( private val vertSource: String, private val fragSource: String, private val blendState: BlendState, + preLink: (program: Int) -> Unit, ) : UShader { private var program: Int = UGraphics.glCreateProgram() private var vertShader: Int = UGraphics.glCreateShader(GL20.GL_VERTEX_SHADER) @@ -36,9 +44,14 @@ internal class GlShader( private var prevBlendState: BlendState? = null init { - createShader() + createShader(preLink) } + //#if STANDALONE + //$$ private val projectionMatrixUniform = getFloatMatrixUniformOrNull("ProjMat") + //$$ private val modelViewMatrixUniform = getFloatMatrixUniformOrNull("ModelViewMat") + //#endif + override fun bind() { prevActiveTexture = GL11.glGetInteger(GL_ACTIVE_TEXTURE) for (sampler in samplers.values) { @@ -48,22 +61,28 @@ internal class GlShader( blendState.activate() UGraphics.glUseProgram(program) + //#if STANDALONE + //$$ projectionMatrixUniform?.setValue(createOrthoProjectionMatrix().toColumnMajor()) + //$$ modelViewMatrixUniform?.setValue(UMatrixStack.GLOBAL_STACK.peek().model.toColumnMajor()) + //#endif bound = true } internal fun doBindTexture(textureUnit: Int, textureId: Int) { - GlStateManager.setActiveTexture(GL_TEXTURE0 + textureUnit) + UGraphics.setActiveTexture(GL_TEXTURE0 + textureUnit) prevTextureBindings.computeIfAbsent(textureUnit) { GL11.glGetInteger(GL_TEXTURE_BINDING_2D) } - GlStateManager.bindTexture(textureId) + @Suppress("DEPRECATION") // we actively manage the active texture unit, so this is fine + UGraphics.bindTexture(textureId) } override fun unbind() { for ((textureUnit, textureId) in prevTextureBindings) { - GlStateManager.setActiveTexture(GL_TEXTURE0 + textureUnit) - GlStateManager.bindTexture(textureId) + UGraphics.setActiveTexture(GL_TEXTURE0 + textureUnit) + @Suppress("DEPRECATION") // we actively manage the active texture unit, so this is fine + UGraphics.bindTexture(textureId) } prevTextureBindings.clear() - GlStateManager.setActiveTexture(prevActiveTexture) + UGraphics.setActiveTexture(prevActiveTexture) prevBlendState?.activate() UGraphics.glUseProgram(0) @@ -107,7 +126,7 @@ internal class GlShader( return uniform } - private fun createShader() { + private fun createShader(preLink: (program: Int) -> Unit) { for ((shader, source) in listOf(vertShader to vertSource, fragShader to fragSource)) { if (CORE) glShaderSource(shader, source) else glShaderSourceARB(shader, source) UGraphics.glCompileShader(shader) @@ -120,6 +139,8 @@ internal class GlShader( UGraphics.glAttachShader(program, shader) } + preLink(program) + UGraphics.glLinkProgram(program) if (CORE) { @@ -139,7 +160,19 @@ internal class GlShader( return } + //#if STANDALONE + //$$ if (GL.getCapabilities().OpenGL30) { + //$$ val vao = GL30C.glGenVertexArrays() + //$$ GL30C.glBindVertexArray(vao) + //$$ glValidateProgram(program) + //$$ GL30C.glBindVertexArray(0) + //$$ GL30C.glDeleteVertexArrays(vao) + //$$ } else { + //$$ if (CORE) glValidateProgram(program) else glValidateProgramARB(program) + //$$ } + //#else if (CORE) glValidateProgram(program) else glValidateProgramARB(program) + //#endif if (UGraphics.glGetProgrami(program, GL20.GL_VALIDATE_STATUS) != 1) { println(UGraphics.glGetProgramInfoLog(program, 32768)) diff --git a/src/main/kotlin/gg/essential/universal/shader/ShaderTransformer.kt b/src/main/kotlin/gg/essential/universal/shader/ShaderTransformer.kt new file mode 100644 index 0000000..88404de --- /dev/null +++ b/src/main/kotlin/gg/essential/universal/shader/ShaderTransformer.kt @@ -0,0 +1,131 @@ +package gg.essential.universal.shader + +import gg.essential.universal.UGraphics.CommonVertexFormats + +internal class ShaderTransformer(private val vertexFormat: CommonVertexFormats?, private val targetVersion: Int) { + init { + check(targetVersion in listOf(110, 130, 150)) + } + + val attributes = mutableListOf() + val samplers = mutableSetOf() + val uniforms = mutableMapOf() + + fun transform(originalSource: String): String { + var source = originalSource + + source = source.replace("gl_ModelViewProjectionMatrix", "gl_ProjectionMatrix * gl_ModelViewMatrix") + if (targetVersion >= 130) { + source = source.replace("texture2D", "texture") + } + + val replacements = mutableMapOf() + val transformed = mutableListOf() + transformed.add("#version $targetVersion") + + val frag = "gl_FragColor" in source + val vert = !frag + + val attributeIn = if (targetVersion >= 130) "in" else "attribute" + val varyingIn = if (targetVersion >= 130) "in" else "varying" + val varyingOut = if (targetVersion >= 130) "out" else "varying" + + if (frag && targetVersion >= 130) { + transformed.add("$varyingOut vec4 uc_FragColor;") + replacements["gl_FragColor"] = "uc_FragColor" + } + + if (vert && "gl_FrontColor" in source) { + transformed.add("$varyingOut vec4 uc_FrontColor;") + replacements["gl_FrontColor"] = "uc_FrontColor" + } + if (frag && "gl_Color" in source) { + transformed.add("$varyingIn vec4 uc_FrontColor;") + replacements["gl_Color"] = "uc_FrontColor" + } + + fun replaceAttribute(newAttributes: MutableList>, needle: String, type: String, replacementName: String = "uc_" + needle.substringAfter("_"), replacement: String = replacementName) { + if (needle in source) { + replacements[needle] = replacement + newAttributes.add(replacementName to "$attributeIn $type $replacementName;") + } + } + if (vert) { + val newAttributes = mutableListOf>() + replaceAttribute(newAttributes, "gl_Vertex", "vec3", "uc_Position", replacement = "vec4(uc_Position, 1.0)") + replaceAttribute(newAttributes, "gl_Color", "vec4") + replaceAttribute(newAttributes, "gl_MultiTexCoord0.st", "vec2", "uc_UV0") + replaceAttribute(newAttributes, "gl_MultiTexCoord1.st", "vec2", "uc_UV1") + replaceAttribute(newAttributes, "gl_MultiTexCoord2.st", "vec2", "uc_UV2") + + if (vertexFormat != null) { + //#if MC>=11700 && !STANDALONE + //$$ newAttributes.sortedBy { vertexFormat.mc.shaderAttributes.indexOf(it.first.removePrefix("uc_")) } + //$$ .forEach { + //$$ attributes.add(it.first) + //$$ transformed.add(it.second) + //$$ } + //#else + newAttributes.forEach { + attributes.add(it.first) + transformed.add(it.second) + } + //#endif + } else { + newAttributes.forEach { + attributes.add(it.first) + transformed.add(it.second) + } + } + } + + fun replaceUniform(needle: String, type: UniformType, replacementName: String, replacement: String = replacementName) { + if (needle in source) { + replacements[needle] = replacement + if (replacementName !in uniforms) { + uniforms[replacementName] = type + transformed.add("uniform ${type.glslName} $replacementName;") + } + } + } + replaceUniform("gl_ModelViewMatrix", UniformType.Mat4, "ModelViewMat") + replaceUniform("gl_ProjectionMatrix", UniformType.Mat4, "ProjMat") + + + for (line in source.lines()) { + transformed.add(when { + line.startsWith("#version") -> continue + line.startsWith("varying ") && targetVersion >= 130 -> (if (frag) "in " else "out ") + line.substringAfter("varying ") + line.startsWith("uniform ") -> { + val (_, glslType, name) = line.trimEnd(';').split(" ") + if (glslType == "sampler2D") { + samplers.add(name) + } else { + uniforms[name] = UniformType.fromGlsl(glslType) + } + line + } + else -> replacements.entries.fold(line) { acc, (needle, replacement) -> acc.replace(needle, replacement) } + }) + } + + return transformed.joinToString("\n") + } +} + +internal enum class UniformType(val typeName: String, val glslName: String, val default: IntArray) { + Int1("int", "int", intArrayOf(0)), + Float1("float", "float", intArrayOf(0)), + Float2("float", "vec2", intArrayOf(0, 0)), + Float3("float", "vec3", intArrayOf(0, 0, 0)), + Float4("float", "vec4", intArrayOf(0, 0, 0, 0)), + Mat2("matrix2x2", "mat2", intArrayOf(1, 0, 0, 1)), + Mat3("matrix3x3", "mat3", intArrayOf(1, 0, 0, 0, 1, 0, 0, 0, 1)), + Mat4("matrix4x4", "mat4", intArrayOf(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)), + ; + + companion object { + fun fromGlsl(glslName: String): UniformType = + values().find { it.glslName == glslName } ?: throw NoSuchElementException(glslName) + } +} diff --git a/src/main/kotlin/gg/essential/universal/shader/UShader.kt b/src/main/kotlin/gg/essential/universal/shader/UShader.kt index 537a1c9..2984228 100644 --- a/src/main/kotlin/gg/essential/universal/shader/UShader.kt +++ b/src/main/kotlin/gg/essential/universal/shader/UShader.kt @@ -2,7 +2,11 @@ package gg.essential.universal.shader import gg.essential.universal.UGraphics -//#if MC>=11700 +//#if STANDALONE +//$$ import gg.essential.universal.standalone.render.VertexFormat.Part +//$$ import org.lwjgl.opengl.GL +//$$ import org.lwjgl.opengl.GL20C +//#elseif MC>=11700 //$$ import net.minecraft.client.render.Shader //#endif @@ -34,23 +38,48 @@ interface UShader { replaceWith = ReplaceWith("UShader.fromLegacyShader(vertSource, fragSource, blendState, vertexFormat)") ) fun fromLegacyShader(vertSource: String, fragSource: String, blendState: BlendState): UShader { - //#if MC>=11700 + //#if STANDALONE + //$$ return fromLegacyShader(vertSource, fragSource, blendState, UGraphics.CommonVertexFormats.POSITION_COLOR) + //#elseif MC>=11700 //$$ return MCShader.fromLegacyShader(vertSource, fragSource, blendState, null) //#else - return GlShader(vertSource, fragSource, blendState) + return GlShader(vertSource, fragSource, blendState) {} //#endif } fun fromLegacyShader(vertSource: String, fragSource: String, blendState: BlendState, vertexFormat: UGraphics.CommonVertexFormats): UShader { - //#if MC>=11700 + //#if STANDALONE + //$$ return fromLegacyShader(vertSource, fragSource, blendState, vertexFormat.mc.parts) + //#elseif MC>=11700 //$$ return MCShader.fromLegacyShader(vertSource, fragSource, blendState, vertexFormat) //#else @Suppress("UNUSED_EXPRESSION") vertexFormat // only relevant to MCShader - return GlShader(vertSource, fragSource, blendState) + return GlShader(vertSource, fragSource, blendState) {} //#endif } - //#if MC>=11700 + //#if STANDALONE + //$$ internal fun fromLegacyShader(vertSource: String, fragSource: String, blendState: BlendState, attributes: List): UShader { + //$$ val caps = GL.getCapabilities() + //$$ val targetGlslVersion = when { + //$$ caps.OpenGL32 -> 150 + //$$ caps.OpenGL30 -> 130 + //$$ else -> 110 + //$$ } + //$$ val transformer = ShaderTransformer(null, targetGlslVersion) + //$$ return GlShader(transformer.transform(vertSource), transformer.transform(fragSource), blendState) { program -> + //$$ for ((index, attribute) in attributes.withIndex()) { + //$$ GL20C.glBindAttribLocation(program, index, when (attribute) { + //$$ Part.POSITION -> "uc_Position" + //$$ Part.TEXTURE -> "uc_UV0" + //$$ Part.COLOR -> "uc_Color" + //$$ Part.LIGHT -> "uc_UV1" + //$$ Part.NORMAL -> "uc_Normal" + //$$ }) + //$$ } + //$$ } + //$$ } + //#elseif MC>=11700 //$$ fun fromMcShader(shader: Shader, blendState: BlendState): UShader { //$$ return MCShader(shader, blendState) //$$ } diff --git a/src/main/kotlin/gg/essential/universal/utils/ReleasedDynamicTexture.kt b/src/main/kotlin/gg/essential/universal/utils/ReleasedDynamicTexture.kt index 2b99d68..b285bb5 100644 --- a/src/main/kotlin/gg/essential/universal/utils/ReleasedDynamicTexture.kt +++ b/src/main/kotlin/gg/essential/universal/utils/ReleasedDynamicTexture.kt @@ -1,11 +1,18 @@ package gg.essential.universal.utils import gg.essential.universal.UGraphics + +//#if STANDALONE +//$$ import org.lwjgl.BufferUtils +//$$ import org.lwjgl.opengl.GL20C +//$$ import java.nio.Buffer +//#else import net.minecraft.client.renderer.texture.AbstractTexture import net.minecraft.client.renderer.texture.TextureUtil import net.minecraft.client.resources.IResourceManager +//#endif -//#if MC<11502 +//#if MC<11502 || STANDALONE import java.awt.image.BufferedImage //#else //$$ import net.minecraft.client.renderer.texture.NativeImage @@ -23,16 +30,20 @@ import java.util.concurrent.ConcurrentHashMap class ReleasedDynamicTexture private constructor( val width: Int, val height: Int, - //#if MC>=11400 + //#if MC>=11400 && !STANDALONE //$$ textureData: NativeImage?, //#else textureData: IntArray?, //#endif +//#if STANDALONE +//$$ ) { +//#else ) : AbstractTexture() { +//#endif private var resources = Resources(this) - //#if MC>=11400 + //#if MC>=11400 && !STANDALONE //$$ init { //$$ resources.textureData = textureData ?: NativeImage(width, height, true) //$$ } @@ -45,7 +56,7 @@ class ReleasedDynamicTexture private constructor( constructor(width: Int, height: Int) : this(width, height, null) - //#if MC>=11400 + //#if MC>=11400 && !STANDALONE //$$ constructor(nativeImage: NativeImage) : this(nativeImage.width, nativeImage.height, nativeImage) //#else constructor(bufferedImage: BufferedImage) : this(bufferedImage.width, bufferedImage.height) { @@ -53,9 +64,11 @@ class ReleasedDynamicTexture private constructor( } //#endif + //#if !STANDALONE @Throws(IOException::class) override fun loadTexture(resourceManager: IResourceManager) { } + //#endif fun updateDynamicTexture() { uploadTexture() @@ -63,6 +76,35 @@ class ReleasedDynamicTexture private constructor( fun uploadTexture() { if (!uploaded) { + //#if STANDALONE + //$$ val glId = GL20C.glGenTextures() + //$$ + //$$ GL20C.glBindTexture(GL20C.GL_TEXTURE_2D, glId) + //$$ + //$$ GL20C.glTexParameteri(GL20C.GL_TEXTURE_2D, GL20C.GL_TEXTURE_MIN_FILTER, GL20C.GL_LINEAR) + //$$ GL20C.glTexParameteri(GL20C.GL_TEXTURE_2D, GL20C.GL_TEXTURE_MAG_FILTER, GL20C.GL_NEAREST) + //$$ GL20C.glTexParameteri(GL20C.GL_TEXTURE_2D, GL20C.GL_TEXTURE_WRAP_S, GL20C.GL_CLAMP_TO_EDGE) + //$$ GL20C.glTexParameteri(GL20C.GL_TEXTURE_2D, GL20C.GL_TEXTURE_WRAP_T, GL20C.GL_CLAMP_TO_EDGE) + //$$ + //$$ val nativeBuffer = BufferUtils.createIntBuffer(textureData.size) + //$$ nativeBuffer.put(textureData) + //$$ (nativeBuffer as Buffer).rewind() + //$$ GL20C.glTexImage2D( + //$$ GL20C.GL_TEXTURE_2D, + //$$ 0, + //$$ GL20C.GL_RGBA, + //$$ width, + //$$ height, + //$$ 0, + //$$ GL20C.GL_BGRA, + //$$ GL20C.GL_UNSIGNED_BYTE, + //$$ nativeBuffer + //$$ ) + //$$ textureData = IntArray(0) + //$$ + //$$ uploaded = true + //$$ resources.glId = glId + //#else TextureUtil.allocateTexture(allocGlId(), width, height) //#if MC>=11400 @@ -80,15 +122,29 @@ class ReleasedDynamicTexture private constructor( uploaded = true resources.glId = allocGlId() + //#endif Resources.drainCleanupQueue() } } + //#if !STANDALONE private fun allocGlId() = super.getGlTextureId() + //#endif val dynamicGlId: Int get() = getGlTextureId() + //#if STANDALONE + //$$ fun getGlTextureId(): Int { + //$$ uploadTexture() + //$$ return resources.glId + //$$ } + //$$ + //$$ fun deleteGlTexture() { + //$$ UGraphics.deleteTexture(resources.glId) + //$$ resources.glId = -1 + //$$ } + //#else override fun getGlTextureId(): Int { uploadTexture() return super.getGlTextureId() @@ -105,10 +161,11 @@ class ReleasedDynamicTexture private constructor( //$$ resources.close() //$$ } //#endif + //#endif private class Resources(referent: ReleasedDynamicTexture) : PhantomReference(referent, referenceQueue), Closeable { var glId: Int = -1 - //#if MC>=11400 + //#if MC>=11400 && !STANDALONE //$$ var textureData: NativeImage? = null //$$ set(value) { //$$ field?.close() @@ -128,7 +185,7 @@ class ReleasedDynamicTexture private constructor( glId = -1 } - //#if MC>=11400 + //#if MC>=11400 && !STANDALONE //$$ textureData = null //#endif } diff --git a/src/main/kotlin/gg/essential/universal/vertex/UVertexConsumer.kt b/src/main/kotlin/gg/essential/universal/vertex/UVertexConsumer.kt index 7aefa5d..59636c8 100644 --- a/src/main/kotlin/gg/essential/universal/vertex/UVertexConsumer.kt +++ b/src/main/kotlin/gg/essential/universal/vertex/UVertexConsumer.kt @@ -27,6 +27,7 @@ interface UVertexConsumer { fun endVertex(): UVertexConsumer companion object { + //#if !STANDALONE @JvmStatic fun of( //#if MC>=11600 @@ -35,5 +36,6 @@ interface UVertexConsumer { wrapped: net.minecraft.client.renderer.WorldRenderer, //#endif ): UVertexConsumer = VanillaVertexConsumer(wrapped) + //#endif } } diff --git a/standalone/build.gradle.kts b/standalone/build.gradle.kts new file mode 100644 index 0000000..b9daf56 --- /dev/null +++ b/standalone/build.gradle.kts @@ -0,0 +1,114 @@ +import com.replaymod.gradle.preprocess.PreprocessTask + +plugins { + kotlin("jvm") + id("gg.essential.defaults") + id("gg.essential.defaults.maven-publish") +} + +val parent = evaluationDependsOn(project.parent!!.path) + +group = "gg.essential" +version = parent.version +base.archivesName = "universalcraft-standalone" +kotlin.compilerOptions.moduleName = "universalcraft-standalone" +publishing.publications.named("maven") { artifactId = "universalcraft-standalone" } +java.withSourcesJar() +kotlin.jvmToolchain(8) + +dependencies { + api(kotlin("stdlib-jdk8", "2.0.20-RC")) + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") + + implementation("dev.folomeev.kotgl:kotgl-matrix:0.0.1-beta") + + val lwjglModules = listOf("lwjgl", "lwjgl-glfw", "lwjgl-opengl", "lwjgl-stb", "lwjgl-nanovg") + val lwjglNatives = listOf("linux", "macos", "macos-arm64", "windows") + api(platform("org.lwjgl:lwjgl-bom:3.3.3")) + for (module in lwjglModules) { + api("org.lwjgl", module) + for (native in lwjglNatives) { + api("org.lwjgl", module, classifier = "natives-$native") + } + } + + // MC provides these and (at least some of) our libs depend on them + api("org.slf4j:slf4j-api:2.0.13") + // Same as above but we do not want to expose them to our downstream so we can eventually remove them + implementation("org.apache.logging.log4j:log4j-api:2.23.1") + implementation("org.apache.logging.log4j:log4j-core:2.23.1") + implementation("org.apache.logging.log4j:log4j-slf4j-impl:2.23.1") + implementation("com.google.code.gson:gson:2.11.0") + implementation("commons-codec:commons-codec:1.17.1") + implementation("org.apache.httpcomponents:httpclient:4.5.14") +} + +tasks.processResources { + exclude("pack.mcmeta", "mcmod.info", "META-INF/mods.toml", "META-INF/neoforge.mods.toml", "fabric.mod.json") +} + +fun setupPreprocessor() { + val inherited = parent.evaluationDependsOn("1.8.9-forge") + + fun Provider.dir(path: String): Provider = + map { it.dir(path) } + + val generatedRoot = layout.buildDirectory.dir("preprocessed/main") + val generatedKotlin = generatedRoot.dir("kotlin") + val generatedJava = generatedRoot.dir("java") + val generatedResources = generatedRoot.dir("resources") + + val overwritesKotlin = file("src/main/kotlin").also { it.mkdirs() } + val overwritesJava = file("src/main/java").also { it.mkdirs() } + val overwriteResources = file("src/main/resources").also { it.mkdirs() } + + val inheritedSourceSet = inherited.sourceSets.main.get() + + val preprocessCode = tasks.register("preprocessCode") { + entry( + source = inherited.files(inheritedSourceSet.java.srcDirs), + overwrites = overwritesJava, + generated = generatedJava.get().asFile, + ) + entry( + source = inherited.files(inheritedSourceSet.kotlin.srcDirs.filter { it.endsWith("kotlin") }), + overwrites = overwritesKotlin, + generated = generatedKotlin.get().asFile, + ) + keywords = mapOf( + ".java" to PreprocessTask.DEFAULT_KEYWORDS, + ".kt" to PreprocessTask.DEFAULT_KEYWORDS, + ) + vars = mapOf( + "MC" to 99999, + "FABRIC" to 1, + "FORGE" to 0, + "NEOFORGE" to 0, + "FORGELIKE" to 0, + "STANDALONE" to 1, + "!STANDALONE" to 0, + ) + } + sourceSets.main { + java.setSrcDirs(listOf(overwritesJava, preprocessCode.map { generatedJava })) + kotlin.setSrcDirs(listOf( + overwritesKotlin, + preprocessCode.map { generatedKotlin }, + overwritesJava, + preprocessCode.map { generatedJava }, + )) + } + + val preprocessResources = tasks.register("preprocessResources") { + entry( + source = inherited.files(inheritedSourceSet.resources.srcDirs), + overwrites = overwriteResources, + generated = generatedResources.get().asFile, + ) + } + tasks.processResources { dependsOn(preprocessResources) } + sourceSets.main { + resources.setSrcDirs(listOf(overwriteResources, preprocessResources.map { generatedResources })) + } +} +setupPreprocessor() diff --git a/standalone/example/build.gradle.kts b/standalone/example/build.gradle.kts new file mode 100644 index 0000000..e32a694 --- /dev/null +++ b/standalone/example/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + kotlin("jvm") + application + // Optional, to create a single output jar containing the application and all necessary dependencies + id("com.gradleup.shadow") version "8.3.0" +} + +// Setup repositories; most dependencies are served by Maven Central, UniversalCraft is served by Essential's repo. +repositories { + mavenCentral() + maven("https://repo.essential.gg/repository/maven-public") +} + +dependencies { + // Note: To use this outside of this repository, replace 0 with the latest version (can be found in the README). + val universalCraftVersion = 0 + implementation("gg.essential:universalcraft-standalone:$universalCraftVersion") + + // The example will be using Elementa's LayoutDSL to build its GUI + // We do recommend you use it too, but it's not a hard requirement, you may even just use raw UScreen + UGraphics + // to draw your GUI. + // Note: Be sure to check Elementa's README for its latest version: https://github.com/EssentialGG/Elementa + val elementaVersion = 659 + implementation("gg.essential:elementa:$elementaVersion") + implementation("gg.essential:elementa-unstable-statev2:$elementaVersion") + implementation("gg.essential:elementa-unstable-layoutdsl:$elementaVersion") +} + +// Use Java 8 to compile our application. +// You may chose to use a more recent version, 8 is the minimum required by UniversalCraft. +kotlin.jvmToolchain(8) + +// Configure our main class so the `:run` task works and the `Main` attribute of the output jar is set accordingly +application { + mainClass.set("gg.essential.example.MainKt") +} + +// Optional, combine the application and all its dependencies into a single fat jar using the Shadow Gradle plugin +tasks.shadowJar { + // Optional, remove unused classes + // (doesn't really remove much from the example until https://github.com/GradleUp/shadow/issues/522 is fixed) + minimize { + exclude(dependency("org.lwjgl:lwjgl-nanovg:.*")) // segfaults in nvgCreate + exclude(dependency("gg.essential:universalcraft-standalone:.*")) // https://github.com/GradleUp/shadow/issues/522 + exclude(project(":standalone")) // same as the line above, but for the local version in this repository + } +} + +// This section exists only so the above universalcraft-standalone dependency is resolved directly to the local +// `:standalone` project instead of being fetched from maven. +// If you use this example as a base for your own application, remove this and instead fill in a proper version above. +configurations.all { + resolutionStrategy.dependencySubstitution { + substitute(module("gg.essential:universalcraft-standalone")).using(project(":standalone")) + } +} diff --git a/standalone/example/src/main/kotlin/gg/essential/example/Fonts.kt b/standalone/example/src/main/kotlin/gg/essential/example/Fonts.kt new file mode 100644 index 0000000..886e87c --- /dev/null +++ b/standalone/example/src/main/kotlin/gg/essential/example/Fonts.kt @@ -0,0 +1,39 @@ +package gg.essential.example + +import gg.essential.universal.standalone.nanovg.NvgContext +import gg.essential.universal.standalone.nanovg.NvgFontFace +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.URI +import java.net.URL +import kotlin.io.path.div +import kotlin.io.path.exists +import kotlin.io.path.readBytes +import kotlin.io.path.writeBytes + +object Fonts { + val nvgContext = NvgContext() + + val GEIST_REGULAR = NvgFontFace(nvgContext, Fonts::class.java.getResource("/fonts/Geist-Regular.otf")!!.readBytes()) + + suspend fun loadFallback() { + val font = try { + val bytes = withContext(Dispatchers.IO) { + val cachedFile = appBaseDir / "GoNotoCurrent-Regular.ttf" + if (cachedFile.exists()) { + cachedFile.readBytes() + } else { + val url = "https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoCurrent-Regular.ttf" + URI(url).toURL().readBytes().also { bytes -> + cachedFile.writeBytes(bytes) + } + } + } + NvgFontFace(nvgContext, bytes) + } catch (e: Throwable) { + e.printStackTrace() + return + } + GEIST_REGULAR.addFallback(font) + } +} diff --git a/standalone/example/src/main/kotlin/gg/essential/example/main.kt b/standalone/example/src/main/kotlin/gg/essential/example/main.kt new file mode 100644 index 0000000..be4082e --- /dev/null +++ b/standalone/example/src/main/kotlin/gg/essential/example/main.kt @@ -0,0 +1,86 @@ +package gg.essential.example + +import gg.essential.elementa.components.UIRoundedRectangle +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.font.DefaultFonts +import gg.essential.elementa.layoutdsl.* +import gg.essential.elementa.state.v2.State +import gg.essential.elementa.state.v2.mutableStateOf +import gg.essential.universal.UMinecraft +import gg.essential.universal.standalone.runUniversalCraft +import gg.essential.universal.UResolution +import gg.essential.universal.UScreen +import kotlinx.coroutines.launch +import java.awt.Color + +fun main() = runUniversalCraft("Example", 1000, 600) { window -> + val extraFontsLoaded = mutableStateOf(false) + launch { + Fonts.loadFallback() + extraFontsLoaded.set(true) + } + + UMinecraft.guiScale = 2 * (UResolution.viewportWidth / UResolution.windowWidth) + UScreen.displayScreen(LayoutDslScreen { exampleScreen(extraFontsLoaded) }) + + window.renderScreenUntilClosed() +} + +fun LayoutScope.exampleScreen(extraFontsLoaded: State) { + column(Arrangement.spacedBy(10f)) { + image("/100px-Tabby_cat_with_blue_eyes-3336579.jpg", Modifier.width(50f).height(60f)) + row(Arrangement.spacedBy(5f)) { + box(Modifier.width(100f).height(20f).color(Color.CYAN).hoverColor(Color.BLUE).hoverScope()) + UIRoundedRectangle(10f)(Modifier.width(100f).height(20f).color(Color.RED)) + box(Modifier.width(100f).height(20f).color(Color.CYAN).hoverColor(Color.BLUE).hoverScope()) + } + row(Arrangement.spacedBy(20f)) { + box(Modifier.color(Color.DARK_GRAY)) { + text("Hello, MinecraftFive!").constrain { + fontProvider = DefaultFonts.MINECRAFT_FIVE + } + } + row { + column { + repeat(4) { + box(Modifier.width(1f).height(1f).color(Color.RED)) + box(Modifier.width(1f).height(1f).color(Color.GREEN)) + } + } + box(Modifier.color(Color.DARK_GRAY)) { + text("Hello, NanoVG!") + } + } + text("Hello, Color!", Modifier.color(Color.RED)) + } + box(Modifier.color(Color.DARK_GRAY)) { + text("Geist Regular 32", Fonts.GEIST_REGULAR(32f)) + } + box(Modifier.color(Color.DARK_GRAY)) { + text("Geist Regular 16", Fonts.GEIST_REGULAR(16f)) + } + if_(extraFontsLoaded) { + row(Arrangement.spacedBy(20f)) { + geistText("Olá Mundo") + geistText("Chào thế giới!") + geistText("здравствуй, мир") + } + row(Arrangement.spacedBy(20f)) { + geistText("こんにちは世界") + geistText("হ্যালো, ওয়ার্ল্ড!") + geistText("أهلا بالعالم") + } + } `else` { + row(Modifier.height(17f)) { + text("Loading extra fonts...") + } + row(Modifier.height(17f)) {} + } + box(Modifier.childBasedSize(5f).color(Color.GRAY).hoverColor(Color.LIGHT_GRAY).hoverScope()) { + geistText("Quit") + }.onMouseClick { + UScreen.displayScreen(null) + } + } + text("Press `=` to open the Inspector.", Modifier.alignHorizontal(Alignment.End(5f)).alignVertical(Alignment.Start(5f))) +} diff --git a/standalone/example/src/main/kotlin/gg/essential/example/utils.kt b/standalone/example/src/main/kotlin/gg/essential/example/utils.kt new file mode 100644 index 0000000..6245d03 --- /dev/null +++ b/standalone/example/src/main/kotlin/gg/essential/example/utils.kt @@ -0,0 +1,116 @@ +package gg.essential.example + +import gg.essential.elementa.ElementaVersion +import gg.essential.elementa.UIComponent +import gg.essential.elementa.WindowScreen +import gg.essential.elementa.components.UIImage +import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.inspector.Inspector +import gg.essential.elementa.constraints.ConstraintType +import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.pixels +import gg.essential.elementa.font.FontProvider +import gg.essential.elementa.layoutdsl.LayoutScope +import gg.essential.elementa.layoutdsl.Modifier +import gg.essential.elementa.layoutdsl.layoutAsBox +import gg.essential.elementa.layoutdsl.then +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.util.isInComponentTree +import gg.essential.universal.UMatrixStack +import gg.essential.universal.standalone.nanovg.NvgFont +import gg.essential.universal.standalone.nanovg.NvgFontFace +import java.awt.Color +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.div + +val appBaseDir: Path by lazy { + val dir = Path("build") / "app_run_dir" + dir.createDirectories() +} + +class LayoutDslScreen( + block: LayoutScope.() -> Unit, +) : WindowScreen(ElementaVersion.V6) { + init { + window.layoutAsBox { + block() + } + + val inspector by lazy { Inspector(window).apply { parent = window } } + window.onKeyType { typedChar, _ -> + if (typedChar == '=') { + if (inspector.isInComponentTree()) { + inspector.hide() + } else { + inspector.unhide() + } + } + } + } +} + +fun LayoutScope.geistText(text: String, modifier: Modifier = Modifier, size: Float = 16f, scale: Float = 1f, shadow: Boolean = true) = + text(text, Fonts.GEIST_REGULAR(size).then(modifier), scale, shadow) + +fun LayoutScope.text(text: String, modifier: Modifier = Modifier, scale: Float = 1f, shadow: Boolean = true) = + text(BasicState(text), modifier, scale, shadow) + +fun LayoutScope.text(text: State, modifier: Modifier = Modifier, scale: Float = 1f, shadow: Boolean = true) = + UIText(shadow = shadow).bindText(text).constrain { textScale = scale.pixels() }(modifier) + +fun LayoutScope.image(path: String, modifier: Modifier = Modifier) = + UIImage.ofResourceCached(path)(modifier) + +operator fun NvgFontFace.invoke(size: Float): Modifier = Modifier.font(NvgFont(this@invoke, size)) + +fun Modifier.font(font: NvgFont): Modifier = + font(NvgFontProvider(font)) + +fun Modifier.font(fontProvider: FontProvider): Modifier = then { + val prevFontProvider = getFontProvider() + setFontProvider(fontProvider) + ; { setFontProvider(prevFontProvider) } +} + +class NvgFontProvider(private val font: NvgFont) : FontProvider { + + /* Required by Elementa but unused for this type of constraint */ + override var cachedValue: FontProvider = this + override var recalculate: Boolean = false + override var constrainTo: UIComponent? = null + + override fun drawString( + matrixStack: UMatrixStack, + string: String, + color: Color, + x: Float, + y: Float, + originalPointSize: Float, + scale: Float, + shadow: Boolean, + shadowColor: Color? + ) = font.drawString(matrixStack, string, color, x, y, originalPointSize, scale, shadow, shadowColor) + + override fun getBaseLineHeight(): Float = + font.getBaseLineHeight() + + override fun getBelowLineHeight(): Float = + font.getBelowLineHeight() + + override fun getShadowHeight(): Float = + font.getShadowHeight() + + override fun getStringHeight(string: String, pointSize: Float): Float = + (font.getBaseLineHeight() + font.getBelowLineHeight()) * pointSize / 10f + + override fun getStringWidth(string: String, pointSize: Float): Float = + font.getStringWidth(string, pointSize) + + override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { + } + +} diff --git a/standalone/example/src/main/resources/100px-Tabby_cat_with_blue_eyes-3336579.jpg b/standalone/example/src/main/resources/100px-Tabby_cat_with_blue_eyes-3336579.jpg new file mode 100644 index 0000000..bf8c6d6 Binary files /dev/null and b/standalone/example/src/main/resources/100px-Tabby_cat_with_blue_eyes-3336579.jpg differ diff --git a/standalone/example/src/main/resources/fonts/Geist-LICENSE.txt b/standalone/example/src/main/resources/fonts/Geist-LICENSE.txt new file mode 100644 index 0000000..df71062 --- /dev/null +++ b/standalone/example/src/main/resources/fonts/Geist-LICENSE.txt @@ -0,0 +1,92 @@ +Geist Sans and Geist Mono Font +(C) 2023 Vercel, made in collaboration with basement.studio + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is available with a FAQ at: http://scripts.sil.org/OFL and copied below + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/standalone/example/src/main/resources/fonts/Geist-Regular.otf b/standalone/example/src/main/resources/fonts/Geist-Regular.otf new file mode 100644 index 0000000..6deee3d Binary files /dev/null and b/standalone/example/src/main/resources/fonts/Geist-Regular.otf differ diff --git a/standalone/src/main/java/gg/essential/universal/PositionedSoundRecordFactory.java b/standalone/src/main/java/gg/essential/universal/PositionedSoundRecordFactory.java new file mode 100644 index 0000000..12f1c2f --- /dev/null +++ b/standalone/src/main/java/gg/essential/universal/PositionedSoundRecordFactory.java @@ -0,0 +1 @@ +// Not applicable diff --git a/standalone/src/main/kotlin/gg/essential/universal/UGuiButton.kt b/standalone/src/main/kotlin/gg/essential/universal/UGuiButton.kt new file mode 100644 index 0000000..147f185 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/UGuiButton.kt @@ -0,0 +1,3 @@ +package gg.essential.universal + +// Not applicable diff --git a/standalone/src/main/kotlin/gg/essential/universal/UMatrixStack.kt b/standalone/src/main/kotlin/gg/essential/universal/UMatrixStack.kt new file mode 100644 index 0000000..e2c6b44 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/UMatrixStack.kt @@ -0,0 +1,219 @@ +package gg.essential.universal + +import dev.folomeev.kotgl.matrix.matrices.identityMat3 +import dev.folomeev.kotgl.matrix.matrices.identityMat4 +import dev.folomeev.kotgl.matrix.matrices.mat3 +import dev.folomeev.kotgl.matrix.matrices.mutables.MutableMat3 +import dev.folomeev.kotgl.matrix.matrices.mutables.MutableMat4 +import dev.folomeev.kotgl.matrix.matrices.mutables.set +import dev.folomeev.kotgl.matrix.matrices.mutables.timesSelf +import dev.folomeev.kotgl.matrix.matrices.mutables.toMutable +import gg.essential.universal.standalone.utils.toRowMajor +import gg.essential.universal.standalone.utils.toMat4 +import java.util.* +import kotlin.math.PI +import kotlin.math.cbrt +import kotlin.math.cos +import kotlin.math.sin + +/** + * A stack of matrices which can be manipulated via common transformations, just like MC's MatrixStack. + * + * For MC versions 1.16 and above, methods exist to convert from (via the constructor) and to (via Entry.toMCStack) the + * vanilla stack type if required. + * For MC versions below 1.17, the *GlobalState methods can be used to transfer the state of this matrix stack into the + * global GL state. For 1.17, they transfer state into Mojang's global MatrixStack in RenderSystem. + */ +class UMatrixStack private constructor( + private val stack: MutableList, +) { + + constructor() : this(mutableListOf(Entry(identityMat4().toMutable(), identityMat3().toMutable()))) + + fun translate(x: Double, y: Double, z: Double) = translate(x.toFloat(), y.toFloat(), z.toFloat()) + + fun translate(x: Float, y: Float, z: Float) { + if (x == 0f && y == 0f && z == 0f) return + stack.last().run { + // kotgl's builtin translate functions put the translation in the wrong place (last row + // instead of column) + model.timesSelf( + identityMat4().toMutable().apply { + m03 = x + m13 = y + m23 = z + } + ) + } + } + + fun scale(x: Double, y: Double, z: Double) = scale(x.toFloat(), y.toFloat(), z.toFloat()) + + fun scale(x: Float, y: Float, z: Float) { + if (x == 1f && y == 1f && z == 1f) return + return stack.last().run { + // kotgl's builtin scale functions also scale the translate values + model.timesSelf( + identityMat4().toMutable().apply { + m00 = x + m11 = y + m22 = z + } + ) + if (x == y && y == z) { + if (x < 0f) { + normal.timesSelf(-1f) + } + } else { + val ix = 1f / x + val iy = 1f / y + val iz = 1f / z + val rt = cbrt(ix * iy * iz) + normal.timesSelf( + identityMat3().toMutable().apply { + m00 = rt * ix + m11 = rt * iy + m22 = rt * iz + } + ) + } + } + } + + @JvmOverloads + fun rotate(angle: Float, x: Float, y: Float, z: Float, degrees: Boolean = true) { + if (angle == 0f) return + stack.last().run { + val angleRadians = if (degrees) (angle / 180 * PI).toFloat() else angle + val c = cos(angleRadians) + val s = sin(angleRadians) + val oneMinusC = 1 - c + val xx = x * x + val xy = x * y + val xz = x * z + val yy = y * y + val yz = y * z + val zz = z * z + val xs = x * s + val ys = y * s + val zs = z * s + val rotation = mat3( + xx * oneMinusC + c, + xy * oneMinusC - zs, + xz * oneMinusC + ys, + xy * oneMinusC + zs, + yy * oneMinusC + c, + yz * oneMinusC - xs, + xz * oneMinusC - ys, + yz * oneMinusC + xs, + zz * oneMinusC + c, + ) + model.timesSelf(rotation.toMat4()) + normal.timesSelf(rotation) + } + } + + fun fork() = UMatrixStack(mutableListOf(stack.last().deepCopy())) + + fun push() { + stack.add(stack.last().deepCopy()) + } + + fun pop() { + stack.removeLast() + } + + fun peek() = stack.last() + + fun isEmpty(): Boolean = stack.size == 1 + + fun applyToGlobalState() { + GLOBAL_STACK.stack.last().model.timesSelf(stack.last().model) + } + + fun replaceGlobalState() { + GLOBAL_STACK.stack.last().model.set(stack.last().model) + } + + fun runWithGlobalState(block: Runnable) = runWithGlobalState { block.run() } + + fun runWithGlobalState(block: () -> R): R = withGlobalStackPushed { + applyToGlobalState() + block() + } + + fun runReplacingGlobalState(block: Runnable) = runReplacingGlobalState { block.run() } + + fun runReplacingGlobalState(block: () -> R): R = withGlobalStackPushed { + replaceGlobalState() + block() + } + + private inline fun withGlobalStackPushed(block: () -> R) : R { + GLOBAL_STACK.push() + return block().also { + GLOBAL_STACK.pop() + } + } + + data class Entry(val model: MutableMat4, val normal: MutableMat3) { + fun deepCopy() = + Entry(model.copyOf(), normal.copyOf()) + + /** + * Returns the model matrix in row-major order. + */ + val modelAsArray: FloatArray + get() = model.toRowMajor() + } + + object Compat { + const val DEPRECATED = """For 1.17 this method requires you pass a UMatrixStack as the first argument. + +If you are currently extending this method, you should instead extend the method with the added argument. +Note however for this to be non-breaking, your parent class needs to transition before you do. + +If you are calling this method and you cannot guarantee that your target class has been fully updated (such as when +calling an open method on an open class), you should instead call the method with the "Compat" suffix, which will +call both methods, the new and the deprecated one. +If you are sure that your target class has been updated (such as when calling the super method), you should +(for super calls you must!) instead just call the method with the original name and added argument.""" + + private val stack = mutableListOf() + + /** + * To preserve backwards compatibility with old subclasses of UScreen or similar hierarchies, + * this method allows one to sneak in an artificial matrix stack argument when calling the legacy method + * which can then later be retrieved via [get] when the base legacy method calls the new one. + * + * For an example see [UScreen.onDrawScreenCompat]. + */ + fun runLegacyMethod(matrixStack: UMatrixStack, block: () -> R): R { + stack.add(matrixStack) + return block().also { + stack.removeAt(stack.lastIndex) + } + } + + fun get(): UMatrixStack = stack.lastOrNull() ?: UMatrixStack() + } + + companion object { + @JvmField + val GLOBAL_STACK = UMatrixStack() + + /** + * Represents an empty matrix stack. That is, a stack with the identity matrix as its sole entry. + * + * This stack may be passed to consuming APIs which may then assume that the stack is in fact a unit stack and + * can therefore skip math that would be redundant in such cases. + * + * **This stack must not be modified.** + * Consumers may compare this stack by reference and ignore its content. + * Consumers which are not aware of this stack must still behave correctly, so its content must be correct. + * [fork] is fine, [push] is not! + */ + @JvmField + val UNIT = UMatrixStack() + } +} \ No newline at end of file diff --git a/standalone/src/main/kotlin/gg/essential/universal/UPacket.kt b/standalone/src/main/kotlin/gg/essential/universal/UPacket.kt new file mode 100644 index 0000000..147f185 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/UPacket.kt @@ -0,0 +1,3 @@ +package gg.essential.universal + +// Not applicable diff --git a/standalone/src/main/kotlin/gg/essential/universal/UScreen.kt b/standalone/src/main/kotlin/gg/essential/universal/UScreen.kt new file mode 100644 index 0000000..5daabb1 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/UScreen.kt @@ -0,0 +1,88 @@ +package gg.essential.universal + +abstract class UScreen( + val restoreCurrentGuiOnClose: Boolean = false, + open var newGuiScale: Int = -1, + open var unlocalizedName: String? = null +) { + @JvmOverloads + constructor( + restoreCurrentGuiOnClose: Boolean = false, + newGuiScale: Int = -1, + ) : this(restoreCurrentGuiOnClose, newGuiScale, null) + + private var guiScaleToRestore = -1 + private val screenToRestore: UScreen? = if (restoreCurrentGuiOnClose) currentScreen else null + + fun initGui() { + updateGuiScale() + initScreen(UResolution.scaledWidth, UResolution.scaledHeight) + } + + fun onGuiClosed() { + onScreenClose() + if (guiScaleToRestore != -1) + UMinecraft.guiScale = guiScaleToRestore + } + + constructor(restoreCurrentGuiOnClose: Boolean, newGuiScale: GuiScale) : this( + restoreCurrentGuiOnClose, + newGuiScale.ordinal + ) + + fun restorePreviousScreen() { + displayScreen(screenToRestore) + } + + open fun updateGuiScale() { + if (newGuiScale != -1) { + if (guiScaleToRestore == -1) + guiScaleToRestore = UMinecraft.guiScale + UMinecraft.guiScale = newGuiScale + } + } + + open fun initScreen(width: Int, height: Int) { + } + + open fun onDrawScreen(matrixStack: UMatrixStack, mouseX: Int, mouseY: Int, partialTicks: Float) { + } + + open fun onKeyPressed(keyCode: Int, typedChar: Char, modifiers: UKeyboard.Modifiers?) { + } + + open fun onKeyReleased(keyCode: Int, typedChar: Char, modifiers: UKeyboard.Modifiers?) { + } + + open fun onMouseClicked(mouseX: Double, mouseY: Double, mouseButton: Int) { + } + + open fun onMouseReleased(mouseX: Double, mouseY: Double, state: Int) { + } + + open fun onMouseDragged(x: Double, y: Double, clickedButton: Int, timeSinceLastClick: Long) { + } + + open fun onMouseScrolled(delta: Double) { + } + + open fun onTick() { + } + + open fun onScreenClose() { + } + + open fun onDrawBackground(matrixStack: UMatrixStack, tint: Int) { + } + + companion object { + var currentScreen: UScreen? = null + private set + + fun displayScreen(screen: UScreen?) { + currentScreen?.onGuiClosed() + currentScreen = screen + currentScreen?.initGui() + } + } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/UCMainDispatcher.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/UCMainDispatcher.kt new file mode 100644 index 0000000..ccf8132 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/UCMainDispatcher.kt @@ -0,0 +1,52 @@ +package gg.essential.universal.standalone + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.internal.MainDispatcherFactory +import java.io.Closeable +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext + +/** + * A coroutine dispatcher for use as [Dispatchers.Main] backed by a plain single-threaded executor service. + */ +internal object UCMainDispatcher : MainCoroutineDispatcher(), Closeable { + private val threadGroup = ThreadGroup("Main") + private val executorService = Executors.newSingleThreadExecutor { + Thread(threadGroup, it, "Main").apply { isDaemon = true } + } + + override val immediate: MainCoroutineDispatcher + get() = Immediate + + override fun dispatch(context: CoroutineContext, block: Runnable) { + executorService.execute(block) + } + + override fun close() { + executorService.shutdown() + } + + object Immediate : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher + get() = this + + override fun dispatch(context: CoroutineContext, block: Runnable) { + executorService.execute(block) + } + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = + Thread.currentThread().threadGroup != threadGroup + } +} + +@OptIn(InternalCoroutinesApi::class) +internal class UCDispatcherFactory : MainDispatcherFactory { + override val loadPriority: Int + get() = 1000 + + override fun createDispatcher(allFactories: List): MainCoroutineDispatcher = + UCMainDispatcher +} \ No newline at end of file diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/UCWindow.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/UCWindow.kt new file mode 100644 index 0000000..f823b60 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/UCWindow.kt @@ -0,0 +1,143 @@ +package gg.essential.universal.standalone + +import gg.essential.universal.standalone.glfw.Glfw +import gg.essential.universal.standalone.glfw.GlfwWindow +import gg.essential.universal.UKeyboard +import gg.essential.universal.UKeyboard.toModifiers +import gg.essential.universal.UMatrixStack +import gg.essential.universal.UMouse +import gg.essential.universal.UResolution +import gg.essential.universal.UScreen +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.lwjgl.glfw.GLFW +import org.lwjgl.system.MemoryStack + +/** Must be initialized on the GLFW main thread! */ +class UCWindow(val glfwWindow: GlfwWindow, val uiScope: CoroutineScope) { + init { + GLFW.glfwSetWindowSizeCallback(glfwWindow.glfwId) { _, width, height -> + uiScope.launch { + UResolution.windowWidth = width + UResolution.windowHeight = height + } + } + + GLFW.glfwSetFramebufferSizeCallback(glfwWindow.glfwId) { _, width, height -> + uiScope.launch { + UResolution.viewportWidth = width + UResolution.viewportHeight = height + } + } + + GLFW.glfwSetCursorPosCallback(glfwWindow.glfwId) { _, x, y -> + uiScope.launch { + UMouse.Raw.x = x + UMouse.Raw.y = y + } + } + + GLFW.glfwSetMouseButtonCallback(glfwWindow.glfwId) { _, button, action, _ -> + uiScope.launch { + when (action) { + GLFW.GLFW_PRESS -> { + UKeyboard.keysDown.add(button) + UScreen.currentScreen?.onMouseClicked(UMouse.Scaled.x, UMouse.Scaled.y, button) + } + + GLFW.GLFW_RELEASE -> { + UKeyboard.keysDown.remove(button) + UScreen.currentScreen?.onMouseReleased(UMouse.Scaled.x, UMouse.Scaled.y, button) + } + } + } + } + + GLFW.glfwSetScrollCallback(glfwWindow.glfwId) { _, _, y -> + uiScope.launch { + UScreen.currentScreen?.onMouseScrolled(y) + } + } + + GLFW.glfwSetCharModsCallback(glfwWindow.glfwId) { _, codepoint, modifiers -> + uiScope.launch { + for (char in Character.toChars(codepoint)) { + UScreen.currentScreen?.onKeyPressed(0, char, modifiers.toModifiers()) + } + } + } + + GLFW.glfwSetKeyCallback(glfwWindow.glfwId) { _, key, _, action, modifiers -> + uiScope.launch { + when (action) { + GLFW.GLFW_PRESS -> { + UKeyboard.keysDown.add(key) + UScreen.currentScreen?.onKeyPressed(key, 0.toChar(), modifiers.toModifiers()) + } + + GLFW.GLFW_RELEASE -> { + UKeyboard.keysDown.remove(key) + UScreen.currentScreen?.onKeyReleased(key, 0.toChar(), modifiers.toModifiers()) + } + } + } + } + + MemoryStack.stackPush().use { stack -> + val width = stack.mallocInt(1) + val height = stack.mallocInt(1) + GLFW.glfwGetWindowSize(glfwWindow.glfwId, width, height) + UResolution.windowWidth = width.get(0) + UResolution.windowHeight = height.get(0) + GLFW.glfwGetFramebufferSize(glfwWindow.glfwId, width, height) + UResolution.viewportWidth = width.get(0) + UResolution.viewportHeight = height.get(0) + } + } + + suspend fun renderLoop(render: (time: Float, deltaTime: Float) -> Boolean) { + var firstFrame = true + var lastTime = 0f + while (!GLFW.glfwWindowShouldClose(glfwWindow.glfwId)) { + val time = GLFW.glfwGetTime().toFloat() + val deltaTime = time - lastTime + lastTime = time + + glfwWindow.prepareFrame(UResolution.viewportWidth, UResolution.viewportHeight) + if (!render(time, deltaTime)) { + break + } + GLFW.glfwSwapBuffers(glfwWindow.glfwId) + + if (firstFrame) { + firstFrame = false + withContext(Dispatchers.Glfw) { + GLFW.glfwShowWindow(glfwWindow.glfwId) + } + } + + withContext(Dispatchers.Glfw) { + GLFW.glfwPollEvents() + } + } + } + + suspend fun renderScreenUntilClosed() { + var nextTick = 0f + renderLoop { _, deltaTime -> + val screen = UScreen.currentScreen ?: return@renderLoop false + + nextTick += deltaTime + while (nextTick >= 0) { + nextTick -= 1 / 20f + screen.onTick() + } + + screen.onDrawScreen(UMatrixStack(), UMouse.Scaled.x.toInt(), UMouse.Scaled.y.toInt(), nextTick + 1 / 20f) + + return@renderLoop true + } + } +} \ No newline at end of file diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/GlfwDispatcher.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/GlfwDispatcher.kt new file mode 100644 index 0000000..452ad20 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/GlfwDispatcher.kt @@ -0,0 +1,73 @@ +package gg.essential.universal.standalone.glfw + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainCoroutineDispatcher +import java.io.Closeable +import java.util.concurrent.LinkedBlockingQueue +import kotlin.coroutines.CoroutineContext + + +/** + * A coroutine dispatcher that is confined to the "Main Thread" as required by various GLFW functions. + * This is not the same as [Dispatchers.Main] and meant primarily for running GLFW functions, not UI in general. + * + * Must be driven via [runGlfw] which will not return until this dispatcher is fully shut down. + */ +val Dispatchers.Glfw: MainCoroutineDispatcher + get() = GlfwDispatcher + +internal object GlfwDispatcher : MainCoroutineDispatcher(), Closeable { + private val mainThread = Thread.currentThread() + private val tasks = LinkedBlockingQueue() + private var shuttingDown = false + private var shutDown = false + + fun runTasks() { + while (!shutDown) { + val task = tasks.take() + task.run() + } + while (true) { + val task = tasks.poll() ?: return + task.run() + } + } + + /** Gracefully shuts down this dispatcher. Must be called from its thread. */ + override fun close() { + if (shuttingDown) { + return + } + shuttingDown = true + tasks.put { shutDown = true } + } + + override val immediate: MainCoroutineDispatcher + get() = Immediate + + override fun dispatch(context: CoroutineContext, block: Runnable) { + if (shuttingDown) { + throw IllegalStateException("$this was shut down.") + } + tasks.put(block) + } + + override fun toString(): String { + return "Dispatchers.Glfw" + } + + object Immediate : MainCoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) = + GlfwDispatcher.dispatch(context, block) + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = + Thread.currentThread() != mainThread + + override val immediate: MainCoroutineDispatcher + get() = this + + override fun toString(): String { + return "Dispatchers.Glfw.immediate" + } + } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/GlfwWindow.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/GlfwWindow.kt new file mode 100644 index 0000000..d817e44 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/GlfwWindow.kt @@ -0,0 +1,133 @@ +package gg.essential.universal.standalone.glfw + +import gg.essential.universal.UDesktop +import gg.essential.universal.standalone.render.DEBUG_GL +import org.lwjgl.glfw.GLFW +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL11.* +import org.lwjgl.stb.STBIWriteCallback +import org.lwjgl.stb.STBImageWrite +import org.lwjgl.system.MemoryStack +import org.lwjgl.system.MemoryUtil +import java.io.Closeable +import java.io.IOException +import java.nio.ByteBuffer + +/** + * Creates and manages a GLFW window. + * GLFW must already have been initialized. + * + * Unless noted otherwise, all methods must be called from the main thread! + */ +class GlfwWindow( + private val title: String, + width: Int, + height: Int, + resizable: Boolean = true, +) : Closeable { + + val glfwId: Long = run { + GLFW.glfwDefaultWindowHints() + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE) + GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, if (resizable) GLFW.GLFW_TRUE else GLFW.GLFW_FALSE) + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_DEBUG, if (DEBUG_GL) GLFW.GLFW_TRUE else GLFW.GLFW_FALSE) + + // Ideally we'd have a forward-compatible core profile (available from OpenGL 3.2) so we can't accidentally use + // deprecated stuff, and so we can make full use of tools like RenderDoc + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3) + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2) + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE) + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE) + + GLFW.glfwCreateWindow(width, height, title, MemoryUtil.NULL, MemoryUtil.NULL) + .takeUnless { it == MemoryUtil.NULL } + ?.let { return@run it } + + // If we can't get one, we can still make great use of any kind of OpenGL 3.0+ + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3) + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 0) + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_ANY_PROFILE) + + GLFW.glfwCreateWindow(width, height, title, MemoryUtil.NULL, MemoryUtil.NULL) + .takeUnless { it == MemoryUtil.NULL } + ?.let { return@run it } + + // If we can't even get that, we can mostly still work so long as we get at least OpenGL 2.0 (released in 2004) + // with a few extensions + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 2) + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 0) + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_ANY_PROFILE) + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_FALSE) + + GLFW.glfwCreateWindow(width, height, title, MemoryUtil.NULL, MemoryUtil.NULL) + .takeUnless { it == MemoryUtil.NULL } + ?.let { return@run it } + + // Failed to create any context, fetch the error and throw + val message = MemoryStack.stackPush().use { stack -> + val pointer = stack.mallocPointer(1) + val error = GLFW.glfwGetError(pointer) + if (error != GLFW.GLFW_NO_ERROR) { + "${MemoryUtil.memUTF8Safe(pointer.get(0))} (code $error)" + } else { + "unknown error" + } + } + throw RuntimeException("Failed to create the GLFW window: $message") + } + + init { + GLFW.glfwMakeContextCurrent(glfwId) + GLFW.glfwSwapInterval(1) + + GL.createCapabilities() + + UDesktop.glfwWindow = this + } + + /** Must be called from the thread on which this window's OpenGL context is current. */ + fun prepareFrame(width: Int, height: Int) { + glViewport(0, 0, width, height) + glClearColor(0f, 0f, 0f, 1f) + glClearDepth(1.0) + glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) + } + + /** Must be called from the thread on which this window's OpenGL context is current. */ + fun capturePng(width: Int, height: Int): ByteArray { + val buffer = MemoryUtil.memAlloc(width * height * 4) + try { + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer) + return pixelsToPng(width, height, buffer) + } finally { + MemoryUtil.memFree(buffer) + } + } + + private fun pixelsToPng(width: Int, height: Int, buffer: ByteBuffer): ByteArray { + var output = ByteArray(0) + + val writeCallback = + STBIWriteCallback.create { _, data, size -> + val byteBuffer = STBIWriteCallback.getData(data, size) + output = output.copyOf(output.size + size) + byteBuffer.get(output, output.size - size, size) + } + try { + STBImageWrite.stbi_flip_vertically_on_write(true) + val success = + STBImageWrite.stbi_write_png_to_func(writeCallback, 0L, width, height, 4, buffer, 0) + STBImageWrite.stbi_flip_vertically_on_write(false) + if (!success) { + throw IOException("Failed to encode image") + } + return output + } finally { + writeCallback.free() + } + } + + override fun close() { + GLFW.glfwDestroyWindow(glfwId) + } +} \ No newline at end of file diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/runGlfw.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/runGlfw.kt new file mode 100644 index 0000000..3b7abbe --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/glfw/runGlfw.kt @@ -0,0 +1,41 @@ +package gg.essential.universal.standalone.glfw + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.async +import kotlinx.coroutines.plus +import kotlinx.coroutines.runBlocking +import org.lwjgl.glfw.GLFW +import org.lwjgl.glfw.GLFWErrorCallback + +/** + * Initializes GLFW and runs the given block on [Dispatchers.Glfw]. + * Terminates GLFW once the given block returns. + * + * Must be called from the main thread! + */ +fun runGlfw(main: suspend CoroutineScope.() -> Unit) { + // This may be required to use AWT together with GLFW on macOS + System.setProperty("java.awt.headless", "true") + + GLFWErrorCallback.createPrint(System.err).set() + + check(GLFW.glfwInit()) { "Unable to initialize GLFW" } + + val scope = MainScope() + Dispatchers.Glfw + val mainJob = scope.async { + try { + main() + } finally { + GlfwDispatcher.close() + } + } + + GlfwDispatcher.runTasks() + + GLFW.glfwTerminate() + GLFW.glfwSetErrorCallback(null)?.free() + + runBlocking { mainJob.await() } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgContext.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgContext.kt new file mode 100644 index 0000000..5548558 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgContext.kt @@ -0,0 +1,45 @@ +package gg.essential.universal.standalone.nanovg + +import org.lwjgl.nanovg.NanoVGGL2 +import org.lwjgl.nanovg.NanoVGGL3 +import org.lwjgl.opengl.GL +import java.io.Closeable + +/** + * A NanoVG context. + * + * Must be created and used on a thread with an active OpenGL context. + * Must be cleaned up via [close], otherwise native memory will be leaked. + */ +class NvgContext : Closeable { + private val gl3 = GL.getCapabilities().OpenGL30 + var ctx: Long + get() { + if (field == 0L) { + throw IllegalStateException("This NanoVG context has already been deleted!") + } + return field + } + private set + + init { + val ctx = if (gl3) { + NanoVGGL3.nvgCreate(NanoVGGL3.NVG_ANTIALIAS) + } else { + NanoVGGL2.nvgCreate(NanoVGGL2.NVG_ANTIALIAS) + } + if (ctx == 0L) { + throw RuntimeException("Failed to create nvg context") + } + this.ctx = ctx + } + + override fun close() { + if (gl3) { + NanoVGGL3.nvgDelete(ctx) + } else { + NanoVGGL2.nvgDelete(ctx) + } + ctx = 0 + } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgFont.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgFont.kt new file mode 100644 index 0000000..a8456d5 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgFont.kt @@ -0,0 +1,147 @@ +package gg.essential.universal.standalone.nanovg + +import gg.essential.universal.UMatrixStack +import gg.essential.universal.UResolution +import org.lwjgl.BufferUtils +import org.lwjgl.nanovg.NVGColor +import org.lwjgl.nanovg.NVGGlyphPosition +import org.lwjgl.nanovg.NanoVG.* +import org.lwjgl.opengl.GL20C +import org.lwjgl.system.MemoryStack +import java.awt.Color + +/** + * Provides methods for rendering a given [NvgFontFace] at a given size. + */ +class NvgFont( + private val fontFace: NvgFontFace, + private val fontSize: Float, + baseLineHeightOverride: Float? = null, + belowLineHeightOverride: Float? = null, +) { + private val ctx: Long + get() = fontFace.ctx.ctx + + private val baseLineHeight: Float + private val belowLineHeight: Float + + init { + nvgFontFaceId(ctx, fontFace.id) + nvgFontSize(ctx, fontSize) + + val buf = BufferUtils.createFloatBuffer(4) + nvgTextBounds(ctx, -1f, 0f, "", buf) + val (_, y1, _, y2) = (0..3).map { buf.get(it) } + baseLineHeight = baseLineHeightOverride ?: -y1 + belowLineHeight = belowLineHeightOverride ?: y2 + } + + @Suppress("UNUSED_PARAMETER") // signature intentionally matches Elementa's FontProvider + fun drawString( + matrixStack: UMatrixStack, + string: String, + color: Color, + x: Float, + y: Float, + originalPointSize: Float, + scale: Float, + shadow: Boolean, + shadowColor: Color? + ) { + val scissorEnabled = GL20C.glIsEnabled(GL20C.GL_SCISSOR_TEST) + + val scaleFactor = UResolution.scaleFactor.toFloat() + nvgBeginFrame( + ctx, + UResolution.viewportWidth.toFloat() / scaleFactor, + UResolution.viewportHeight.toFloat() / scaleFactor, + scaleFactor, + ) + + nvgResetTransform(ctx) + nvgResetScissor(ctx) + + if (scissorEnabled) { + val box = IntArray(4) + GL20C.glGetIntegerv(GL20C.GL_SCISSOR_BOX, box) + var (sx, sy, sw, sh) = box + sy = UResolution.viewportHeight - sy - sh + nvgScissor(ctx, sx / scaleFactor, sy / scaleFactor, sw / scaleFactor, sh / scaleFactor) + } + + with(UMatrixStack.GLOBAL_STACK.peek().model) { + nvgTransform(ctx, m00, m10, m01, m11, m03, m13) + } + with(matrixStack.peek().model) { + nvgTransform(ctx, m00, m10, m01, m11, m03, m13) + } + + nvgFontFaceId(ctx, fontFace.id) + nvgFontSize(ctx, fontSize * scale) + + if (shadow) { + nvgFillColor( + ctx, + (shadowColor ?: Color(color.red shr 2, color.green shr 2, color.blue shr 2, 0xff)).toNVG() + ) + nvgText(ctx, x + 1, y + 1 + baseLineHeight * scale, string) + } + nvgFillColor(ctx, color.toNVG()) + nvgText(ctx, x, y + baseLineHeight * scale, string) + + nvgEndFrame(ctx) + + if (scissorEnabled) { + GL20C.glEnable(GL20C.GL_SCISSOR_TEST) + } + } + + fun getBaseLineHeight(): Float { + return baseLineHeight + } + + fun getBelowLineHeight(): Float { + return belowLineHeight + } + + fun getShadowHeight(): Float { + return 1f + } + + fun getStringWidth(string: String, pointSize: Float): Float { + nvgFontFaceId(ctx, fontFace.id) + nvgFontSize(ctx, fontSize * pointSize / 10) + // Ideally we'd use the following, but nvgTextBounds is broken: https://github.com/memononen/nanovg/issues/636 + // val buf = BufferUtils.createFloatBuffer(4) + // nvgTextBounds(ctx, 0f, 0f, string, buf) + // return buf.get(2) + // so we'll have to work around it by using nvgTextGlyphPositions instead: + var capacity = 256 + MemoryStack.stackPush().use { stack -> + val buf = NVGGlyphPosition.malloc(capacity, stack) + val count = nvgTextGlyphPositions(ctx, 0f, 0f, string, buf) + if (count < capacity) { + if (count == 0) { + return 0f + } + return buf.get(count - 1).maxx() + } + } + while (true) { + capacity *= 2 + val buf = NVGGlyphPosition.malloc(capacity) + try { + val count = nvgTextGlyphPositions(ctx, 0f, 0f, string, buf) + if (count < capacity) { + return buf.get(count - 1).maxx() + } + } finally { + buf.free() + } + } + } + + private fun Color.toNVG() = NVGColor.create().also { + nvgRGBA(red.toByte(), green.toByte(), blue.toByte(), alpha.toByte(), it) + } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgFontFace.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgFontFace.kt new file mode 100644 index 0000000..c7dbecd --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/nanovg/NvgFontFace.kt @@ -0,0 +1,47 @@ +package gg.essential.universal.standalone.nanovg + +import org.lwjgl.nanovg.NanoVG.* +import org.lwjgl.system.MemoryUtil + +/** + * Parses and registers a new font face with the given NanoVG context. + * + * The number of fallback fonts is limited to a fairly small number by NanoVG. + * + * Note that this new font will occupy memory in the context even if this [NvgFontFace] object is garbage collected. + * + * @see NvgFont + */ +class NvgFontFace(val ctx: NvgContext, fontData: ByteArray, vararg fallbacks: NvgFontFace) { + private var fallbackFonts: List = emptyList() + + val id: Int = nvgCreateFontMem( + ctx.ctx, + "", + MemoryUtil.memAlloc(fontData.size).apply { + put(fontData) + rewind() + }, + true, + ) + + init { + setFallbacks(fallbacks.toList()) + } + + fun setFallbacks(fallbacks: List) { + check(fallbacks.size < 20) { "NanoVG supports at most 20 fallback fonts." } + + fallbackFonts = fallbacks.toList() + + nvgResetFallbackFontsId(ctx.ctx, id) + for (fallbackFont in fallbackFonts) { + nvgAddFallbackFontId(ctx.ctx, id, fallbackFont.id) + } + } + + fun addFallback(fallback: NvgFontFace) { + // We must call nvgResetFallbackFontsId to clear the glyph cache, just nvgAddFallbackFontId is not enough + setFallbacks(fallbackFonts + fallback) + } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/render/BufferBuilder.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/BufferBuilder.kt new file mode 100644 index 0000000..3101cde --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/BufferBuilder.kt @@ -0,0 +1,75 @@ +package gg.essential.universal.standalone.render + +import dev.folomeev.kotgl.matrix.vectors.mutables.mutableVec3 +import dev.folomeev.kotgl.matrix.vectors.mutables.mutableVec4 +import gg.essential.universal.UMatrixStack +import gg.essential.universal.standalone.render.VertexFormat.Part +import gg.essential.universal.standalone.utils.timesSelf +import gg.essential.universal.vertex.UVertexConsumer + +internal class BufferBuilder( + val attributes: List, +) : UVertexConsumer { + /** How many floats there are in one vertex */ + val stride: Int = attributes.sumOf { it.size } + + private var idx: Int = 0 + internal var array: FloatArray = FloatArray(stride * 64) + + /** How many vertices there are in this buffer */ + val count: Int + get() = idx / stride + + override fun pos(stack: UMatrixStack, x: Double, y: Double, z: Double) = apply { + val vec = mutableVec4(x.toFloat(), y.toFloat(), z.toFloat(), 1f) + vec.timesSelf(stack.peek().model) + array[idx + 0] = vec.x + array[idx + 1] = vec.y + array[idx + 2] = vec.z + array[idx + 3] = vec.w + idx += 4 + } + + override fun tex(u: Double, v: Double) = apply { + array[idx + 0] = u.toFloat() + array[idx + 1] = v.toFloat() + idx += 2 + } + + override fun norm(stack: UMatrixStack, x: Float, y: Float, z: Float) = apply { + val vec = mutableVec3(x, y, z) + vec.timesSelf(stack.peek().normal) + array[idx + 0] = vec.x + array[idx + 1] = vec.y + array[idx + 2] = vec.z + idx += 3 + } + + override fun color(red: Int, green: Int, blue: Int, alpha: Int): UVertexConsumer = + color(red / 255f, green / 255f, blue / 255f, alpha / 255f) + + override fun color(red: Float, green: Float, blue: Float, alpha: Float): UVertexConsumer = apply { + array[idx + 0] = red + array[idx + 1] = green + array[idx + 2] = blue + array[idx + 3] = alpha + idx += 4 + } + + override fun light(u: Int, v: Int): UVertexConsumer = apply { + array[idx + 0] = u / 240f + array[idx + 1] = v / 240f + idx += 2 + } + + override fun overlay(u: Int, v: Int): UVertexConsumer { + TODO("not yet supported") + } + + override fun endVertex() = apply { + if (idx > array.lastIndex) { + array = array.copyOf(array.size * 2) + } + } +} + diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/render/DefaultShader.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/DefaultShader.kt new file mode 100644 index 0000000..b71542d --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/DefaultShader.kt @@ -0,0 +1,64 @@ +package gg.essential.universal.standalone.render + +import gg.essential.universal.shader.BlendState +import gg.essential.universal.shader.UShader +import gg.essential.universal.standalone.render.VertexFormat.Part + +internal class DefaultShader(val shader: UShader) { + + val uSampler = shader.getSamplerUniformOrNull("uSampler") + + companion object { + private val cache = mutableMapOf, DefaultShader>() + + fun get(attributes: List): DefaultShader { + return cache.getOrPut(attributes) { + val texture = Part.TEXTURE in attributes + val color = Part.COLOR in attributes + val light = Part.LIGHT in attributes + val normal = Part.NORMAL in attributes + if (light || normal) { + TODO("Light and normals not yet supported.") + } + val shader = UShader.fromLegacyShader( + genVertexShaderSource(texture, color), + genFragmentShaderSource(texture, color), + BlendState.NORMAL, + attributes, + ) + DefaultShader(shader) + } + } + + private fun genVertexShaderSource(texture: Boolean, color: Boolean): String { + return """ + ${if (texture) "varying vec2 vTextureCoord;" else ""} + ${if (color) "varying vec4 vColor;" else ""} + + void main() { + gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex; + ${ if (texture) "vTextureCoord = gl_MultiTexCoord0.st;" else ""} + ${ if (color) "vColor = gl_Color;" else ""} + } + """.trimIndent() + } + + private fun genFragmentShaderSource(texture: Boolean, color: Boolean): String { + return """ + ${if (texture) "uniform sampler2D uSampler;" else ""} + + ${if (texture) "varying vec2 vTextureCoord;" else ""} + ${if (color) "varying vec4 vColor;" else ""} + + void main() { + vec4 color = ${if (texture) "texture2D(uSampler, vTextureCoord)" else "vec4(1.0)"}; + ${if (color) "color *= vColor;" else ""} + if (color.a == 0.0) { + discard; + } + gl_FragColor = color; + } + """.trimIndent() + } + } +} \ No newline at end of file diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/render/DefaultVertexFormats.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/DefaultVertexFormats.kt new file mode 100644 index 0000000..bf4f23c --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/DefaultVertexFormats.kt @@ -0,0 +1,20 @@ +package gg.essential.universal.standalone.render + +import gg.essential.universal.standalone.render.VertexFormat.Part +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +enum class DefaultVertexFormats(vararg parts: Part) : VertexFormat { + POSITION(Part.POSITION), + POSITION_COLOR(Part.POSITION, Part.COLOR), + POSITION_TEX(Part.POSITION, Part.TEXTURE), + POSITION_TEX_COLOR(Part.POSITION, Part.TEXTURE, Part.COLOR), + BLOCK(Part.POSITION, Part.COLOR, Part.TEXTURE, Part.LIGHT), + POSITION_TEX_LMAP_COLOR(Part.POSITION, Part.TEXTURE, Part.LIGHT, Part.COLOR), + PARTICLE_POSITION_TEX_COLOR_LMAP(Part.POSITION, Part.TEXTURE, Part.COLOR, Part.LIGHT), + POSITION_TEX_COLOR_NORMAL(Part.POSITION, Part.TEXTURE, Part.COLOR, Part.NORMAL), + ; + + override val parts: List = listOf(*parts) + override val stride: Int = parts.sumOf { it.size } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/render/Gl2Renderer.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/Gl2Renderer.kt new file mode 100644 index 0000000..c9b168e --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/Gl2Renderer.kt @@ -0,0 +1,84 @@ +package gg.essential.universal.standalone.render + +import gg.essential.universal.UGraphics +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL20C +import org.lwjgl.opengl.GL30C + +internal class Gl2Renderer { + private val vao = if (GL.getCapabilities().OpenGL30) GL30C.glGenVertexArrays() else 0 + private val vertexBuffer = GL20C.glGenBuffers() + private val indexBuffer = GL20C.glGenBuffers() + + fun draw(bufferBuilder: BufferBuilder, drawMode: UGraphics.DrawMode, shader: DefaultShader?) { + if (GL.getCapabilities().OpenGL30) { + GL30C.glBindVertexArray(vao) + } + + for (i in bufferBuilder.attributes.indices) { + GL20C.glEnableVertexAttribArray(i) + } + + shader?.uSampler?.setValue(GL20C.glGetInteger(GL20C.GL_TEXTURE_BINDING_2D)) + + shader?.shader?.bind() + + GL20C.glBindBuffer(GL20C.GL_ARRAY_BUFFER, vertexBuffer) + GL20C.glBufferData(GL20C.GL_ARRAY_BUFFER, bufferBuilder.array, GL20C.GL_STATIC_DRAW) + + when (drawMode) { + UGraphics.DrawMode.QUADS -> { + GL20C.glBindBuffer(GL20C.GL_ELEMENT_ARRAY_BUFFER, indexBuffer) + GL20C.glBufferData(GL20C.GL_ELEMENT_ARRAY_BUFFER, IndexBufferBuilder.forQuads(bufferBuilder.count / 4), GL20C.GL_STATIC_DRAW) + renderInBatches(bufferBuilder, 4, 6) + } + UGraphics.DrawMode.TRIANGLES -> { + setupVertexAttribPointers(bufferBuilder, 0) + GL20C.glDrawArrays(GL20C.GL_TRIANGLES, 0, bufferBuilder.count) + } + UGraphics.DrawMode.TRIANGLE_FAN -> { + GL20C.glBindBuffer(GL20C.GL_ELEMENT_ARRAY_BUFFER, indexBuffer) + GL20C.glBufferData(GL20C.GL_ELEMENT_ARRAY_BUFFER, IndexBufferBuilder.forTriangleFan(bufferBuilder.count - 2), GL20C.GL_STATIC_DRAW) + if (bufferBuilder.count > UShort.MAX_VALUE.toInt()) { + TODO("Render triangle fans with more than ${UShort.MAX_VALUE} vertices.") + } + setupVertexAttribPointers(bufferBuilder, 0) + GL20C.glDrawElements(GL20C.GL_TRIANGLES, (bufferBuilder.count - 2) * 3, GL20C.GL_UNSIGNED_SHORT, 0) + } + else -> TODO("Support rendering $drawMode") + } + + shader?.shader?.unbind() + + for (i in bufferBuilder.attributes.indices) { + GL20C.glDisableVertexAttribArray(i) + } + } + + private fun renderInBatches(bufferBuilder: BufferBuilder, vertsPerPrimitive: Int, indicesPerPrimitive: Int) { + val vertices = bufferBuilder.count.toLong() + // Limited by the fact that our index buffer is using Short; aligned at at shape boundaries + val maxBatchSize = UShort.MAX_VALUE.toLong() / vertsPerPrimitive * vertsPerPrimitive + for (offset in 0 until vertices step maxBatchSize) { + val batchSize = (vertices - offset).coerceAtMost(maxBatchSize).toInt() + + setupVertexAttribPointers(bufferBuilder, offset) + GL20C.glDrawElements(GL20C.GL_TRIANGLES, batchSize / vertsPerPrimitive * indicesPerPrimitive, GL20C.GL_UNSIGNED_SHORT, 0) + } + } + + private fun setupVertexAttribPointers(bufferBuilder: BufferBuilder, vertexOffset: Long) { + var attrOffset = 0 + for ((index, attribute) in bufferBuilder.attributes.withIndex()) { + GL20C.glVertexAttribPointer( + index, + attribute.size, + GL20C.GL_FLOAT, + false, + bufferBuilder.stride * 4 /* bytes per float */, + (attrOffset + vertexOffset * bufferBuilder.stride) * 4 /* bytes per float */ + ) + attrOffset += attribute.size + } + } +} \ No newline at end of file diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/render/IndexBufferBuilder.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/IndexBufferBuilder.kt new file mode 100644 index 0000000..3d4e95c --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/IndexBufferBuilder.kt @@ -0,0 +1,46 @@ +package gg.essential.universal.standalone.render + +internal object IndexBufferBuilder { + private var quadsBuffer: ShortArray = ShortArray(0) + private var triangleFanBuffer: ShortArray = ShortArray(0) + + /** Returns an index buffer for drawing [count] quads as triangles with glDrawElements. */ + fun forQuads(count: Int): ShortArray { + val bufSize = count * 6 + if (quadsBuffer.size < bufSize) { + val buf = ShortArray(bufSize) + var index = 0 + for (i in 0 until bufSize step 6) { + // First triangle + buf[i + 0] = (index + 0).toShort() + buf[i + 1] = (index + 1).toShort() + buf[i + 2] = (index + 2).toShort() + // Second triangle + buf[i + 3] = (index + 0).toShort() + buf[i + 4] = (index + 2).toShort() + buf[i + 5] = (index + 3).toShort() + // Increment buffer index (4 vertices per quad) + index += 4 + } + quadsBuffer = buf + } + return quadsBuffer + } + + /** Returns an index buffer for drawing [count] triangles arranged in a fan as individual triangles with glDrawElements. */ + fun forTriangleFan(count: Int): ShortArray { + val bufSize = count * 3 + if (triangleFanBuffer.size < bufSize) { + val buf = ShortArray(bufSize) + var index = 1 + for (i in 0 until bufSize step 3) { + buf[i + 0] = 0 + buf[i + 1] = (index + 0).toShort() + buf[i + 2] = (index + 1).toShort() + index++ + } + triangleFanBuffer = buf + } + return triangleFanBuffer + } +} \ No newline at end of file diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/render/VertexFormat.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/VertexFormat.kt new file mode 100644 index 0000000..5d702e8 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/VertexFormat.kt @@ -0,0 +1,18 @@ +package gg.essential.universal.standalone.render + +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +interface VertexFormat { + val parts: List + val stride: Int + + @ApiStatus.Internal + enum class Part(val size: Int) { + POSITION(4), + TEXTURE(2), + COLOR(4), + LIGHT(2), + NORMAL(3), + } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/render/misc.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/misc.kt new file mode 100644 index 0000000..1ad75b5 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/render/misc.kt @@ -0,0 +1,17 @@ +package gg.essential.universal.standalone.render + +import dev.folomeev.kotgl.matrix.matrices.Mat4 +import dev.folomeev.kotgl.matrix.matrices.mutables.orthogonal +import dev.folomeev.kotgl.matrix.matrices.mutables.transposeSelf +import gg.essential.universal.UResolution + +internal val DEBUG_GL = System.getProperty("universalcraft.standalone.debug.gl", "false").toBooleanStrict() + +internal fun createOrthoProjectionMatrix(): Mat4 { + val scaleFactor = UResolution.scaleFactor.toFloat() + return orthogonal( + 0f, UResolution.viewportWidth / scaleFactor, + UResolution.viewportHeight / scaleFactor, 0f, + 0f, 10000f + ).transposeSelf() // kotgl places translation in the last column, we use the last row +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/runUniversalcraft.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/runUniversalcraft.kt new file mode 100644 index 0000000..3bd3b02 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/runUniversalcraft.kt @@ -0,0 +1,66 @@ +package gg.essential.universal.standalone + +import gg.essential.universal.UGraphics +import gg.essential.universal.standalone.glfw.Glfw +import gg.essential.universal.standalone.glfw.GlfwWindow +import gg.essential.universal.standalone.glfw.runGlfw +import gg.essential.universal.standalone.render.DEBUG_GL +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import org.lwjgl.glfw.GLFW +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL43C +import org.lwjgl.opengl.GLUtil +import org.lwjgl.system.MemoryUtil +import java.io.PrintStream + +/** + * Initializes the standalone UniversalCraft framework and runs the given [main] function on the main dispatcher. + * Once the [main] function returns, any remaining child jobs launched in its [CoroutineScope] are cancelled and the + * framework is shut down once they have all completed. + * + * Takes control of the main thread and does not return until the coroutine completes and the framework has shut down. + * + * Must be called from the main thread! + */ +fun runUniversalCraft( + title: String, + width: Int, + height: Int, + resizable: Boolean = true, + main: suspend CoroutineScope.(window: UCWindow) -> Unit, +) = runGlfw { + GlfwWindow(title, width, height, resizable).use { glfwWindow -> + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) + withContext(Dispatchers.Main) { + GLFW.glfwMakeContextCurrent(glfwWindow.glfwId) + val caps = GL.createCapabilities() + try { + if (DEBUG_GL) { + GLUtil.setupDebugMessageCallback(object : PrintStream(System.err) { + override fun print(obj: Any?) { + super.print(obj) + Throwable().printStackTrace() + } + }) + if (caps.OpenGL43) { + GL43C.glEnable(GL43C.GL_DEBUG_OUTPUT_SYNCHRONOUS) + } + } + + UGraphics.init() + + coroutineScope mainScope@{ + val ucWindow = withContext(Dispatchers.Glfw) { UCWindow(glfwWindow, this@mainScope) } + main(ucWindow) + coroutineContext.cancelChildren() + } + } finally { + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) + } + } + } +} diff --git a/standalone/src/main/kotlin/gg/essential/universal/standalone/utils/kotgl.kt b/standalone/src/main/kotlin/gg/essential/universal/standalone/utils/kotgl.kt new file mode 100644 index 0000000..7228ee7 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/standalone/utils/kotgl.kt @@ -0,0 +1,93 @@ +package gg.essential.universal.standalone.utils + +import dev.folomeev.kotgl.matrix.matrices.Mat3 +import dev.folomeev.kotgl.matrix.matrices.Mat4 +import dev.folomeev.kotgl.matrix.matrices.mat3 +import dev.folomeev.kotgl.matrix.matrices.mat4 +import dev.folomeev.kotgl.matrix.vectors.Vec3 +import dev.folomeev.kotgl.matrix.vectors.Vec4 +import dev.folomeev.kotgl.matrix.vectors.mutables.MutableVec3 +import dev.folomeev.kotgl.matrix.vectors.mutables.MutableVec4 +import dev.folomeev.kotgl.matrix.vectors.mutables.set +import dev.folomeev.kotgl.matrix.vectors.vec3 +import dev.folomeev.kotgl.matrix.vectors.vec4 + +inline fun Vec3.times(mat: Mat3, out: (Float, Float, Float) -> T) = + out( + x * mat.m00 + y * mat.m01 + z * mat.m02, + x * mat.m10 + y * mat.m11 + z * mat.m12, + x * mat.m20 + y * mat.m21 + z * mat.m22, + ) + +inline fun Vec4.times(mat: Mat4, out: (Float, Float, Float, Float) -> T) = + out( + x * mat.m00 + y * mat.m01 + z * mat.m02 + w * mat.m03, + x * mat.m10 + y * mat.m11 + z * mat.m12 + w * mat.m13, + x * mat.m20 + y * mat.m21 + z * mat.m22 + w * mat.m23, + x * mat.m30 + y * mat.m31 + z * mat.m32 + w * mat.m33, + ) + +/** + * Computes the matrix-vector product `mat * this`. + */ +fun Vec3.times(mat: Mat3) = times(mat, ::vec3) + +/** + * Computes the matrix-vector product `mat * this`. + */ +fun Vec4.times(mat: Mat4) = times(mat, ::vec4) + +/** + * Computes the matrix-vector product `mat * this`, storing the result in `this`. + */ +fun MutableVec3.timesSelf(mat: Mat3) = times(mat, ::set) + +/** + * Computes the matrix-vector product `mat * this`, storing the result in `this`. + */ +fun MutableVec4.timesSelf(mat: Mat4) = times(mat, ::set) + +fun Mat4.toMat3() = mat3(m00, m01, m02, m10, m11, m12, m20, m21, m22) +fun Mat3.toMat4() = mat4(m00, m01, m02, 0f, m10, m11, m12, 0f, m20, m21, m22, 0f, 0f, 0f, 0f, 1f) + +fun FloatArray.toMat4() = mat4 { row, col -> this[row * 4 + col] } + +fun Mat4.toRowMajor() = + floatArrayOf( + m00, + m01, + m02, + m03, + m10, + m11, + m12, + m13, + m20, + m21, + m22, + m23, + m30, + m31, + m32, + m33, + ) + +fun Mat4.toColumnMajor() = + floatArrayOf( + m00, + m10, + m20, + m30, + m01, + m11, + m21, + m31, + m02, + m12, + m22, + m32, + m03, + m13, + m23, + m33, + ) diff --git a/standalone/src/main/kotlin/gg/essential/universal/utils/typealises.kt b/standalone/src/main/kotlin/gg/essential/universal/utils/typealises.kt new file mode 100644 index 0000000..ed1094b --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/utils/typealises.kt @@ -0,0 +1,3 @@ +package gg.essential.universal.utils + +// Not applicable diff --git a/standalone/src/main/kotlin/gg/essential/universal/vertex/VanillaVertexConsumer.kt b/standalone/src/main/kotlin/gg/essential/universal/vertex/VanillaVertexConsumer.kt new file mode 100644 index 0000000..27740e6 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/vertex/VanillaVertexConsumer.kt @@ -0,0 +1,3 @@ +package gg.essential.universal.vertex + +// Not applicable diff --git a/standalone/src/main/kotlin/gg/essential/universal/wrappers/UPlayer.kt b/standalone/src/main/kotlin/gg/essential/universal/wrappers/UPlayer.kt new file mode 100644 index 0000000..01b6116 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/wrappers/UPlayer.kt @@ -0,0 +1,3 @@ +package gg.essential.universal.wrappers + +// Not applicable diff --git a/standalone/src/main/kotlin/gg/essential/universal/wrappers/message/UMessage.kt b/standalone/src/main/kotlin/gg/essential/universal/wrappers/message/UMessage.kt new file mode 100644 index 0000000..87af708 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/wrappers/message/UMessage.kt @@ -0,0 +1,3 @@ +package gg.essential.universal.wrappers.message + +// Not applicable diff --git a/standalone/src/main/kotlin/gg/essential/universal/wrappers/message/UTextComponent.kt b/standalone/src/main/kotlin/gg/essential/universal/wrappers/message/UTextComponent.kt new file mode 100644 index 0000000..87af708 --- /dev/null +++ b/standalone/src/main/kotlin/gg/essential/universal/wrappers/message/UTextComponent.kt @@ -0,0 +1,3 @@ +package gg.essential.universal.wrappers.message + +// Not applicable diff --git a/standalone/src/main/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory b/standalone/src/main/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory new file mode 100644 index 0000000..5c42200 --- /dev/null +++ b/standalone/src/main/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory @@ -0,0 +1 @@ +gg.essential.universal.standalone.UCDispatcherFactory diff --git a/standalone/src/main/resources/fonts/Minecraft-Regular.otf b/standalone/src/main/resources/fonts/Minecraft-Regular.otf new file mode 100644 index 0000000..54f08ad Binary files /dev/null and b/standalone/src/main/resources/fonts/Minecraft-Regular.otf differ diff --git a/versions/1.17.1-fabric/src/main/kotlin/gg/essential/universal/shader/MCShader.kt b/versions/1.17.1-fabric/src/main/kotlin/gg/essential/universal/shader/MCShader.kt index 4a7c3b5..8cf3288 100644 --- a/versions/1.17.1-fabric/src/main/kotlin/gg/essential/universal/shader/MCShader.kt +++ b/versions/1.17.1-fabric/src/main/kotlin/gg/essential/universal/shader/MCShader.kt @@ -59,7 +59,7 @@ internal class MCShader( private val DEBUG_LEGACY = System.getProperty("universalcraft.shader.legacy.debug", "") == "true" fun fromLegacyShader(vertSource: String, fragSource: String, blendState: BlendState, vertexFormat: CommonVertexFormats?): MCShader { - val transformer = ShaderTransformer(vertexFormat) + val transformer = ShaderTransformer(vertexFormat, 150) val transformedVertSource = transformer.transform(vertSource) val transformedFragSource = transformer.transform(fragSource) @@ -169,114 +169,3 @@ internal class MCSamplerUniform(val mc: Shader, val name: String) : SamplerUnifo mc.addSampler(name, textureId) } } - -internal class ShaderTransformer(private val vertexFormat: CommonVertexFormats?) { - val attributes = mutableListOf() - val samplers = mutableSetOf() - val uniforms = mutableMapOf() - - fun transform(originalSource: String): String { - var source = originalSource - - source = source.replace("gl_ModelViewProjectionMatrix", "gl_ProjectionMatrix * gl_ModelViewMatrix") - source = source.replace("texture2D", "texture") - - val replacements = mutableMapOf() - val transformed = mutableListOf() - transformed.add("#version 150") - - val frag = "gl_FragColor" in source - val vert = !frag - - if (frag) { - transformed.add("out vec4 uc_FragColor;") - replacements["gl_FragColor"] = "uc_FragColor" - } - - if (vert && "gl_FrontColor" in source) { - transformed.add("out vec4 uc_FrontColor;") - replacements["gl_FrontColor"] = "uc_FrontColor" - } - if (frag && "gl_Color" in source) { - transformed.add("in vec4 uc_FrontColor;") - replacements["gl_Color"] = "uc_FrontColor" - } - - fun replaceAttribute(newAttributes: MutableList>, needle: String, type: String, replacementName: String = "uc_" + needle.substringAfter("_"), replacement: String = replacementName) { - if (needle in source) { - replacements[needle] = replacement - newAttributes.add(replacementName to "in $type $replacementName;") - } - } - if (vert) { - val newAttributes = mutableListOf>() - replaceAttribute(newAttributes, "gl_Vertex", "vec3", "uc_Position", replacement = "vec4(uc_Position, 1.0)") - replaceAttribute(newAttributes, "gl_Color", "vec4") - replaceAttribute(newAttributes, "gl_MultiTexCoord0.st", "vec2", "uc_UV0") - replaceAttribute(newAttributes, "gl_MultiTexCoord1.st", "vec2", "uc_UV1") - replaceAttribute(newAttributes, "gl_MultiTexCoord2.st", "vec2", "uc_UV2") - - if (vertexFormat != null) { - newAttributes.sortedBy { vertexFormat.mc.shaderAttributes.indexOf(it.first.removePrefix("uc_")) } - .forEach { - attributes.add(it.first) - transformed.add(it.second) - } - } else { - newAttributes.forEach { - attributes.add(it.first) - transformed.add(it.second) - } - } - } - - fun replaceUniform(needle: String, type: UniformType, replacementName: String, replacement: String = replacementName) { - if (needle in source) { - replacements[needle] = replacement - if (replacementName !in uniforms) { - uniforms[replacementName] = type - transformed.add("uniform ${type.glslName} $replacementName;") - } - } - } - replaceUniform("gl_ModelViewMatrix", UniformType.Mat4, "ModelViewMat") - replaceUniform("gl_ProjectionMatrix", UniformType.Mat4, "ProjMat") - - - for (line in source.lines()) { - transformed.add(when { - line.startsWith("#version") -> continue - line.startsWith("varying ") -> (if (frag) "in " else "out ") + line.substringAfter("varying ") - line.startsWith("uniform ") -> { - val (_, glslType, name) = line.trimEnd(';').split(" ") - if (glslType == "sampler2D") { - samplers.add(name) - } else { - uniforms[name] = UniformType.fromGlsl(glslType) - } - line - } - else -> replacements.entries.fold(line) { acc, (needle, replacement) -> acc.replace(needle, replacement) } - }) - } - - return transformed.joinToString("\n") - } -} - -internal enum class UniformType(val typeName: String, val glslName: String, val default: IntArray) { - Int1("int", "int", intArrayOf(0)), - Float1("float", "float", intArrayOf(0)), - Float2("float", "vec2", intArrayOf(0, 0)), - Float3("float", "vec3", intArrayOf(0, 0, 0)), - Float4("float", "vec4", intArrayOf(0, 0, 0, 0)), - Mat2("matrix2x2", "mat2", intArrayOf(1, 0, 0, 1)), - Mat3("matrix3x3", "mat3", intArrayOf(1, 0, 0, 0, 1, 0, 0, 0, 1)), - Mat4("matrix4x4", "mat4", intArrayOf(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)), - ; - - companion object { - fun fromGlsl(glslName: String): UniformType = - values().find { it.glslName == glslName } ?: throw NoSuchElementException(glslName) - } -}