diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 43a60f7..e977170 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 @@ -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/.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/.gitignore b/.gitignore index 0ecd304..28456e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .gradle .DS_Store .idea/ +.kotlin/ 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 new file mode 100644 index 0000000..3bbb5c1 --- /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("pylonBuild") { + id = "pylon.build" + implementationClass = "pl.tomgirl.pylon.build.PylonBuildPlugin" + } + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..0a4b371 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "pylon-build-logic" diff --git a/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/AL10Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/AL10Patch.java new file mode 100644 index 0000000..6559d8a --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/AL10Patch.java @@ -0,0 +1,16 @@ +package pl.tomgirl.pylon.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/pylon/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/pylon/bakery/patch/ALPatch.java index a81e9c6..e79b39f 100644 --- a/src/main/java/pl/tomgirl/lenis/mixin/ALMixin.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ALPatch.java @@ -1,6 +1,4 @@ -package pl.tomgirl.lenis.mixin; - -import pl.tomgirl.lenis.plugin.CompatStub; +package pl.tomgirl.pylon.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/pylon/bakery/patch/ARBShaderObjectsPatch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ARBShaderObjectsPatch.java new file mode 100644 index 0000000..af67e7c --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/ARBShaderObjectsPatch.java @@ -0,0 +1,16 @@ +package pl.tomgirl.pylon.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/pylon/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/pylon/bakery/patch/CompatStub.java index 13164c6..5eaf7d3 100644 --- a/src/main/java/pl/tomgirl/lenis/plugin/CompatStub.java +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/CompatStub.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis.plugin; +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/pylon/bakery/patch/GL11Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL11Patch.java new file mode 100644 index 0000000..cba8472 --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL11Patch.java @@ -0,0 +1,53 @@ +package pl.tomgirl.pylon.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("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) {} + @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/pylon/bakery/patch/GL20Patch.java b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL20Patch.java new file mode 100644 index 0000000..d337aee --- /dev/null +++ b/build-logic/src/baked-patches/java/pl/tomgirl/pylon/bakery/patch/GL20Patch.java @@ -0,0 +1,29 @@ +package pl.tomgirl.pylon.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/pylon/build/BakeLwjglTask.kt b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/BakeLwjglTask.kt new file mode 100644 index 0000000..25eb1a2 --- /dev/null +++ b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/BakeLwjglTask.kt @@ -0,0 +1,164 @@ +package pl.tomgirl.pylon.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) + } + // 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 + 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/pylon/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/pylon/build/PylonBuildPlugin.kt b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt new file mode 100644 index 0000000..3a72992 --- /dev/null +++ b/build-logic/src/main/kotlin/pl/tomgirl/pylon/build/PylonBuildPlugin.kt @@ -0,0 +1,176 @@ +package pl.tomgirl.pylon.build + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +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.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.* +import org.gradle.language.jvm.tasks.ProcessResources + +class PylonBuildPlugin : 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 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") + runtimeClasspath += output + compileClasspath + } + + extensions.configure { + 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")) + + 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(listOf("fabric.mod.json", "mcmod.info")) { + expand("version" to project.version) + } + } + + tasks.named("jar") { + dependsOn(fabric.classesTaskName, forge.classesTaskName) + from(fabric.output, forge.output) + archiveClassifier.set("slim") + 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", + ) + } + + val shadowJar = tasks.named("shadowJar") { + archiveClassifier.set("relocated") + destinationDirectory.set(layout.buildDirectory.dir("intermediates")) + 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() + } + + val unpatchedJar = tasks.register("unpatchedJar") { + dependsOn(shadowJar, fabric.classesTaskName, forge.classesTaskName) + from(shadowJar.flatMap { it.archiveFile }.map { zipTree(it.asFile) }) { + exclude("META-INF/MANIFEST.MF") + } + from(fabric.output, forge.output) + archiveClassifier.set("unpatched") + 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", + ) + } + + 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, forge.allSource, launchwrapper.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..1681231 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("pylon.build") } diff --git a/gradle.properties b/gradle.properties index 21acf40..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.4 +base_name = pylon +version = 0.1.6 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..9a6af14 --- /dev/null +++ b/runs/fabric/build.gradle.kts @@ -0,0 +1,31 @@ +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") + } + } +} + +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..d772d33 --- /dev/null +++ b/runs/logging.properties @@ -0,0 +1,6 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO + +java.util.logging.ConsoleHandler.level=ALL +Pylon.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..77f97a5 --- /dev/null +++ b/runs/vanilla/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.loom) +} + +val pylon = configurations.create("pylon") + +dependencies { + minecraft("com.mojang:minecraft:1.6.4") + pylon(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:${pylon.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") + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 15bd70d..0ea1d53 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" +rootProject.name = "pylon" + +include(":runs:fabric", ":runs:vanilla") diff --git a/src/fabric/java/pl/tomgirl/pylon/fabric/MixinPlugin.java b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinPlugin.java new file mode 100644 index 0000000..9c119e2 --- /dev/null +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinPlugin.java @@ -0,0 +1,58 @@ +package pl.tomgirl.pylon.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/pylon/fabric/MixinTransformer.java b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformer.java new file mode 100644 index 0000000..18b86b3 --- /dev/null +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformer.java @@ -0,0 +1,29 @@ +package pl.tomgirl.pylon.fabric; + +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; +import org.spongepowered.asm.transformers.TreeTransformer; +import pl.tomgirl.pylon.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.pylon")) { + 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/pylon/fabric/MixinTransformerDelegate.java b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformerDelegate.java new file mode 100644 index 0000000..6855712 --- /dev/null +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/MixinTransformerDelegate.java @@ -0,0 +1,73 @@ +package pl.tomgirl.pylon.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/pylon/fabric/PreLaunch.java similarity index 87% rename from src/main/java/pl/tomgirl/lenis/PreLaunch.java rename to src/fabric/java/pl/tomgirl/pylon/fabric/PreLaunch.java index 0e27b3f..df11e48 100644 --- a/src/main/java/pl/tomgirl/lenis/PreLaunch.java +++ b/src/fabric/java/pl/tomgirl/pylon/fabric/PreLaunch.java @@ -1,4 +1,4 @@ -package pl.tomgirl.lenis; +package pl.tomgirl.pylon.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/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/PylonTransformer.java b/src/forge/java/pl/tomgirl/pylon/forge/PylonTransformer.java new file mode 100644 index 0000000..dc8b8e9 --- /dev/null +++ b/src/forge/java/pl/tomgirl/pylon/forge/PylonTransformer.java @@ -0,0 +1,14 @@ +package pl.tomgirl.pylon.forge; + +import net.minecraft.launchwrapper.IClassTransformer; +import pl.tomgirl.pylon.game.GameTransformer; + +public final class PylonTransformer implements IClassTransformer { + @Override + public byte[] transform(String name, String transformedName, byte[] bytecode) { + 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/pylon/forge/PylonTweaker.java b/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java new file mode 100644 index 0000000..9aa4b31 --- /dev/null +++ b/src/forge/java/pl/tomgirl/pylon/forge/PylonTweaker.java @@ -0,0 +1,84 @@ +package pl.tomgirl.pylon.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 PylonTweaker 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 = 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.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 runtime class loader", e); + } + + classLoader.addClassLoaderExclusion("pl.tomgirl.pylon."); + classLoader.addTransformerExclusion("pl.tomgirl.pylonfml."); + classLoader.registerTransformer(PylonTransformer.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.pylon.")) { + 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."); + 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..cdd68fa --- /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-256x.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..dcd6f54 --- /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-256x.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/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..42228bf --- /dev/null +++ b/src/launchwrapper/java/net/minecraft/launchwrapper/LaunchClassLoader.java @@ -0,0 +1,8 @@ +package net.minecraft.launchwrapper; + +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/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 0eb3cc6..bff05fe 100644 --- a/src/main/java/org/lwjgl/LWJGLUtil.java +++ b/src/main/java/org/lwjgl/LWJGLUtil.java @@ -1,18 +1,37 @@ package org.lwjgl; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import pl.tomgirl.pylon.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 Logger LOG = LogManager.getLogger("LWJGL"); + private static final Logger LOG = Logger.getLogger("LWJGL"); private LWJGLUtil() {} public static void log(CharSequence msg) { if (DEBUG) { - LOG.debug(msg); + 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/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/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/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 6e8d235..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; @@ -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 ccfd61b..f7a6564 100644 --- a/src/main/java/org/lwjgl/opengl/Display.java +++ b/src/main/java/org/lwjgl/opengl/Display.java @@ -1,12 +1,14 @@ 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.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 { @@ -60,7 +62,16 @@ 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) { + Pylon.LOG.log(Level.SEVERE, "Failed to create display", e); + throw new LWJGLException(e); + } + } + + public static boolean isFullscreen() { + return SDL.isFullscreen(); } public static void setFullscreen(boolean fullscreen) { @@ -80,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(); } @@ -119,4 +139,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) {} } 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/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/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/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/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..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,12 +157,24 @@ 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) { - 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/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); + } +} 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/Lenis.java b/src/main/java/pl/tomgirl/lenis/Lenis.java deleted file mode 100644 index 8e7c61a..0000000 --- a/src/main/java/pl/tomgirl/lenis/Lenis.java +++ /dev/null @@ -1,8 +0,0 @@ -package pl.tomgirl.lenis; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class Lenis { - public static final Logger LOG = LogManager.getLogger("Lenis"); -} 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/pylon/Agent.java b/src/main/java/pl/tomgirl/pylon/Agent.java new file mode 100644 index 0000000..effa260 --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/Agent.java @@ -0,0 +1,40 @@ +package pl.tomgirl.pylon; + +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()) { + Pylon.LOG.log(Level.INFO, "Injected Fabric mod"); + return; + } + + instrumentation.addTransformer(new pl.tomgirl.pylon.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)) { + 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 89% rename from src/main/java/pl/tomgirl/lenis/Platform.java rename to src/main/java/pl/tomgirl/pylon/Platform.java index cef1220..8bbcce9 100644 --- a/src/main/java/pl/tomgirl/lenis/Platform.java +++ b/src/main/java/pl/tomgirl/pylon/Platform.java @@ -1,8 +1,9 @@ -package pl.tomgirl.lenis; +package pl.tomgirl.pylon; 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.error("Couldn't open uri '{}'", uri, e); + Pylon.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/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/pylon/game/GameHooks.java b/src/main/java/pl/tomgirl/pylon/game/GameHooks.java new file mode 100644 index 0000000..c038772 --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/game/GameHooks.java @@ -0,0 +1,54 @@ +package pl.tomgirl.pylon.game; + +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; + +import org.lwjgl.sdl.SDLClipboard; + +import pl.tomgirl.pylon.Pylon; +import pl.tomgirl.pylon.window.DisplaySdl; + +@SuppressWarnings("unused") +public final class GameHooks { + public static volatile boolean screenPatchAvailable; + + private GameHooks() {} + + public static String getClipboard() { + String text = MainThread.call(SDLClipboard::SDL_GetClipboardText); + return text == null ? "" : text; + } + + public static void setClipboard(String text) { + MainThread.run(() -> SDLClipboard.SDL_SetClipboardText(text)); + } + + public static void setIcon() { + 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"); + 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)); + } + } + pixels.flip(); + DisplaySdl.instance().setIcon(new ByteBuffer[] { pixels }); + } 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/pylon/game/GameTransformer.java b/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java new file mode 100644 index 0000000..51bd13f --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/game/GameTransformer.java @@ -0,0 +1,43 @@ +package pl.tomgirl.pylon.game; + +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; +import org.objectweb.asm.ClassWriter; +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 + public byte[] transform(ClassLoader loader, String name, Class type, ProtectionDomain domain, byte[] bytecode) { + return transform(bytecode); + } + + public static byte[] transform(byte[] bytecode) { + 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, fml).filter(Patch::matches).collect(Collectors.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); + 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/pylon/game/patch/Patch.java b/src/main/java/pl/tomgirl/pylon/game/patch/Patch.java new file mode 100644 index 0000000..3c8d310 --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/game/patch/Patch.java @@ -0,0 +1,45 @@ +package pl.tomgirl.pylon.game.patch; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import pl.tomgirl.pylon.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 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/pylon/game/patch/impl/FmlModContainerPatch.java b/src/main/java/pl/tomgirl/pylon/game/patch/impl/FmlModContainerPatch.java new file mode 100644 index 0000000..56f79e0 --- /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.FmlModContainer"; + } 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/java/pl/tomgirl/pylon/game/patch/impl/MinecraftPatch.java b/src/main/java/pl/tomgirl/pylon/game/patch/impl/MinecraftPatch.java new file mode 100644 index 0000000..f13a894 --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/game/patch/impl/MinecraftPatch.java @@ -0,0 +1,123 @@ +package pl.tomgirl.pylon.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.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"; + 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.get(0); + } + + 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/pylon/game/patch/impl/ScreenPatch.java b/src/main/java/pl/tomgirl/pylon/game/patch/impl/ScreenPatch.java new file mode 100644 index 0000000..a997655 --- /dev/null +++ b/src/main/java/pl/tomgirl/pylon/game/patch/impl/ScreenPatch.java @@ -0,0 +1,72 @@ +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.pylon.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); + 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, argumentSlot); + method.visitMethodInsn(Opcodes.INVOKESTATIC, HOOKS, "setClipboard", "(Ljava/lang/String;)V", false); + method.visitInsn(Opcodes.RETURN); + } + method.visitMaxs(1, argumentSlot + (patch == ClipboardMethod.GET ? 0 : 1)); + method.visitEnd(); + return null; + } + }; + } + + private enum ClipboardMethod { GET, SET } +} diff --git a/src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java b/src/main/java/pl/tomgirl/pylon/window/BoundedQueue.java similarity index 91% rename from src/main/java/pl/tomgirl/lenis/window/BoundedQueue.java rename to src/main/java/pl/tomgirl/pylon/window/BoundedQueue.java index d5f44f2..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; @@ -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/pylon/window/DisplaySdl.java similarity index 74% rename from src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java rename to src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java index f0757b7..376cc87 100644 --- a/src/main/java/pl/tomgirl/lenis/window/DisplaySdl.java +++ b/src/main/java/pl/tomgirl/pylon/window/DisplaySdl.java @@ -1,15 +1,15 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; import java.nio.ByteBuffer; import java.nio.IntBuffer; 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; -import pl.tomgirl.lenis.Lenis; -import net.fabricmc.loader.api.FabricLoader; +import pl.tomgirl.pylon.Pylon; import org.jetbrains.annotations.NotNull; import org.lwjgl.LWJGLException; import org.lwjgl.PointerBuffer; @@ -33,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(); @@ -60,7 +61,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; @@ -102,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; @@ -124,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; @@ -141,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; } @@ -158,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)); } } @@ -215,7 +220,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()) { @@ -259,14 +264,17 @@ public int setIcon(@NotNull ByteBuffer[] icons) { ByteBuffer pixels = MemoryUtil.memAlloc(icon.remaining()); try { pixels.put(icon.duplicate()).flip(); - try (var 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.error("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); } @@ -288,12 +296,27 @@ public void update() { } public void processMessages() { + 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, 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()) { @@ -302,36 +325,52 @@ 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(); - Mouse.poll(); } private static void checkSdlError(boolean success) { @@ -340,11 +379,16 @@ 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; } + MainThread.run(this::initSdlVideo); + videoInitialized = true; + } + + private void initSdlVideo() { SDL_SetMemoryFunctions( MemoryUtil::nmemAllocChecked, MemoryUtil::nmemCallocChecked, @@ -356,20 +400,23 @@ 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")); 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()); } + } + + 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; @@ -384,21 +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)); - 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); @@ -423,6 +476,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; @@ -433,7 +490,7 @@ public void setFullscreen(boolean fullscreen) { if (fullscreen) { int display = SDL_GetPrimaryDisplay(); if (display == 0) { - Lenis.LOG.warn("Failed to find display"); + Pylon.LOG.log(Level.WARNING, "Failed to find display"); return; } if (!this.fullscreen) { @@ -444,11 +501,13 @@ 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) { - Lenis.LOG.warn("Failed to set fullscreen: ", t); + Pylon.LOG.log(Level.WARNING, "Failed to set fullscreen: ", t); } } @@ -462,7 +521,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"); } @@ -491,15 +550,18 @@ 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); @@ -527,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)); } } @@ -569,6 +631,11 @@ private GpuSurface requireSurface() { } private final class SurfaceDrawable implements Drawable { + @Override + public boolean isCurrent() { + return false; + } + @Override public void makeCurrent() { DisplaySdl.this.makeCurrent(); diff --git a/src/main/java/pl/tomgirl/lenis/window/GlSurface.java b/src/main/java/pl/tomgirl/pylon/window/GlSurface.java similarity index 82% rename from src/main/java/pl/tomgirl/lenis/window/GlSurface.java rename to src/main/java/pl/tomgirl/pylon/window/GlSurface.java index 3f094d0..1153e0b 100644 --- a/src/main/java/pl/tomgirl/lenis/window/GlSurface.java +++ b/src/main/java/pl/tomgirl/pylon/window/GlSurface.java @@ -1,8 +1,10 @@ -package pl.tomgirl.lenis.window; +package pl.tomgirl.pylon.window; 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; @@ -31,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)); @@ -48,20 +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; - GL.createCapabilities(MemoryUtil::memCallocPointer); - capabilitiesCreated = true; - return window; - } catch (RuntimeException | Error throwable) { - destroy(); - throw throwable; - } } private void configureContext() { @@ -79,12 +84,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 +115,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(); @@ -115,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/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 84% rename from src/main/java/pl/tomgirl/lenis/window/KeyboardSdl.java rename to src/main/java/pl/tomgirl/pylon/window/KeyboardSdl.java index d55f866..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.Objects; +import java.util.logging.Level; -import pl.tomgirl.lenis.Lenis; +import pl.tomgirl.pylon.Pylon; import org.lwjgl.input.Keyboard; import org.lwjgl.sdl.*; @@ -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.warn("Untranslated key: {} ({})", 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/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/lenis/window/MouseSdl.java b/src/main/java/pl/tomgirl/pylon/window/MouseSdl.java similarity index 85% rename from src/main/java/pl/tomgirl/lenis/window/MouseSdl.java rename to src/main/java/pl/tomgirl/pylon/window/MouseSdl.java index 1f38291..0a8355b 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,6 +6,7 @@ import org.lwjgl.sdl.SDL_MouseButtonEvent; import org.lwjgl.sdl.SDL_MouseMotionEvent; import org.lwjgl.sdl.SDL_MouseWheelEvent; +import pl.tomgirl.pylon.game.GameHooks; import static org.lwjgl.sdl.SDLEvents.*; import static org.lwjgl.sdl.SDLMouse.*; @@ -122,16 +123,21 @@ 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) { + DISPLAY.setTextInputActive(!grab); + } } public boolean hasWheel() { @@ -148,14 +154,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); @@ -172,8 +185,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; @@ -191,8 +205,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(); @@ -209,9 +224,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/assets/lenis/icon.png b/src/main/resources/assets/lenis/icon.png deleted file mode 100644 index 06d486e..0000000 Binary files a/src/main/resources/assets/lenis/icon.png and /dev/null differ 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 0000000..243f3f1 Binary files /dev/null and b/src/main/resources/assets/pylon/icon-256x.png differ diff --git a/src/main/resources/assets/pylon/icon.png b/src/main/resources/assets/pylon/icon.png new file mode 100644 index 0000000..3f6354e Binary files /dev/null and b/src/main/resources/assets/pylon/icon.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 120840c..3686084 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,22 +1,23 @@ { "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.PreLaunch" + "pl.tomgirl.pylon.fabric.PreLaunch" ] }, "mixins": [ - "lenis.mixins.json" + "pylon.mixins.json" ], "depends": { "fabricloader": ">=0.17.3", - "minecraft": "<1.13.0-alpha.17.43.a" + "minecraft": "<1.13.0-alpha.17.43.a", + "java": ">=8" } } diff --git a/src/main/resources/lenis.mixins.json b/src/main/resources/lenis.mixins.json deleted file mode 100644 index e2a7a8a..0000000 --- a/src/main/resources/lenis.mixins.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "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 - } -} diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 0000000..fd1a1a9 --- /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-256x.png", + "parent": "", + "screenshots": [], + "dependencies": [] +}] diff --git a/src/main/resources/pylon.mixins.json b/src/main/resources/pylon.mixins.json new file mode 100644 index 0000000..ddb342b --- /dev/null +++ b/src/main/resources/pylon.mixins.json @@ -0,0 +1,5 @@ +{ + "required": true, + "compatibilityLevel": "JAVA_8", + "plugin": "pl.tomgirl.pylon.fabric.MixinPlugin" +} \ No newline at end of file