From abf67dd663f7e93e160405a585ce5f7f81499851 Mon Sep 17 00:00:00 2001 From: lily Date: Fri, 10 Jul 2026 17:36:51 -0400 Subject: [PATCH] Fixes and Text --- build.gradle | 18 ++++++ gradle.properties | 2 +- .../kotlin/xyz/meowing/lattice/Lattice.kt | 7 +- .../xyz/meowing/lattice/command/Commodore.kt | 14 +++- .../command/functions/FunctionInvoker.kt | 38 ++++++++++- .../lattice/command/nodes/Executable.kt | 27 +++++++- .../lattice/command/nodes/LiteralNode.kt | 28 ++++++-- .../xyz/meowing/lattice/render/GLState.kt | 6 ++ .../xyz/meowing/lattice/render/NVGRenderer.kt | 64 ++++++++++++++----- .../kotlin/xyz/meowing/lattice/ui/UIScreen.kt | 4 +- 10 files changed, 171 insertions(+), 37 deletions(-) diff --git a/build.gradle b/build.gradle index 9137174..f593012 100644 --- a/build.gradle +++ b/build.gradle @@ -24,9 +24,13 @@ dependencies { implementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" implementation "com.mojang:brigadier:1.2.9" + // NanoVG is not part of Minecraft's LWJGL modules, so bundle it (and its + // natives) into the mod jar via include so it is present at runtime. implementation "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}" + include "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}" ['linux', 'linux-arm64', 'windows', 'macos', 'macos-arm64'].each { platform -> runtimeOnly "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}:natives-${platform}" + include "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}:natives-${platform}" } } @@ -63,3 +67,17 @@ publishing { mavenLocal() } } + +// Copies the built lattice and example mod jars into a Minecraft mods folder. +// Override the target with -PmodsDir=/path/to/mods (defaults to the Prism instance). +def defaultModsDir = "${System.getProperty('user.home')}/.local/share/PrismLauncher/instances/Lattice testing/minecraft/mods" + +tasks.register('deployMods', Copy) { + group = 'lattice' + description = 'Copies the lattice and example mod jars into a Minecraft mods folder.' + dependsOn tasks.named('jar'), project(':example').tasks.named('jar') + + from tasks.named('jar').flatMap { it.archiveFile } + from project(':example').tasks.named('jar').flatMap { it.archiveFile } + into providers.gradleProperty('modsDir').orElse(defaultModsDir) +} diff --git a/gradle.properties b/gradle.properties index 83c034e..4e1e06c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx4G +org.gradle.jvmargs=-Xmx10G org.gradle.parallel=true org.gradle.configuration-cache=false loom.ignoreDependencyLoomVersionValidation=true diff --git a/src/main/kotlin/xyz/meowing/lattice/Lattice.kt b/src/main/kotlin/xyz/meowing/lattice/Lattice.kt index c8266c0..32134ab 100644 --- a/src/main/kotlin/xyz/meowing/lattice/Lattice.kt +++ b/src/main/kotlin/xyz/meowing/lattice/Lattice.kt @@ -1,7 +1,5 @@ package xyz.meowing.lattice -import net.minecraft.resources.Identifier -import xyz.meowing.lattice.client.Client.minecraft import xyz.meowing.lattice.event.EventBus import xyz.meowing.lattice.render.Font import xyz.meowing.lattice.render.NVGRenderer @@ -14,10 +12,7 @@ object Lattice { val eventBus = EventBus() @JvmStatic - val defaultFont: Font by lazy { - val id = Identifier.fromNamespaceAndPath(MOD_ID, "font.ttf") - Font("Default", minecraft.resourceManager.getResourceStack(id).first().open()) - } + val defaultFont: Font by lazy { Font("Default", "/assets/$MOD_ID/font.ttf") } private var _renderer: Renderer? = null diff --git a/src/main/kotlin/xyz/meowing/lattice/command/Commodore.kt b/src/main/kotlin/xyz/meowing/lattice/command/Commodore.kt index e96d19c..ccd31c0 100644 --- a/src/main/kotlin/xyz/meowing/lattice/command/Commodore.kt +++ b/src/main/kotlin/xyz/meowing/lattice/command/Commodore.kt @@ -19,7 +19,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal * The tree must always end in an [Executable][xyz.meowing.lattice.command.nodes.Executable], * which serves as an exit point for a command. */ -open class Commodore(private val root: LiteralNode) { +open class Commodore(@PublishedApi internal val root: LiteralNode) { constructor( vararg name: String, @@ -35,10 +35,18 @@ open class Commodore(private val root: LiteralNode) { /** * DSL access to the root node for object-style definitions. */ - fun runs(function: Function) = root.runs(function) - fun runs(block: () -> Unit) = root.runs(block) + inline fun runs(noinline block: (P1) -> Unit) = root.runs(block) + + inline fun runs(noinline block: (P1, P2) -> Unit) = root.runs(block) + + inline fun runs(noinline block: (P1, P2, P3) -> Unit) = root.runs(block) + + inline fun runs(noinline block: (P1, P2, P3, P4) -> Unit) = root.runs(block) + + inline fun runs(noinline block: (P1, P2, P3, P4, P5) -> Unit) = root.runs(block) + fun literal(string: String, block: LiteralNode.() -> Unit = {}): LiteralNode { return root.literal(string, block) } diff --git a/src/main/kotlin/xyz/meowing/lattice/command/functions/FunctionInvoker.kt b/src/main/kotlin/xyz/meowing/lattice/command/functions/FunctionInvoker.kt index 33f7401..7c3d380 100644 --- a/src/main/kotlin/xyz/meowing/lattice/command/functions/FunctionInvoker.kt +++ b/src/main/kotlin/xyz/meowing/lattice/command/functions/FunctionInvoker.kt @@ -2,6 +2,7 @@ package xyz.meowing.lattice.command.functions import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles +import java.lang.reflect.ParameterizedType /** * # FunctionInvoker @@ -59,7 +60,21 @@ sealed interface FunctionInvoker { mHandle = MethodHandles.lookup().unreflect(invokeMethod).bindTo(lambda) if (parameterTypes.isEmpty()) { - parameters = invokeMethod.parameterTypes.mapIndexed { index, type -> + val rawTypes = invokeMethod.parameterTypes + // Kotlin K2/IR may compile lambdas so the only non-bridge invoke method + // has erased Object types. Recover concrete types from the FunctionN + // generic interface that the lambda class still declares. + val actualTypes = if (rawTypes.isNotEmpty() && rawTypes.all { it == Any::class.java }) { + recoverFromFunctionInterface(lambdaClass, rawTypes.size) + ?: throw IllegalStateException( + "Cannot recover parameter types for lambda ${lambdaClass.name}. " + + "Interfaces: ${lambdaClass.genericInterfaces.toList()}. " + + "Use a typed runs { p: Type -> } overload instead of Function." + ) + } else { + rawTypes + } + parameters = actualTypes.mapIndexed { index, type -> Parameter("param$index", type, false) } } else { @@ -76,6 +91,27 @@ sealed interface FunctionInvoker { override fun invoke(arguments: List): T { return mHandle.invokeWithArguments(arguments) as T } + + private companion object { + fun recoverFromFunctionInterface(lambdaClass: Class<*>, paramCount: Int): Array>? { + for (iface in lambdaClass.genericInterfaces) { + val pt = iface as? ParameterizedType ?: continue + val raw = pt.rawType as? Class<*> ?: continue + if (!raw.name.startsWith("kotlin.jvm.functions.Function")) continue + val args = pt.actualTypeArguments + // FunctionN — last arg is return type + if (args.size != paramCount + 1) continue + return Array(paramCount) { i -> + when (val arg = args[i]) { + is Class<*> -> arg + is ParameterizedType -> arg.rawType as? Class<*> ?: Any::class.java + else -> Any::class.java + } + } + } + return null + } + } } companion object { diff --git a/src/main/kotlin/xyz/meowing/lattice/command/nodes/Executable.kt b/src/main/kotlin/xyz/meowing/lattice/command/nodes/Executable.kt index 4708a05..6c115ee 100644 --- a/src/main/kotlin/xyz/meowing/lattice/command/nodes/Executable.kt +++ b/src/main/kotlin/xyz/meowing/lattice/command/nodes/Executable.kt @@ -72,8 +72,31 @@ class Executable : Node() { * - Use a [ParameterModifier] to apply a custom parser to a certain parameter. * You need to ensure you're parsing for the correct class. */ - fun runs(function: Function) { - funInvoker = FunctionInvoker.from(function) + fun runs(block: () -> Unit) = define(block) + + inline fun runs(noinline block: (P1) -> Unit) = + define(block, P1::class.java) + + inline fun runs(noinline block: (P1, P2) -> Unit) = + define(block, P1::class.java, P2::class.java) + + inline fun runs(noinline block: (P1, P2, P3) -> Unit) = + define(block, P1::class.java, P2::class.java, P3::class.java) + + inline fun runs(noinline block: (P1, P2, P3, P4) -> Unit) = + define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java) + + inline fun runs(noinline block: (P1, P2, P3, P4, P5) -> Unit) = + define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java, P5::class.java) + + /** + * Low-level entry point that builds the invoker from a function and its + * explicit parameter types. Prefer the typed [runs] overloads, which capture + * the types via reified generics instead of relying on runtime reflection. + */ + @PublishedApi + internal fun define(function: Function, vararg explicitTypes: Class<*>) { + funInvoker = FunctionInvoker.from(function, *explicitTypes) for (parameter in funInvoker.parameters) { val modifier = parameterModifiers[parameter.name] diff --git a/src/main/kotlin/xyz/meowing/lattice/command/nodes/LiteralNode.kt b/src/main/kotlin/xyz/meowing/lattice/command/nodes/LiteralNode.kt index 40bd20d..6da3946 100644 --- a/src/main/kotlin/xyz/meowing/lattice/command/nodes/LiteralNode.kt +++ b/src/main/kotlin/xyz/meowing/lattice/command/nodes/LiteralNode.kt @@ -94,15 +94,29 @@ open class LiteralNode(val name: String) : Node() { } /** - * Creates an [Executable] and apply function directly. + * Creates an [Executable] that runs [block] with typed command arguments. * - * This won't allow you to apply [modifiers][Executable.ParameterModifier] - * to the function's parameters. - * - * @see runs + * Each parameter type is captured via reified generics, so it works with + * Kotlin's invokedynamic lambdas where the types are erased at runtime. */ - fun runs(function: Function) = executable { - runs(function) + inline fun runs(noinline block: (P1) -> Unit) = executable { + define(block, P1::class.java) + } + + inline fun runs(noinline block: (P1, P2) -> Unit) = executable { + define(block, P1::class.java, P2::class.java) + } + + inline fun runs(noinline block: (P1, P2, P3) -> Unit) = executable { + define(block, P1::class.java, P2::class.java, P3::class.java) + } + + inline fun runs(noinline block: (P1, P2, P3, P4) -> Unit) = executable { + define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java) + } + + inline fun runs(noinline block: (P1, P2, P3, P4, P5) -> Unit) = executable { + define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java, P5::class.java) } /** diff --git a/src/main/kotlin/xyz/meowing/lattice/render/GLState.kt b/src/main/kotlin/xyz/meowing/lattice/render/GLState.kt index 45e7a7b..89efa07 100644 --- a/src/main/kotlin/xyz/meowing/lattice/render/GLState.kt +++ b/src/main/kotlin/xyz/meowing/lattice/render/GLState.kt @@ -11,6 +11,12 @@ object GLState { @JvmStatic var previousProgram = -1 + @JvmStatic + var previousSampler = -1 + + @JvmStatic + var previousVao = -1 + @JvmStatic var drawing = false } diff --git a/src/main/kotlin/xyz/meowing/lattice/render/NVGRenderer.kt b/src/main/kotlin/xyz/meowing/lattice/render/NVGRenderer.kt index 48ae032..3fdb9f7 100644 --- a/src/main/kotlin/xyz/meowing/lattice/render/NVGRenderer.kt +++ b/src/main/kotlin/xyz/meowing/lattice/render/NVGRenderer.kt @@ -2,6 +2,7 @@ package xyz.meowing.lattice.render import com.mojang.blaze3d.opengl.GlStateManager import com.mojang.blaze3d.systems.RenderSystem +import org.apache.logging.log4j.LogManager import org.lwjgl.nanovg.NVGColor import org.lwjgl.nanovg.NVGPaint import org.lwjgl.nanovg.NanoSVG @@ -11,6 +12,7 @@ import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL13 import org.lwjgl.opengl.GL20 import org.lwjgl.opengl.GL30 +import org.lwjgl.opengl.GL33C import org.lwjgl.stb.STBImage import org.lwjgl.system.MemoryUtil import xyz.meowing.lattice.client.Client.minecraft @@ -18,7 +20,6 @@ import xyz.meowing.lattice.render.Color.Companion.alpha import xyz.meowing.lattice.render.Color.Companion.blue import xyz.meowing.lattice.render.Color.Companion.green import xyz.meowing.lattice.render.Color.Companion.red -import java.lang.reflect.Field import java.lang.reflect.Method import java.nio.ByteBuffer import kotlin.math.max @@ -48,23 +49,30 @@ object NVGRenderer : Renderer { // Resolved reflectively once: GlTexture.getFbo(GlStateManager$DirectStateAccess, depth) is not public API. private var fboLookup: Pair? = null private var fboLookupFailed = false + private var loggedFbo = false - private fun mainFramebuffer(colorTex: Any): Int { + private fun fieldValue(obj: Any, name: String): Any? { + var clazz: Class<*>? = obj.javaClass + while (clazz != null) { + try { + return clazz.getDeclaredField(name).apply { isAccessible = true }.get(obj) + } catch (_: NoSuchFieldException) { + clazz = clazz.superclass + } + } + return null + } + + private fun mainFramebuffer(colorTex: Any, depthTex: Any?): Int { if (fboLookupFailed) return 0 try { val lookup = fboLookup ?: run { val device = RenderSystem.getDevice() ?: return 0 - var dsaField: Field? = null - var clazz: Class<*>? = device.javaClass - while (clazz != null && dsaField == null) { - try { - dsaField = clazz.getDeclaredField("directStateAccess") - } catch (_: NoSuchFieldException) {} - clazz = clazz.superclass - } - dsaField?.isAccessible = true - val dsa = dsaField?.get(device) ?: throw IllegalStateException("no directStateAccess") + // GpuDevice wraps the GL backend (GlDevice), which owns directStateAccess. + val backend = fieldValue(device, "backend") ?: device + val dsa = fieldValue(backend, "directStateAccess") + ?: throw IllegalStateException("no directStateAccess") var getFbo: Method? = null var texClazz: Class<*>? = colorTex.javaClass @@ -78,9 +86,12 @@ object NVGRenderer : Renderer { Pair(dsa, getFbo).also { fboLookup = it } } - return (lookup.second.invoke(colorTex, lookup.first, null) as? Int) ?: 0 - } catch (_: Exception) { + // Pass the depth texture so getFbo returns the FBO with the depth+stencil + // attachment; NanoVG's stencil fills render nothing on a color-only FBO. + return (lookup.second.invoke(colorTex, lookup.first, depthTex) as? Int) ?: 0 + } catch (e: Throwable) { fboLookupFailed = true + LogManager.getLogger("Lattice").warn("FBO lookup failed: device=${RenderSystem.getDevice()?.javaClass?.name} color=${colorTex.javaClass.name} depth=${depthTex?.javaClass?.name}", e) return 0 } } @@ -95,10 +106,23 @@ object NVGRenderer : Renderer { val colorTex = renderTarget.getColorTexture() ?: return if (vg == -1L) return - GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, mainFramebuffer(colorTex)) + val fbo = mainFramebuffer(colorTex, renderTarget.getDepthTexture()) + if (!loggedFbo) { + loggedFbo = true + LogManager.getLogger("Lattice").info("NVG beginFrame: fbo=$fbo failed=$fboLookupFailed size=${width}x$height target=${renderTarget.width}x${renderTarget.height}") + } + GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo) GlStateManager._viewport(0, 0, renderTarget.width, renderTarget.height) GlStateManager._activeTexture(GL30.GL_TEXTURE0) + // NanoVG binds its own VAO without restoring the previous one. + GLState.previousVao = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING) + + // Unbind MC's sampler from unit 0 so NanoVG's font atlas texture samples + // with its own parameters; otherwise glyphs render blank while shapes are fine. + GLState.previousSampler = GL11.glGetInteger(GL33C.GL_SAMPLER_BINDING) + GL33C.glBindSampler(0, 0) + NanoVG.nvgBeginFrame(vg, width, height, 1f) NanoVG.nvgTextAlign(vg, NanoVG.NVG_ALIGN_LEFT or NanoVG.NVG_ALIGN_TOP) GLState.drawing = true @@ -114,13 +138,23 @@ object NVGRenderer : Renderer { GlStateManager._enableBlend() GlStateManager._blendFuncSeparate(770, 771, 1, 0) + // NanoVG's stencil strokes can leave the stencil test on and the color mask off. + GL11.glDisable(GL11.GL_STENCIL_TEST) + GL11.glColorMask(true, true, true, true) + if (GLState.previousProgram != -1) GlStateManager._glUseProgram(GLState.previousProgram) + // Restore the sampler on unit 0 that we unbound for the font atlas. + GlStateManager._activeTexture(GL30.GL_TEXTURE0) + if (GLState.previousSampler != -1) GL33C.glBindSampler(0, GLState.previousSampler) + if (GLState.previousActiveTexture != -1) { GlStateManager._activeTexture(GLState.previousActiveTexture) if (GLState.previousBoundTexture != -1) GlStateManager._bindTexture(GLState.previousBoundTexture) } + if (GLState.previousVao != -1) GL30.glBindVertexArray(GLState.previousVao) + GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0) GLState.drawing = false } diff --git a/src/main/kotlin/xyz/meowing/lattice/ui/UIScreen.kt b/src/main/kotlin/xyz/meowing/lattice/ui/UIScreen.kt index 1444471..0710b5e 100644 --- a/src/main/kotlin/xyz/meowing/lattice/ui/UIScreen.kt +++ b/src/main/kotlin/xyz/meowing/lattice/ui/UIScreen.kt @@ -95,10 +95,10 @@ abstract class UIScreen(screenName: String = "Lattice-Screen") : Screen(Componen super.onClose() } - /** Opens this screen on the next client tick, safe to call from any thread. */ + /** Opens this screen on the render thread, safe to call from any thread. */ fun display() { TimeScheduler.schedule(50) { - minecraft.setScreen(this@UIScreen) + minecraft.execute { minecraft.setScreen(this@UIScreen) } } } }