Fixes and Text

This commit is contained in:
lily 2026-07-10 17:36:51 -04:00
parent e6ca5fce3b
commit abf67dd663
10 changed files with 171 additions and 37 deletions

View file

@ -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)
}

View file

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx4G
org.gradle.jvmargs=-Xmx10G
org.gradle.parallel=true
org.gradle.configuration-cache=false
loom.ignoreDependencyLoomVersionValidation=true

View file

@ -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

View file

@ -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<Unit>) = root.runs(function)
fun runs(block: () -> Unit) = root.runs(block)
inline fun <reified P1> runs(noinline block: (P1) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2> runs(noinline block: (P1, P2) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2, reified P3> runs(noinline block: (P1, P2, P3) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2, reified P3, reified P4> runs(noinline block: (P1, P2, P3, P4) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5> 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)
}

View file

@ -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<T> {
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<Unit>."
)
} else {
rawTypes
}
parameters = actualTypes.mapIndexed { index, type ->
Parameter("param$index", type, false)
}
} else {
@ -76,6 +91,27 @@ sealed interface FunctionInvoker<T> {
override fun invoke(arguments: List<Any?>): T {
return mHandle.invokeWithArguments(arguments) as T
}
private companion object {
fun recoverFromFunctionInterface(lambdaClass: Class<*>, paramCount: Int): Array<Class<*>>? {
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<P1,...,Pn,R> — 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 {

View file

@ -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<Unit>) {
funInvoker = FunctionInvoker.from(function)
fun runs(block: () -> Unit) = define(block)
inline fun <reified P1> runs(noinline block: (P1) -> Unit) =
define(block, P1::class.java)
inline fun <reified P1, reified P2> runs(noinline block: (P1, P2) -> Unit) =
define(block, P1::class.java, P2::class.java)
inline fun <reified P1, reified P2, reified P3> runs(noinline block: (P1, P2, P3) -> Unit) =
define(block, P1::class.java, P2::class.java, P3::class.java)
inline fun <reified P1, reified P2, reified P3, reified P4> runs(noinline block: (P1, P2, P3, P4) -> Unit) =
define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java)
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5> 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<Unit>, vararg explicitTypes: Class<*>) {
funInvoker = FunctionInvoker.from(function, *explicitTypes)
for (parameter in funInvoker.parameters) {
val modifier = parameterModifiers[parameter.name]

View file

@ -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<Unit>) = executable {
runs(function)
inline fun <reified P1> runs(noinline block: (P1) -> Unit) = executable {
define(block, P1::class.java)
}
inline fun <reified P1, reified P2> runs(noinline block: (P1, P2) -> Unit) = executable {
define(block, P1::class.java, P2::class.java)
}
inline fun <reified P1, reified P2, reified P3> runs(noinline block: (P1, P2, P3) -> Unit) = executable {
define(block, P1::class.java, P2::class.java, P3::class.java)
}
inline fun <reified P1, reified P2, reified P3, reified P4> 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 <reified P1, reified P2, reified P3, reified P4, reified P5> 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)
}
/**

View file

@ -11,6 +11,12 @@ object GLState {
@JvmStatic
var previousProgram = -1
@JvmStatic
var previousSampler = -1
@JvmStatic
var previousVao = -1
@JvmStatic
var drawing = false
}

View file

@ -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<Any, Method>? = 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
}

View file

@ -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) }
}
}
}