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 |
+
+
+ |
+
1.21 | fabric | |
1.20.6 | fabric | |
1.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)
- }
-}