From c70fc93316905c2a22a1146dfcc0bed2e4975355 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:44:44 +0200 Subject: [PATCH 01/12] feat: agent/fabric asm migration --- .gitignore | 1 + build-logic/build.gradle.kts | 23 +++ build-logic/settings.gradle.kts | 1 + .../tomgirl/lenis/bakery/patch/AL10Patch.java | 16 ++ .../tomgirl/lenis/bakery/patch/ALPatch.java | 44 +++-- .../bakery/patch/ARBShaderObjectsPatch.java | 16 ++ .../lenis/bakery/patch}/CompatStub.java | 2 +- .../tomgirl/lenis/bakery/patch/GL11Patch.java | 52 ++++++ .../tomgirl/lenis/bakery/patch/GL20Patch.java | 29 ++++ .../pl/tomgirl/lenis/build/BakeLwjglTask.kt | 153 +++++++++++++++++ .../tomgirl/lenis/build/LenisBuildPlugin.kt | 161 ++++++++++++++++++ build.gradle.kts | 82 +-------- gradle/libs.versions.toml | 16 +- runs/fabric/build.gradle.kts | 32 ++++ runs/logging.properties | 6 + runs/vanilla/build.gradle.kts | 45 +++++ settings.gradle.kts | 5 +- .../pl/tomgirl/lenis/fabric/MixinPlugin.java | 58 +++++++ .../lenis/fabric/MixinTransformer.java | 29 ++++ .../fabric/MixinTransformerDelegate.java | 73 ++++++++ .../pl/tomgirl/lenis/fabric}/PreLaunch.java | 4 +- src/main/java/org/lwjgl/LWJGLUtil.java | 6 +- src/main/java/pl/tomgirl/lenis/Agent.java | 39 +++++ src/main/java/pl/tomgirl/lenis/Lenis.java | 7 +- src/main/java/pl/tomgirl/lenis/Platform.java | 2 +- .../java/pl/tomgirl/lenis/game/GameHooks.java | 52 ++++++ .../tomgirl/lenis/game/GameTransformer.java | 40 +++++ .../pl/tomgirl/lenis/game/patch/Patch.java | 21 +++ .../lenis/game/patch/impl/MinecraftPatch.java | 123 +++++++++++++ .../lenis/game/patch/impl/ScreenPatch.java | 71 ++++++++ .../pl/tomgirl/lenis/mixin/AL10Mixin.java | 22 --- .../lenis/mixin/ARBShaderObjectsMixin.java | 22 --- .../pl/tomgirl/lenis/mixin/GL11Mixin.java | 57 ------- .../pl/tomgirl/lenis/mixin/GL20Mixin.java | 34 ---- .../lenis/mixin/GLCapabilitiesMixin.java | 36 ---- .../lenis/mixin/LibraryLWJGLOpenALMixin.java | 41 ----- .../lenis/mixin/game/MixinMinecraft_Icon.java | 48 ------ .../mixin/game/MixinMinecraft_Resize.java | 23 --- .../mixin/game/MixinMinecraft_TextInput.java | 17 -- .../mixin/game/MixinScreen_Clipboard.java | 26 --- .../mixin/game/MixinTextFieldWidget_Ime.java | 44 ----- .../plugin/CompatibilityStubBuilder.java | 65 ------- .../plugin/PaulscodeClasspathPlugin.java | 52 ------ .../pl/tomgirl/lenis/window/DisplaySdl.java | 16 +- .../pl/tomgirl/lenis/window/KeyboardSdl.java | 2 +- .../pl/tomgirl/lenis/window/MouseSdl.java | 4 + src/main/resources/fabric.mod.json | 3 +- src/main/resources/lenis.mixins.json | 25 +-- 48 files changed, 1097 insertions(+), 649 deletions(-) create mode 100644 build-logic/build.gradle.kts create mode 100644 build-logic/settings.gradle.kts create mode 100644 build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/AL10Patch.java rename src/main/java/pl/tomgirl/lenis/mixin/ALMixin.java => build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ALPatch.java (79%) create mode 100644 build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ARBShaderObjectsPatch.java rename {src/main/java/pl/tomgirl/lenis/plugin => build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch}/CompatStub.java (88%) create mode 100644 build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java create mode 100644 build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL20Patch.java create mode 100644 build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt create mode 100644 build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt create mode 100644 runs/fabric/build.gradle.kts create mode 100644 runs/logging.properties create mode 100644 runs/vanilla/build.gradle.kts create mode 100644 src/fabric/java/pl/tomgirl/lenis/fabric/MixinPlugin.java create mode 100644 src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformer.java create mode 100644 src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformerDelegate.java rename src/{main/java/pl/tomgirl/lenis => fabric/java/pl/tomgirl/lenis/fabric}/PreLaunch.java (87%) create mode 100644 src/main/java/pl/tomgirl/lenis/Agent.java create mode 100644 src/main/java/pl/tomgirl/lenis/game/GameHooks.java create mode 100644 src/main/java/pl/tomgirl/lenis/game/GameTransformer.java create mode 100644 src/main/java/pl/tomgirl/lenis/game/patch/Patch.java create mode 100644 src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java create mode 100644 src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/AL10Mixin.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/ARBShaderObjectsMixin.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/GL11Mixin.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/GL20Mixin.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/GLCapabilitiesMixin.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/LibraryLWJGLOpenALMixin.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Icon.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Resize.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_TextInput.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/game/MixinScreen_Clipboard.java delete mode 100644 src/main/java/pl/tomgirl/lenis/mixin/game/MixinTextFieldWidget_Ime.java delete mode 100644 src/main/java/pl/tomgirl/lenis/plugin/CompatibilityStubBuilder.java delete mode 100644 src/main/java/pl/tomgirl/lenis/plugin/PaulscodeClasspathPlugin.java diff --git a/.gitignore b/.gitignore index 0ecd304..4b7e0ec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .DS_Store .idea/ build/ +!**/tomgirl/lenis/build run/ \ No newline at end of file diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..e9b8a9d --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation("com.gradleup.shadow:shadow-gradle-plugin:9.3.0") + implementation("org.ow2.asm:asm:9.10.1") + implementation("org.ow2.asm:asm-commons:9.10.1") +} + +gradlePlugin { + plugins { + create("lenisBuild") { + id = "lenis.build" + implementationClass = "pl.tomgirl.lenis.build.LenisBuildPlugin" + } + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..c6aa055 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "lenis-build-logic" diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/AL10Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/AL10Patch.java new file mode 100644 index 0000000..51258d7 --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/AL10Patch.java @@ -0,0 +1,16 @@ +package pl.tomgirl.lenis.bakery.patch; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +@SuppressWarnings("unused") +public abstract class AL10Patch { + @CompatStub("alListenerfv") + private static void alListener(int paramName, FloatBuffer values) {} + + @CompatStub("alSourcefv") + private static void alSource(int source, int param, FloatBuffer values) {} + + @CompatStub("alSourceStopv") + private static void alSourceStop(IntBuffer sources) {} +} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/ALMixin.java b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ALPatch.java similarity index 79% rename from src/main/java/pl/tomgirl/lenis/mixin/ALMixin.java rename to build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ALPatch.java index a81e9c6..03d8e3c 100644 --- a/src/main/java/pl/tomgirl/lenis/mixin/ALMixin.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ALPatch.java @@ -1,6 +1,4 @@ -package pl.tomgirl.lenis.mixin; - -import pl.tomgirl.lenis.plugin.CompatStub; +package pl.tomgirl.lenis.bakery.patch; import java.nio.IntBuffer; @@ -10,29 +8,26 @@ import org.lwjgl.openal.ALC10; import org.lwjgl.openal.ALCCapabilities; import org.lwjgl.system.MemoryStack; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; import static org.lwjgl.openal.AL.createCapabilities; @SuppressWarnings("unused") -@Mixin(AL.class) -public abstract class ALMixin { - @Unique private static long _contextPtr; - @Unique private static long _devicePtr; - @Unique private static boolean _created; +public abstract class ALPatch { + private static long _contextPtr; + private static long _devicePtr; + private static boolean _created; - @Unique @CompatStub + @CompatStub private static boolean isCreated() { return _created; } - @Unique @CompatStub + @CompatStub private static void create(String deviceArguments, int contextFrequency, int contextRefresh, boolean contextSynchronized) throws LWJGLException { create(deviceArguments, contextFrequency, contextRefresh, contextSynchronized, true); } - @Unique @CompatStub + @CompatStub private static void create(String deviceArguments, int contextFrequency, int contextRefresh, boolean contextSynchronized, boolean openDevice) throws LWJGLException { if (_created) { throw new IllegalStateException("Only one OpenAL context may be instantiated"); @@ -42,7 +37,6 @@ private static void create(String deviceArguments, int contextFrequency, int con } } - @Unique private static void init(String deviceArguments, int contextFrequency, int contextRefresh, boolean contextSynchronized, boolean openDevice) throws LWJGLException { try { if (!openDevice) { @@ -50,7 +44,7 @@ private static void init(String deviceArguments, int contextFrequency, int conte } _devicePtr = ALC10.alcOpenDevice(deviceArguments); - if (_devicePtr == -1L) { + if (_devicePtr == 0L) { throw new LWJGLException("Could not open ALC device"); } @@ -63,20 +57,21 @@ private static void init(String deviceArguments, int contextFrequency, int conte } } - ALC10.alcMakeContextCurrent(_contextPtr); + if (_contextPtr == 0L || !ALC10.alcMakeContextCurrent(_contextPtr)) { + throw new LWJGLException("Could not create or make current an OpenAL context"); + } createCapabilities(deviceCaps); - } catch (LWJGLException ex) { + } catch (LWJGLException | RuntimeException | Error ex) { exit(); throw ex; } } - @Unique @CompatStub + @CompatStub private static void create() throws LWJGLException { create(null, 44100, 60, false); } - @Unique private static IntBuffer createAttributeList(int contextFrequency, int contextRefresh, int contextSynchronized, MemoryStack stack) { IntBuffer buffer = stack.callocInt(7); buffer.put(0, 4103); @@ -89,19 +84,20 @@ private static IntBuffer createAttributeList(int contextFrequency, int contextRe return buffer; } - @Unique @CompatStub + @CompatStub private static void exit() { - if (_contextPtr != -1L) { + if (_contextPtr != 0L) { ALC10.alcMakeContextCurrent(0L); ALC10.alcDestroyContext(_contextPtr); - _contextPtr = -1L; + _contextPtr = 0L; } - if (_devicePtr != -1L) { + if (_devicePtr != 0L) { ALC10.alcCloseDevice(_devicePtr); - _devicePtr = -1L; + _devicePtr = 0L; } + AL.setCurrentProcess(null); _created = false; } } diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ARBShaderObjectsPatch.java b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ARBShaderObjectsPatch.java new file mode 100644 index 0000000..eb58f9a --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ARBShaderObjectsPatch.java @@ -0,0 +1,16 @@ +package pl.tomgirl.lenis.bakery.patch; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +@SuppressWarnings("unused") +public abstract class ARBShaderObjectsPatch { + @CompatStub("glGetObjectParameterivARB") + private static void glGetObjectParameterARB(int obj, int pname, IntBuffer params) {} + + @CompatStub("glUniformMatrix4fvARB") + private static void glUniformMatrix4ARB(int location, boolean transpose, FloatBuffer value) {} + + @CompatStub("glUniform1ivARB") + private static void glUniform1ARB(int location, IntBuffer value) {} +} diff --git a/src/main/java/pl/tomgirl/lenis/plugin/CompatStub.java b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/CompatStub.java similarity index 88% rename from src/main/java/pl/tomgirl/lenis/plugin/CompatStub.java rename to build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/CompatStub.java index 13164c6..e63b3ed 100644 --- a/src/main/java/pl/tomgirl/lenis/plugin/CompatStub.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/CompatStub.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.plugin; +package pl.tomgirl.lenis.bakery.patch; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java new file mode 100644 index 0000000..eb64813 --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java @@ -0,0 +1,52 @@ +package pl.tomgirl.lenis.bakery.patch; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.system.MemoryUtil; + +@SuppressWarnings("unused") +public abstract class GL11Patch { + @CompatStub + private static void glDrawElements(int mode, int count, int type, ByteBuffer indices) { + GL11.nglDrawElements(mode, count, type, MemoryUtil.memAddress(indices)); + } + + @CompatStub + private static void glTexCoordPointer(int i, int stride, FloatBuffer pointer) { + GL11.glTexCoordPointer(i, 0x1406, stride, pointer); + } + + @CompatStub + private static void glTexCoordPointer(int i, int stride, ShortBuffer pointer) { + GL11.glTexCoordPointer(i, 0x1402, stride, pointer); + } + + @CompatStub + private static void glColorPointer(int i, boolean bl, int i2, ByteBuffer pointer) { + GL11.glColorPointer(i, 0x1401, i2, pointer); + } + + @CompatStub + private static void glVertexPointer(int i, int i2, FloatBuffer pointer) { + GL11.glVertexPointer(i, 0x1406, i2, pointer); + } + + @CompatStub + private static void glNormalPointer(int stride, ByteBuffer pointer) { + GL11.glNormalPointer(0x1400, stride, pointer); + } + + @CompatStub("glGetFloatv") private static void glGetFloat(int pname, FloatBuffer params) {} + @CompatStub("glGetIntegerv") private static void glGetInteger(int pname, IntBuffer params) {} + @CompatStub("glFogfv") private static void glFog(int pname, FloatBuffer params) {} + @CompatStub("glLightfv") private static void glLight(int light, int pname, FloatBuffer params) {} + @CompatStub("glLightModelfv") private static void glLightModel(int pname, FloatBuffer params) {} + @CompatStub("glMultMatrixf") private static void glMultMatrix(FloatBuffer matrix) {} + @CompatStub("glTexEnvfv") private static void glTexEnv(int target, int pname, FloatBuffer params) {} + @CompatStub("glTexGenfv") private static void glTexGen(int coord, int pname, FloatBuffer params) {} + @CompatStub("glLoadMatrixf") private static void glLoadMatrix(FloatBuffer matrix) {} +} diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL20Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL20Patch.java new file mode 100644 index 0000000..aa4662f --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL20Patch.java @@ -0,0 +1,29 @@ +package pl.tomgirl.lenis.bakery.patch; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import org.lwjgl.opengl.GL20; + +@SuppressWarnings("unused") +public abstract class GL20Patch { + @CompatStub + private static void glShaderSource(int shader, java.nio.ByteBuffer string) { + byte[] data = new byte[string.limit()]; + string.position(0); + string.get(data); + string.position(0); + GL20.glShaderSource(shader, new String(data)); + } + + @CompatStub("glUniformMatrix4fv") private static void glUniformMatrix4(int location, boolean transpose, FloatBuffer value) {} + @CompatStub("glUniform2fv") private static void glUniform2(int location, FloatBuffer value) {} + @CompatStub("glUniform1iv") private static void glUniform1(int location, IntBuffer value) {} + @CompatStub("glUniform1fv") private static void glUniform1(int location, FloatBuffer value) {} + @CompatStub("glUniform2iv") private static void glUniform2i(int location, IntBuffer value) {} + @CompatStub("glUniform3iv") private static void glUniform3(int location, IntBuffer value) {} + @CompatStub("glUniform3fv") private static void glUniform3(int location, FloatBuffer value) {} + @CompatStub("glUniform4iv") private static void glUniform4(int location, IntBuffer value) {} + @CompatStub("glUniform4fv") private static void glUniform4(int location, FloatBuffer value) {} + @CompatStub("glUniformMatrix2fv") private static void glUniformMatrix2(int location, boolean transpose, FloatBuffer value) {} + @CompatStub("glUniformMatrix3fv") private static void glUniformMatrix3(int location, boolean transpose, FloatBuffer value) {} +} diff --git a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt new file mode 100644 index 0000000..374b430 --- /dev/null +++ b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt @@ -0,0 +1,153 @@ +package pl.tomgirl.lenis.build + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.* +import org.objectweb.asm.* +import org.objectweb.asm.commons.ClassRemapper +import org.objectweb.asm.commons.SimpleRemapper +import org.objectweb.asm.tree.* +import java.nio.file.Files +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream + +abstract class BakeLwjglTask : DefaultTask() { + @get:InputFile @get:Classpath + abstract val inputJar: RegularFileProperty + + @get:Classpath + abstract val patchClasses: ConfigurableFileCollection + + @get:OutputFile + abstract val outputJar: RegularFileProperty + + @TaskAction + fun bake() { + val output = outputJar.get().asFile.toPath() + val patched = mutableSetOf() + Files.createDirectories(output.parent) + JarFile(inputJar.get().asFile).use { input -> + JarOutputStream(Files.newOutputStream(output)).use { jar -> + input.stream().sorted(compareBy(JarEntry::getName)).forEach { entry -> + var bytes = input.getInputStream(entry).use { it.readAllBytes() } + val name = entry.name.removeSuffix(".class") + if (name in patches || name == CAPABILITIES) { + val target = ClassNode().also { ClassReader(bytes).accept(it, 0) } + patches[name]?.let { merge(target, it) } ?: extendCapabilities(target) + bytes = ClassWriter(ClassWriter.COMPUTE_MAXS).also(target::accept).toByteArray() + patched += name + } + jar.putNextEntry(JarEntry(entry.name).apply { time = 0 }) + jar.write(bytes) + jar.closeEntry() + } + } + } + val missing = patches.keys + CAPABILITIES - patched + if (missing.isNotEmpty()) { + Files.delete(output) + error("Missing LWJGL patch targets: $missing") + } + logger.lifecycle("Baked LWJGL compatibility into $output") + } + + private fun merge(target: ClassNode, template: String) { + val sourceName = "$TEMPLATES$template" + val templateFile = patchClasses.files.asSequence() + .map { it.toPath().resolve("$sourceName.class") } + .firstOrNull(Files::isRegularFile) + ?: error("Missing template: $template") + val source = ClassNode() + Files.newInputStream(templateFile).use { + ClassReader(it).accept( + ClassRemapper(source, SimpleRemapper(Opcodes.ASM9, mapOf(sourceName to target.name))), 0, + ) + } + source.fields.forEach { field -> + check(target.fields.none { it.name == field.name }) { "Field collision: ${target.name}.${field.name}" } + target.fields.add(field) + } + source.methods.filterNot { it.name == "" }.forEach { method -> + check(method.name != "") { "Template static initializers are not supported: $template" } + check(target.find(method.name, method.desc) == null) { + "Method collision: ${target.name}.${method.name}${method.desc}" + } + method.invisibleAnnotations?.filter { it.desc == STUB }?.forEach { annotation -> + check(method.access and Opcodes.ACC_STATIC != 0) { "CompatStub must be static: ${method.name}" } + method.access = method.access and (Opcodes.ACC_PRIVATE or Opcodes.ACC_PROTECTED).inv() or Opcodes.ACC_PUBLIC + annotation.values?.chunked(2)?.firstOrNull { it[0] == "value" } + ?.let { delegate(target, method, it[1] as String) } + } + method.invisibleAnnotations?.removeIf { it.desc == STUB } + target.methods.add(method) + } + if (target.name == "org/lwjgl/openal/AL") { + val destroy = target.find("destroy", "()V") ?: error("Missing AL.destroy") + destroy.access = destroy.access and (Opcodes.ACC_PRIVATE or Opcodes.ACC_PROTECTED).inv() or Opcodes.ACC_PUBLIC + destroy.instructions.toArray().filter { it.opcode == Opcodes.RETURN }.forEach { + destroy.instructions.insertBefore( + it, MethodInsnNode(Opcodes.INVOKESTATIC, target.name, "exit", "()V", false), + ) + } + } + } + + private fun delegate(owner: ClassNode, stub: MethodNode, name: String) { + if (name.isEmpty()) return + val target = owner.find(name, stub.desc) + check(target != null && target.access and Opcodes.ACC_STATIC != 0) { + "Missing static delegate: ${owner.name}.$name${stub.desc}" + } + stub.instructions.clear() + stub.tryCatchBlocks.clear() + stub.localVariables?.clear() + var local = 0 + Type.getArgumentTypes(stub.desc).forEach { + stub.instructions.add(VarInsnNode(it.getOpcode(Opcodes.ILOAD), local)) + local += it.size + } + stub.instructions.add(MethodInsnNode(Opcodes.INVOKESTATIC, owner.name, name, stub.desc, false)) + stub.instructions.add(InsnNode(Type.getReturnType(stub.desc).getOpcode(Opcodes.IRETURN))) + } + + private fun extendCapabilities(target: ClassNode) { + val constructor = target.find( + "", "(Lorg/lwjgl/system/FunctionProvider;Ljava/util/Set;ZLjava/util/function/IntFunction;)V", + ) ?: error("Unexpected GLCapabilities constructor") + capabilityExtensions.forEach { extension -> + check(target.fields.none { it.name == extension }) { "Capability field collision: $extension" } + target.fields.add(FieldNode(Opcodes.ACC_PUBLIC or Opcodes.ACC_FINAL, extension, "Z", null, null)) + constructor.instructions.toArray().filter { it.opcode == Opcodes.RETURN }.forEach { + constructor.instructions.insertBefore(it, InsnList().apply { + add(VarInsnNode(Opcodes.ALOAD, 0)) + add(VarInsnNode(Opcodes.ALOAD, 2)) + add(LdcInsnNode(extension)) + add(MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/Set", "contains", "(Ljava/lang/Object;)Z", true)) + add(FieldInsnNode(Opcodes.PUTFIELD, target.name, extension, "Z")) + }) + } + } + } + + private fun ClassNode.find(name: String, descriptor: String) = + methods.find { it.name == name && it.desc == descriptor } + + private companion object { + const val TEMPLATES = "pl/tomgirl/lenis/bakery/patch/" + const val STUB = "L${TEMPLATES}CompatStub;" + const val CAPABILITIES = "org/lwjgl/opengl/GLCapabilities" + val patches = mapOf( + "org/lwjgl/openal/AL" to "ALPatch", + "org/lwjgl/openal/AL10" to "AL10Patch", + "org/lwjgl/opengl/GL11" to "GL11Patch", + "org/lwjgl/opengl/GL20" to "GL20Patch", + "org/lwjgl/opengl/ARBShaderObjects" to "ARBShaderObjectsPatch", + ) + val capabilityExtensions = listOf( + "GL_EXT_multi_draw_arrays", "GL_EXT_paletted_texture", "GL_EXT_rescale_normal", + "GL_EXT_texture_3d", "GL_EXT_texture_lod_bias", "GL_EXT_vertex_shader", "GL_EXT_vertex_weighting", + ) + } +} diff --git a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt new file mode 100644 index 0000000..fedf88c --- /dev/null +++ b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt @@ -0,0 +1,161 @@ +package pl.tomgirl.lenis.build + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.plugins.BasePluginExtension +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.* +import org.gradle.language.jvm.tasks.ProcessResources + +class LenisBuildPlugin : Plugin { + override fun apply(project: Project) = with(project) { + pluginManager.apply("java-library") + pluginManager.apply("maven-publish") + pluginManager.apply("com.gradleup.shadow") + + repositories { + mavenCentral() + maven("https://maven.fabricmc.net/") + } + + val libs = extensions.getByType().named("libs") + val lwjglVersion = libs.findVersion("lwjgl").get().requiredVersion + val sourceSets = extensions.getByType() + val main = sourceSets.named("main") + val fabric = sourceSets.create("fabric") { + compileClasspath += main.get().output + runtimeClasspath += output + compileClasspath + } + configurations.named(fabric.implementationConfigurationName) { + extendsFrom(configurations.getByName("implementation")) + } + configurations.named(fabric.compileOnlyConfigurationName) { + extendsFrom(configurations.getByName("compileOnly")) + } + configurations.named(fabric.runtimeOnlyConfigurationName) { + extendsFrom(configurations.getByName("runtimeOnly")) + } + val bakedPatches = sourceSets.create("bakedPatches") { + java.srcDir(rootProject.layout.projectDirectory.dir("build-logic/src/baked-patches/java")) + compileClasspath += main.get().output + configurations.getByName("compileClasspath") + runtimeClasspath += output + compileClasspath + } + + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + withSourcesJar() + } + + group = providers.gradleProperty("maven_group").get() + val base = extensions.getByType() + base.archivesName.set(providers.gradleProperty("base_name")) + + dependencies { + add("implementation", libs.findLibrary("lwjgl").get()) + add("implementation", libs.findLibrary("lwjgl-openal").get()) + add("implementation", libs.findLibrary("lwjgl-opengl").get()) + add("implementation", libs.findLibrary("lwjgl-sdl").get()) + add("implementation", libs.findLibrary("asm").get()) + add("compileOnly", "org.jetbrains:annotations:26.0.2") + add(fabric.compileOnlyConfigurationName, libs.findLibrary("fabric-loader").get()) + add(fabric.compileOnlyConfigurationName, libs.findLibrary("fabric-mixin").get()) + + for (module in listOf("lwjgl", "lwjgl-openal", "lwjgl-opengl", "lwjgl-sdl")) { + for (platform in listOf("windows", "linux", "macos", "macos-arm64")) { + add("runtimeOnly", "org.lwjgl:$module:$lwjglVersion:natives-$platform") + } + } + } + + tasks.named("processResources") { + inputs.property("version", version) + filesMatching("fabric.mod.json") { + expand("version" to project.version) + } + } + + tasks.named("jar") { + dependsOn(fabric.classesTaskName) + from(fabric.output) + archiveClassifier.set("slim") + destinationDirectory.set(layout.buildDirectory.dir("intermediates")) + manifest.attributes["Premain-Class"] = "pl.tomgirl.lenis.Agent" + } + + val shadowJar = tasks.named("shadowJar") { + archiveClassifier.set("relocated") + destinationDirectory.set(layout.buildDirectory.dir("intermediates")) + relocate("org.objectweb.asm", "pl.tomgirl.lenis.internal.asm") + exclude("module-info.class", "META-INF/versions/**/module-info.class") + exclude("META-INF/*.SF", "META-INF/*.RSA", "META-INF/*.DSA") + mergeServiceFiles() + } + + val unpatchedJar = tasks.register("unpatchedJar") { + dependsOn(shadowJar, fabric.classesTaskName) + from(shadowJar.flatMap { it.archiveFile }.map { zipTree(it.asFile) }) { + exclude("META-INF/MANIFEST.MF") + } + from(fabric.output) + archiveClassifier.set("unpatched") + destinationDirectory.set(layout.buildDirectory.dir("intermediates")) + manifest.attributes( + "Premain-Class" to "pl.tomgirl.lenis.Agent", + "Multi-Release" to "true", + ) + } + + val distributionFile = layout.buildDirectory.file( + providers.zip(base.archivesName, provider { version.toString() }) { name, release -> + "libs/$name-$release.jar" + }, + ) + val bakeLwjgl = tasks.register("bakeLwjgl") { + group = "build" + description = "Bakes the legacy LWJGL compatibility patches into the distribution jar." + dependsOn(unpatchedJar, bakedPatches.classesTaskName) + inputJar.set(unpatchedJar.flatMap { it.archiveFile }) + patchClasses.from(bakedPatches.output.classesDirs) + outputJar.set(distributionFile) + } + + val distribution = configurations.create("distribution") { + isCanBeConsumed = true + isCanBeResolved = false + } + artifacts.add(distribution.name, distributionFile) { builtBy(bakeLwjgl) } + tasks.named("assemble") { dependsOn(bakeLwjgl) } + tasks.named("sourcesJar") { + from(fabric.allSource) + } + + extensions.configure { + publications { + create("mavenJava") { + artifactId = providers.gradleProperty("base_name").get() + artifact(distributionFile) { builtBy(bakeLwjgl) } + artifact(tasks.named("sourcesJar")) + } + } + + providers.environmentVariable("MAVEN_URL").orNull?.let { repositoryUrl -> + repositories.maven { + name = "release" + url = uri(repositoryUrl) + credentials { + username = providers.environmentVariable("MAVEN_USER").orNull + password = providers.environmentVariable("MAVEN_PASS").orNull + } + } + } + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 1602435..73d8390 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,83 +1,3 @@ plugins { - id("java") - id("maven-publish") - alias(libs.plugins.loom) - alias(libs.plugins.ploceus) -} - -loom {} -ploceus.setIntermediaryGeneration(2) - -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - withSourcesJar() -} - -group = providers.gradleProperty("maven_group").get() - -base { - archivesName = providers.gradleProperty("base_name").get() -} - -dependencies { - minecraft(libs.minecraft) - modImplementation(libs.loader) - mappings(ploceus.featherMappings(libs.versions.feather.build.get())) - - bundled(libs.lwjgl.asProvider()) - bundled(libs.lwjgl.openal) - bundled(libs.lwjgl.opengl) - bundled(libs.lwjgl.sdl) - - lwjglDesktopNatives("lwjgl") - lwjglDesktopNatives("lwjgl-openal") - lwjglDesktopNatives("lwjgl-opengl") - lwjglDesktopNatives("lwjgl-sdl") -} - -tasks.processResources { - filesMatching("fabric.mod.json") { - expand(Pair("version", project.version)) - } -} - -publishing { - publications { - create("mavenJava") { - artifactId = providers.gradleProperty("base_name").get() - from(components["java"]) - } - } - - repositories { - providers.environmentVariable("MAVEN_URL").orNull?.let { repositoryUrl -> - maven { - name = "release" - url = uri(repositoryUrl) - credentials { - username = providers.environmentVariable("MAVEN_USER").orNull - password = providers.environmentVariable("MAVEN_PASS").orNull - } - } - } - } -} - -fun DependencyHandlerScope.bundled(dependency: Provider) { - add("api", dependency) - add("include", dependency) -} - -fun DependencyHandlerScope.lwjglNative(module: String, classifier: String) { - val notation = "org.lwjgl:$module:${libs.versions.lwjgl.get()}:$classifier" - add("runtimeOnly", notation) - add("include", notation) -} - -fun DependencyHandlerScope.lwjglDesktopNatives(module: String) { - lwjglNative(module, "natives-windows") - lwjglNative(module, "natives-linux") - lwjglNative(module, "natives-macos") - lwjglNative(module, "natives-macos-arm64") + id("lenis.build") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 052c3fc..049e06a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,19 @@ [versions] -minecraft = "1.8.9" -feather_build = "1" -loader = "0.19.3" +fabric-loader = "0.19.3" +fabric-mixin = "0.17.3+mixin.0.8.7" loom = "1.17.19" -ploceus = "1.17.7" lwjgl = "3.4.3" +asm = "9.10.1" [libraries] -minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } -loader = { module = "net.fabricmc:fabric-loader", version.ref = "loader" } +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabric-mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "fabric-mixin" } lwjgl = { module = "org.lwjgl:lwjgl", version.ref = "lwjgl" } lwjgl-opengl = { module = "org.lwjgl:lwjgl-opengl", version.ref = "lwjgl" } lwjgl-openal = { module = "org.lwjgl:lwjgl-openal", version.ref = "lwjgl" } lwjgl-sdl = { module = "org.lwjgl:lwjgl-sdl", version.ref = "lwjgl" } +asm = { module = "org.ow2.asm:asm", version.ref = "asm" } +asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } [plugins] -ploceus = { id = "ploceus", version.ref = "ploceus" } -loom = { id = "net.fabricmc.fabric-loom-remap", version.ref = "loom" } +loom = { id = "net.fabricmc.fabric-loom", version.ref = "loom" } diff --git a/runs/fabric/build.gradle.kts b/runs/fabric/build.gradle.kts new file mode 100644 index 0000000..1e5ed6d --- /dev/null +++ b/runs/fabric/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(libs.plugins.loom) +} + +loom { + clientOnlyMinecraftJar() + runs { + remove(getByName("server")) + named("client") { + displayName = "Fabric 1.8.9" + generateRunConfig = true + preferGradleTask = true + programArguments.addAll("--username", "Player", "--accessToken", "0") + jvmArguments.add("-Djava.util.logging.config.file=${rootProject.file("runs/logging.properties")}") + jvmArguments.add("--enable-native-access=ALL-UNNAMED") + if (System.getProperty("os.name").startsWith("Mac")) jvmArguments.add("-XstartOnFirstThread") + } + } +} + +dependencies { + minecraft("com.mojang:minecraft:1.8.9") + implementation(libs.fabric.loader) + runtimeOnly("org.apache.logging.log4j:log4j-api:2.26.0") + runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0") + runtimeOnly(project(path = ":", configuration = "distribution")) +} + +configurations.configureEach { + exclude(group = "org.lwjgl.lwjgl") + exclude(group = "org.lwjgl") +} diff --git a/runs/logging.properties b/runs/logging.properties new file mode 100644 index 0000000..2a46713 --- /dev/null +++ b/runs/logging.properties @@ -0,0 +1,6 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO + +java.util.logging.ConsoleHandler.level=ALL +Lenis.level=FINE +LWJGL.level=FINE diff --git a/runs/vanilla/build.gradle.kts b/runs/vanilla/build.gradle.kts new file mode 100644 index 0000000..9c7fa1b --- /dev/null +++ b/runs/vanilla/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(libs.plugins.loom) +} + +val lenis = configurations.create("lenis") + +dependencies { + minecraft("com.mojang:minecraft:1.6.4") + lenis(project(path = ":", configuration = "distribution")) + runtimeOnly(project(path = ":", configuration = "distribution")) +} + +configurations.configureEach { + exclude(group = "org.lwjgl.lwjgl") + exclude(group = "org.lwjgl") +} + +loom { + clientOnlyMinecraftJar() + runs { + remove(getByName("server")) + remove(getByName("client")) + create("base") { + client() + mainClass = "net.minecraft.client.main.Main" + displayName = "Vanilla 1.6.4 (base)" + runDirectory = layout.projectDirectory.dir("run/base") + } + create("agent") { + client() + mainClass = "net.minecraft.client.main.Main" + displayName = "Vanilla 1.6.4 (agent)" + runDirectory = layout.projectDirectory.dir("run/agent") + jvmArguments.add(provider { "-javaagent:${lenis.singleFile.absolutePath}" }) + } + configureEach { + generateRunConfig = true + preferGradleTask = true + programArguments.addAll("--username", "Player", "--session", "0", "--version", "1.6.4") + jvmArguments.add("-Djava.util.logging.config.file=${rootProject.file("runs/logging.properties")}") + jvmArguments.add("--enable-native-access=ALL-UNNAMED") + if (System.getProperty("os.name").startsWith("Mac")) jvmArguments.add("-XstartOnFirstThread") + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 15bd70d..e983b00 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,11 +1,12 @@ pluginManagement { + includeBuild("build-logic") repositories { maven("https://maven.fabricmc.net/") - maven("https://maven.ornithemc.net/releases") - maven("https://maven.ornithemc.net/snapshots") mavenLocal() gradlePluginPortal() } } rootProject.name = "lenis" + +include(":runs:fabric", ":runs:vanilla") diff --git a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinPlugin.java b/src/fabric/java/pl/tomgirl/lenis/fabric/MixinPlugin.java new file mode 100644 index 0000000..0bae7d2 --- /dev/null +++ b/src/fabric/java/pl/tomgirl/lenis/fabric/MixinPlugin.java @@ -0,0 +1,58 @@ +package pl.tomgirl.lenis.fabric; + +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import org.spongepowered.asm.transformers.TreeTransformer; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Set; + +@SuppressWarnings("unused") +public class MixinPlugin implements IMixinConfigPlugin { + static { + try { + ClassLoader knotClassLoader = MixinPlugin.class.getClassLoader(); // net.fabricmc.loader.impl.launch.knot.KnotClassLoader + Field delegateField = knotClassLoader.getClass().getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(knotClassLoader); // net.fabricmc.loader.impl.launch.knot.KnotClassDelegate + Field mixinTransformerField = delegate.getClass().getDeclaredField("mixinTransformer"); + mixinTransformerField.setAccessible(true); + mixinTransformerField.set(delegate, new MixinTransformer((TreeTransformer) mixinTransformerField.get(delegate))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean shouldApplyMixin(String s, String s1) { + return true; + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void onLoad(String s) { + } + + @Override + public void acceptTargets(Set set, Set set1) { + } + + @Override + public void preApply(String s, ClassNode classNode, String s1, IMixinInfo iMixinInfo) { + } + + @Override + public void postApply(String s, ClassNode classNode, String s1, IMixinInfo iMixinInfo) { + } +} \ No newline at end of file diff --git a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformer.java b/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformer.java new file mode 100644 index 0000000..039ca04 --- /dev/null +++ b/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformer.java @@ -0,0 +1,29 @@ +package pl.tomgirl.lenis.fabric; + +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; +import org.spongepowered.asm.transformers.TreeTransformer; +import pl.tomgirl.lenis.game.GameTransformer; + +public class MixinTransformer extends MixinTransformerDelegate { + MixinTransformer(T delegate) throws Exception { + this.delegate = delegate; + } + + @Override + public byte[] transformClassBytes(String name, String transformedName, byte[] bytecode) { + if (bytecode == null) { + return super.transformClassBytes(name, transformedName, null); + } + + if (name.startsWith("pl.tomgirl.lenis")) { + return super.transformClassBytes(name, transformedName, bytecode); + } + + byte[] transformed = GameTransformer.transform(bytecode); + if (transformed != null) { + bytecode = transformed; + } + + return super.transformClassBytes(name, transformedName, bytecode); + } +} \ No newline at end of file diff --git a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformerDelegate.java b/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformerDelegate.java new file mode 100644 index 0000000..7e0054c --- /dev/null +++ b/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformerDelegate.java @@ -0,0 +1,73 @@ +package pl.tomgirl.lenis.fabric; + +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; +import org.spongepowered.asm.mixin.transformer.ext.IExtensionRegistry; +import org.spongepowered.asm.transformers.TreeTransformer; + +import java.util.List; + +public class MixinTransformerDelegate extends TreeTransformer implements IMixinTransformer { + protected T delegate; + + @Override + public void audit(MixinEnvironment mixinEnvironment) { + delegate.audit(mixinEnvironment); + } + + @Override + public List reload(String s, ClassNode classNode) { + return delegate.reload(s, classNode); + } + + @Override + public boolean computeFramesForClass(MixinEnvironment mixinEnvironment, String s, ClassNode classNode) { + return delegate.computeFramesForClass(mixinEnvironment, s, classNode); + } + + @Override + public byte[] transformClass(MixinEnvironment mixinEnvironment, String s, byte[] bytes) { + return delegate.transformClass(mixinEnvironment, s, bytes); + } + + @Override + public boolean transformClass(MixinEnvironment mixinEnvironment, String s, ClassNode classNode) { + return delegate.transformClass(mixinEnvironment, s, classNode); + } + + @Override + public boolean couldTransformClass(MixinEnvironment mixinEnvironment, String s) { + return delegate.couldTransformClass(mixinEnvironment, s); + } + + @Override + public byte[] generateClass(MixinEnvironment mixinEnvironment, String s) { + return delegate.generateClass(mixinEnvironment, s); + } + + @Override + public boolean generateClass(MixinEnvironment mixinEnvironment, String s, ClassNode classNode) { + return delegate.generateClass(mixinEnvironment, s, classNode); + } + + @Override + public IExtensionRegistry getExtensions() { + return delegate.getExtensions(); + } + + @Override + public byte[] transformClassBytes(String s, String s1, byte[] bytes) { + return delegate.transformClassBytes(s, s1, bytes); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public boolean isDelegationExcluded() { + return delegate.isDelegationExcluded(); + } +} diff --git a/src/main/java/pl/tomgirl/lenis/PreLaunch.java b/src/fabric/java/pl/tomgirl/lenis/fabric/PreLaunch.java similarity index 87% rename from src/main/java/pl/tomgirl/lenis/PreLaunch.java rename to src/fabric/java/pl/tomgirl/lenis/fabric/PreLaunch.java index 0e27b3f..1a2668a 100644 --- a/src/main/java/pl/tomgirl/lenis/PreLaunch.java +++ b/src/fabric/java/pl/tomgirl/lenis/fabric/PreLaunch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis; +package pl.tomgirl.lenis.fabric; import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; @@ -7,4 +7,4 @@ public class PreLaunch implements PreLaunchEntrypoint { public void onPreLaunch() { System.setProperty("java.awt.headless", "true"); } -} +} \ No newline at end of file diff --git a/src/main/java/org/lwjgl/LWJGLUtil.java b/src/main/java/org/lwjgl/LWJGLUtil.java index 0eb3cc6..ef19b38 100644 --- a/src/main/java/org/lwjgl/LWJGLUtil.java +++ b/src/main/java/org/lwjgl/LWJGLUtil.java @@ -1,18 +1,16 @@ package org.lwjgl; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; @SuppressWarnings("unused") public class LWJGLUtil { public static final boolean DEBUG = Boolean.getBoolean("org.lwjgl.util.Debug"); - private static final Logger LOG = LogManager.getLogger("LWJGL"); + private static final System.Logger LOG = System.getLogger("LWJGL"); private LWJGLUtil() {} public static void log(CharSequence msg) { if (DEBUG) { - LOG.debug(msg); + LOG.log(System.Logger.Level.DEBUG, msg.toString()); } } } diff --git a/src/main/java/pl/tomgirl/lenis/Agent.java b/src/main/java/pl/tomgirl/lenis/Agent.java new file mode 100644 index 0000000..94327e7 --- /dev/null +++ b/src/main/java/pl/tomgirl/lenis/Agent.java @@ -0,0 +1,39 @@ +package pl.tomgirl.lenis; + +import java.io.File; +import java.lang.instrument.Instrumentation; + +public final class Agent { + private Agent() {} + + public static void premain(String arguments, Instrumentation instrumentation) { + if (injectFabric()) { + Lenis.LOG.log(System.Logger.Level.INFO, "Injected Fabric mod"); + return; + } + + instrumentation.addTransformer(new pl.tomgirl.lenis.game.GameTransformer()); + } + + public static boolean injectFabric() { + try { + Class.forName("net.fabricmc.loader.api.FabricLoader"); + + File agentFile = new File(Agent.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + + String addMods = System.getProperty("fabric.addMods", ""); + if (!"".equals(addMods)) { + addMods += File.pathSeparator; + } + addMods += agentFile.getAbsolutePath(); + System.setProperty("fabric.addMods", addMods); + + return true; + } catch (Exception e) { + if (!(e instanceof ClassNotFoundException)) { + Lenis.LOG.log(System.Logger.Level.ERROR, "Error injecting into Fabric"); + } + return false; + } + } +} diff --git a/src/main/java/pl/tomgirl/lenis/Lenis.java b/src/main/java/pl/tomgirl/lenis/Lenis.java index 8e7c61a..2d2be44 100644 --- a/src/main/java/pl/tomgirl/lenis/Lenis.java +++ b/src/main/java/pl/tomgirl/lenis/Lenis.java @@ -1,8 +1,7 @@ package pl.tomgirl.lenis; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +public final class Lenis { + public static final System.Logger LOG = System.getLogger("Lenis"); -public class Lenis { - public static final Logger LOG = LogManager.getLogger("Lenis"); + private Lenis() {} } diff --git a/src/main/java/pl/tomgirl/lenis/Platform.java b/src/main/java/pl/tomgirl/lenis/Platform.java index cef1220..d075644 100644 --- a/src/main/java/pl/tomgirl/lenis/Platform.java +++ b/src/main/java/pl/tomgirl/lenis/Platform.java @@ -29,7 +29,7 @@ public void open(String uri) { // TODO: Validate URI process.getErrorStream().close(); process.getOutputStream().close(); } catch (IOException e) { - Lenis.LOG.error("Couldn't open uri '{}'", uri, e); + Lenis.LOG.log(System.Logger.Level.ERROR, "Could not open uri " + uri, e); } } diff --git a/src/main/java/pl/tomgirl/lenis/game/GameHooks.java b/src/main/java/pl/tomgirl/lenis/game/GameHooks.java new file mode 100644 index 0000000..89fd42d --- /dev/null +++ b/src/main/java/pl/tomgirl/lenis/game/GameHooks.java @@ -0,0 +1,52 @@ +package pl.tomgirl.lenis.game; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Objects; + +import javax.imageio.ImageIO; + +import org.lwjgl.sdl.SDLClipboard; + +import pl.tomgirl.lenis.Lenis; +import pl.tomgirl.lenis.window.DisplaySdl; + +@SuppressWarnings("unused") +public final class GameHooks { + public static volatile boolean screenPatchAvailable; + + private GameHooks() {} + + public static String getClipboard() { + return Objects.requireNonNullElse(SDLClipboard.SDL_GetClipboardText(), ""); + } + + public static void setClipboard(String text) { + SDLClipboard.SDL_SetClipboardText(text); + } + + public static void setIcon() { + try (InputStream stream = Lenis.class.getResourceAsStream("/icon_256x.png")) { + if (stream == null) throw new IllegalStateException("Missing default icon"); + BufferedImage image = ImageIO.read(stream); + if (image == null) throw new IllegalStateException("Unable to decode default icon"); + ByteBuffer pixels = ByteBuffer.allocate(image.getWidth() * image.getHeight() * 4); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int argb = image.getRGB(x, y); + pixels.put((byte) (argb >> 16)).put((byte) (argb >> 8)); + pixels.put((byte) argb).put((byte) (argb >> 24)); + } + } + DisplaySdl.instance().setIcon(new ByteBuffer[] { pixels.flip() }); + } catch (IOException ex) { + throw new IllegalStateException("Unable to load default icon", ex); + } + } + + public static void screenOpened(Object screen) { + DisplaySdl.instance().setTextInputActive(screen != null); + } +} diff --git a/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java b/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java new file mode 100644 index 0000000..872360c --- /dev/null +++ b/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java @@ -0,0 +1,40 @@ +package pl.tomgirl.lenis.game; + +import java.lang.instrument.ClassFileTransformer; +import java.security.ProtectionDomain; +import java.util.List; +import java.util.stream.Stream; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import pl.tomgirl.lenis.Lenis; +import pl.tomgirl.lenis.game.patch.Patch; +import pl.tomgirl.lenis.game.patch.impl.*; + +public final class GameTransformer implements ClassFileTransformer { + @Override + public byte[] transform(ClassLoader loader, String name, Class type, ProtectionDomain domain, byte[] bytecode) { + return transform(bytecode); + } + + public static byte[] transform(byte[] bytecode) { + ScreenPatch screen = new ScreenPatch(null); + MinecraftPatch minecraft = new MinecraftPatch(screen); + ClassReader reader = new ClassReader(bytecode); + reader.accept(minecraft, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + List matches = Stream.of(minecraft, screen).filter(Patch::matches).toList(); + if (matches.isEmpty()) return bytecode; + + ClassWriter writer = new ClassWriter(reader, 0); + ClassVisitor patcher = writer; + for (Patch patch : matches) { + patch.applied(); + patcher = patch.apply(patcher); + } + reader.accept(patcher, 0); + Lenis.LOG.log(System.Logger.Level.DEBUG, "Applied {0} to {1}", matches, reader.getClassName()); + return writer.toByteArray(); + } +} diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java b/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java new file mode 100644 index 0000000..8a3c7af --- /dev/null +++ b/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java @@ -0,0 +1,21 @@ +package pl.tomgirl.lenis.game.patch; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import pl.tomgirl.lenis.game.GameHooks; + +public abstract class Patch extends ClassVisitor { + public static final String HOOKS = Type.getInternalName(GameHooks.class); + + public Patch(ClassVisitor next) { super(Opcodes.ASM9, next); } + + public abstract boolean matches(); + public abstract ClassVisitor apply(ClassVisitor writer); + public void applied() {} + + @Override + public final String toString() { return getClass().getSimpleName(); } + + public record Method(String name, String descriptor) {} +} diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java b/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java new file mode 100644 index 0000000..9c8f5ca --- /dev/null +++ b/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java @@ -0,0 +1,123 @@ +package pl.tomgirl.lenis.game.patch.impl; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import pl.tomgirl.lenis.game.GameHooks; +import pl.tomgirl.lenis.game.patch.Patch; + +public final class MinecraftPatch extends Patch { + private static final String DISPLAY = "org/lwjgl/opengl/Display"; + private final List screenCandidates = new ArrayList<>(); + private String owner; + private Method openScreen; + private boolean displayCreate; + private boolean displayTitle; + + public MinecraftPatch(ClassVisitor next) { super(next); } + + @Override + public void visit(int version, int access, String name, String signature, String parent, String[] interfaces) { + owner = name; + super.visit(version, access, name, signature, parent, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor next = super.visitMethod(access, name, descriptor, signature, exceptions); + Type[] arguments = Type.getArgumentTypes(descriptor); + boolean candidate = (access & Opcodes.ACC_STATIC) == 0 + && Type.getReturnType(descriptor).equals(Type.VOID_TYPE) + && arguments.length == 1 && arguments[0].getSort() == Type.OBJECT; + + return new MethodVisitor(Opcodes.ASM9, next) { + private final Set reads = new HashSet<>(); + private final Set writes = new HashSet<>(); + + @Override + public void visitFieldInsn(int opcode, String fieldOwner, String field, String fieldDescriptor) { + if (candidate && fieldOwner.equals(owner) && fieldDescriptor.equals(arguments[0].getDescriptor())) { + if (opcode == Opcodes.GETFIELD) reads.add(field); + if (opcode == Opcodes.PUTFIELD) writes.add(field); + } + super.visitFieldInsn(opcode, fieldOwner, field, fieldDescriptor); + } + + @Override + public void visitMethodInsn(int opcode, String calledOwner, String called, String calledDescriptor, boolean itf) { + if (calledOwner.equals(DISPLAY)) { + displayCreate |= called.equals("create") && calledDescriptor.endsWith(")V"); + displayTitle |= called.equals("setTitle") && calledDescriptor.equals("(Ljava/lang/String;)V"); + } + super.visitMethodInsn(opcode, calledOwner, called, calledDescriptor, itf); + } + + @Override + public void visitEnd() { + if (writes.stream().anyMatch(reads::contains)) { + screenCandidates.add(new Method(name, descriptor)); + } + super.visitEnd(); + } + }; + } + + @Override + public boolean matches() { + if (!displayCreate || !displayTitle) { + return false; + } + + if (screenCandidates.size() == 1) { + openScreen = screenCandidates.getFirst(); + } + + return true; + } + + @Override + public void applied() { + if (openScreen != null) { + GameHooks.screenPatchAvailable = true; + } + } + + @Override + public ClassVisitor apply(ClassVisitor writer) { + return new ClassVisitor(Opcodes.ASM9, writer) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor method = super.visitMethod(access, name, descriptor, signature, exceptions); + boolean patchesScreen = new Method(name, descriptor).equals(openScreen); + return new MethodVisitor(Opcodes.ASM9, method) { + @Override + public void visitMethodInsn(int opcode, String calledOwner, String called, String calledDescriptor, boolean itf) { + super.visitMethodInsn(opcode, calledOwner, called, calledDescriptor, itf); + if (calledOwner.equals(DISPLAY) && called.equals("create") && calledDescriptor.endsWith(")V")) { + super.visitMethodInsn(Opcodes.INVOKESTATIC, HOOKS, "setIcon", "()V", false); + } + } + + @Override + public void visitInsn(int opcode) { + if (patchesScreen && opcode == Opcodes.RETURN) { + super.visitVarInsn(Opcodes.ALOAD, 1); + super.visitMethodInsn(Opcodes.INVOKESTATIC, HOOKS, "screenOpened", "(Ljava/lang/Object;)V", false); + } + super.visitInsn(opcode); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + super.visitMaxs(Math.max(maxStack, 1), maxLocals); + } + }; + } + }; + } +} diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java b/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java new file mode 100644 index 0000000..b8e5452 --- /dev/null +++ b/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java @@ -0,0 +1,71 @@ +package pl.tomgirl.lenis.game.patch.impl; + +import java.util.HashMap; +import java.util.Map; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import pl.tomgirl.lenis.game.patch.Patch; + +public final class ScreenPatch extends Patch { + private static final String CLIPBOARD = "java/awt/datatransfer/Clipboard"; + private final Map methods = new HashMap<>(); + + public ScreenPatch(ClassVisitor next) { super(next); } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor next = super.visitMethod(access, name, descriptor, signature, exceptions); + return new MethodVisitor(Opcodes.ASM9, next) { + private boolean gets; + private boolean sets; + + @Override + public void visitMethodInsn(int opcode, String owner, String called, String calledDescriptor, boolean itf) { + if (owner.equals(CLIPBOARD)) { + gets |= called.equals("getContents"); + sets |= called.equals("setContents"); + } + super.visitMethodInsn(opcode, owner, called, calledDescriptor, itf); + } + + @Override + public void visitEnd() { + if (gets && descriptor.equals("()Ljava/lang/String;")) methods.put(new Method(name, descriptor), ClipboardMethod.GET); + if (sets && descriptor.equals("(Ljava/lang/String;)V")) methods.put(new Method(name, descriptor), ClipboardMethod.SET); + super.visitEnd(); + } + }; + } + + @Override + public boolean matches() { + return !methods.isEmpty(); + } + + @Override + public ClassVisitor apply(ClassVisitor writer) { + return new ClassVisitor(Opcodes.ASM9, writer) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + ClipboardMethod patch = methods.get(new Method(name, descriptor)); + if (patch == null) return super.visitMethod(access, name, descriptor, signature, exceptions); + MethodVisitor method = super.visitMethod(access, name, descriptor, signature, exceptions); + method.visitCode(); + if (patch == ClipboardMethod.GET) { + method.visitMethodInsn(Opcodes.INVOKESTATIC, HOOKS, "getClipboard", "()Ljava/lang/String;", false); + method.visitInsn(Opcodes.ARETURN); + } else { + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitMethodInsn(Opcodes.INVOKESTATIC, HOOKS, "setClipboard", "(Ljava/lang/String;)V", false); + method.visitInsn(Opcodes.RETURN); + } + method.visitMaxs(1, patch == ClipboardMethod.GET ? 0 : 1); + method.visitEnd(); + return null; + } + }; + } + + private enum ClipboardMethod { GET, SET } +} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/AL10Mixin.java b/src/main/java/pl/tomgirl/lenis/mixin/AL10Mixin.java deleted file mode 100644 index b68be0f..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/AL10Mixin.java +++ /dev/null @@ -1,22 +0,0 @@ -package pl.tomgirl.lenis.mixin; - -import pl.tomgirl.lenis.plugin.CompatStub; - -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import org.lwjgl.openal.AL10; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@SuppressWarnings("unused") -@Mixin(value = AL10.class, remap = false) -public abstract class AL10Mixin { - @Unique @CompatStub("alListenerfv") - private static void alListener(int paramName, FloatBuffer values) {} - - @Unique @CompatStub("alSourcefv") - private static void alSource(int source, int param, FloatBuffer values) {} - - @Unique @CompatStub("alSourceStopv") - private static void alSourceStop(IntBuffer sources) {} -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/ARBShaderObjectsMixin.java b/src/main/java/pl/tomgirl/lenis/mixin/ARBShaderObjectsMixin.java deleted file mode 100644 index 70c7f48..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/ARBShaderObjectsMixin.java +++ /dev/null @@ -1,22 +0,0 @@ -package pl.tomgirl.lenis.mixin; - -import pl.tomgirl.lenis.plugin.CompatStub; - -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import org.lwjgl.opengl.ARBShaderObjects; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@SuppressWarnings("unused") -@Mixin(value = ARBShaderObjects.class, remap = false) -public abstract class ARBShaderObjectsMixin { - @Unique @CompatStub("glGetObjectParameterivARB") - private static void glGetObjectParameterARB(int obj, int pname, IntBuffer params) {} - - @Unique @CompatStub("glUniformMatrix4fvARB") - private static void glUniformMatrix4ARB(int location, boolean transpose, FloatBuffer value) {} - - @Unique @CompatStub("glUniform1ivARB") - private static void glUniform1ARB(int location, IntBuffer value) {} -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/GL11Mixin.java b/src/main/java/pl/tomgirl/lenis/mixin/GL11Mixin.java deleted file mode 100644 index 64ae8dd..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/GL11Mixin.java +++ /dev/null @@ -1,57 +0,0 @@ -package pl.tomgirl.lenis.mixin; - -import pl.tomgirl.lenis.plugin.CompatStub; - -import java.nio.ByteBuffer; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.nio.ShortBuffer; - -import org.lwjgl.opengl.GL11; -import org.lwjgl.system.MemoryUtil; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@SuppressWarnings("unused") -@Mixin(value = GL11.class, remap = false) -public abstract class GL11Mixin { - @Unique @CompatStub - private static void glDrawElements(int mode, int count, int type, ByteBuffer indices) { - GL11.nglDrawElements(mode, count, type, MemoryUtil.memAddress(indices)); - } - - @Unique @CompatStub - private static void glTexCoordPointer(int i, int stride, FloatBuffer pointer) { - GL11.glTexCoordPointer(i, 0x1406, stride, pointer); - } - - @Unique @CompatStub - private static void glTexCoordPointer(int i, int stride, ShortBuffer pointer) { - GL11.glTexCoordPointer(i, 0x1402, stride, pointer); - } - - @Unique @CompatStub - private static void glColorPointer(int i, boolean bl, int i2, ByteBuffer pointer) { - GL11.glColorPointer(i, 0x1401, i2, pointer); - } - - @Unique @CompatStub - private static void glVertexPointer(int i, int i2, FloatBuffer pointer) { - GL11.glVertexPointer(i, 0x1406, i2, pointer); - } - - @Unique @CompatStub - private static void glNormalPointer(int stride, ByteBuffer pointer) { - GL11.glNormalPointer(0x1400, stride, pointer); - } - - @Unique @CompatStub("glGetFloatv") private static void glGetFloat(int pname, FloatBuffer params) {} - @Unique @CompatStub("glGetIntegerv") private static void glGetInteger(int pname, IntBuffer params) {} - @Unique @CompatStub("glFogfv") private static void glFog(int pname, FloatBuffer params) {} - @Unique @CompatStub("glLightfv") private static void glLight(int light, int pname, FloatBuffer params) {} - @Unique @CompatStub("glLightModelfv") private static void glLightModel(int pname, FloatBuffer params) {} - @Unique @CompatStub("glMultMatrixf") private static void glMultMatrix(FloatBuffer matrix) {} - @Unique @CompatStub("glTexEnvfv") private static void glTexEnv(int target, int pname, FloatBuffer params) {} - @Unique @CompatStub("glTexGenfv") private static void glTexGen(int coord, int pname, FloatBuffer params) {} - @Unique @CompatStub("glLoadMatrixf") private static void glLoadMatrix(FloatBuffer matrix) {} -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/GL20Mixin.java b/src/main/java/pl/tomgirl/lenis/mixin/GL20Mixin.java deleted file mode 100644 index cdc471a..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/GL20Mixin.java +++ /dev/null @@ -1,34 +0,0 @@ -package pl.tomgirl.lenis.mixin; - -import pl.tomgirl.lenis.plugin.CompatStub; - -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import org.lwjgl.opengl.GL20; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@SuppressWarnings("unused") -@Mixin(value = GL20.class, remap = false) -public abstract class GL20Mixin { - @Unique @CompatStub - private static void glShaderSource(int shader, java.nio.ByteBuffer string) { - byte[] data = new byte[string.limit()]; - string.position(0); - string.get(data); - string.position(0); - GL20.glShaderSource(shader, new String(data)); - } - - @Unique @CompatStub("glUniformMatrix4fv") private static void glUniformMatrix4(int location, boolean transpose, FloatBuffer value) {} - @Unique @CompatStub("glUniform2fv") private static void glUniform2(int location, FloatBuffer value) {} - @Unique @CompatStub("glUniform1iv") private static void glUniform1(int location, IntBuffer value) {} - @Unique @CompatStub("glUniform1fv") private static void glUniform1(int location, FloatBuffer value) {} - @Unique @CompatStub("glUniform2iv") private static void glUniform2i(int location, IntBuffer value) {} - @Unique @CompatStub("glUniform3iv") private static void glUniform3(int location, IntBuffer value) {} - @Unique @CompatStub("glUniform3fv") private static void glUniform3(int location, FloatBuffer value) {} - @Unique @CompatStub("glUniform4iv") private static void glUniform4(int location, IntBuffer value) {} - @Unique @CompatStub("glUniform4fv") private static void glUniform4(int location, FloatBuffer value) {} - @Unique @CompatStub("glUniformMatrix2fv") private static void glUniformMatrix2(int location, boolean transpose, FloatBuffer value) {} - @Unique @CompatStub("glUniformMatrix3fv") private static void glUniformMatrix3(int location, boolean transpose, FloatBuffer value) {} -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/GLCapabilitiesMixin.java b/src/main/java/pl/tomgirl/lenis/mixin/GLCapabilitiesMixin.java deleted file mode 100644 index b2c3e22..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/GLCapabilitiesMixin.java +++ /dev/null @@ -1,36 +0,0 @@ -package pl.tomgirl.lenis.mixin; - -import java.util.Set; -import java.util.function.IntFunction; -import org.lwjgl.PointerBuffer; -import org.lwjgl.opengl.GLCapabilities; -import org.lwjgl.system.FunctionProvider; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Mutable; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(value = GLCapabilities.class, remap = false) -public abstract class GLCapabilitiesMixin { - @Mutable @Unique @Final public boolean GL_EXT_multi_draw_arrays; - @Mutable @Unique @Final public boolean GL_EXT_paletted_texture; - @Mutable @Unique @Final public boolean GL_EXT_rescale_normal; - @Mutable @Unique @Final public boolean GL_EXT_texture_3d; - @Mutable @Unique @Final public boolean GL_EXT_texture_lod_bias; - @Mutable @Unique @Final public boolean GL_EXT_vertex_shader; - @Mutable @Unique @Final public boolean GL_EXT_vertex_weighting; - - @Inject(method = "", at = @At("TAIL")) - private void initExtendedFields(FunctionProvider provider, Set ext, boolean fc, IntFunction bufferFactory, CallbackInfo ci) { - GL_EXT_multi_draw_arrays = ext.contains("GL_EXT_multi_draw_arrays"); - GL_EXT_paletted_texture = ext.contains("GL_EXT_paletted_texture"); - GL_EXT_rescale_normal = ext.contains("GL_EXT_rescale_normal"); - GL_EXT_texture_3d = ext.contains("GL_EXT_texture_3d"); - GL_EXT_texture_lod_bias = ext.contains("GL_EXT_texture_lod_bias"); - GL_EXT_vertex_shader = ext.contains("GL_EXT_vertex_shader"); - GL_EXT_vertex_weighting = ext.contains("GL_EXT_vertex_weighting"); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/LibraryLWJGLOpenALMixin.java b/src/main/java/pl/tomgirl/lenis/mixin/LibraryLWJGLOpenALMixin.java deleted file mode 100644 index 48a8bfc..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/LibraryLWJGLOpenALMixin.java +++ /dev/null @@ -1,41 +0,0 @@ -package pl.tomgirl.lenis.mixin; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; - -import org.lwjgl.openal.AL; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; -import paulscode.sound.libraries.LibraryLWJGLOpenAL; - -@Mixin(value = LibraryLWJGLOpenAL.class, remap = false) -public abstract class LibraryLWJGLOpenALMixin { - @Unique private static MethodHandle alExit = null; - - static { - try { - //noinspection JavaLangInvokeHandleSignature - alExit = MethodHandles.lookup().findStatic(AL.class, "exit", MethodType.methodType(void.class)); - } catch (Exception ignored) {} - } - - @Redirect(method = "libraryCompatible", at = @At(value = "INVOKE", target = "Lorg/lwjgl/openal/AL;destroy()V")) - private static void libraryCompatible() { - exitAL(); - } - - @Redirect(method = "cleanup", at = @At(value = "INVOKE", target = "Lorg/lwjgl/openal/AL;destroy()V")) - private void cleanup() { - exitAL(); - } - - @Unique - private static void exitAL() { - try { - alExit.invoke(); - } catch (Throwable ignored) {} - } -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Icon.java b/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Icon.java deleted file mode 100644 index db5851d..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Icon.java +++ /dev/null @@ -1,48 +0,0 @@ -package pl.tomgirl.lenis.mixin.game; - -import net.minecraft.client.Minecraft; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import pl.tomgirl.lenis.Lenis; -import pl.tomgirl.lenis.window.DisplaySdl; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -@Mixin(Minecraft.class) -public class MixinMinecraft_Icon { - @Inject(method = "initIcon", at = @At("HEAD"), cancellable = true) - private void initIcon(CallbackInfo ci) { - try (InputStream stream = Lenis.class.getResourceAsStream("/icon_256x.png")) { - if (stream == null) { - throw new IllegalStateException("Missing default icon"); - } - - BufferedImage image = ImageIO.read(stream); - if (image == null) { - throw new IllegalStateException("Unable to decode default icon"); - } - - ByteBuffer pixels = ByteBuffer.allocate(image.getWidth() * image.getHeight() * 4); - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - int argb = image.getRGB(x, y); - pixels.put((byte) (argb >> 16)); - pixels.put((byte) (argb >> 8)); - pixels.put((byte) argb); - pixels.put((byte) (argb >> 24)); - } - } - pixels.flip(); - DisplaySdl.instance().setIcon(new ByteBuffer[] { pixels }); - } catch (IOException ex) { - throw new IllegalStateException("Unable to load default icon", ex); - } - ci.cancel(); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Resize.java b/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Resize.java deleted file mode 100644 index 9a01a07..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_Resize.java +++ /dev/null @@ -1,23 +0,0 @@ -package pl.tomgirl.lenis.mixin.game; - -import net.minecraft.client.Minecraft; -import org.lwjgl.opengl.Display; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(Minecraft.class) -public abstract class MixinMinecraft_Resize { - @Shadow public int width; - @Shadow public int height; - @Shadow protected abstract void resize(int width, int height); - - @Inject(method = "init", at = @At("TAIL")) - private void init(CallbackInfo ci) { - this.width = Math.max(1, Display.getWidth()); - this.height = Math.max(1, Display.getHeight()); - this.resize(this.width, this.height); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_TextInput.java b/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_TextInput.java deleted file mode 100644 index ad6f401..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinMinecraft_TextInput.java +++ /dev/null @@ -1,17 +0,0 @@ -package pl.tomgirl.lenis.mixin.game; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.Screen; -import pl.tomgirl.lenis.window.DisplaySdl; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(Minecraft.class) -public abstract class MixinMinecraft_TextInput { - @Inject(method = "openScreen", at = @At("TAIL")) - private void openScreen(Screen screen, CallbackInfo ci) { - DisplaySdl.instance().setTextInputActive(screen != null); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinScreen_Clipboard.java b/src/main/java/pl/tomgirl/lenis/mixin/game/MixinScreen_Clipboard.java deleted file mode 100644 index 79d9b89..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinScreen_Clipboard.java +++ /dev/null @@ -1,26 +0,0 @@ -package pl.tomgirl.lenis.mixin.game; - -import net.minecraft.client.gui.screen.Screen; -import org.lwjgl.sdl.SDLClipboard; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.Objects; - -@SuppressWarnings({"unused"}) -@Mixin(Screen.class) -public abstract class MixinScreen_Clipboard { - @Inject(at = @At("HEAD"), method = "getClipboard", cancellable = true) - private static void getClipboard(CallbackInfoReturnable cir) { - cir.setReturnValue(Objects.requireNonNullElse(SDLClipboard.SDL_GetClipboardText(), "")); - } - - @Inject(at = @At("HEAD"), method = "setClipboard", cancellable = true) - private static void setClipboard(String text, CallbackInfo ci) { - SDLClipboard.SDL_SetClipboardText(text); - ci.cancel(); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinTextFieldWidget_Ime.java b/src/main/java/pl/tomgirl/lenis/mixin/game/MixinTextFieldWidget_Ime.java deleted file mode 100644 index 388bf39..0000000 --- a/src/main/java/pl/tomgirl/lenis/mixin/game/MixinTextFieldWidget_Ime.java +++ /dev/null @@ -1,44 +0,0 @@ -package pl.tomgirl.lenis.mixin.game; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.render.TextRenderer; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import pl.tomgirl.lenis.window.DisplaySdl; - -@Mixin(TextFieldWidget.class) -public abstract class MixinTextFieldWidget_Ime { - @Shadow public int x; - @Shadow public int y; - @Shadow @Final private int height; - @Shadow @Final private TextRenderer textRenderer; - @Shadow private String text; - @Shadow private boolean hasBorder; - @Shadow private boolean focused; - @Shadow private boolean editable; - @Shadow private boolean visible; - @Shadow private int firstCharacterIndex; - @Shadow private int selectionStart; - @Shadow public abstract int getInnerWidth(); - - @Inject(method = "render", at = @At("HEAD")) - private void updateTextInputArea(CallbackInfo ci) { - Minecraft minecraft = Minecraft.getInstance(); - if (!focused || !editable || !visible || minecraft.screen == null) { - return; - } - - int lineX = hasBorder ? x + 4 : x; - int lineY = hasBorder ? y + (height - 8) / 2 : y; - int cursor = textRenderer.getWidth(text.substring(firstCharacterIndex, Math.max(firstCharacterIndex, selectionStart))); - DisplaySdl.instance().setTextInputArea( - lineX, lineY, getInnerWidth(), textRenderer.fontHeight, - Math.min(cursor, getInnerWidth()), minecraft.screen.width, minecraft.screen.height - ); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/plugin/CompatibilityStubBuilder.java b/src/main/java/pl/tomgirl/lenis/plugin/CompatibilityStubBuilder.java deleted file mode 100644 index 952e174..0000000 --- a/src/main/java/pl/tomgirl/lenis/plugin/CompatibilityStubBuilder.java +++ /dev/null @@ -1,65 +0,0 @@ -package pl.tomgirl.lenis.plugin; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AnnotationNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.VarInsnNode; -import org.spongepowered.asm.util.Annotations; - -final class CompatibilityStubBuilder { - private CompatibilityStubBuilder() { - } - - static void build(ClassNode targetClass) { - for (MethodNode method : targetClass.methods) { - AnnotationNode annotation = Annotations.getInvisible(method, CompatStub.class); - if (annotation == null) { - continue; - } - if ((method.access & Opcodes.ACC_STATIC) == 0) { - throw new IllegalStateException("CompatStub must be static: " + targetClass.name + "::" + method.name + method.desc); - } - method.access &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); - method.access |= Opcodes.ACC_PUBLIC; - - String delegate = Annotations.getValue(annotation, "value", ""); - if (!delegate.isEmpty()) { - buildDelegate(targetClass, method, delegate); - } - } - } - - private static void buildDelegate(ClassNode targetClass, MethodNode stub, String delegate) { - MethodNode target = targetClass.methods.stream() - .filter(method -> method.name.equals(delegate) && method.desc.equals(stub.desc)) - .findFirst() - .orElseThrow(() -> new IllegalStateException( - "CompatStub target does not exist: " + targetClass.name + "::" + delegate + stub.desc - )); - if ((target.access & Opcodes.ACC_STATIC) == 0) { - throw new IllegalStateException("CompatStub target must be static: " + targetClass.name + "::" + delegate + stub.desc); - } - - stub.instructions.clear(); - stub.tryCatchBlocks.clear(); - if (stub.localVariables != null) { - stub.localVariables.clear(); - } - - int local = 0; - int stack = 0; - for (Type argument : Type.getArgumentTypes(stub.desc)) { - stub.instructions.add(new VarInsnNode(argument.getOpcode(Opcodes.ILOAD), local)); - local += argument.getSize(); - stack += argument.getSize(); - } - stub.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, targetClass.name, delegate, stub.desc, false)); - stub.instructions.add(new InsnNode(Type.getReturnType(stub.desc).getOpcode(Opcodes.IRETURN))); - stub.maxLocals = local; - stub.maxStack = Math.max(stack, Type.getReturnType(stub.desc).getSize()); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/plugin/PaulscodeClasspathPlugin.java b/src/main/java/pl/tomgirl/lenis/plugin/PaulscodeClasspathPlugin.java deleted file mode 100644 index 50471ae..0000000 --- a/src/main/java/pl/tomgirl/lenis/plugin/PaulscodeClasspathPlugin.java +++ /dev/null @@ -1,52 +0,0 @@ -package pl.tomgirl.lenis.plugin; - -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import net.fabricmc.loader.impl.launch.FabricLauncherBase; -import org.objectweb.asm.tree.ClassNode; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; -import org.spongepowered.asm.mixin.extensibility.IMixinInfo; - -public final class PaulscodeClasspathPlugin implements IMixinConfigPlugin { - @Override - public void onLoad(String mixinPackage) { - findPaulscodePath().ifPresent(FabricLauncherBase.getLauncher()::addToClassPath); - } - - private static Optional findPaulscodePath() { - return FabricLauncherBase.getLauncher().getClassPath().stream() - .filter(path -> path.toString().contains("librarylwjglopenal")) - .findFirst(); - } - - @Override - public String getRefMapperConfig() { - return null; - } - - @Override - public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { - return true; - } - - @Override - public void acceptTargets(Set myTargets, Set otherTargets) { - } - - @Override - public List getMixins() { - return List.of(); - } - - @Override - public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - } - - @Override - public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - CompatibilityStubBuilder.build(targetClass); - } -} diff --git a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java b/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java index f0757b7..8cb6849 100644 --- a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java +++ b/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java @@ -9,7 +9,6 @@ import org.lwjgl.opengl.DisplayMode; import org.lwjgl.opengl.Drawable; import pl.tomgirl.lenis.Lenis; -import net.fabricmc.loader.api.FabricLoader; import org.jetbrains.annotations.NotNull; import org.lwjgl.LWJGLException; import org.lwjgl.PointerBuffer; @@ -60,7 +59,7 @@ public class DisplaySdl { private boolean focused; private boolean closeRequested; - private boolean textInputRequested; + private boolean textInputRequested = true; private boolean textInputActive; private int textInputX = -1; private int textInputY; @@ -265,7 +264,7 @@ public int setIcon(@NotNull ByteBuffer[] icons) { checkSdlError(surface != null); checkSdlError(SDL_SetWindowIcon(handle, surface)); } catch (Exception e) { - Lenis.LOG.error("Failed to set window icon", e); + Lenis.LOG.log(System.Logger.Level.ERROR, "Failed to set window icon", e); } } finally { MemoryUtil.memFree(pixels); @@ -356,12 +355,7 @@ public void create(@NotNull GpuSurface fallbackSurface) throws LWJGLException { checkSdlError(SDLHints.SDL_SetHint(SDLHints.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")); hints.forEach((k, v) -> checkSdlError(SDLHints.SDL_SetHint(k, v))); - checkSdlError(SDL_SetAppMetadata( - "Minecraft", - FabricLoader.getInstance().getModContainer("minecraft").orElseThrow(IllegalStateException::new) - .getMetadata().getVersion().getFriendlyString(), - "com.mojang.minecraft" - )); + checkSdlError(SDL_SetAppMetadata("Minecraft", null, "com.mojang.minecraft")); checkSdlError(SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_URL_STRING, "https://minecraft.net")); checkSdlError(SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_CREATOR_STRING, "Mojang AB")); checkSdlError(SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_COPYRIGHT_STRING, "Minecraft EULA: https://minecraft.net/eula")); @@ -433,7 +427,7 @@ public void setFullscreen(boolean fullscreen) { if (fullscreen) { int display = SDL_GetPrimaryDisplay(); if (display == 0) { - Lenis.LOG.warn("Failed to find display"); + Lenis.LOG.log(System.Logger.Level.WARNING, "Failed to find display"); return; } if (!this.fullscreen) { @@ -448,7 +442,7 @@ public void setFullscreen(boolean fullscreen) { SDL_SetWindowSize(handle, windowedWidth, windowedHeight); windowResized = true; } catch (Throwable t) { - Lenis.LOG.warn("Failed to set fullscreen: ", t); + Lenis.LOG.log(System.Logger.Level.WARNING, "Failed to set fullscreen: ", t); } } diff --git a/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java b/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java index d55f866..f87d99c 100644 --- a/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java +++ b/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java @@ -108,7 +108,7 @@ private int translateKeyFromSDL(int key) { if (translated != Keymap.UNMAPPED) { return translated; } - Lenis.LOG.warn("Untranslated key: {} ({})", key, SDLKeyboard.SDL_GetScancodeName(key)); + Lenis.LOG.log(System.Logger.Level.WARNING, "Untranslated key: {0} ({1})", key, SDLKeyboard.SDL_GetScancodeName(key)); return Keyboard.KEY_NONE; } diff --git a/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java b/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java index 1f38291..8866dc8 100644 --- a/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java +++ b/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java @@ -6,6 +6,7 @@ import org.lwjgl.sdl.SDL_MouseButtonEvent; import org.lwjgl.sdl.SDL_MouseMotionEvent; import org.lwjgl.sdl.SDL_MouseWheelEvent; +import pl.tomgirl.lenis.game.GameHooks; import static org.lwjgl.sdl.SDLEvents.*; import static org.lwjgl.sdl.SDLMouse.*; @@ -132,6 +133,9 @@ public void grabMouse(boolean grab) { SDL_SetWindowRelativeMouseMode(handle, grab); grabbed = grab; reset(); + if (!GameHooks.screenPatchAvailable) { + DISPLAY.setTextInputActive(!grab); + } } public boolean hasWheel() { diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 120840c..0a98235 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -17,6 +17,7 @@ ], "depends": { "fabricloader": ">=0.17.3", - "minecraft": "<1.13.0-alpha.17.43.a" + "minecraft": "<1.13.0-alpha.17.43.a", + "java": ">=21" } } diff --git a/src/main/resources/lenis.mixins.json b/src/main/resources/lenis.mixins.json index e2a7a8a..7546742 100644 --- a/src/main/resources/lenis.mixins.json +++ b/src/main/resources/lenis.mixins.json @@ -1,24 +1,5 @@ { "required": true, - "minVersion": "0.8", - "package": "pl.tomgirl.lenis.mixin", - "plugin": "pl.tomgirl.lenis.plugin.PaulscodeClasspathPlugin", - "compatibilityLevel": "JAVA_17", - "client": [ - "AL10Mixin", - "ALMixin", - "ARBShaderObjectsMixin", - "GL11Mixin", - "GL20Mixin", - "GLCapabilitiesMixin", - "LibraryLWJGLOpenALMixin", - "game.MixinMinecraft_Resize", - "game.MixinMinecraft_TextInput", - "game.MixinMinecraft_Icon", - "game.MixinScreen_Clipboard", - "game.MixinTextFieldWidget_Ime" - ], - "injectors": { - "defaultRequire": 1 - } -} + "compatibilityLevel": "JAVA_8", + "plugin": "pl.tomgirl.lenis.fabric.MixinPlugin" +} \ No newline at end of file From 06ace71c3584d8609749a87223e2e405fcfe6bd3 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:50:21 +0200 Subject: [PATCH 02/12] fix: prelaunch class --- src/main/resources/fabric.mod.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 0a98235..28f6314 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -9,7 +9,7 @@ "environment": "client", "entrypoints": { "preLaunch": [ - "pl.tomgirl.lenis.PreLaunch" + "pl.tomgirl.lenis.fabric.PreLaunch" ] }, "mixins": [ From 8cac7541a070372c393bcd90efe60e4759c16f36 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:02:37 +0200 Subject: [PATCH 03/12] fix: mc beta compat --- src/main/java/org/lwjgl/input/Controllers.java | 18 ++++++++++++++++++ src/main/java/org/lwjgl/opengl/Display.java | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/org/lwjgl/input/Controllers.java diff --git a/src/main/java/org/lwjgl/input/Controllers.java b/src/main/java/org/lwjgl/input/Controllers.java new file mode 100644 index 0000000..8479de7 --- /dev/null +++ b/src/main/java/org/lwjgl/input/Controllers.java @@ -0,0 +1,18 @@ +package org.lwjgl.input; + +import org.lwjgl.LWJGLException; + +@SuppressWarnings("unused") +public final class Controllers { + private Controllers() {} + + public static void create() throws LWJGLException { + } + + public static void destroy() { + } + + public static boolean isCreated() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/lwjgl/opengl/Display.java b/src/main/java/org/lwjgl/opengl/Display.java index ccfd61b..697ece4 100644 --- a/src/main/java/org/lwjgl/opengl/Display.java +++ b/src/main/java/org/lwjgl/opengl/Display.java @@ -119,4 +119,8 @@ public static boolean isCloseRequested() { public static void swapBuffers() { SDL.swapBuffers(); } + + public static void setParent(java.awt.Canvas o) {} + + public static void setDisplayConfiguration(float gamma, float brightness, float contrast) {} } From e4926fba80e21986e15003d5134b137632d334e1 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:02:10 +0200 Subject: [PATCH 04/12] feat: fml support --- .github/workflows/nightly.yml | 4 +- .github/workflows/release.yml | 4 +- .../tomgirl/lenis/bakery/patch/GL11Patch.java | 1 + .../tomgirl/lenis/build/LenisBuildPlugin.kt | 31 +++++-- gradle.properties | 2 +- .../tomgirl/lenis/forge/LenisTransformer.java | 14 +++ .../pl/tomgirl/lenis/forge/LenisTweaker.java | 82 +++++++++++++++++ .../launchwrapper/IClassTransformer.java | 5 + .../net/minecraft/launchwrapper/ITweaker.java | 11 +++ .../launchwrapper/LaunchClassLoader.java | 7 ++ src/main/java/org/lwjgl/LWJGLException.java | 3 - src/main/java/org/lwjgl/LWJGLUtil.java | 25 ++++- src/main/java/org/lwjgl/input/Mouse.java | 9 +- src/main/java/org/lwjgl/opengl/Display.java | 9 +- .../java/org/lwjgl/opengl/DisplayMode.java | 3 +- .../org/lwjgl/opengl/OpenGLException.java | 3 - src/main/java/org/lwjgl/opengl/Util.java | 34 ++++--- src/main/java/org/lwjgl/util/glu/GLU.java | 16 ++-- src/main/java/org/lwjgl/util/glu/Util.java | 61 ++++++++---- src/main/java/pl/tomgirl/lenis/Agent.java | 5 +- src/main/java/pl/tomgirl/lenis/Lenis.java | 4 +- src/main/java/pl/tomgirl/lenis/Platform.java | 5 +- .../java/pl/tomgirl/lenis/game/GameHooks.java | 6 +- .../tomgirl/lenis/game/GameTransformer.java | 6 +- .../pl/tomgirl/lenis/game/patch/Patch.java | 26 +++++- .../lenis/game/patch/impl/MinecraftPatch.java | 2 +- .../pl/tomgirl/lenis/window/BoundedQueue.java | 2 +- .../pl/tomgirl/lenis/window/DisplaySdl.java | 92 +++++++++++++------ .../pl/tomgirl/lenis/window/GlSurface.java | 22 +++-- .../pl/tomgirl/lenis/window/KeyboardSdl.java | 22 +++-- .../pl/tomgirl/lenis/window/MouseSdl.java | 34 +++++-- src/main/resources/fabric.mod.json | 2 +- 32 files changed, 423 insertions(+), 129 deletions(-) create mode 100644 src/forge/java/pl/tomgirl/lenis/forge/LenisTransformer.java create mode 100644 src/forge/java/pl/tomgirl/lenis/forge/LenisTweaker.java create mode 100644 src/launchwrapper/java/net/minecraft/launchwrapper/IClassTransformer.java create mode 100644 src/launchwrapper/java/net/minecraft/launchwrapper/ITweaker.java create mode 100644 src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 43a60f7..16e3167 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -11,11 +11,11 @@ jobs: - name: Checkout sources uses: actions/checkout@v7 - - name: Setup JDK 25 + - name: Setup JDK 21 uses: actions/setup-java@v5 with: distribution: temurin - java-version: 25 + java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42ef88c..e65d6fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,11 @@ jobs: - name: Checkout sources uses: actions/checkout@v7 - - name: Setup JDK 25 + - name: Setup JDK 21 uses: actions/setup-java@v5 with: distribution: temurin - java-version: 25 + java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java index eb64813..eefb176 100644 --- a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java @@ -40,6 +40,7 @@ private static void glNormalPointer(int stride, ByteBuffer pointer) { GL11.glNormalPointer(0x1400, stride, pointer); } + @CompatStub("glGetBooleanv") private static void glGetBoolean(int pname, ByteBuffer params) {} @CompatStub("glGetFloatv") private static void glGetFloat(int pname, FloatBuffer params) {} @CompatStub("glGetIntegerv") private static void glGetInteger(int pname, IntBuffer params) {} @CompatStub("glFogfv") private static void glFog(int pname, FloatBuffer params) {} diff --git a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt index fedf88c..a605604 100644 --- a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt +++ b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt @@ -1,7 +1,6 @@ package pl.tomgirl.lenis.build import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.VersionCatalogsExtension @@ -11,6 +10,7 @@ import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.bundling.Jar +import org.gradle.api.tasks.compile.JavaCompile import org.gradle.kotlin.dsl.* import org.gradle.language.jvm.tasks.ProcessResources @@ -42,6 +42,12 @@ class LenisBuildPlugin : Plugin { configurations.named(fabric.runtimeOnlyConfigurationName) { extendsFrom(configurations.getByName("runtimeOnly")) } + val launchwrapper = sourceSets.create("launchwrapper") + val forge = sourceSets.create("forge") { + compileClasspath += main.get().output + launchwrapper.output + + configurations.getByName("compileClasspath") + runtimeClasspath += output + compileClasspath + } val bakedPatches = sourceSets.create("bakedPatches") { java.srcDir(rootProject.layout.projectDirectory.dir("build-logic/src/baked-patches/java")) compileClasspath += main.get().output + configurations.getByName("compileClasspath") @@ -49,11 +55,14 @@ class LenisBuildPlugin : Plugin { } extensions.configure { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() } + tasks.withType().configureEach { + options.release.set(8) + options.compilerArgs.add("-Xlint:-options") + } + group = providers.gradleProperty("maven_group").get() val base = extensions.getByType() base.archivesName.set(providers.gradleProperty("base_name")) @@ -83,11 +92,14 @@ class LenisBuildPlugin : Plugin { } tasks.named("jar") { - dependsOn(fabric.classesTaskName) - from(fabric.output) + dependsOn(fabric.classesTaskName, forge.classesTaskName) + from(fabric.output, forge.output) archiveClassifier.set("slim") destinationDirectory.set(layout.buildDirectory.dir("intermediates")) - manifest.attributes["Premain-Class"] = "pl.tomgirl.lenis.Agent" + manifest.attributes( + "Premain-Class" to "pl.tomgirl.lenis.Agent", + "TweakClass" to "pl.tomgirl.lenis.forge.LenisTweaker", + ) } val shadowJar = tasks.named("shadowJar") { @@ -100,15 +112,16 @@ class LenisBuildPlugin : Plugin { } val unpatchedJar = tasks.register("unpatchedJar") { - dependsOn(shadowJar, fabric.classesTaskName) + dependsOn(shadowJar, fabric.classesTaskName, forge.classesTaskName) from(shadowJar.flatMap { it.archiveFile }.map { zipTree(it.asFile) }) { exclude("META-INF/MANIFEST.MF") } - from(fabric.output) + from(fabric.output, forge.output) archiveClassifier.set("unpatched") destinationDirectory.set(layout.buildDirectory.dir("intermediates")) manifest.attributes( "Premain-Class" to "pl.tomgirl.lenis.Agent", + "TweakClass" to "pl.tomgirl.lenis.forge.LenisTweaker", "Multi-Release" to "true", ) } @@ -134,7 +147,7 @@ class LenisBuildPlugin : Plugin { artifacts.add(distribution.name, distributionFile) { builtBy(bakeLwjgl) } tasks.named("assemble") { dependsOn(bakeLwjgl) } tasks.named("sourcesJar") { - from(fabric.allSource) + from(fabric.allSource, forge.allSource, launchwrapper.allSource) } extensions.configure { diff --git a/gradle.properties b/gradle.properties index 21acf40..175ac79 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.parallel = true maven_group = pl.tomgirl base_name = lenis -version = 0.1.4 +version = 0.1.5 diff --git a/src/forge/java/pl/tomgirl/lenis/forge/LenisTransformer.java b/src/forge/java/pl/tomgirl/lenis/forge/LenisTransformer.java new file mode 100644 index 0000000..76e9260 --- /dev/null +++ b/src/forge/java/pl/tomgirl/lenis/forge/LenisTransformer.java @@ -0,0 +1,14 @@ +package pl.tomgirl.lenis.forge; + +import net.minecraft.launchwrapper.IClassTransformer; +import pl.tomgirl.lenis.game.GameTransformer; + +public final class LenisTransformer implements IClassTransformer { + @Override + public byte[] transform(String name, String transformedName, byte[] bytecode) { + if (bytecode == null || name.startsWith("pl.tomgirl.lenis.") || name.startsWith("org.lwjgl.")) { + return bytecode; + } + return GameTransformer.transform(bytecode); + } +} diff --git a/src/forge/java/pl/tomgirl/lenis/forge/LenisTweaker.java b/src/forge/java/pl/tomgirl/lenis/forge/LenisTweaker.java new file mode 100644 index 0000000..1f2c3f8 --- /dev/null +++ b/src/forge/java/pl/tomgirl/lenis/forge/LenisTweaker.java @@ -0,0 +1,82 @@ +package pl.tomgirl.lenis.forge; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.lang.reflect.Field; +import java.util.List; + +import net.minecraft.launchwrapper.ITweaker; +import net.minecraft.launchwrapper.LaunchClassLoader; + +@SuppressWarnings("unused") +public final class LenisTweaker implements ITweaker { + private LaunchClassLoader classLoader; + + @Override + public void acceptOptions(List arguments, File gameDirectory, File assetsDirectory, String profile) { + System.setProperty("java.awt.headless", "true"); + } + + @Override + public void injectIntoClassLoader(LaunchClassLoader classLoader) { + this.classLoader = classLoader; + try { + Field parent = classLoader.getClass().getDeclaredField("parent"); + parent.setAccessible(true); + ClassLoader launcherParent = (ClassLoader) parent.get(classLoader); + URL runtime = LenisTweaker.class.getProtectionDomain().getCodeSource().getLocation(); + RuntimeClassLoader runtimeLoader = new RuntimeClassLoader(runtime, launcherParent); + parent.set(classLoader, runtimeLoader); + if (System.getProperty("os.name", "").startsWith("Mac")) { + Class display = runtimeLoader.loadClass("pl.tomgirl.lenis.window.DisplaySdl"); + Object instance = display.getMethod("instance").invoke(null); + display.getMethod("initializeVideo").invoke(instance); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to install the Lenis runtime class loader", e); + } + + classLoader.addClassLoaderExclusion("pl.tomgirl.lenis."); + classLoader.registerTransformer(LenisTransformer.class.getName()); + } + + private static class RuntimeClassLoader extends URLClassLoader { + RuntimeClassLoader(URL runtime, ClassLoader parent) { + super(new URL[]{runtime}, parent); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("org.lwjgl.") || name.startsWith("pl.tomgirl.lenis.")) { + Class loaded = findLoadedClass(name); + if (loaded == null) { + try { + loaded = findClass(name); + } catch (ClassNotFoundException ignored) { + loaded = super.loadClass(name, false); + } + } + + if (resolve) { + resolveClass(loaded); + } + + return loaded; + } + + return super.loadClass(name, resolve); + } + } + + @Override + public String getLaunchTarget() { + return ""; + } + + @Override + public String[] getLaunchArguments() { + classLoader.addClassLoaderExclusion("org.lwjgl."); + return new String[0]; + } +} diff --git a/src/launchwrapper/java/net/minecraft/launchwrapper/IClassTransformer.java b/src/launchwrapper/java/net/minecraft/launchwrapper/IClassTransformer.java new file mode 100644 index 0000000..b90c75a --- /dev/null +++ b/src/launchwrapper/java/net/minecraft/launchwrapper/IClassTransformer.java @@ -0,0 +1,5 @@ +package net.minecraft.launchwrapper; + +public interface IClassTransformer { + byte[] transform(String name, String transformedName, byte[] bytecode); +} diff --git a/src/launchwrapper/java/net/minecraft/launchwrapper/ITweaker.java b/src/launchwrapper/java/net/minecraft/launchwrapper/ITweaker.java new file mode 100644 index 0000000..28cd90b --- /dev/null +++ b/src/launchwrapper/java/net/minecraft/launchwrapper/ITweaker.java @@ -0,0 +1,11 @@ +package net.minecraft.launchwrapper; + +import java.io.File; +import java.util.List; + +public interface ITweaker { + void acceptOptions(List arguments, File gameDirectory, File assetsDirectory, String profile); + void injectIntoClassLoader(LaunchClassLoader classLoader); + String getLaunchTarget(); + String[] getLaunchArguments(); +} diff --git a/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java b/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java new file mode 100644 index 0000000..194dc48 --- /dev/null +++ b/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java @@ -0,0 +1,7 @@ +package net.minecraft.launchwrapper; + +public abstract class LaunchClassLoader extends ClassLoader { + private LaunchClassLoader() {} + public native void registerTransformer(String name); + public native void addClassLoaderExclusion(String prefix); +} diff --git a/src/main/java/org/lwjgl/LWJGLException.java b/src/main/java/org/lwjgl/LWJGLException.java index 0804134..450b3de 100644 --- a/src/main/java/org/lwjgl/LWJGLException.java +++ b/src/main/java/org/lwjgl/LWJGLException.java @@ -1,10 +1,7 @@ package org.lwjgl; -import java.io.Serial; - @SuppressWarnings("unused") public class LWJGLException extends Exception { - @Serial private static final long serialVersionUID = 1L; public LWJGLException() { diff --git a/src/main/java/org/lwjgl/LWJGLUtil.java b/src/main/java/org/lwjgl/LWJGLUtil.java index ef19b38..5f92b54 100644 --- a/src/main/java/org/lwjgl/LWJGLUtil.java +++ b/src/main/java/org/lwjgl/LWJGLUtil.java @@ -1,16 +1,37 @@ package org.lwjgl; +import pl.tomgirl.lenis.Platform; + +import java.util.logging.Level; +import java.util.logging.Logger; @SuppressWarnings("unused") public class LWJGLUtil { public static final boolean DEBUG = Boolean.getBoolean("org.lwjgl.util.Debug"); - private static final System.Logger LOG = System.getLogger("LWJGL"); + private static final Logger LOG = Logger.getLogger("LWJGL"); private LWJGLUtil() {} public static void log(CharSequence msg) { if (DEBUG) { - LOG.log(System.Logger.Level.DEBUG, msg.toString()); + LOG.log(Level.FINE, msg.toString()); + } + } + + public static int getPlatform() { + return Platform.CURRENT.ordinal() + 1; + } + + public static String getPlatformName() { + switch (Platform.CURRENT) { + case UNIX: + return "linux"; + case MACOS: + return "macosx"; + case WINDOWS: + return "windows"; + default: + return "unknown"; } } } diff --git a/src/main/java/org/lwjgl/input/Mouse.java b/src/main/java/org/lwjgl/input/Mouse.java index 6e8d235..3a21027 100644 --- a/src/main/java/org/lwjgl/input/Mouse.java +++ b/src/main/java/org/lwjgl/input/Mouse.java @@ -41,6 +41,13 @@ public class Mouse { private static double lastRawEventY; private static long eventNanos; + // compatibility fields + // shout out to a mod which sets these w/ reflection + private static int x; + private static int y; + private static int event_x; + private static int event_y; + private Mouse() {} private static void checkCreated() { @@ -50,7 +57,7 @@ private static void checkCreated() { } private static double clamp(double coordinate, int displaySize) { - return Math.clamp(coordinate, 0, displaySize - 1); + return Math.max(0, Math.min(coordinate, displaySize - 1)); } private static void initialize() { diff --git a/src/main/java/org/lwjgl/opengl/Display.java b/src/main/java/org/lwjgl/opengl/Display.java index 697ece4..b56f2fa 100644 --- a/src/main/java/org/lwjgl/opengl/Display.java +++ b/src/main/java/org/lwjgl/opengl/Display.java @@ -1,10 +1,12 @@ package org.lwjgl.opengl; import java.nio.ByteBuffer; +import java.util.logging.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.LWJGLException; +import pl.tomgirl.lenis.Lenis; import pl.tomgirl.lenis.window.GlSurface; import pl.tomgirl.lenis.window.DisplaySdl; @@ -60,7 +62,12 @@ public static void create() throws LWJGLException { } public static void create(@NotNull PixelFormat pixelFormat) throws LWJGLException { - SDL.create(new GlSurface(pixelFormat)); + try { + SDL.create(new GlSurface(pixelFormat)); + } catch (RuntimeException e) { + Lenis.LOG.log(Level.SEVERE, "Failed to create display", e); + throw new LWJGLException(e); + } } public static void setFullscreen(boolean fullscreen) { diff --git a/src/main/java/org/lwjgl/opengl/DisplayMode.java b/src/main/java/org/lwjgl/opengl/DisplayMode.java index 9a55eb7..363b11e 100644 --- a/src/main/java/org/lwjgl/opengl/DisplayMode.java +++ b/src/main/java/org/lwjgl/opengl/DisplayMode.java @@ -42,10 +42,11 @@ public int getFrequency() { } public boolean equals(Object obj) { - if (!(obj instanceof DisplayMode dm)) { + if (!(obj instanceof DisplayMode)) { return false; } + DisplayMode dm = (DisplayMode) obj; return dm.width == width && dm.height == height && dm.bpp == bpp && dm.freq == freq; } diff --git a/src/main/java/org/lwjgl/opengl/OpenGLException.java b/src/main/java/org/lwjgl/opengl/OpenGLException.java index d998b86..016bc84 100644 --- a/src/main/java/org/lwjgl/opengl/OpenGLException.java +++ b/src/main/java/org/lwjgl/opengl/OpenGLException.java @@ -1,10 +1,7 @@ package org.lwjgl.opengl; -import java.io.Serial; - @SuppressWarnings("unused") public class OpenGLException extends RuntimeException { - @Serial private static final long serialVersionUID = 1L; public OpenGLException(int gl_error_code) { diff --git a/src/main/java/org/lwjgl/opengl/Util.java b/src/main/java/org/lwjgl/opengl/Util.java index a2c2d33..018bd1d 100644 --- a/src/main/java/org/lwjgl/opengl/Util.java +++ b/src/main/java/org/lwjgl/opengl/Util.java @@ -21,17 +21,27 @@ public static void checkGLError() throws OpenGLException { } public static String translateGLErrorString(int error_code) { - return switch (error_code) { - case GL_NO_ERROR -> "No error"; - case GL_INVALID_ENUM -> "Invalid enum"; - case GL_INVALID_VALUE -> "Invalid value"; - case GL_INVALID_OPERATION -> "Invalid operation"; - case GL_STACK_OVERFLOW -> "Stack overflow"; - case GL_STACK_UNDERFLOW -> "Stack underflow"; - case GL_OUT_OF_MEMORY -> "Out of memory"; - case GL_TABLE_TOO_LARGE -> "Table too large"; - case GL_INVALID_FRAMEBUFFER_OPERATION -> "Invalid framebuffer operation"; - default -> null; - }; + switch (error_code) { + case GL_NO_ERROR: + return "No error"; + case GL_INVALID_ENUM: + return "Invalid enum"; + case GL_INVALID_VALUE: + return "Invalid value"; + case GL_INVALID_OPERATION: + return "Invalid operation"; + case GL_STACK_OVERFLOW: + return "Stack overflow"; + case GL_STACK_UNDERFLOW: + return "Stack underflow"; + case GL_OUT_OF_MEMORY: + return "Out of memory"; + case GL_TABLE_TOO_LARGE: + return "Table too large"; + case GL_INVALID_FRAMEBUFFER_OPERATION: + return "Invalid framebuffer operation"; + default: + return null; + } } } diff --git a/src/main/java/org/lwjgl/util/glu/GLU.java b/src/main/java/org/lwjgl/util/glu/GLU.java index 7199506..879c5d6 100644 --- a/src/main/java/org/lwjgl/util/glu/GLU.java +++ b/src/main/java/org/lwjgl/util/glu/GLU.java @@ -157,11 +157,15 @@ public static boolean gluCheckExtension(String extName, String extString) { } public static String gluErrorString(int error_code) { - return switch (error_code) { - case GLU_INVALID_ENUM -> "Invalid enum (glu)"; - case GLU_INVALID_VALUE -> "Invalid value (glu)"; - case GLU_OUT_OF_MEMORY -> "Out of memory (glu)"; - default -> Util.translateGLErrorString(error_code); - }; + switch (error_code) { + case GLU_INVALID_ENUM: + return "Invalid enum (glu)"; + case GLU_INVALID_VALUE: + return "Invalid value (glu)"; + case GLU_OUT_OF_MEMORY: + return "Out of memory (glu)"; + default: + return Util.translateGLErrorString(error_code); + } } } diff --git a/src/main/java/org/lwjgl/util/glu/Util.java b/src/main/java/org/lwjgl/util/glu/Util.java index ec1777d..020e8fe 100644 --- a/src/main/java/org/lwjgl/util/glu/Util.java +++ b/src/main/java/org/lwjgl/util/glu/Util.java @@ -31,13 +31,27 @@ protected static void cross(float[] v1, float[] v2, float[] result) { } protected static int compPerPix(int format) { - return switch (format) { - case GL_COLOR_INDEX, GL_STENCIL_INDEX, GL_DEPTH_COMPONENT, GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA, GL_LUMINANCE -> 1; - case GL_LUMINANCE_ALPHA -> 2; - case GL_RGB, GL_BGR -> 3; - case GL_RGBA, GL_BGRA -> 4; - default -> -1; - }; + switch (format) { + case GL_COLOR_INDEX: + case GL_STENCIL_INDEX: + case GL_DEPTH_COMPONENT: + case GL_RED: + case GL_GREEN: + case GL_BLUE: + case GL_ALPHA: + case GL_LUMINANCE: + return 1; + case GL_LUMINANCE_ALPHA: + return 2; + case GL_RGB: + case GL_BGR: + return 3; + case GL_RGBA: + case GL_BGRA: + return 4; + default: + return -1; + } } protected static int nearestPower(int value) { @@ -58,20 +72,27 @@ protected static int nearestPower(int value) { } protected static int bytesPerPixel(int format, int type) { - int n = switch (format) { - case GL_COLOR_INDEX, GL_STENCIL_INDEX, GL_DEPTH_COMPONENT, GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA, GL_LUMINANCE -> 1; - case GL_LUMINANCE_ALPHA -> 2; - case GL_RGB, GL_BGR -> 3; - case GL_RGBA, GL_BGRA -> 4; - default -> 0; - }; + int n = Math.max(compPerPix(format), 0); - int m = switch (type) { - case GL_UNSIGNED_BYTE, GL_BITMAP, GL_BYTE -> 1; - case GL_UNSIGNED_SHORT, GL_SHORT -> 2; - case GL_UNSIGNED_INT, GL_FLOAT, GL_INT -> 4; - default -> 0; - }; + int m; + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BITMAP: + case GL_BYTE: + m = 1; + break; + case GL_UNSIGNED_SHORT: + case GL_SHORT: + m = 2; + break; + case GL_UNSIGNED_INT: + case GL_FLOAT: + case GL_INT: + m = 4; + break; + default: + m = 0; + } return n * m; } diff --git a/src/main/java/pl/tomgirl/lenis/Agent.java b/src/main/java/pl/tomgirl/lenis/Agent.java index 94327e7..bd6e623 100644 --- a/src/main/java/pl/tomgirl/lenis/Agent.java +++ b/src/main/java/pl/tomgirl/lenis/Agent.java @@ -2,13 +2,14 @@ import java.io.File; import java.lang.instrument.Instrumentation; +import java.util.logging.Level; public final class Agent { private Agent() {} public static void premain(String arguments, Instrumentation instrumentation) { if (injectFabric()) { - Lenis.LOG.log(System.Logger.Level.INFO, "Injected Fabric mod"); + Lenis.LOG.log(Level.INFO, "Injected Fabric mod"); return; } @@ -31,7 +32,7 @@ public static boolean injectFabric() { return true; } catch (Exception e) { if (!(e instanceof ClassNotFoundException)) { - Lenis.LOG.log(System.Logger.Level.ERROR, "Error injecting into Fabric"); + Lenis.LOG.log(Level.SEVERE, "Error injecting into Fabric"); } return false; } diff --git a/src/main/java/pl/tomgirl/lenis/Lenis.java b/src/main/java/pl/tomgirl/lenis/Lenis.java index 2d2be44..bf23901 100644 --- a/src/main/java/pl/tomgirl/lenis/Lenis.java +++ b/src/main/java/pl/tomgirl/lenis/Lenis.java @@ -1,7 +1,9 @@ package pl.tomgirl.lenis; +import java.util.logging.Logger; + public final class Lenis { - public static final System.Logger LOG = System.getLogger("Lenis"); + public static final Logger LOG = Logger.getLogger("Lenis"); private Lenis() {} } diff --git a/src/main/java/pl/tomgirl/lenis/Platform.java b/src/main/java/pl/tomgirl/lenis/Platform.java index d075644..3ca75c3 100644 --- a/src/main/java/pl/tomgirl/lenis/Platform.java +++ b/src/main/java/pl/tomgirl/lenis/Platform.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.net.URI; import java.util.Locale; +import java.util.logging.Level; public enum Platform { UNIX(), @@ -29,13 +30,13 @@ public void open(String uri) { // TODO: Validate URI process.getErrorStream().close(); process.getOutputStream().close(); } catch (IOException e) { - Lenis.LOG.log(System.Logger.Level.ERROR, "Could not open uri " + uri, e); + Lenis.LOG.log(Level.SEVERE, "Could not open uri " + uri, e); } } protected String[] getProcessArguments(String uri) { try { - var parsed = new URI(uri); + URI parsed = new URI(uri); if ("file".equals(parsed.getScheme())) { uri = uri.replace("file:", "file://"); } diff --git a/src/main/java/pl/tomgirl/lenis/game/GameHooks.java b/src/main/java/pl/tomgirl/lenis/game/GameHooks.java index 89fd42d..b04a34a 100644 --- a/src/main/java/pl/tomgirl/lenis/game/GameHooks.java +++ b/src/main/java/pl/tomgirl/lenis/game/GameHooks.java @@ -20,7 +20,8 @@ public final class GameHooks { private GameHooks() {} public static String getClipboard() { - return Objects.requireNonNullElse(SDLClipboard.SDL_GetClipboardText(), ""); + String text = SDLClipboard.SDL_GetClipboardText(); + return text == null ? "" : text; } public static void setClipboard(String text) { @@ -40,7 +41,8 @@ public static void setIcon() { pixels.put((byte) argb).put((byte) (argb >> 24)); } } - DisplaySdl.instance().setIcon(new ByteBuffer[] { pixels.flip() }); + pixels.flip(); + DisplaySdl.instance().setIcon(new ByteBuffer[] { pixels }); } catch (IOException ex) { throw new IllegalStateException("Unable to load default icon", ex); } diff --git a/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java b/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java index 872360c..ad3caf2 100644 --- a/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java +++ b/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java @@ -3,7 +3,9 @@ import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.logging.Level; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -24,7 +26,7 @@ public static byte[] transform(byte[] bytecode) { ClassReader reader = new ClassReader(bytecode); reader.accept(minecraft, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - List matches = Stream.of(minecraft, screen).filter(Patch::matches).toList(); + List matches = Stream.of(minecraft, screen).filter(Patch::matches).collect(Collectors.toList()); if (matches.isEmpty()) return bytecode; ClassWriter writer = new ClassWriter(reader, 0); @@ -34,7 +36,7 @@ public static byte[] transform(byte[] bytecode) { patcher = patch.apply(patcher); } reader.accept(patcher, 0); - Lenis.LOG.log(System.Logger.Level.DEBUG, "Applied {0} to {1}", matches, reader.getClassName()); + Lenis.LOG.log(Level.FINE, "Applied {0} to {1}", new Object[]{matches, reader.getClassName()}); return writer.toByteArray(); } } diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java b/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java index 8a3c7af..3abafeb 100644 --- a/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java +++ b/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java @@ -17,5 +17,29 @@ public void applied() {} @Override public final String toString() { return getClass().getSimpleName(); } - public record Method(String name, String descriptor) {} + public static final class Method { + private final String name; + private final String descriptor; + + public Method(String name, String descriptor) { + this.name = name; + this.descriptor = descriptor; + } + + public String name() { return name; } + public String descriptor() { return descriptor; } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Method)) return false; + Method method = (Method) other; + return name.equals(method.name) && descriptor.equals(method.descriptor); + } + + @Override + public int hashCode() { return name.hashCode() * 31 + descriptor.hashCode(); } + + @Override + public String toString() { return name + descriptor; } + } } diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java b/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java index 9c8f5ca..e783928 100644 --- a/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java +++ b/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java @@ -74,7 +74,7 @@ public boolean matches() { } if (screenCandidates.size() == 1) { - openScreen = screenCandidates.getFirst(); + openScreen = screenCandidates.get(0); } return true; diff --git a/src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java b/src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java index d5f44f2..7249243 100644 --- a/src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java +++ b/src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java @@ -41,7 +41,7 @@ E peekLast() { E get(int index) { if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException(index); + throw new IndexOutOfBoundsException(String.valueOf(index)); } return entryAt(index); } diff --git a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java b/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java index 8cb6849..781c4d8 100644 --- a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java +++ b/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; import org.lwjgl.opengl.DisplayMode; import org.lwjgl.opengl.Drawable; @@ -32,6 +33,7 @@ public class DisplaySdl { private static final DisplaySdl INSTANCE = new DisplaySdl(); + private boolean videoInitialized; private SDL_Event event; private SDL_WindowEvent windowEvent; private final Drawable drawable = new SurfaceDrawable(); @@ -214,7 +216,7 @@ public void setWindowHint(String hint, String value) { @SuppressWarnings("resource") public DisplayMode getDesktopDisplayMode() { - var mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay()); + SDL_DisplayMode mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay()); if (mode == null) { DisplayMode best = null; for (DisplayMode displayMode : getAvailableDisplayModes()) { @@ -258,13 +260,13 @@ public int setIcon(@NotNull ByteBuffer[] icons) { ByteBuffer pixels = MemoryUtil.memAlloc(icon.remaining()); try { pixels.put(icon.duplicate()).flip(); - try (var surface = SDLSurface.SDL_CreateSurfaceFrom( + try (SDL_Surface surface = SDLSurface.SDL_CreateSurfaceFrom( size, size, SDLPixels.SDL_PIXELFORMAT_RGBA32, pixels, size * 4 )) { checkSdlError(surface != null); checkSdlError(SDL_SetWindowIcon(handle, surface)); } catch (Exception e) { - Lenis.LOG.log(System.Logger.Level.ERROR, "Failed to set window icon", e); + Lenis.LOG.log(Level.SEVERE, "Failed to set window icon", e); } } finally { MemoryUtil.memFree(pixels); @@ -290,9 +292,14 @@ public void processMessages() { windowResized = false; while (SDL_PollEvent(event)) { switch (event.type()) { - case SDL_EVENT_QUIT, SDL_EVENT_WINDOW_CLOSE_REQUESTED -> closeRequested = true; - case SDL_EVENT_WINDOW_FOCUS_GAINED -> focused = true; - case SDL_EVENT_WINDOW_FOCUS_LOST -> { + case SDL_EVENT_QUIT: + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + closeRequested = true; + break; + case SDL_EVENT_WINDOW_FOCUS_GAINED: + focused = true; + break; + case SDL_EVENT_WINDOW_FOCUS_LOST: { focused = false; long nanos = windowEvent.timestamp(); if (Keyboard.isCreated()) { @@ -301,32 +308,50 @@ public void processMessages() { if (Mouse.isCreated()) { MouseSdl.instance().releaseAll(nanos); } + break; } - case SDL_EVENT_WINDOW_SHOWN, SDL_EVENT_WINDOW_RESTORED, SDL_EVENT_WINDOW_MAXIMIZED -> minimized = false; - case SDL_EVENT_WINDOW_HIDDEN, SDL_EVENT_WINDOW_MINIMIZED -> minimized = true; - case SDL_EVENT_WINDOW_RESIZED -> resizeCallback(handle, windowEvent.data1(), windowEvent.data2()); - case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED -> - onFramebufferResize(handle, windowEvent.data1(), windowEvent.data2()); - case SDL_EVENT_WINDOW_ENTER_FULLSCREEN -> { + case SDL_EVENT_WINDOW_SHOWN: + case SDL_EVENT_WINDOW_RESTORED: + case SDL_EVENT_WINDOW_MAXIMIZED: + minimized = false; + break; + case SDL_EVENT_WINDOW_HIDDEN: + case SDL_EVENT_WINDOW_MINIMIZED: + minimized = true; + break; + case SDL_EVENT_WINDOW_RESIZED: + resizeCallback(handle, windowEvent.data1(), windowEvent.data2()); + break; + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + onFramebufferResize(handle, windowEvent.data1(), windowEvent.data2()); + break; + case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: fullscreen = true; windowResized = true; - } - case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN -> { + break; + case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: fullscreen = false; windowResized = true; - } - case SDL_EVENT_KEY_DOWN, SDL_EVENT_KEY_UP, SDL_EVENT_TEXT_INPUT, SDL_EVENT_TEXT_EDITING, - SDL_EVENT_TEXT_EDITING_CANDIDATES -> { + break; + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + case SDL_EVENT_TEXT_INPUT: + case SDL_EVENT_TEXT_EDITING: + case SDL_EVENT_TEXT_EDITING_CANDIDATES: if (Keyboard.isCreated()) { KeyboardSdl.instance().processKeyboardEvent(event); } - } - case SDL_EVENT_MOUSE_BUTTON_DOWN, SDL_EVENT_MOUSE_BUTTON_UP, SDL_EVENT_MOUSE_MOTION, - SDL_EVENT_MOUSE_WHEEL, SDL_EVENT_WINDOW_MOUSE_ENTER, SDL_EVENT_WINDOW_MOUSE_LEAVE -> { + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_WHEEL: + case SDL_EVENT_WINDOW_MOUSE_ENTER: + case SDL_EVENT_WINDOW_MOUSE_LEAVE: if (Mouse.isCreated()) { MouseSdl.instance().processMouseEvent(event); } - } + break; } } Keyboard.poll(); @@ -339,9 +364,9 @@ private static void checkSdlError(boolean success) { } } - public void create(@NotNull GpuSurface fallbackSurface) throws LWJGLException { - if (isCreated()) { - throw new IllegalStateException("Display has already been created"); + public synchronized void initializeVideo() { + if (videoInitialized) { + return; } SDL_SetMemoryFunctions( @@ -362,9 +387,19 @@ public void create(@NotNull GpuSurface fallbackSurface) throws LWJGLException { checkSdlError(SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_TYPE_STRING, "game")); if (!SDL_Init(SDL_INIT_VIDEO)) { - throw new IllegalStateException("Unable to initialize SDL" + SDL_GetError()); + throw new IllegalStateException("Unable to initialize SDL: " + SDL_GetError()); } + videoInitialized = true; + } + + public void create(@NotNull GpuSurface fallbackSurface) throws LWJGLException { + if (isCreated()) { + throw new IllegalStateException("Display has already been created"); + } + + initializeVideo(); + GpuSurface surface = this.surface != null ? this.surface : fallbackSurface; this.surface = surface; windowedWidth = width; @@ -427,7 +462,7 @@ public void setFullscreen(boolean fullscreen) { if (fullscreen) { int display = SDL_GetPrimaryDisplay(); if (display == 0) { - Lenis.LOG.log(System.Logger.Level.WARNING, "Failed to find display"); + Lenis.LOG.log(Level.WARNING, "Failed to find display"); return; } if (!this.fullscreen) { @@ -442,7 +477,7 @@ public void setFullscreen(boolean fullscreen) { SDL_SetWindowSize(handle, windowedWidth, windowedHeight); windowResized = true; } catch (Throwable t) { - Lenis.LOG.log(System.Logger.Level.WARNING, "Failed to set fullscreen: ", t); + Lenis.LOG.log(Level.WARNING, "Failed to set fullscreen: ", t); } } @@ -456,7 +491,7 @@ public DisplayMode[] getAvailableDisplayModes() { return new DisplayMode[0]; } - var buf = SDL_GetFullscreenDisplayModes(currDisplay); + PointerBuffer buf = SDL_GetFullscreenDisplayModes(currDisplay); if (buf == null) { throw new IllegalStateException("No display modes found"); } @@ -494,6 +529,7 @@ public void destroy() { windowEvent = null; } SDL_Quit(); + videoInitialized = false; try (MemoryStack stack = stackPush()) { PointerBuffer funcs = stack.mallocPointer(4); diff --git a/src/main/java/pl/tomgirl/lenis/window/GlSurface.java b/src/main/java/pl/tomgirl/lenis/window/GlSurface.java index 3f094d0..a02c15e 100644 --- a/src/main/java/pl/tomgirl/lenis/window/GlSurface.java +++ b/src/main/java/pl/tomgirl/lenis/window/GlSurface.java @@ -3,6 +3,8 @@ import java.nio.ByteBuffer; import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GLCapabilities; import org.lwjgl.opengl.PixelFormat; import org.lwjgl.sdl.SDLPlatform; import org.lwjgl.sdl.SDLVideo; @@ -19,7 +21,7 @@ public class GlSurface implements GpuSurface { private long window; private long context; private boolean glInitialized; - private boolean capabilitiesCreated; + private GLCapabilities capabilities; public GlSurface(PixelFormat pixelFormat) { this.pixelFormat = pixelFormat; @@ -55,8 +57,7 @@ public long createWindow(String title, int width, int height, boolean resizable) Configuration.OPENGL_EXPLICIT_INIT.set(true); GL.create(SDLVideo::SDL_GL_GetProcAddress); glInitialized = true; - GL.createCapabilities(MemoryUtil::memCallocPointer); - capabilitiesCreated = true; + capabilities = GL.createCapabilities(MemoryUtil::memCallocPointer); return window; } catch (RuntimeException | Error throwable) { destroy(); @@ -79,12 +80,21 @@ private void configureContext() { @Override public void makeCurrent() { + if ("macOS".equals(SDLPlatform.SDL_GetPlatform()) + && SDL_GL_GetCurrentContext() == context + && SDL_GL_GetCurrentWindow() == window + && GL11.glGetString(GL11.GL_VERSION) == null + ) { + check(SDL_GL_MakeCurrent(window, 0)); + } check(SDL_GL_MakeCurrent(window, context)); + GL.setCapabilities(capabilities); } @Override public void releaseCurrent() { check(SDL_GL_MakeCurrent(window, 0)); + GL.setCapabilities(null); } @Override @@ -101,10 +111,10 @@ public void setVSyncEnabled(boolean enabled) { @Override public void destroy() { - if (capabilitiesCreated) { - memFree(GL.getCapabilities().getAddressBuffer()); + if (capabilities != null) { + memFree(capabilities.getAddressBuffer()); GL.setCapabilities(null); - capabilitiesCreated = false; + capabilities = null; } if (glInitialized) { GL.destroy(); diff --git a/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java b/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java index f87d99c..9ed2652 100644 --- a/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java +++ b/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java @@ -2,7 +2,7 @@ import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Objects; +import java.util.logging.Level; import pl.tomgirl.lenis.Lenis; import org.lwjgl.input.Keyboard; @@ -85,20 +85,24 @@ public int getNumKeyboardEvents(boolean includeRepeats) { public void processKeyboardEvent(SDL_Event event) { switch (event.type()) { - case SDL_EVENT_KEY_DOWN, SDL_EVENT_KEY_UP -> { + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: { int key = translateKeyFromSDL(keyboardEvent.scancode()); byte state = keyboardEvent.down() ? (byte) 1 : 0; if (key != Keyboard.KEY_NONE) { this.keyDownBuffer[key] = state; } putKeyboardEvent(key, state, 0, keyboardEvent.timestamp(), keyboardEvent.repeat()); + break; + } + case SDL_EVENT_TEXT_INPUT: { + String text = textInputEvent.textString(); + if (text == null) text = ""; + text.chars().forEach(character -> + putKeyboardEvent(TEXT_EVENT_KEY, (byte) -1, character, textInputEvent.timestamp(), false) + ); + break; } - case SDL_EVENT_TEXT_INPUT -> - Objects.requireNonNullElse(textInputEvent.textString(), "") - .chars() - .forEach(character -> - putKeyboardEvent(TEXT_EVENT_KEY, (byte) -1, character, textInputEvent.timestamp(), false) - ); } } @@ -108,7 +112,7 @@ private int translateKeyFromSDL(int key) { if (translated != Keymap.UNMAPPED) { return translated; } - Lenis.LOG.log(System.Logger.Level.WARNING, "Untranslated key: {0} ({1})", key, SDLKeyboard.SDL_GetScancodeName(key)); + Lenis.LOG.log(Level.WARNING, "Untranslated key: {0} ({1})", new Object[]{key, SDLKeyboard.SDL_GetScancodeName(key)}); return Keyboard.KEY_NONE; } diff --git a/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java b/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java index 8866dc8..2764870 100644 --- a/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java +++ b/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java @@ -152,14 +152,21 @@ public boolean isInsideWindow() { public void processMouseEvent(SDL_Event event) { switch (event.type()) { - case SDL_EVENT_MOUSE_BUTTON_UP, SDL_EVENT_MOUSE_BUTTON_DOWN -> { + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_BUTTON_DOWN: { boolean down = buttonEvent.down(); long timestamp = buttonEvent.timestamp(); - byte button = switch (buttonEvent.button()) { - case SDL_BUTTON_RIGHT -> 1; - case SDL_BUTTON_MIDDLE -> 2; - default -> (byte) (buttonEvent.button() - 1); - }; + byte button; + switch (buttonEvent.button()) { + case SDL_BUTTON_RIGHT: + button = 1; + break; + case SDL_BUTTON_MIDDLE: + button = 2; + break; + default: + button = (byte) (buttonEvent.button() - 1); + } if (grabbed) { putMouseEvent(button, down, 0, timestamp); @@ -176,8 +183,9 @@ public void processMouseEvent(SDL_Event event) { if (button >= 0 && button < buttonStates.length) { buttonStates[button] = down; } + break; } - case SDL_EVENT_MOUSE_WHEEL -> { + case SDL_EVENT_MOUSE_WHEEL: { int yOffset = wheelEvent.integer_y(); if (yOffset == 0) { break; @@ -195,8 +203,9 @@ public void processMouseEvent(SDL_Event event) { wheelEvent.timestamp() ); } + break; } - case SDL_EVENT_MOUSE_MOTION -> { + case SDL_EVENT_MOUSE_MOTION: { double x = toLwjglX(motionEvent.x()); double y = toLwjglY(motionEvent.y()); double dx = motionEvent.xrel() * scaleX(); @@ -213,9 +222,14 @@ public void processMouseEvent(SDL_Event event) { putMouseMotionEvent(x, y, nanos); } } + break; } - case SDL_EVENT_WINDOW_MOUSE_ENTER -> insideWindow = true; - case SDL_EVENT_WINDOW_MOUSE_LEAVE -> insideWindow = false; + case SDL_EVENT_WINDOW_MOUSE_ENTER: + insideWindow = true; + break; + case SDL_EVENT_WINDOW_MOUSE_LEAVE: + insideWindow = false; + break; } } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 28f6314..e23cf6e 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -18,6 +18,6 @@ "depends": { "fabricloader": ">=0.17.3", "minecraft": "<1.13.0-alpha.17.43.a", - "java": ">=21" + "java": ">=8" } } From bded6007df8194f78230fe967fc52fb38ab8c444 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:42:48 +0200 Subject: [PATCH 05/12] feat: GLU funcs --- src/main/java/org/lwjgl/util/glu/GLU.java | 9 + src/main/java/org/lwjgl/util/glu/MipMap.java | 237 ++++++++++++++++++ .../org/lwjgl/util/glu/PixelStoreState.java | 40 +++ 3 files changed, 286 insertions(+) create mode 100644 src/main/java/org/lwjgl/util/glu/MipMap.java create mode 100644 src/main/java/org/lwjgl/util/glu/PixelStoreState.java diff --git a/src/main/java/org/lwjgl/util/glu/GLU.java b/src/main/java/org/lwjgl/util/glu/GLU.java index 879c5d6..72f1039 100644 --- a/src/main/java/org/lwjgl/util/glu/GLU.java +++ b/src/main/java/org/lwjgl/util/glu/GLU.java @@ -2,6 +2,7 @@ import org.lwjgl.opengl.Util; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; @@ -156,6 +157,14 @@ public static boolean gluCheckExtension(String extName, String extString) { return Registry.gluCheckExtension(extName, extString); } + public static int gluBuild2DMipmaps(int target, int components, int width, int height, int format, int type, ByteBuffer data) { + return MipMap.gluBuild2DMipmaps(target, components, width, height, format, type, data); + } + + public static int gluScaleImage(int format, int widthIn, int heightIn, int typeIn, ByteBuffer dataIn, int widthOut, int heightOut, int typeOut, ByteBuffer dataOut) { + return MipMap.gluScaleImage(format, widthIn, heightIn, typeIn, dataIn, widthOut, heightOut, typeOut, dataOut); + } + public static String gluErrorString(int error_code) { switch (error_code) { case GLU_INVALID_ENUM: diff --git a/src/main/java/org/lwjgl/util/glu/MipMap.java b/src/main/java/org/lwjgl/util/glu/MipMap.java new file mode 100644 index 0000000..61d203a --- /dev/null +++ b/src/main/java/org/lwjgl/util/glu/MipMap.java @@ -0,0 +1,237 @@ +package org.lwjgl.util.glu; + +import org.lwjgl.BufferUtils; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.util.glu.GLU.*; + +public class MipMap extends Util { + public static int gluBuild2DMipmaps(int target, int components, int width, int height, int format, int type, ByteBuffer data) { + if (width < 1 || height < 1) { + return GLU_INVALID_VALUE; + } + + int bpp = bytesPerPixel(format, type); + if (bpp == 0) { + return GLU_INVALID_ENUM; + } + + int maxSize = glGetIntegerv(GL_MAX_TEXTURE_SIZE); + int w = nearestPower(width); + if (w > maxSize) { + w = maxSize; + } + + int h = nearestPower(height); + if (h > maxSize) { + h = maxSize; + } + + PixelStoreState pss = new PixelStoreState(); + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); + glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + + int err = 0; + boolean done = false; + ByteBuffer image; + if (w == width && h == height) { + image = data; + } else { + image = BufferUtils.createByteBuffer((w + 4) * h * bpp); + int error = gluScaleImage(format, width, height, type, data, w, h, type, image); + if (error != 0) { + err = error; + done = true; + } + } + + ByteBuffer scratch = image != data ? image : null; + + for (int level = 0; !done; level++) { + if (image != data) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + } + + glTexImage2D(target, level, components, w, h, 0, format, type, image); + if (w == 1 && h == 1) { + break; + } + + int newW = Math.max(1, w >> 1); + int newH = Math.max(1, h >> 1); + if (scratch == null) { + scratch = BufferUtils.createByteBuffer((newW + 4) * newH * bpp); + } + + int error = gluScaleImage(format, w, h, type, image, newW, newH, type, scratch); + if (error != 0) { + err = error; + break; + } + + image = scratch; + w = newW; + h = newH; + } + + pss.save(); + return err; + } + + public static int gluScaleImage(int format, int widthIn, int heightIn, int typein, ByteBuffer dataIn, int widthOut, int heightOut, int typeOut, ByteBuffer dataOut) { + int components = compPerPix(format); + if (components == -1) { + return GLU_INVALID_ENUM; + } + + float[] tempIn = new float[widthIn * heightIn * components]; + float[] tempOut = new float[widthOut * heightOut * components]; + + int sizein; + switch (typein) { + case GL_UNSIGNED_BYTE: + sizein = 1; + break; + case GL_FLOAT: + sizein = 4; + break; + default: + return GL_INVALID_ENUM; + } + + int sizeout; + switch (typeOut) { + case GL_UNSIGNED_BYTE: + sizeout = 1; + break; + case GL_FLOAT: + sizeout = 4; + break; + default: + return GL_INVALID_ENUM; + } + + PixelStoreState pss = new PixelStoreState(); + int rowlen; + if (pss.unpackRowLength > 0) { + rowlen = pss.unpackRowLength; + } else { + rowlen = widthIn; + } + + int rowstride; + if (sizein >= pss.unpackAlignment) { + rowstride = components * rowlen; + } else { + rowstride = pss.unpackAlignment / sizein * ceil(components * rowlen * sizein, pss.unpackAlignment); + } + + int temp = 0; + switch (typein) { + case GL_UNSIGNED_BYTE: + dataIn.rewind(); + for (int i = 0; i < heightIn; i++ ) { + int ubptr = (i + pss.unpackSkipRows) * rowstride + pss.unpackSkipPixels * components; + for (int j = 0; j < widthIn * components; j++ ) { + tempIn[temp++] = dataIn.get(ubptr++) & 0xff; + } + } + break; + case GL_FLOAT: + dataIn.rewind(); + for (int i = 0; i < heightIn; i++ ) { + int fptr = 4 * ((i + pss.unpackSkipRows) * rowstride + pss.unpackSkipPixels * components); + for (int j = 0; j < widthIn * components; j++ ) { + tempIn[temp++] = dataIn.getFloat(fptr); + fptr += 4; + } + } + break; + } + + float sx = (float)widthIn / widthOut; + float sy = (float)heightIn / heightOut; + float[] c = new float[components]; + + for (int iy = 0; iy < heightOut; iy++) { + for (int ix = 0; ix < widthOut; ix++) { + int x0 = (int)(ix * sx); + int x1 = (int)((ix + 1) * sx); + int y0 = (int)(iy * sy); + int y1 = (int)((iy + 1) * sy); + int readPix = 0; + + Arrays.fill(c, 0.0F); + + for (int ix0 = x0; ix0 < x1; ix0++) { + for (int iy0 = y0; iy0 < y1; iy0++) { + int src = (iy0 * widthIn + ix0) * components; + + for (int ic = 0; ic < components; ic++) { + c[ic] += tempIn[src + ic]; + } + + readPix++; + } + } + + int dst = (iy * widthOut + ix) * components; + if (readPix == 0) { + int src = (y0 * widthIn + x0) * components; + + for (int ic = 0; ic < components; ic++) { + tempOut[dst++] = tempIn[src + ic]; + } + } else { + for (int i = 0; i < components; i++) { + tempOut[dst++] = c[i] / readPix; + } + } + } + } + + if (pss.packRowLength > 0) { + rowlen = pss.packRowLength; + } else { + rowlen = widthOut; + } + + if (sizeout >= pss.packAlignment) { + rowstride = components * rowlen; + } else { + rowstride = pss.packAlignment / sizeout * ceil(components * rowlen * sizeout, pss.packAlignment); + } + + temp = 0; + switch (typeOut) { + case GL_UNSIGNED_BYTE: + for (int i = 0; i < heightOut; i++) { + int ubptr = (i + pss.packSkipRows) * rowstride + pss.packSkipPixels * components; + + for (int j = 0; j < widthOut * components; j++) { + dataOut.put(ubptr++, (byte)tempOut[temp++]); + } + } + break; + case GL_FLOAT: + for (int i = 0; i < heightOut; i++) { + int fptr = 4 * ((i + pss.unpackSkipRows) * rowstride + pss.unpackSkipPixels * components); + + for (int j = 0; j < widthOut * components; j++) { + dataOut.putFloat(fptr, tempOut[temp++]); + fptr += 4; + } + } + break; + } + + return 0; + } +} diff --git a/src/main/java/org/lwjgl/util/glu/PixelStoreState.java b/src/main/java/org/lwjgl/util/glu/PixelStoreState.java new file mode 100644 index 0000000..a521a27 --- /dev/null +++ b/src/main/java/org/lwjgl/util/glu/PixelStoreState.java @@ -0,0 +1,40 @@ +package org.lwjgl.util.glu; + +import static org.lwjgl.opengl.GL11.*; + +class PixelStoreState extends Util { + public int unpackRowLength; + public int unpackAlignment; + public int unpackSkipRows; + public int unpackSkipPixels; + public int packRowLength; + public int packAlignment; + public int packSkipRows; + public int packSkipPixels; + + PixelStoreState() { + this.load(); + } + + public void load() { + this.unpackRowLength = glGetIntegerv(GL_UNPACK_ROW_LENGTH); + this.unpackAlignment = glGetIntegerv(GL_UNPACK_ALIGNMENT); + this.unpackSkipRows = glGetIntegerv(GL_UNPACK_SKIP_ROWS); + this.unpackSkipPixels = glGetIntegerv(GL_UNPACK_SKIP_PIXELS); + this.packRowLength = glGetIntegerv(GL_PACK_ROW_LENGTH); + this.packAlignment = glGetIntegerv(GL_PACK_ALIGNMENT); + this.packSkipRows = glGetIntegerv(GL_PACK_SKIP_ROWS); + this.packSkipPixels = glGetIntegerv(GL_PACK_SKIP_PIXELS); + } + + public void save() { + glPixelStorei(GL_UNPACK_ROW_LENGTH, this.unpackRowLength); + glPixelStorei(GL_UNPACK_ALIGNMENT, this.unpackAlignment); + glPixelStorei(GL_UNPACK_SKIP_ROWS, this.unpackSkipRows); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, this.unpackSkipPixels); + glPixelStorei(GL_PACK_ROW_LENGTH, this.packRowLength); + glPixelStorei(GL_PACK_ALIGNMENT, this.packAlignment); + glPixelStorei(GL_PACK_SKIP_ROWS, this.packSkipRows); + glPixelStorei(GL_PACK_SKIP_PIXELS, this.packSkipPixels); + } +} From 5290fcc079110c0543c89633ba1aa1b0525d06a6 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:17:42 +0200 Subject: [PATCH 06/12] fix: clipboard patch bytecode corruption --- .../java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java b/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java index b8e5452..1fe7bc4 100644 --- a/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java +++ b/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java @@ -51,16 +51,17 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str ClipboardMethod patch = methods.get(new Method(name, descriptor)); if (patch == null) return super.visitMethod(access, name, descriptor, signature, exceptions); MethodVisitor method = super.visitMethod(access, name, descriptor, signature, exceptions); + int argumentSlot = (access & Opcodes.ACC_STATIC) != 0 ? 0 : 1; method.visitCode(); if (patch == ClipboardMethod.GET) { method.visitMethodInsn(Opcodes.INVOKESTATIC, HOOKS, "getClipboard", "()Ljava/lang/String;", false); method.visitInsn(Opcodes.ARETURN); } else { - method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitVarInsn(Opcodes.ALOAD, argumentSlot); method.visitMethodInsn(Opcodes.INVOKESTATIC, HOOKS, "setClipboard", "(Ljava/lang/String;)V", false); method.visitInsn(Opcodes.RETURN); } - method.visitMaxs(1, patch == ClipboardMethod.GET ? 0 : 1); + method.visitMaxs(1, argumentSlot + (patch == ClipboardMethod.GET ? 0 : 1)); method.visitEnd(); return null; } From 59c54196a21e3550447b1a83bab3641d42795e20 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:30:30 +0200 Subject: [PATCH 07/12] fix: more forge compatibility --- .../pl/tomgirl/lenis/build/BakeLwjglTask.kt | 11 +++ src/main/java/org/lwjgl/opengl/Display.java | 13 ++++ src/main/java/org/lwjgl/opengl/Drawable.java | 1 + .../java/org/lwjgl/opengl/SharedDrawable.java | 67 +++++++++++++++++-- .../pl/tomgirl/lenis/window/DisplaySdl.java | 16 +++++ 5 files changed, 102 insertions(+), 6 deletions(-) diff --git a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt index 374b430..1c405b4 100644 --- a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt +++ b/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt @@ -83,6 +83,17 @@ abstract class BakeLwjglTask : DefaultTask() { method.invisibleAnnotations?.removeIf { it.desc == STUB } target.methods.add(method) } + // forge workaround + if (target.name == "org/lwjgl/opengl/GL11") { + val getString = target.find("glGetString", "(I)Ljava/lang/String;") ?: error("Missing GL11.glGetString") + getString.instructions.insert(InsnList().apply { + add(MethodInsnNode( + Opcodes.INVOKESTATIC, "org/lwjgl/opengl/GL", "getCapabilities", + "()Lorg/lwjgl/opengl/GLCapabilities;", false, + )) + add(InsnNode(Opcodes.POP)) + }) + } if (target.name == "org/lwjgl/openal/AL") { val destroy = target.find("destroy", "()V") ?: error("Missing AL.destroy") destroy.access = destroy.access and (Opcodes.ACC_PRIVATE or Opcodes.ACC_PROTECTED).inv() or Opcodes.ACC_PUBLIC diff --git a/src/main/java/org/lwjgl/opengl/Display.java b/src/main/java/org/lwjgl/opengl/Display.java index b56f2fa..e2ef6a7 100644 --- a/src/main/java/org/lwjgl/opengl/Display.java +++ b/src/main/java/org/lwjgl/opengl/Display.java @@ -70,6 +70,10 @@ public static void create(@NotNull PixelFormat pixelFormat) throws LWJGLExceptio } } + public static boolean isFullscreen() { + return SDL.isFullscreen(); + } + public static void setFullscreen(boolean fullscreen) { SDL.setFullscreen(fullscreen); } @@ -87,6 +91,15 @@ public static boolean isCreated() { return SDL.isCreated(); } + public static boolean isCurrent() { + try { + return SDL.getDrawable().isCurrent(); + } catch (LWJGLException e) { + e.printStackTrace(); + return false; + } + } + public static boolean isActive() { return SDL.isActive(); } diff --git a/src/main/java/org/lwjgl/opengl/Drawable.java b/src/main/java/org/lwjgl/opengl/Drawable.java index f9a7101..10ea4c2 100644 --- a/src/main/java/org/lwjgl/opengl/Drawable.java +++ b/src/main/java/org/lwjgl/opengl/Drawable.java @@ -4,6 +4,7 @@ @SuppressWarnings("unused") public interface Drawable { + boolean isCurrent() throws LWJGLException; void makeCurrent() throws LWJGLException; void releaseContext() throws LWJGLException; void destroy(); diff --git a/src/main/java/org/lwjgl/opengl/SharedDrawable.java b/src/main/java/org/lwjgl/opengl/SharedDrawable.java index b3d921a..fb3d8c6 100644 --- a/src/main/java/org/lwjgl/opengl/SharedDrawable.java +++ b/src/main/java/org/lwjgl/opengl/SharedDrawable.java @@ -2,26 +2,81 @@ import org.lwjgl.LWJGLException; +import static org.lwjgl.sdl.SDLError.SDL_GetError; +import static org.lwjgl.sdl.SDLVideo.*; +import static org.lwjgl.system.MemoryUtil.memFree; + @SuppressWarnings("unused") public class SharedDrawable implements Drawable { - private final Drawable drawable; + private final long window; + private long context; + private GLCapabilities capabilities; + + public SharedDrawable(final Drawable drawable) throws LWJGLException { + long previousWindow = SDL_GL_GetCurrentWindow(); + long previousContext = SDL_GL_GetCurrentContext(); + GLCapabilities previousCapabilities = previousContext == 0 ? null : GL.getCapabilities(); + try { + drawable.makeCurrent(); + window = SDL_GL_GetCurrentWindow(); + check(SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1)); + try { + context = SDL_GL_CreateContext(window); + check(context != 0); + } finally { + SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0); + } + capabilities = GL.createCapabilities(org.lwjgl.system.MemoryUtil::memCallocPointer); + } catch (LWJGLException | RuntimeException | Error e) { + destroy(); + throw e; + } finally { + check(SDL_GL_MakeCurrent(previousWindow, previousContext)); + GL.setCapabilities(previousCapabilities); + } + } - public SharedDrawable(final Drawable drawable) { - this.drawable = drawable; + @Override + public boolean isCurrent() throws LWJGLException { + return context != 0 && SDL_GL_GetCurrentContext() == context; } @Override public void makeCurrent() throws LWJGLException { - drawable.makeCurrent(); + if (context == 0) { + throw new LWJGLException("Shared context has been destroyed"); + } + check(SDL_GL_MakeCurrent(window, context)); + GL.setCapabilities(capabilities); } @Override public void releaseContext() throws LWJGLException { - drawable.releaseContext(); + if (isCurrent()) { + check(SDL_GL_MakeCurrent(window, 0)); + GL.setCapabilities(null); + } } @Override public void destroy() { - drawable.destroy(); + if (context != 0) { + if (SDL_GL_GetCurrentContext() == context) { + SDL_GL_MakeCurrent(window, 0); + GL.setCapabilities(null); + } + SDL_GL_DestroyContext(context); + context = 0; + } + if (capabilities != null) { + memFree(capabilities.getAddressBuffer()); + capabilities = null; + } + } + + private static void check(boolean success) throws LWJGLException { + if (!success) { + throw new LWJGLException("SDL error encountered: " + SDL_GetError()); + } } } diff --git a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java b/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java index 781c4d8..8d4cb53 100644 --- a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java +++ b/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java @@ -289,6 +289,10 @@ public void update() { } public void processMessages() { + if (!SDL_IsMainThread()) { + return; + } + windowResized = false; while (SDL_PollEvent(event)) { switch (event.type()) { @@ -427,6 +431,9 @@ public void create(@NotNull GpuSurface fallbackSurface) throws LWJGLException { Keyboard.create(); checkSdlError(SDL_ShowWindow(handle)); checkSdlError(SDL_RaiseWindow(handle)); + if (SDL_IsMainThread()) { + SDL_PumpEvents(); + } focused = (SDL_GetWindowFlags(handle) & SDL_WINDOW_INPUT_FOCUS) != 0; updateTextInputState(); if (cachedIcons != null) { @@ -452,6 +459,10 @@ private void onFramebufferResize(long window, int framebufferWidth, int framebuf } } + public boolean isFullscreen() { + return this.fullscreen; + } + public void setFullscreen(boolean fullscreen) { if (!isCreated()) { fullscreenDeferred = fullscreen; @@ -599,6 +610,11 @@ private GpuSurface requireSurface() { } private final class SurfaceDrawable implements Drawable { + @Override + public boolean isCurrent() { + return false; + } + @Override public void makeCurrent() { DisplaySdl.this.makeCurrent(); From 35fde0bc87744da0bb5ad9329adec4c7a128b32c Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:36:41 +0200 Subject: [PATCH 08/12] feat: rdh marketing team rebrand --- .github/workflows/nightly.yml | 2 +- .gitignore | 3 ++- README.md | 16 +++++++--------- build-logic/build.gradle.kts | 6 +++--- build-logic/settings.gradle.kts | 2 +- .../bakery/patch/AL10Patch.java | 2 +- .../{lenis => pylon}/bakery/patch/ALPatch.java | 2 +- .../bakery/patch/ARBShaderObjectsPatch.java | 2 +- .../bakery/patch/CompatStub.java | 2 +- .../bakery/patch/GL11Patch.java | 2 +- .../bakery/patch/GL20Patch.java | 2 +- .../{lenis => pylon}/build/BakeLwjglTask.kt | 4 ++-- .../build/PylonBuildPlugin.kt} | 14 +++++++------- build.gradle.kts | 2 +- gradle.properties | 4 ++-- runs/logging.properties | 2 +- runs/vanilla/build.gradle.kts | 6 +++--- settings.gradle.kts | 2 +- .../{lenis => pylon}/fabric/MixinPlugin.java | 2 +- .../fabric/MixinTransformer.java | 6 +++--- .../fabric/MixinTransformerDelegate.java | 2 +- .../{lenis => pylon}/fabric/PreLaunch.java | 2 +- .../forge/PylonTransformer.java} | 8 ++++---- .../forge/PylonTweaker.java} | 16 ++++++++-------- src/main/java/org/lwjgl/LWJGLUtil.java | 2 +- src/main/java/org/lwjgl/Sys.java | 2 +- src/main/java/org/lwjgl/input/Keyboard.java | 2 +- src/main/java/org/lwjgl/input/Mouse.java | 2 +- src/main/java/org/lwjgl/opengl/Display.java | 8 ++++---- src/main/java/pl/tomgirl/lenis/Lenis.java | 9 --------- .../java/pl/tomgirl/{lenis => pylon}/Agent.java | 8 ++++---- .../pl/tomgirl/{lenis => pylon}/Platform.java | 4 ++-- src/main/java/pl/tomgirl/pylon/Pylon.java | 9 +++++++++ .../{lenis => pylon}/game/GameHooks.java | 9 ++++----- .../{lenis => pylon}/game/GameTransformer.java | 10 +++++----- .../{lenis => pylon}/game/patch/Patch.java | 4 ++-- .../game/patch/impl/MinecraftPatch.java | 6 +++--- .../game/patch/impl/ScreenPatch.java | 4 ++-- .../{lenis => pylon}/window/BoundedQueue.java | 2 +- .../{lenis => pylon}/window/DisplaySdl.java | 10 +++++----- .../{lenis => pylon}/window/GlSurface.java | 2 +- .../{lenis => pylon}/window/GpuSurface.java | 2 +- .../{lenis => pylon}/window/KeyboardSdl.java | 6 +++--- .../tomgirl/{lenis => pylon}/window/Keymap.java | 2 +- .../{lenis => pylon}/window/MouseSdl.java | 4 ++-- .../resources/assets/{lenis => pylon}/icon.png | Bin src/main/resources/fabric.mod.json | 10 +++++----- .../{lenis.mixins.json => pylon.mixins.json} | 2 +- 48 files changed, 114 insertions(+), 116 deletions(-) rename build-logic/src/baked-patches/java/pl/tomgirl/{lenis => pylon}/bakery/patch/AL10Patch.java (91%) rename build-logic/src/baked-patches/java/pl/tomgirl/{lenis => pylon}/bakery/patch/ALPatch.java (98%) rename build-logic/src/baked-patches/java/pl/tomgirl/{lenis => pylon}/bakery/patch/ARBShaderObjectsPatch.java (92%) rename build-logic/src/baked-patches/java/pl/tomgirl/{lenis => pylon}/bakery/patch/CompatStub.java (88%) rename build-logic/src/baked-patches/java/pl/tomgirl/{lenis => pylon}/bakery/patch/GL11Patch.java (98%) rename build-logic/src/baked-patches/java/pl/tomgirl/{lenis => pylon}/bakery/patch/GL20Patch.java (97%) rename build-logic/src/main/kotlin/pl/tomgirl/{lenis => pylon}/build/BakeLwjglTask.kt (98%) rename build-logic/src/main/kotlin/pl/tomgirl/{lenis/build/LenisBuildPlugin.kt => pylon/build/PylonBuildPlugin.kt} (94%) rename src/fabric/java/pl/tomgirl/{lenis => pylon}/fabric/MixinPlugin.java (98%) rename src/fabric/java/pl/tomgirl/{lenis => pylon}/fabric/MixinTransformer.java (87%) rename src/fabric/java/pl/tomgirl/{lenis => pylon}/fabric/MixinTransformerDelegate.java (98%) rename src/fabric/java/pl/tomgirl/{lenis => pylon}/fabric/PreLaunch.java (87%) rename src/forge/java/pl/tomgirl/{lenis/forge/LenisTransformer.java => pylon/forge/PylonTransformer.java} (50%) rename src/forge/java/pl/tomgirl/{lenis/forge/LenisTweaker.java => pylon/forge/PylonTweaker.java} (87%) delete mode 100644 src/main/java/pl/tomgirl/lenis/Lenis.java rename src/main/java/pl/tomgirl/{lenis => pylon}/Agent.java (82%) rename src/main/java/pl/tomgirl/{lenis => pylon}/Platform.java (94%) create mode 100644 src/main/java/pl/tomgirl/pylon/Pylon.java rename src/main/java/pl/tomgirl/{lenis => pylon}/game/GameHooks.java (89%) rename src/main/java/pl/tomgirl/{lenis => pylon}/game/GameTransformer.java (86%) rename src/main/java/pl/tomgirl/{lenis => pylon}/game/patch/Patch.java (94%) rename src/main/java/pl/tomgirl/{lenis => pylon}/game/patch/impl/MinecraftPatch.java (97%) rename src/main/java/pl/tomgirl/{lenis => pylon}/game/patch/impl/ScreenPatch.java (97%) rename src/main/java/pl/tomgirl/{lenis => pylon}/window/BoundedQueue.java (97%) rename src/main/java/pl/tomgirl/{lenis => pylon}/window/DisplaySdl.java (98%) rename src/main/java/pl/tomgirl/{lenis => pylon}/window/GlSurface.java (99%) rename src/main/java/pl/tomgirl/{lenis => pylon}/window/GpuSurface.java (88%) rename src/main/java/pl/tomgirl/{lenis => pylon}/window/KeyboardSdl.java (97%) rename src/main/java/pl/tomgirl/{lenis => pylon}/window/Keymap.java (99%) rename src/main/java/pl/tomgirl/{lenis => pylon}/window/MouseSdl.java (99%) rename src/main/resources/assets/{lenis => pylon}/icon.png (100%) rename src/main/resources/{lenis.mixins.json => pylon.mixins.json} (53%) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 16e3167..e977170 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -26,5 +26,5 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v7 with: - name: lenis-nightly + name: pylon-nightly path: build/libs diff --git a/.gitignore b/.gitignore index 4b7e0ec..28456e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle .DS_Store .idea/ +.kotlin/ build/ -!**/tomgirl/lenis/build +!**/tomgirl/pylon/build run/ \ No newline at end of file diff --git a/README.md b/README.md index f4f36e3..d7cccbc 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ -Lenis logo +Pylon logo -# Lenis -Lenis allows legacy Minecraft versions to run with LWJGL 3. It replaces the outdated windowing system with SDL 3, and serves as a library for mods. - -Currently only 1.8.9 on Ornithe is supported, with support for more versions coming soon. +# Pylon +Pylon allows legacy Minecraft versions to run with LWJGL 3. It replaces the outdated windowing system with SDL 3, and serves as a library for mods. ## Downloads -* [Latest Release](https://github.com/notdevcody/lenis/releases/latest) -* [Latest Nightly](https://nightly.link/notdevcody/lenis/workflows/nightly/main/lenis-nightly.zip) +* [Latest Release](https://github.com/notdevcody/pylon/releases/latest) +* [Latest Nightly](https://nightly.link/notdevcody/pylon/workflows/nightly/main/pylon-nightly.zip) ## Features ### Fixes @@ -24,13 +22,13 @@ Currently only 1.8.9 on Ornithe is supported, with support for more versions com - Better fullscreen and window resizing ## Developers -Lenis is published to the Clover Client Maven, and can be added as a dependency like so: +Pylon is published to the Clover Client Maven, and can be added as a dependency like so: ```kts repositories { maven("https://maven.cloverclient.com/releases") } dependencies { - modImplementation("pl.tomgirl:lenis:${version}") + modImplementation("pl.tomgirl:pylon:${version}") } ``` diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index e9b8a9d..3bbb5c1 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -15,9 +15,9 @@ dependencies { gradlePlugin { plugins { - create("lenisBuild") { - id = "lenis.build" - implementationClass = "pl.tomgirl.lenis.build.LenisBuildPlugin" + create("pylonBuild") { + id = "pylon.build" + implementationClass = "pl.tomgirl.pylon.build.PylonBuildPlugin" } } } diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index c6aa055..0a4b371 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "lenis-build-logic" +rootProject.name = "pylon-build-logic" diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/AL10Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/AL10Patch.java similarity index 91% rename from build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/AL10Patch.java rename to build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/AL10Patch.java index 51258d7..6559d8a 100644 --- a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/AL10Patch.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/AL10Patch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.bakery.patch; +package pl.tomgirl.pylon.bakery.patch; import java.nio.FloatBuffer; import java.nio.IntBuffer; diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ALPatch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ALPatch.java similarity index 98% rename from build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ALPatch.java rename to build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ALPatch.java index 03d8e3c..e79b39f 100644 --- a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ALPatch.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ALPatch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.bakery.patch; +package pl.tomgirl.pylon.bakery.patch; import java.nio.IntBuffer; diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ARBShaderObjectsPatch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ARBShaderObjectsPatch.java similarity index 92% rename from build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ARBShaderObjectsPatch.java rename to build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ARBShaderObjectsPatch.java index eb58f9a..af67e7c 100644 --- a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/ARBShaderObjectsPatch.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ARBShaderObjectsPatch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.bakery.patch; +package pl.tomgirl.pylon.bakery.patch; import java.nio.FloatBuffer; import java.nio.IntBuffer; diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/CompatStub.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/CompatStub.java similarity index 88% rename from build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/CompatStub.java rename to build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/CompatStub.java index e63b3ed..5eaf7d3 100644 --- a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/CompatStub.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/CompatStub.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.bakery.patch; +package pl.tomgirl.pylon.bakery.patch; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL11Patch.java similarity index 98% rename from build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java rename to build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL11Patch.java index eefb176..cba8472 100644 --- a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL11Patch.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL11Patch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.bakery.patch; +package pl.tomgirl.pylon.bakery.patch; import java.nio.ByteBuffer; import java.nio.FloatBuffer; diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL20Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL20Patch.java similarity index 97% rename from build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL20Patch.java rename to build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL20Patch.java index aa4662f..d337aee 100644 --- a/build-logic/src/baked-patches/java/pl/tomgirl/lenis/bakery/patch/GL20Patch.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL20Patch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.bakery.patch; +package pl.tomgirl.pylon.bakery.patch; import java.nio.FloatBuffer; import java.nio.IntBuffer; diff --git a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/BakeLwjglTask.kt similarity index 98% rename from build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt rename to build-logic/src/main/kotlin/pl/tomgirl/pylon/build/BakeLwjglTask.kt index 1c405b4..25eb1a2 100644 --- a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/BakeLwjglTask.kt +++ b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/BakeLwjglTask.kt @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.build +package pl.tomgirl.pylon.build import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection @@ -146,7 +146,7 @@ abstract class BakeLwjglTask : DefaultTask() { methods.find { it.name == name && it.desc == descriptor } private companion object { - const val TEMPLATES = "pl/tomgirl/lenis/bakery/patch/" + const val TEMPLATES = "pl/tomgirl/pylon/bakery/patch/" const val STUB = "L${TEMPLATES}CompatStub;" const val CAPABILITIES = "org/lwjgl/opengl/GLCapabilities" val patches = mapOf( diff --git a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt similarity index 94% rename from build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt rename to build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt index a605604..abeb40b 100644 --- a/build-logic/src/main/kotlin/pl/tomgirl/lenis/build/LenisBuildPlugin.kt +++ b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.build +package pl.tomgirl.pylon.build import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.api.Plugin @@ -14,7 +14,7 @@ import org.gradle.api.tasks.compile.JavaCompile import org.gradle.kotlin.dsl.* import org.gradle.language.jvm.tasks.ProcessResources -class LenisBuildPlugin : Plugin { +class PylonBuildPlugin : Plugin { override fun apply(project: Project) = with(project) { pluginManager.apply("java-library") pluginManager.apply("maven-publish") @@ -97,15 +97,15 @@ class LenisBuildPlugin : Plugin { archiveClassifier.set("slim") destinationDirectory.set(layout.buildDirectory.dir("intermediates")) manifest.attributes( - "Premain-Class" to "pl.tomgirl.lenis.Agent", - "TweakClass" to "pl.tomgirl.lenis.forge.LenisTweaker", + "Premain-Class" to "pl.tomgirl.pylon.Agent", + "TweakClass" to "pl.tomgirl.pylon.forge.PylonTweaker", ) } val shadowJar = tasks.named("shadowJar") { archiveClassifier.set("relocated") destinationDirectory.set(layout.buildDirectory.dir("intermediates")) - relocate("org.objectweb.asm", "pl.tomgirl.lenis.internal.asm") + relocate("org.objectweb.asm", "pl.tomgirl.pylon.internal.asm") exclude("module-info.class", "META-INF/versions/**/module-info.class") exclude("META-INF/*.SF", "META-INF/*.RSA", "META-INF/*.DSA") mergeServiceFiles() @@ -120,8 +120,8 @@ class LenisBuildPlugin : Plugin { archiveClassifier.set("unpatched") destinationDirectory.set(layout.buildDirectory.dir("intermediates")) manifest.attributes( - "Premain-Class" to "pl.tomgirl.lenis.Agent", - "TweakClass" to "pl.tomgirl.lenis.forge.LenisTweaker", + "Premain-Class" to "pl.tomgirl.pylon.Agent", + "TweakClass" to "pl.tomgirl.pylon.forge.PylonTweaker", "Multi-Release" to "true", ) } diff --git a/build.gradle.kts b/build.gradle.kts index 73d8390..1681231 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,3 @@ plugins { - id("lenis.build") + id("pylon.build") } diff --git a/gradle.properties b/gradle.properties index 175ac79..00a7f8c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,5 +2,5 @@ org.gradle.jvmargs = -Xmx512M org.gradle.parallel = true maven_group = pl.tomgirl -base_name = lenis -version = 0.1.5 +base_name = pylon +version = 0.1.6 diff --git a/runs/logging.properties b/runs/logging.properties index 2a46713..d772d33 100644 --- a/runs/logging.properties +++ b/runs/logging.properties @@ -2,5 +2,5 @@ handlers=java.util.logging.ConsoleHandler .level=INFO java.util.logging.ConsoleHandler.level=ALL -Lenis.level=FINE +Pylon.level=FINE LWJGL.level=FINE diff --git a/runs/vanilla/build.gradle.kts b/runs/vanilla/build.gradle.kts index 9c7fa1b..0aae4f3 100644 --- a/runs/vanilla/build.gradle.kts +++ b/runs/vanilla/build.gradle.kts @@ -2,11 +2,11 @@ plugins { alias(libs.plugins.loom) } -val lenis = configurations.create("lenis") +val pylon = configurations.create("pylon") dependencies { minecraft("com.mojang:minecraft:1.6.4") - lenis(project(path = ":", configuration = "distribution")) + pylon(project(path = ":", configuration = "distribution")) runtimeOnly(project(path = ":", configuration = "distribution")) } @@ -31,7 +31,7 @@ loom { mainClass = "net.minecraft.client.main.Main" displayName = "Vanilla 1.6.4 (agent)" runDirectory = layout.projectDirectory.dir("run/agent") - jvmArguments.add(provider { "-javaagent:${lenis.singleFile.absolutePath}" }) + jvmArguments.add(provider { "-javaagent:${pylon.singleFile.absolutePath}" }) } configureEach { generateRunConfig = true diff --git a/settings.gradle.kts b/settings.gradle.kts index e983b00..0ea1d53 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,6 @@ pluginManagement { } } -rootProject.name = "lenis" +rootProject.name = "pylon" include(":runs:fabric", ":runs:vanilla") diff --git a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinPlugin.java b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinPlugin.java similarity index 98% rename from src/fabric/java/pl/tomgirl/lenis/fabric/MixinPlugin.java rename to src/fabric/java/pl/tomgirl/pylon/fabric/MixinPlugin.java index 0bae7d2..9c119e2 100644 --- a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinPlugin.java +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinPlugin.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.fabric; +package pl.tomgirl.pylon.fabric; import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; diff --git a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformer.java b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformer.java similarity index 87% rename from src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformer.java rename to src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformer.java index 039ca04..18b86b3 100644 --- a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformer.java +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformer.java @@ -1,8 +1,8 @@ -package pl.tomgirl.lenis.fabric; +package pl.tomgirl.pylon.fabric; import org.spongepowered.asm.mixin.transformer.IMixinTransformer; import org.spongepowered.asm.transformers.TreeTransformer; -import pl.tomgirl.lenis.game.GameTransformer; +import pl.tomgirl.pylon.game.GameTransformer; public class MixinTransformer extends MixinTransformerDelegate { MixinTransformer(T delegate) throws Exception { @@ -15,7 +15,7 @@ public byte[] transformClassBytes(String name, String transformedName, byte[] by return super.transformClassBytes(name, transformedName, null); } - if (name.startsWith("pl.tomgirl.lenis")) { + if (name.startsWith("pl.tomgirl.pylon")) { return super.transformClassBytes(name, transformedName, bytecode); } diff --git a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformerDelegate.java b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformerDelegate.java similarity index 98% rename from src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformerDelegate.java rename to src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformerDelegate.java index 7e0054c..6855712 100644 --- a/src/fabric/java/pl/tomgirl/lenis/fabric/MixinTransformerDelegate.java +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformerDelegate.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.fabric; +package pl.tomgirl.pylon.fabric; import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.mixin.MixinEnvironment; diff --git a/src/fabric/java/pl/tomgirl/lenis/fabric/PreLaunch.java b/src/fabric/java/pl/tomgirl/pylon/fabric/PreLaunch.java similarity index 87% rename from src/fabric/java/pl/tomgirl/lenis/fabric/PreLaunch.java rename to src/fabric/java/pl/tomgirl/pylon/fabric/PreLaunch.java index 1a2668a..df11e48 100644 --- a/src/fabric/java/pl/tomgirl/lenis/fabric/PreLaunch.java +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/PreLaunch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.fabric; +package pl.tomgirl.pylon.fabric; import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; diff --git a/src/forge/java/pl/tomgirl/lenis/forge/LenisTransformer.java b/src/forge/java/pl/tomgirl/pylon/forge/PylonTransformer.java similarity index 50% rename from src/forge/java/pl/tomgirl/lenis/forge/LenisTransformer.java rename to src/forge/java/pl/tomgirl/pylon/forge/PylonTransformer.java index 76e9260..dc8b8e9 100644 --- a/src/forge/java/pl/tomgirl/lenis/forge/LenisTransformer.java +++ b/src/forge/java/pl/tomgirl/pylon/forge/PylonTransformer.java @@ -1,12 +1,12 @@ -package pl.tomgirl.lenis.forge; +package pl.tomgirl.pylon.forge; import net.minecraft.launchwrapper.IClassTransformer; -import pl.tomgirl.lenis.game.GameTransformer; +import pl.tomgirl.pylon.game.GameTransformer; -public final class LenisTransformer implements IClassTransformer { +public final class PylonTransformer implements IClassTransformer { @Override public byte[] transform(String name, String transformedName, byte[] bytecode) { - if (bytecode == null || name.startsWith("pl.tomgirl.lenis.") || name.startsWith("org.lwjgl.")) { + if (bytecode == null || name.startsWith("pl.tomgirl.pylon.") || name.startsWith("org.lwjgl.")) { return bytecode; } return GameTransformer.transform(bytecode); diff --git a/src/forge/java/pl/tomgirl/lenis/forge/LenisTweaker.java b/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java similarity index 87% rename from src/forge/java/pl/tomgirl/lenis/forge/LenisTweaker.java rename to src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java index 1f2c3f8..27998dc 100644 --- a/src/forge/java/pl/tomgirl/lenis/forge/LenisTweaker.java +++ b/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.forge; +package pl.tomgirl.pylon.forge; import java.io.File; import java.net.URL; @@ -10,7 +10,7 @@ import net.minecraft.launchwrapper.LaunchClassLoader; @SuppressWarnings("unused") -public final class LenisTweaker implements ITweaker { +public final class PylonTweaker implements ITweaker { private LaunchClassLoader classLoader; @Override @@ -25,20 +25,20 @@ public void injectIntoClassLoader(LaunchClassLoader classLoader) { Field parent = classLoader.getClass().getDeclaredField("parent"); parent.setAccessible(true); ClassLoader launcherParent = (ClassLoader) parent.get(classLoader); - URL runtime = LenisTweaker.class.getProtectionDomain().getCodeSource().getLocation(); + URL runtime = PylonTweaker.class.getProtectionDomain().getCodeSource().getLocation(); RuntimeClassLoader runtimeLoader = new RuntimeClassLoader(runtime, launcherParent); parent.set(classLoader, runtimeLoader); if (System.getProperty("os.name", "").startsWith("Mac")) { - Class display = runtimeLoader.loadClass("pl.tomgirl.lenis.window.DisplaySdl"); + Class display = runtimeLoader.loadClass("pl.tomgirl.pylon.window.DisplaySdl"); Object instance = display.getMethod("instance").invoke(null); display.getMethod("initializeVideo").invoke(instance); } } catch (Exception e) { - throw new IllegalStateException("Failed to install the Lenis runtime class loader", e); + throw new IllegalStateException("Failed to install the runtime class loader", e); } - classLoader.addClassLoaderExclusion("pl.tomgirl.lenis."); - classLoader.registerTransformer(LenisTransformer.class.getName()); + classLoader.addClassLoaderExclusion("pl.tomgirl.pylon."); + classLoader.registerTransformer(PylonTransformer.class.getName()); } private static class RuntimeClassLoader extends URLClassLoader { @@ -48,7 +48,7 @@ private static class RuntimeClassLoader extends URLClassLoader { @Override protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith("org.lwjgl.") || name.startsWith("pl.tomgirl.lenis.")) { + if (name.startsWith("org.lwjgl.") || name.startsWith("pl.tomgirl.pylon.")) { Class loaded = findLoadedClass(name); if (loaded == null) { try { diff --git a/src/main/java/org/lwjgl/LWJGLUtil.java b/src/main/java/org/lwjgl/LWJGLUtil.java index 5f92b54..bff05fe 100644 --- a/src/main/java/org/lwjgl/LWJGLUtil.java +++ b/src/main/java/org/lwjgl/LWJGLUtil.java @@ -1,6 +1,6 @@ package org.lwjgl; -import pl.tomgirl.lenis.Platform; +import pl.tomgirl.pylon.Platform; import java.util.logging.Level; import java.util.logging.Logger; diff --git a/src/main/java/org/lwjgl/Sys.java b/src/main/java/org/lwjgl/Sys.java index 5ffa56e..38e7457 100644 --- a/src/main/java/org/lwjgl/Sys.java +++ b/src/main/java/org/lwjgl/Sys.java @@ -1,6 +1,6 @@ package org.lwjgl; -import pl.tomgirl.lenis.Platform; +import pl.tomgirl.pylon.Platform; @SuppressWarnings("unused") public class Sys { diff --git a/src/main/java/org/lwjgl/input/Keyboard.java b/src/main/java/org/lwjgl/input/Keyboard.java index 7d5ceb0..abc5fb6 100644 --- a/src/main/java/org/lwjgl/input/Keyboard.java +++ b/src/main/java/org/lwjgl/input/Keyboard.java @@ -10,7 +10,7 @@ import org.lwjgl.Sys; import org.lwjgl.opengl.Display; import org.lwjgl.sdl.SDLKeyboard; -import pl.tomgirl.lenis.window.KeyboardSdl; +import pl.tomgirl.pylon.window.KeyboardSdl; @SuppressWarnings("unused") public class Keyboard { diff --git a/src/main/java/org/lwjgl/input/Mouse.java b/src/main/java/org/lwjgl/input/Mouse.java index 3a21027..e62094a 100644 --- a/src/main/java/org/lwjgl/input/Mouse.java +++ b/src/main/java/org/lwjgl/input/Mouse.java @@ -3,7 +3,7 @@ import java.util.HashMap; import java.util.Map; -import pl.tomgirl.lenis.window.MouseSdl; +import pl.tomgirl.pylon.window.MouseSdl; import org.lwjgl.LWJGLException; import org.lwjgl.Sys; import org.lwjgl.opengl.Display; diff --git a/src/main/java/org/lwjgl/opengl/Display.java b/src/main/java/org/lwjgl/opengl/Display.java index e2ef6a7..f7a6564 100644 --- a/src/main/java/org/lwjgl/opengl/Display.java +++ b/src/main/java/org/lwjgl/opengl/Display.java @@ -6,9 +6,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.LWJGLException; -import pl.tomgirl.lenis.Lenis; -import pl.tomgirl.lenis.window.GlSurface; -import pl.tomgirl.lenis.window.DisplaySdl; +import pl.tomgirl.pylon.Pylon; +import pl.tomgirl.pylon.window.GlSurface; +import pl.tomgirl.pylon.window.DisplaySdl; @SuppressWarnings("unused") public class Display { @@ -65,7 +65,7 @@ public static void create(@NotNull PixelFormat pixelFormat) throws LWJGLExceptio try { SDL.create(new GlSurface(pixelFormat)); } catch (RuntimeException e) { - Lenis.LOG.log(Level.SEVERE, "Failed to create display", e); + Pylon.LOG.log(Level.SEVERE, "Failed to create display", e); throw new LWJGLException(e); } } diff --git a/src/main/java/pl/tomgirl/lenis/Lenis.java b/src/main/java/pl/tomgirl/lenis/Lenis.java deleted file mode 100644 index bf23901..0000000 --- a/src/main/java/pl/tomgirl/lenis/Lenis.java +++ /dev/null @@ -1,9 +0,0 @@ -package pl.tomgirl.lenis; - -import java.util.logging.Logger; - -public final class Lenis { - public static final Logger LOG = Logger.getLogger("Lenis"); - - private Lenis() {} -} diff --git a/src/main/java/pl/tomgirl/lenis/Agent.java b/src/main/java/pl/tomgirl/pylon/Agent.java similarity index 82% rename from src/main/java/pl/tomgirl/lenis/Agent.java rename to src/main/java/pl/tomgirl/pylon/Agent.java index bd6e623..effa260 100644 --- a/src/main/java/pl/tomgirl/lenis/Agent.java +++ b/src/main/java/pl/tomgirl/pylon/Agent.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis; +package pl.tomgirl.pylon; import java.io.File; import java.lang.instrument.Instrumentation; @@ -9,11 +9,11 @@ private Agent() {} public static void premain(String arguments, Instrumentation instrumentation) { if (injectFabric()) { - Lenis.LOG.log(Level.INFO, "Injected Fabric mod"); + Pylon.LOG.log(Level.INFO, "Injected Fabric mod"); return; } - instrumentation.addTransformer(new pl.tomgirl.lenis.game.GameTransformer()); + instrumentation.addTransformer(new pl.tomgirl.pylon.game.GameTransformer()); } public static boolean injectFabric() { @@ -32,7 +32,7 @@ public static boolean injectFabric() { return true; } catch (Exception e) { if (!(e instanceof ClassNotFoundException)) { - Lenis.LOG.log(Level.SEVERE, "Error injecting into Fabric"); + Pylon.LOG.log(Level.SEVERE, "Error injecting into Fabric"); } return false; } diff --git a/src/main/java/pl/tomgirl/lenis/Platform.java b/src/main/java/pl/tomgirl/pylon/Platform.java similarity index 94% rename from src/main/java/pl/tomgirl/lenis/Platform.java rename to src/main/java/pl/tomgirl/pylon/Platform.java index 3ca75c3..8bbcce9 100644 --- a/src/main/java/pl/tomgirl/lenis/Platform.java +++ b/src/main/java/pl/tomgirl/pylon/Platform.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis; +package pl.tomgirl.pylon; import java.io.IOException; import java.net.URI; @@ -30,7 +30,7 @@ public void open(String uri) { // TODO: Validate URI process.getErrorStream().close(); process.getOutputStream().close(); } catch (IOException e) { - Lenis.LOG.log(Level.SEVERE, "Could not open uri " + uri, e); + Pylon.LOG.log(Level.SEVERE, "Could not open uri " + uri, e); } } diff --git a/src/main/java/pl/tomgirl/pylon/Pylon.java b/src/main/java/pl/tomgirl/pylon/Pylon.java new file mode 100644 index 0000000..72396bf --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/Pylon.java @@ -0,0 +1,9 @@ +package pl.tomgirl.pylon; + +import java.util.logging.Logger; + +public final class Pylon { + public static final Logger LOG = Logger.getLogger("Pylon"); + + private Pylon() {} +} diff --git a/src/main/java/pl/tomgirl/lenis/game/GameHooks.java b/src/main/java/pl/tomgirl/pylon/game/GameHooks.java similarity index 89% rename from src/main/java/pl/tomgirl/lenis/game/GameHooks.java rename to src/main/java/pl/tomgirl/pylon/game/GameHooks.java index b04a34a..b093a9f 100644 --- a/src/main/java/pl/tomgirl/lenis/game/GameHooks.java +++ b/src/main/java/pl/tomgirl/pylon/game/GameHooks.java @@ -1,17 +1,16 @@ -package pl.tomgirl.lenis.game; +package pl.tomgirl.pylon.game; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -import java.util.Objects; import javax.imageio.ImageIO; import org.lwjgl.sdl.SDLClipboard; -import pl.tomgirl.lenis.Lenis; -import pl.tomgirl.lenis.window.DisplaySdl; +import pl.tomgirl.pylon.Pylon; +import pl.tomgirl.pylon.window.DisplaySdl; @SuppressWarnings("unused") public final class GameHooks { @@ -29,7 +28,7 @@ public static void setClipboard(String text) { } public static void setIcon() { - try (InputStream stream = Lenis.class.getResourceAsStream("/icon_256x.png")) { + try (InputStream stream = Pylon.class.getResourceAsStream("/icon_256x.png")) { if (stream == null) throw new IllegalStateException("Missing default icon"); BufferedImage image = ImageIO.read(stream); if (image == null) throw new IllegalStateException("Unable to decode default icon"); diff --git a/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java b/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java similarity index 86% rename from src/main/java/pl/tomgirl/lenis/game/GameTransformer.java rename to src/main/java/pl/tomgirl/pylon/game/GameTransformer.java index ad3caf2..f760a88 100644 --- a/src/main/java/pl/tomgirl/lenis/game/GameTransformer.java +++ b/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.game; +package pl.tomgirl.pylon.game; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; @@ -10,9 +10,9 @@ import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; -import pl.tomgirl.lenis.Lenis; -import pl.tomgirl.lenis.game.patch.Patch; -import pl.tomgirl.lenis.game.patch.impl.*; +import pl.tomgirl.pylon.Pylon; +import pl.tomgirl.pylon.game.patch.Patch; +import pl.tomgirl.pylon.game.patch.impl.*; public final class GameTransformer implements ClassFileTransformer { @Override @@ -36,7 +36,7 @@ public static byte[] transform(byte[] bytecode) { patcher = patch.apply(patcher); } reader.accept(patcher, 0); - Lenis.LOG.log(Level.FINE, "Applied {0} to {1}", new Object[]{matches, reader.getClassName()}); + Pylon.LOG.log(Level.FINE, "Applied {0} to {1}", new Object[]{matches, reader.getClassName()}); return writer.toByteArray(); } } diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java b/src/main/java/pl/tomgirl/pylon/game/patch/Patch.java similarity index 94% rename from src/main/java/pl/tomgirl/lenis/game/patch/Patch.java rename to src/main/java/pl/tomgirl/pylon/game/patch/Patch.java index 3abafeb..3c8d310 100644 --- a/src/main/java/pl/tomgirl/lenis/game/patch/Patch.java +++ b/src/main/java/pl/tomgirl/pylon/game/patch/Patch.java @@ -1,9 +1,9 @@ -package pl.tomgirl.lenis.game.patch; +package pl.tomgirl.pylon.game.patch; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; -import pl.tomgirl.lenis.game.GameHooks; +import pl.tomgirl.pylon.game.GameHooks; public abstract class Patch extends ClassVisitor { public static final String HOOKS = Type.getInternalName(GameHooks.class); diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java b/src/main/java/pl/tomgirl/pylon/game/patch/impl/MinecraftPatch.java similarity index 97% rename from src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java rename to src/main/java/pl/tomgirl/pylon/game/patch/impl/MinecraftPatch.java index e783928..f13a894 100644 --- a/src/main/java/pl/tomgirl/lenis/game/patch/impl/MinecraftPatch.java +++ b/src/main/java/pl/tomgirl/pylon/game/patch/impl/MinecraftPatch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.game.patch.impl; +package pl.tomgirl.pylon.game.patch.impl; import java.util.ArrayList; import java.util.HashSet; @@ -8,8 +8,8 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; -import pl.tomgirl.lenis.game.GameHooks; -import pl.tomgirl.lenis.game.patch.Patch; +import pl.tomgirl.pylon.game.GameHooks; +import pl.tomgirl.pylon.game.patch.Patch; public final class MinecraftPatch extends Patch { private static final String DISPLAY = "org/lwjgl/opengl/Display"; diff --git a/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java b/src/main/java/pl/tomgirl/pylon/game/patch/impl/ScreenPatch.java similarity index 97% rename from src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java rename to src/main/java/pl/tomgirl/pylon/game/patch/impl/ScreenPatch.java index 1fe7bc4..a997655 100644 --- a/src/main/java/pl/tomgirl/lenis/game/patch/impl/ScreenPatch.java +++ b/src/main/java/pl/tomgirl/pylon/game/patch/impl/ScreenPatch.java @@ -1,11 +1,11 @@ -package pl.tomgirl.lenis.game.patch.impl; +package pl.tomgirl.pylon.game.patch.impl; import java.util.HashMap; import java.util.Map; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; -import pl.tomgirl.lenis.game.patch.Patch; +import pl.tomgirl.pylon.game.patch.Patch; public final class ScreenPatch extends Patch { private static final String CLIPBOARD = "java/awt/datatransfer/Clipboard"; diff --git a/src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java b/src/main/java/pl/tomgirl/pylon/window/BoundedQueue.java similarity index 97% rename from src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java rename to src/main/java/pl/tomgirl/pylon/window/BoundedQueue.java index 7249243..b2b47d5 100644 --- a/src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java +++ b/src/main/java/pl/tomgirl/pylon/window/BoundedQueue.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; import java.util.function.Supplier; diff --git a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java b/src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java similarity index 98% rename from src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java rename to src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java index 8d4cb53..f52f45b 100644 --- a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java +++ b/src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; import java.nio.ByteBuffer; import java.nio.IntBuffer; @@ -9,7 +9,7 @@ import org.lwjgl.opengl.DisplayMode; import org.lwjgl.opengl.Drawable; -import pl.tomgirl.lenis.Lenis; +import pl.tomgirl.pylon.Pylon; import org.jetbrains.annotations.NotNull; import org.lwjgl.LWJGLException; import org.lwjgl.PointerBuffer; @@ -266,7 +266,7 @@ public int setIcon(@NotNull ByteBuffer[] icons) { checkSdlError(surface != null); checkSdlError(SDL_SetWindowIcon(handle, surface)); } catch (Exception e) { - Lenis.LOG.log(Level.SEVERE, "Failed to set window icon", e); + Pylon.LOG.log(Level.SEVERE, "Failed to set window icon", e); } } finally { MemoryUtil.memFree(pixels); @@ -473,7 +473,7 @@ public void setFullscreen(boolean fullscreen) { if (fullscreen) { int display = SDL_GetPrimaryDisplay(); if (display == 0) { - Lenis.LOG.log(Level.WARNING, "Failed to find display"); + Pylon.LOG.log(Level.WARNING, "Failed to find display"); return; } if (!this.fullscreen) { @@ -488,7 +488,7 @@ public void setFullscreen(boolean fullscreen) { SDL_SetWindowSize(handle, windowedWidth, windowedHeight); windowResized = true; } catch (Throwable t) { - Lenis.LOG.log(Level.WARNING, "Failed to set fullscreen: ", t); + Pylon.LOG.log(Level.WARNING, "Failed to set fullscreen: ", t); } } diff --git a/src/main/java/pl/tomgirl/lenis/window/GlSurface.java b/src/main/java/pl/tomgirl/pylon/window/GlSurface.java similarity index 99% rename from src/main/java/pl/tomgirl/lenis/window/GlSurface.java rename to src/main/java/pl/tomgirl/pylon/window/GlSurface.java index a02c15e..4aef59c 100644 --- a/src/main/java/pl/tomgirl/lenis/window/GlSurface.java +++ b/src/main/java/pl/tomgirl/pylon/window/GlSurface.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; import java.nio.ByteBuffer; diff --git a/src/main/java/pl/tomgirl/lenis/window/GpuSurface.java b/src/main/java/pl/tomgirl/pylon/window/GpuSurface.java similarity index 88% rename from src/main/java/pl/tomgirl/lenis/window/GpuSurface.java rename to src/main/java/pl/tomgirl/pylon/window/GpuSurface.java index 8e61350..2fd0437 100644 --- a/src/main/java/pl/tomgirl/lenis/window/GpuSurface.java +++ b/src/main/java/pl/tomgirl/pylon/window/GpuSurface.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; public interface GpuSurface { long createWindow(String title, int width, int height, boolean resizable); diff --git a/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java b/src/main/java/pl/tomgirl/pylon/window/KeyboardSdl.java similarity index 97% rename from src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java rename to src/main/java/pl/tomgirl/pylon/window/KeyboardSdl.java index 9ed2652..6a2b81d 100644 --- a/src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java +++ b/src/main/java/pl/tomgirl/pylon/window/KeyboardSdl.java @@ -1,10 +1,10 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.logging.Level; -import pl.tomgirl.lenis.Lenis; +import pl.tomgirl.pylon.Pylon; import org.lwjgl.input.Keyboard; import org.lwjgl.sdl.*; @@ -112,7 +112,7 @@ private int translateKeyFromSDL(int key) { if (translated != Keymap.UNMAPPED) { return translated; } - Lenis.LOG.log(Level.WARNING, "Untranslated key: {0} ({1})", new Object[]{key, SDLKeyboard.SDL_GetScancodeName(key)}); + Pylon.LOG.log(Level.WARNING, "Untranslated key: {0} ({1})", new Object[]{key, SDLKeyboard.SDL_GetScancodeName(key)}); return Keyboard.KEY_NONE; } diff --git a/src/main/java/pl/tomgirl/lenis/window/Keymap.java b/src/main/java/pl/tomgirl/pylon/window/Keymap.java similarity index 99% rename from src/main/java/pl/tomgirl/lenis/window/Keymap.java rename to src/main/java/pl/tomgirl/pylon/window/Keymap.java index 17424d3..bc6bfd9 100644 --- a/src/main/java/pl/tomgirl/lenis/window/Keymap.java +++ b/src/main/java/pl/tomgirl/pylon/window/Keymap.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; import java.lang.reflect.Field; import java.util.Arrays; diff --git a/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java b/src/main/java/pl/tomgirl/pylon/window/MouseSdl.java similarity index 99% rename from src/main/java/pl/tomgirl/lenis/window/MouseSdl.java rename to src/main/java/pl/tomgirl/pylon/window/MouseSdl.java index 2764870..09a356d 100644 --- a/src/main/java/pl/tomgirl/lenis/window/MouseSdl.java +++ b/src/main/java/pl/tomgirl/pylon/window/MouseSdl.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; import java.util.Arrays; @@ -6,7 +6,7 @@ import org.lwjgl.sdl.SDL_MouseButtonEvent; import org.lwjgl.sdl.SDL_MouseMotionEvent; import org.lwjgl.sdl.SDL_MouseWheelEvent; -import pl.tomgirl.lenis.game.GameHooks; +import pl.tomgirl.pylon.game.GameHooks; import static org.lwjgl.sdl.SDLEvents.*; import static org.lwjgl.sdl.SDLMouse.*; diff --git a/src/main/resources/assets/lenis/icon.png b/src/main/resources/assets/pylon/icon.png similarity index 100% rename from src/main/resources/assets/lenis/icon.png rename to src/main/resources/assets/pylon/icon.png diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index e23cf6e..3686084 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,19 +1,19 @@ { "schemaVersion": 1, - "id": "lenis", + "id": "pylon", "version": "${version}", - "name": "Lenis", - "icon": "assets/lenis/icon.png", + "name": "Pylon", + "icon": "assets/pylon/icon.png", "authors": [ "notdevcody" ], "license": "LGPL-3.0-only", "environment": "client", "entrypoints": { "preLaunch": [ - "pl.tomgirl.lenis.fabric.PreLaunch" + "pl.tomgirl.pylon.fabric.PreLaunch" ] }, "mixins": [ - "lenis.mixins.json" + "pylon.mixins.json" ], "depends": { "fabricloader": ">=0.17.3", diff --git a/src/main/resources/lenis.mixins.json b/src/main/resources/pylon.mixins.json similarity index 53% rename from src/main/resources/lenis.mixins.json rename to src/main/resources/pylon.mixins.json index 7546742..ddb342b 100644 --- a/src/main/resources/lenis.mixins.json +++ b/src/main/resources/pylon.mixins.json @@ -1,5 +1,5 @@ { "required": true, "compatibilityLevel": "JAVA_8", - "plugin": "pl.tomgirl.lenis.fabric.MixinPlugin" + "plugin": "pl.tomgirl.pylon.fabric.MixinPlugin" } \ No newline at end of file From 9435bd9c6154ee51aded3b041ce422a0b5823268 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Sat, 12 Sep 2026 21:45:57 +0200 Subject: [PATCH 09/12] feat: old fml compat + mod container --- .../tomgirl/pylon/build/PylonBuildPlugin.kt | 4 +- .../pl/tomgirl/pylon/forge/OldFmlCompat.java | 64 +++++++++++++++++ .../pl/tomgirl/pylon/forge/PylonTweaker.java | 2 + .../pl/tomgirl/pylonfml/CpwModContainer.java | 30 ++++++++ .../pl/tomgirl/pylonfml/FmlModContainer.java | 30 ++++++++ .../com/google/common/eventbus/EventBus.java | 4 ++ .../mods/fml/common/DummyModContainer.java | 12 ++++ .../cpw/mods/fml/common/LoadController.java | 4 ++ .../java/cpw/mods/fml/common/ModMetadata.java | 14 ++++ .../launchwrapper/LaunchClassLoader.java | 1 + .../fml/common/DummyModContainer.java | 12 ++++ .../fml/common/LoadController.java | 4 ++ .../fml/common/ModMetadata.java | 14 ++++ .../tomgirl/pylon/game/GameTransformer.java | 5 +- .../game/patch/impl/FmlModContainerPatch.java | 71 +++++++++++++++++++ src/main/resources/mcmod.info | 14 ++++ 16 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 src/forge/java/pl/tomgirl/pylon/forge/OldFmlCompat.java create mode 100644 src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java create mode 100644 src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java create mode 100644 src/launchwrapper/java/com/google/common/eventbus/EventBus.java create mode 100644 src/launchwrapper/java/cpw/mods/fml/common/DummyModContainer.java create mode 100644 src/launchwrapper/java/cpw/mods/fml/common/LoadController.java create mode 100644 src/launchwrapper/java/cpw/mods/fml/common/ModMetadata.java create mode 100644 src/launchwrapper/java/net/minecraftforge/fml/common/DummyModContainer.java create mode 100644 src/launchwrapper/java/net/minecraftforge/fml/common/LoadController.java create mode 100644 src/launchwrapper/java/net/minecraftforge/fml/common/ModMetadata.java create mode 100644 src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java create mode 100644 src/main/resources/mcmod.info diff --git a/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt index abeb40b..3a72992 100644 --- a/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt +++ b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt @@ -86,7 +86,7 @@ class PylonBuildPlugin : Plugin { tasks.named("processResources") { inputs.property("version", version) - filesMatching("fabric.mod.json") { + filesMatching(listOf("fabric.mod.json", "mcmod.info")) { expand("version" to project.version) } } @@ -98,6 +98,7 @@ class PylonBuildPlugin : Plugin { destinationDirectory.set(layout.buildDirectory.dir("intermediates")) manifest.attributes( "Premain-Class" to "pl.tomgirl.pylon.Agent", + "Implementation-Version" to project.version.toString(), "TweakClass" to "pl.tomgirl.pylon.forge.PylonTweaker", ) } @@ -121,6 +122,7 @@ class PylonBuildPlugin : Plugin { destinationDirectory.set(layout.buildDirectory.dir("intermediates")) manifest.attributes( "Premain-Class" to "pl.tomgirl.pylon.Agent", + "Implementation-Version" to project.version.toString(), "TweakClass" to "pl.tomgirl.pylon.forge.PylonTweaker", "Multi-Release" to "true", ) diff --git a/src/forge/java/pl/tomgirl/pylon/forge/OldFmlCompat.java b/src/forge/java/pl/tomgirl/pylon/forge/OldFmlCompat.java new file mode 100644 index 0000000..221af85 --- /dev/null +++ b/src/forge/java/pl/tomgirl/pylon/forge/OldFmlCompat.java @@ -0,0 +1,64 @@ +package pl.tomgirl.pylon.forge; + +import java.io.File; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.jar.JarFile; +import java.util.stream.Stream; + +public class OldFmlCompat { + private OldFmlCompat() {} + + public static void prepare(ClassLoader loader) { + String remapperName = "cpw.mods.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper"; + if (loader.getResource(remapperName.replace('.', '/') + ".class") == null) { + return; + } + + try { + try { + Class.forName("org.objectweb.asm.Opcodes", false, loader).getField("ASM5"); + return; + } catch (NoSuchFieldException ignored) {} + + Class type = Class.forName(remapperName, true, loader); + Object remapper = type.getField("INSTANCE").get(null); + Map> methods = mappings(type, remapper, "methodNameMaps"); + Map> fields = mappings(type, remapper, "fieldNameMaps"); + File source = new File(OldFmlCompat.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + if (source.isDirectory()) { + Path root = source.toPath(); + try (Stream paths = Files.walk(root)) { + paths.filter(Files::isRegularFile).forEach(path -> + exclude(root.relativize(path).toString().replace(File.separatorChar, '/'), methods, fields)); + } + } else { + try (JarFile jar = new JarFile(source)) { + jar.stream().forEach(entry -> exclude(entry.getName(), methods, fields)); + } + } + } catch (Exception e) { + throw new IllegalStateException("Could not configure legacy FML remapping", e); + } + } + + @SuppressWarnings("unchecked") + private static Map> mappings(Class type, Object remapper, String name) + throws ReflectiveOperationException { + Field field = type.getDeclaredField(name); + field.setAccessible(true); + return (Map>) field.get(remapper); + } + + private static void exclude(String entry, Map> methods, Map> fields) { + if (!entry.endsWith(".class") || !(entry.startsWith("org/lwjgl/") || entry.startsWith("pl/tomgirl/pylon/"))) { + return; + } + String name = entry.substring(0, entry.length() - ".class".length()); + methods.put(name, Collections.emptyMap()); + fields.put(name, Collections.emptyMap()); + } +} diff --git a/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java b/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java index 27998dc..9aa4b31 100644 --- a/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java +++ b/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java @@ -38,6 +38,7 @@ public void injectIntoClassLoader(LaunchClassLoader classLoader) { } classLoader.addClassLoaderExclusion("pl.tomgirl.pylon."); + classLoader.addTransformerExclusion("pl.tomgirl.pylonfml."); classLoader.registerTransformer(PylonTransformer.class.getName()); } @@ -77,6 +78,7 @@ public String getLaunchTarget() { @Override public String[] getLaunchArguments() { classLoader.addClassLoaderExclusion("org.lwjgl."); + OldFmlCompat.prepare(classLoader); return new String[0]; } } diff --git a/src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java b/src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java new file mode 100644 index 0000000..8b1b964 --- /dev/null +++ b/src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java @@ -0,0 +1,30 @@ +package pl.tomgirl.pylonfml; + +import com.google.common.eventbus.EventBus; +import cpw.mods.fml.common.DummyModContainer; +import cpw.mods.fml.common.LoadController; +import cpw.mods.fml.common.ModMetadata; + +@SuppressWarnings("unused") +public class CpwModContainer extends DummyModContainer { + public CpwModContainer() { + this(new ModMetadata()); + } + + public CpwModContainer(ModMetadata md) { + super(md); + md.modId = "pylon"; + md.name = "Pylon"; + md.description = "LWJGL3 compatibility layer for legacy Minecraft."; + md.url = "https://github.com/notdevcody/pylon"; + String version = getClass().getPackage().getImplementationVersion(); + md.version = version == null ? "dev" : version; + md.authorList.add("notdevcody"); + md.logoFile = "/assets/pylon/icon.png"; + } + + @Override + public boolean registerBus(EventBus bus, LoadController controller) { + return true; + } +} diff --git a/src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java b/src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java new file mode 100644 index 0000000..97890b0 --- /dev/null +++ b/src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java @@ -0,0 +1,30 @@ +package pl.tomgirl.pylonfml; + +import com.google.common.eventbus.EventBus; +import net.minecraftforge.fml.common.DummyModContainer; +import net.minecraftforge.fml.common.LoadController; +import net.minecraftforge.fml.common.ModMetadata; + +@SuppressWarnings("unused") +public class FmlModContainer extends DummyModContainer { + public FmlModContainer() { + this(new ModMetadata()); + } + + public FmlModContainer(ModMetadata md) { + super(md); + md.modId = "pylon"; + md.name = "Pylon"; + md.description = "LWJGL3 compatibility layer for legacy Minecraft."; + md.url = "https://github.com/notdevcody/pylon"; + String version = getClass().getPackage().getImplementationVersion(); + md.version = version == null ? "dev" : version; + md.authorList.add("notdevcody"); + md.logoFile = "/assets/pylon/icon.png"; + } + + @Override + public boolean registerBus(EventBus bus, LoadController controller) { + return true; + } +} diff --git a/src/launchwrapper/java/com/google/common/eventbus/EventBus.java b/src/launchwrapper/java/com/google/common/eventbus/EventBus.java new file mode 100644 index 0000000..3cfd1ca --- /dev/null +++ b/src/launchwrapper/java/com/google/common/eventbus/EventBus.java @@ -0,0 +1,4 @@ +package com.google.common.eventbus; + +public class EventBus { +} diff --git a/src/launchwrapper/java/cpw/mods/fml/common/DummyModContainer.java b/src/launchwrapper/java/cpw/mods/fml/common/DummyModContainer.java new file mode 100644 index 0000000..2bd8cfa --- /dev/null +++ b/src/launchwrapper/java/cpw/mods/fml/common/DummyModContainer.java @@ -0,0 +1,12 @@ +package cpw.mods.fml.common; + +import com.google.common.eventbus.EventBus; + +public class DummyModContainer { + public DummyModContainer(ModMetadata md) { + } + + public boolean registerBus(EventBus bus, LoadController controller) { + return false; + } +} diff --git a/src/launchwrapper/java/cpw/mods/fml/common/LoadController.java b/src/launchwrapper/java/cpw/mods/fml/common/LoadController.java new file mode 100644 index 0000000..4053b60 --- /dev/null +++ b/src/launchwrapper/java/cpw/mods/fml/common/LoadController.java @@ -0,0 +1,4 @@ +package cpw.mods.fml.common; + +public class LoadController { +} diff --git a/src/launchwrapper/java/cpw/mods/fml/common/ModMetadata.java b/src/launchwrapper/java/cpw/mods/fml/common/ModMetadata.java new file mode 100644 index 0000000..8309930 --- /dev/null +++ b/src/launchwrapper/java/cpw/mods/fml/common/ModMetadata.java @@ -0,0 +1,14 @@ +package cpw.mods.fml.common; + +import java.util.ArrayList; +import java.util.List; + +public class ModMetadata { + public String modId; + public String name; + public String url = ""; + public String description = ""; + public String logoFile = ""; + public String version = ""; + public List authorList = new ArrayList<>(); +} diff --git a/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java b/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java index 194dc48..42228bf 100644 --- a/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java +++ b/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java @@ -4,4 +4,5 @@ public abstract class LaunchClassLoader extends ClassLoader { private LaunchClassLoader() {} public native void registerTransformer(String name); public native void addClassLoaderExclusion(String prefix); + public native void addTransformerExclusion(String prefix); } diff --git a/src/launchwrapper/java/net/minecraftforge/fml/common/DummyModContainer.java b/src/launchwrapper/java/net/minecraftforge/fml/common/DummyModContainer.java new file mode 100644 index 0000000..bc54ed9 --- /dev/null +++ b/src/launchwrapper/java/net/minecraftforge/fml/common/DummyModContainer.java @@ -0,0 +1,12 @@ +package net.minecraftforge.fml.common; + +import com.google.common.eventbus.EventBus; + +public class DummyModContainer { + public DummyModContainer(ModMetadata md) { + } + + public boolean registerBus(EventBus bus, LoadController controller) { + return false; + } +} diff --git a/src/launchwrapper/java/net/minecraftforge/fml/common/LoadController.java b/src/launchwrapper/java/net/minecraftforge/fml/common/LoadController.java new file mode 100644 index 0000000..a8411e9 --- /dev/null +++ b/src/launchwrapper/java/net/minecraftforge/fml/common/LoadController.java @@ -0,0 +1,4 @@ +package net.minecraftforge.fml.common; + +public class LoadController { +} diff --git a/src/launchwrapper/java/net/minecraftforge/fml/common/ModMetadata.java b/src/launchwrapper/java/net/minecraftforge/fml/common/ModMetadata.java new file mode 100644 index 0000000..9793f2d --- /dev/null +++ b/src/launchwrapper/java/net/minecraftforge/fml/common/ModMetadata.java @@ -0,0 +1,14 @@ +package net.minecraftforge.fml.common; + +import java.util.ArrayList; +import java.util.List; + +public class ModMetadata { + public String modId; + public String name; + public String url = ""; + public String description = ""; + public String logoFile = ""; + public String version = ""; + public List authorList = new ArrayList<>(); +} diff --git a/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java b/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java index f760a88..51bd13f 100644 --- a/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java +++ b/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java @@ -21,12 +21,13 @@ public byte[] transform(ClassLoader loader, String name, Class type, Protecti } public static byte[] transform(byte[] bytecode) { - ScreenPatch screen = new ScreenPatch(null); + FmlModContainerPatch fml = new FmlModContainerPatch(null); + ScreenPatch screen = new ScreenPatch(fml); MinecraftPatch minecraft = new MinecraftPatch(screen); ClassReader reader = new ClassReader(bytecode); reader.accept(minecraft, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - List matches = Stream.of(minecraft, screen).filter(Patch::matches).collect(Collectors.toList()); + List matches = Stream.of(minecraft, screen, fml).filter(Patch::matches).collect(Collectors.toList()); if (matches.isEmpty()) return bytecode; ClassWriter writer = new ClassWriter(reader, 0); diff --git a/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java b/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java new file mode 100644 index 0000000..2c746be --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java @@ -0,0 +1,71 @@ +package pl.tomgirl.pylon.game.patch.impl; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import pl.tomgirl.pylon.game.patch.Patch; + +public class FmlModContainerPatch extends Patch { + private String owner; + private String container; + private Method identifyMods; + + public FmlModContainerPatch(ClassVisitor next) { + super(next); + } + + @Override + public void visit(int version, int access, String name, String signature, String parent, String[] interfaces) { + owner = name; + if (name.equals("net/minecraftforge/fml/common/Loader")) { + container = "pl.tomgirl.pylonfml.PylonModContainer"; + } else if (name.equals("cpw/mods/fml/common/Loader")) { + container = "pl.tomgirl.pylonfml.CpwModContainer"; + } + super.visit(version, access, name, signature, parent, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (container != null && name.equals("identifyMods") && (access & Opcodes.ACC_STATIC) == 0) { + String discoverer = "L" + owner.substring(0, owner.lastIndexOf('/') + 1) + "discovery/ModDiscoverer;"; + if (descriptor.equals("()" + discoverer) || descriptor.equals("(Ljava/util/List;)" + discoverer)) { + identifyMods = new Method(name, descriptor); + } + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public boolean matches() { + return identifyMods != null; + } + + @Override + public ClassVisitor apply(ClassVisitor writer) { + return new ClassVisitor(Opcodes.ASM9, writer) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor method = super.visitMethod(access, name, descriptor, signature, exceptions); + if (!new Method(name, descriptor).equals(identifyMods)) { + return method; + } + return new MethodVisitor(Opcodes.ASM9, method) { + @Override + public void visitCode() { + super.visitCode(); + super.visitFieldInsn(Opcodes.GETSTATIC, owner, "injectedContainers", "Ljava/util/List;"); + super.visitLdcInsn(container); + super.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true); + super.visitInsn(Opcodes.POP); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + super.visitMaxs(Math.max(maxStack, 2), maxLocals); + } + }; + } + }; + } +} diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 0000000..eb3fb91 --- /dev/null +++ b/src/main/resources/mcmod.info @@ -0,0 +1,14 @@ +[{ + "modid": "pylon", + "name": "Pylon", + "description": "LWJGL3 compatibility layer for legacy Minecraft.", + "version": "${version}", + "mcversion": "*", + "url": "https://github.com/notdevcody/pylon", + "authors": [ "notdevcody" ], + "credits": "", + "logoFile": "/assets/pylon/icon.png", + "parent": "", + "screenshots": [], + "dependencies": [] +}] From 8447829cb90eb3643891790ae1a71266fa3a4f54 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:43:27 +0200 Subject: [PATCH 10/12] fix: mod container class name --- .../pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java b/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java index 2c746be..56f79e0 100644 --- a/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java +++ b/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java @@ -18,7 +18,7 @@ public FmlModContainerPatch(ClassVisitor next) { public void visit(int version, int access, String name, String signature, String parent, String[] interfaces) { owner = name; if (name.equals("net/minecraftforge/fml/common/Loader")) { - container = "pl.tomgirl.pylonfml.PylonModContainer"; + container = "pl.tomgirl.pylonfml.FmlModContainer"; } else if (name.equals("cpw/mods/fml/common/Loader")) { container = "pl.tomgirl.pylonfml.CpwModContainer"; } From 4666b7f59fa18a6ebd4add55f096dea8f3eefbf8 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Fri, 25 Sep 2026 23:20:11 +0200 Subject: [PATCH 11/12] feat: new icon --- .../pl/tomgirl/pylonfml/CpwModContainer.java | 2 +- .../pl/tomgirl/pylonfml/FmlModContainer.java | 2 +- src/main/resources/assets/pylon/icon-256x.png | Bin 0 -> 5563 bytes src/main/resources/assets/pylon/icon.png | Bin 77122 -> 13451 bytes src/main/resources/mcmod.info | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/assets/pylon/icon-256x.png diff --git a/src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java b/src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java index 8b1b964..cdd68fa 100644 --- a/src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java +++ b/src/forge/java/pl/tomgirl/pylonfml/CpwModContainer.java @@ -20,7 +20,7 @@ public CpwModContainer(ModMetadata md) { String version = getClass().getPackage().getImplementationVersion(); md.version = version == null ? "dev" : version; md.authorList.add("notdevcody"); - md.logoFile = "/assets/pylon/icon.png"; + md.logoFile = "/assets/pylon/icon-256x.png"; } @Override diff --git a/src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java b/src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java index 97890b0..dcd6f54 100644 --- a/src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java +++ b/src/forge/java/pl/tomgirl/pylonfml/FmlModContainer.java @@ -20,7 +20,7 @@ public FmlModContainer(ModMetadata md) { String version = getClass().getPackage().getImplementationVersion(); md.version = version == null ? "dev" : version; md.authorList.add("notdevcody"); - md.logoFile = "/assets/pylon/icon.png"; + md.logoFile = "/assets/pylon/icon-256x.png"; } @Override diff --git a/src/main/resources/assets/pylon/icon-256x.png b/src/main/resources/assets/pylon/icon-256x.png new file mode 100644 index 0000000000000000000000000000000000000000..243f3f1a4e401bb409e7686ca79621ce13d362dd GIT binary patch literal 5563 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 b=g6H^Z>t&4=MM-CK%l2>q*bBe9QpqMK(^AB literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/pylon/icon.png b/src/main/resources/assets/pylon/icon.png index 06d486ea150b05b6cd34d67743b8a71183df9565..3f6354e745c802d7e371b5f02599754eea298b9a 100644 GIT binary patch literal 13451 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

va&$EQa6A~=^z=$Wl9vBL zkl&Vw?>vKf-H8RF5|V_p$cC{`cL0|G!?3C?Ha~ YT{X7cvB>U(-Uk6{N}7r#@|Lgv53k*c;Q#;t literal 77122 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 diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index eb3fb91..fd1a1a9 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -7,7 +7,7 @@ "url": "https://github.com/notdevcody/pylon", "authors": [ "notdevcody" ], "credits": "", - "logoFile": "/assets/pylon/icon.png", + "logoFile": "/assets/pylon/icon-256x.png", "parent": "", "screenshots": [], "dependencies": [] From 2bf6b9b0eac446095cdc4cce1fac8513e4c148e8 Mon Sep 17 00:00:00 2001 From: notdevcody <149296239+notdevcody@users.noreply.github.com> Date: Fri, 25 Sep 2026 23:40:06 +0200 Subject: [PATCH 12/12] feat: make -XstartOnFirstThread optional --- runs/fabric/build.gradle.kts | 1 - runs/vanilla/build.gradle.kts | 1 - .../java/pl/tomgirl/pylon/game/GameHooks.java | 5 +- .../pl/tomgirl/pylon/window/DisplaySdl.java | 115 +++++++++++------- .../pl/tomgirl/pylon/window/GlSurface.java | 32 ++--- .../pl/tomgirl/pylon/window/MainThread.java | 82 +++++++++++++ .../pl/tomgirl/pylon/window/MouseSdl.java | 12 +- 7 files changed, 178 insertions(+), 70 deletions(-) create mode 100644 src/main/java/pl/tomgirl/pylon/window/MainThread.java diff --git a/runs/fabric/build.gradle.kts b/runs/fabric/build.gradle.kts index 1e5ed6d..9a6af14 100644 --- a/runs/fabric/build.gradle.kts +++ b/runs/fabric/build.gradle.kts @@ -13,7 +13,6 @@ loom { programArguments.addAll("--username", "Player", "--accessToken", "0") jvmArguments.add("-Djava.util.logging.config.file=${rootProject.file("runs/logging.properties")}") jvmArguments.add("--enable-native-access=ALL-UNNAMED") - if (System.getProperty("os.name").startsWith("Mac")) jvmArguments.add("-XstartOnFirstThread") } } } diff --git a/runs/vanilla/build.gradle.kts b/runs/vanilla/build.gradle.kts index 0aae4f3..77f97a5 100644 --- a/runs/vanilla/build.gradle.kts +++ b/runs/vanilla/build.gradle.kts @@ -39,7 +39,6 @@ loom { programArguments.addAll("--username", "Player", "--session", "0", "--version", "1.6.4") jvmArguments.add("-Djava.util.logging.config.file=${rootProject.file("runs/logging.properties")}") jvmArguments.add("--enable-native-access=ALL-UNNAMED") - if (System.getProperty("os.name").startsWith("Mac")) jvmArguments.add("-XstartOnFirstThread") } } } diff --git a/src/main/java/pl/tomgirl/pylon/game/GameHooks.java b/src/main/java/pl/tomgirl/pylon/game/GameHooks.java index b093a9f..c038772 100644 --- a/src/main/java/pl/tomgirl/pylon/game/GameHooks.java +++ b/src/main/java/pl/tomgirl/pylon/game/GameHooks.java @@ -3,6 +3,7 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; +import pl.tomgirl.pylon.window.MainThread; import java.nio.ByteBuffer; import javax.imageio.ImageIO; @@ -19,12 +20,12 @@ public final class GameHooks { private GameHooks() {} public static String getClipboard() { - String text = SDLClipboard.SDL_GetClipboardText(); + String text = MainThread.call(SDLClipboard::SDL_GetClipboardText); return text == null ? "" : text; } public static void setClipboard(String text) { - SDLClipboard.SDL_SetClipboardText(text); + MainThread.run(() -> SDLClipboard.SDL_SetClipboardText(text)); } public static void setIcon() { diff --git a/src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java b/src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java index f52f45b..376cc87 100644 --- a/src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java +++ b/src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java @@ -103,7 +103,7 @@ public Drawable getDrawable() { public void setTextInputActive(boolean active) { if (handle > 0 && textInputActive) { - checkSdlError(SDLKeyboard.SDL_ClearComposition(handle)); + MainThread.run(() -> checkSdlError(SDLKeyboard.SDL_ClearComposition(handle))); } textInputX = -1; textInputRequested = active; @@ -125,11 +125,13 @@ public void setTextInputArea(int x, int y, int width, int height, int cursor, in ) { return; } - try (MemoryStack stack = stackPush()) { - SDL_Rect.Buffer area = SDL_Rect.calloc(1, stack) - .x(areaX).y(areaY).w(areaWidth).h(areaHeight); - checkSdlError(SDLKeyboard.SDL_SetTextInputArea(handle, area, areaCursor)); - } + MainThread.run(() -> { + try (MemoryStack stack = stackPush()) { + SDL_Rect.Buffer area = SDL_Rect.calloc(1, stack) + .x(areaX).y(areaY).w(areaWidth).h(areaHeight); + checkSdlError(SDLKeyboard.SDL_SetTextInputArea(handle, area, areaCursor)); + } + }); textInputX = areaX; textInputY = areaY; textInputWidth = areaWidth; @@ -142,11 +144,13 @@ private void updateTextInputState() { return; } - if (textInputRequested) { - checkSdlError(SDLKeyboard.SDL_StartTextInput(handle)); - } else { - SDLKeyboard.SDL_StopTextInput(handle); - } + MainThread.run(() -> { + if (textInputRequested) { + checkSdlError(SDLKeyboard.SDL_StartTextInput(handle)); + } else { + SDLKeyboard.SDL_StopTextInput(handle); + } + }); textInputActive = textInputRequested; textInputX = -1; } @@ -159,7 +163,7 @@ public String getTitle() { public void setTitle(@NotNull String title) { this.title = title; if (isCreated()) { - SDL_SetWindowTitle(handle, title); + MainThread.run(() -> SDL_SetWindowTitle(handle, title)); } } @@ -260,14 +264,17 @@ public int setIcon(@NotNull ByteBuffer[] icons) { ByteBuffer pixels = MemoryUtil.memAlloc(icon.remaining()); try { pixels.put(icon.duplicate()).flip(); - try (SDL_Surface surface = SDLSurface.SDL_CreateSurfaceFrom( - size, size, SDLPixels.SDL_PIXELFORMAT_RGBA32, pixels, size * 4 - )) { - checkSdlError(surface != null); - checkSdlError(SDL_SetWindowIcon(handle, surface)); - } catch (Exception e) { - Pylon.LOG.log(Level.SEVERE, "Failed to set window icon", e); - } + int iconSize = size; + MainThread.run(() -> { + try (SDL_Surface surface = SDLSurface.SDL_CreateSurfaceFrom( + iconSize, iconSize, SDLPixels.SDL_PIXELFORMAT_RGBA32, pixels, iconSize * 4 + )) { + checkSdlError(surface != null); + checkSdlError(SDL_SetWindowIcon(handle, surface)); + } catch (Exception e) { + Pylon.LOG.log(Level.SEVERE, "Failed to set window icon", e); + } + }); } finally { MemoryUtil.memFree(pixels); } @@ -289,11 +296,17 @@ public void update() { } public void processMessages() { - if (!SDL_IsMainThread()) { + if (!MainThread.DISPATCH && !SDL_IsMainThread()) { return; } windowResized = false; + MainThread.run(this::pollEvents); + Keyboard.poll(); + Mouse.poll(); + } + + private void pollEvents() { while (SDL_PollEvent(event)) { switch (event.type()) { case SDL_EVENT_QUIT: @@ -358,8 +371,6 @@ public void processMessages() { break; } } - Keyboard.poll(); - Mouse.poll(); } private static void checkSdlError(boolean success) { @@ -373,6 +384,11 @@ public synchronized void initializeVideo() { return; } + MainThread.run(this::initSdlVideo); + videoInitialized = true; + } + + private void initSdlVideo() { SDL_SetMemoryFunctions( MemoryUtil::nmemAllocChecked, MemoryUtil::nmemCallocChecked, @@ -393,8 +409,6 @@ public synchronized void initializeVideo() { if (!SDL_Init(SDL_INIT_VIDEO)) { throw new IllegalStateException("Unable to initialize SDL: " + SDL_GetError()); } - - videoInitialized = true; } public void create(@NotNull GpuSurface fallbackSurface) throws LWJGLException { @@ -417,24 +431,27 @@ public void create(@NotNull GpuSurface fallbackSurface) throws LWJGLException { throw throwable; } - try (MemoryStack ms = stackPush()) { - setFullscreen(fullscreenDeferred); - - IntBuffer width = ms.mallocInt(1); - IntBuffer height = ms.mallocInt(1); - checkSdlError(SDL_GetWindowSizeInPixels(handle, width, height)); - framebufferWidth = Math.max(1, width.get(0)); - framebufferHeight = Math.max(1, height.get(0)); - } + setFullscreen(fullscreenDeferred); + MainThread.run(() -> { + try (MemoryStack ms = stackPush()) { + IntBuffer width = ms.mallocInt(1); + IntBuffer height = ms.mallocInt(1); + checkSdlError(SDL_GetWindowSizeInPixels(handle, width, height)); + framebufferWidth = Math.max(1, width.get(0)); + framebufferHeight = Math.max(1, height.get(0)); + } + }); Mouse.create(); Keyboard.create(); - checkSdlError(SDL_ShowWindow(handle)); - checkSdlError(SDL_RaiseWindow(handle)); - if (SDL_IsMainThread()) { - SDL_PumpEvents(); - } - focused = (SDL_GetWindowFlags(handle) & SDL_WINDOW_INPUT_FOCUS) != 0; + MainThread.run(() -> { + checkSdlError(SDL_ShowWindow(handle)); + checkSdlError(SDL_RaiseWindow(handle)); + if (SDL_IsMainThread()) { + SDL_PumpEvents(); + } + focused = (SDL_GetWindowFlags(handle) & SDL_WINDOW_INPUT_FOCUS) != 0; + }); updateTextInputState(); if (cachedIcons != null) { setIcon(cachedIcons); @@ -484,8 +501,10 @@ public void setFullscreen(boolean fullscreen) { width = windowedWidth; height = windowedHeight; } - SDL_SetWindowFullscreen(handle, fullscreen); - SDL_SetWindowSize(handle, windowedWidth, windowedHeight); + MainThread.run(() -> { + SDL_SetWindowFullscreen(handle, fullscreen); + SDL_SetWindowSize(handle, windowedWidth, windowedHeight); + }); windowResized = true; } catch (Throwable t) { Pylon.LOG.log(Level.WARNING, "Failed to set fullscreen: ", t); @@ -531,15 +550,17 @@ public void destroy() { } handle = -1L; textInputX = -1; - if (SDL_WasInit(SDL_INIT_VIDEO) != 0) { - SDL_QuitSubSystem(SDL_INIT_VIDEO); - } + MainThread.run(() -> { + if (SDL_WasInit(SDL_INIT_VIDEO) != 0) { + SDL_QuitSubSystem(SDL_INIT_VIDEO); + } + SDL_Quit(); + }); if (event != null) { event.free(); event = null; windowEvent = null; } - SDL_Quit(); videoInitialized = false; try (MemoryStack stack = stackPush()) { PointerBuffer funcs = stack.mallocPointer(4); @@ -568,7 +589,7 @@ public boolean isActive() { public void setResizable(boolean isResizable) { resizable = isResizable; if (isCreated()) { - SDL_SetWindowResizable(handle, resizable); + MainThread.run(() -> SDL_SetWindowResizable(handle, resizable)); } } diff --git a/src/main/java/pl/tomgirl/pylon/window/GlSurface.java b/src/main/java/pl/tomgirl/pylon/window/GlSurface.java index 4aef59c..1153e0b 100644 --- a/src/main/java/pl/tomgirl/pylon/window/GlSurface.java +++ b/src/main/java/pl/tomgirl/pylon/window/GlSurface.java @@ -33,6 +33,23 @@ public long createWindow(String title, int width, int height, boolean resizable) throw new IllegalStateException("Surface has already been created"); } + MainThread.run(() -> createSdlWindow(title, width, height, resizable)); + + try { + context = check(SDL_GL_CreateContext(window)); + check(SDL_GL_LoadLibrary((ByteBuffer) null)); + Configuration.OPENGL_EXPLICIT_INIT.set(true); + GL.create(SDLVideo::SDL_GL_GetProcAddress); + glInitialized = true; + capabilities = GL.createCapabilities(MemoryUtil::memCallocPointer); + return window; + } catch (RuntimeException | Error throwable) { + destroy(); + throw throwable; + } + } + + private void createSdlWindow(String title, int width, int height, boolean resizable) { int properties = (int) check(SDL_CreateProperties()); try { check(SDL_SetNumberProperty(properties, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED)); @@ -50,19 +67,6 @@ public long createWindow(String title, int width, int height, boolean resizable) } finally { SDL_DestroyProperties(properties); } - - try { - context = check(SDL_GL_CreateContext(window)); - check(SDL_GL_LoadLibrary((ByteBuffer) null)); - Configuration.OPENGL_EXPLICIT_INIT.set(true); - GL.create(SDLVideo::SDL_GL_GetProcAddress); - glInitialized = true; - capabilities = GL.createCapabilities(MemoryUtil::memCallocPointer); - return window; - } catch (RuntimeException | Error throwable) { - destroy(); - throw throwable; - } } private void configureContext() { @@ -125,7 +129,7 @@ public void destroy() { context = 0; } if (window != 0) { - SDL_DestroyWindow(window); + MainThread.run(() -> SDL_DestroyWindow(window)); window = 0; } } diff --git a/src/main/java/pl/tomgirl/pylon/window/MainThread.java b/src/main/java/pl/tomgirl/pylon/window/MainThread.java new file mode 100644 index 0000000..5da48ab --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/window/MainThread.java @@ -0,0 +1,82 @@ +package pl.tomgirl.pylon.window; + +import java.util.function.Supplier; + +import org.lwjgl.sdl.SDL_MainThreadCallback; +import org.lwjgl.system.JNI; +import org.lwjgl.system.macosx.LibC; +import pl.tomgirl.pylon.Platform; + +import static org.lwjgl.system.MemoryUtil.NULL; +import static org.lwjgl.system.macosx.DynamicLinkLoader.RTLD_DEFAULT; +import static org.lwjgl.system.macosx.DynamicLinkLoader.dlsym; + +public class MainThread { + public static final boolean DISPATCH = Platform.CURRENT == Platform.MACOS && System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + LibC.getpid()) == null; + + private static long dispatchSyncF; + private static long mainQueue; + private static SDL_MainThreadCallback trampoline; + private static volatile Thread thread; + private static volatile Supplier task; + private static volatile Object result; + private static volatile Throwable failure; + + private MainThread() {} + + public static void run(Runnable task) { + call(() -> { + task.run(); + return null; + }); + } + + @SuppressWarnings("unchecked") + public static synchronized T call(Supplier task) { + if (!DISPATCH || Thread.currentThread() == thread) { + return task.get(); + } + + if (trampoline == null) { + dispatchSyncF = check(dlsym(RTLD_DEFAULT, "dispatch_sync_f")); + mainQueue = check(dlsym(RTLD_DEFAULT, "_dispatch_main_q")); + trampoline = SDL_MainThreadCallback.create(userdata -> execute()); + } + + MainThread.task = task; + try { + JNI.invokePPPV(mainQueue, NULL, trampoline.address(), dispatchSyncF); + Throwable t = failure; + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + if (t instanceof Error) { + throw (Error) t; + } + if (t != null) { + throw new IllegalStateException(t); + } + return (T) result; + } finally { + MainThread.task = null; + result = null; + failure = null; + } + } + + private static void execute() { + thread = Thread.currentThread(); + try { + result = task.get(); + } catch (Throwable t) { + failure = t; + } + } + + private static long check(long address) { + if (address == NULL) { + throw new IllegalStateException("libdispatch symbol not found"); + } + return address; + } +} diff --git a/src/main/java/pl/tomgirl/pylon/window/MouseSdl.java b/src/main/java/pl/tomgirl/pylon/window/MouseSdl.java index 09a356d..0a8355b 100644 --- a/src/main/java/pl/tomgirl/pylon/window/MouseSdl.java +++ b/src/main/java/pl/tomgirl/pylon/window/MouseSdl.java @@ -123,14 +123,16 @@ public void setCursorPosition(double x, double y) { lastX = x; lastY = y; accumDx = accumDy = 0; - SDL_WarpMouseInWindow(handle, toSdlX(x), toSdlY(y)); + MainThread.run(() -> SDL_WarpMouseInWindow(handle, toSdlX(x), toSdlY(y))); } public void grabMouse(boolean grab) { - if (!grab) { - SDL_WarpMouseInWindow(handle, toSdlX(lastX), toSdlY(lastY)); - } - SDL_SetWindowRelativeMouseMode(handle, grab); + MainThread.run(() -> { + if (!grab) { + SDL_WarpMouseInWindow(handle, toSdlX(lastX), toSdlY(lastY)); + } + SDL_SetWindowRelativeMouseMode(handle, grab); + }); grabbed = grab; reset(); if (!GameHooks.screenPatchAvailable) {