GTNewHorizons / lwjgl3ify

A mod to run Minecraft 1.7.10 using LWJGL3 and Java 17+
GNU Lesser General Public License v3.0
156 stars 32 forks source link

LWJGL3ify

Brings LWJGL3 and modern Java versions to Minecraft 1.7.10. Bundles RetroFuturaBootstrap as a flexible early load plugin system.

Usage

Lwjgl3ify depends on Unimixins (https://github.com/LegacyModdingMC/UniMixins/), use at least version 0.1.7 with the GTNHMixins module. Hodgepodge (https://github.com/GTNewHorizons/Hodgepodge) is strongly recommended, it has many Java 17+ compatibility patches for ARR mods (Biomes'o'Plenty, Witchery, JourneyMap, and more) which we could not directly fix in the GTNH forks.

Client

Non-GTNH 1.7.10 instance setup

If you are using a GregTech New Horizons pre-packaged Java 17+ instance, skip to the next section!

To install in PrismLauncher or MultiMC:

Copy the contents of lwjgl3ify-VERSION-multimc.zip to instances/My Modpack/. mmc-pack.json needs to be overwritten. Reload the launcher, and it should show up modified versions of Forge, Minecraft and LWJGL3 in the instance window. Put the mod jar (lwjgl3ify-VERSION.jar) in mods/ in your instance's minecraft folder, it will get loaded as a coremod. The forgePatches jar will get automatically downloaded by the launcher.

Make sure you have installed the UniMixins GTNHMixins and Mixin modules, or the all modules combined jar.

Change the instance java version to 17.0.6, 19.0.2, 20 or 21 (or newer), these are the currently supported versions. You can also use Java 11 if you remove the -Djava.security.manager=allow argument from patches/me.eigenraven.lwjgl3ify.forgepatches.json.

MultiMC setup (GTNH and non-GTNH)

[!IMPORTANT] If you're using GTNH 2.5.1 or older, use the lwjgl3ify 1.x arguments from https://github.com/GTNewHorizons/lwjgl3ify/blob/2229295b9fe5ed3304f62288f76f84bdb79ff5bb/README.MD#multimc-setup-gtnh-and-non-gtnh. The following arguments are relevant to the 2.x version.

For MultiMC, you need to manually put in extra java arguments, Prism Launcher can load them from the patches json files automatically:

-Dfile.encoding=UTF-8 -Djava.system.class.loader=com.gtnewhorizons.retrofuturabootstrap.RfbSystemClassLoader -Djava.security.manager=allow --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.ref=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.net.spi=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio.channels=ALL-UNNAMED --add-opens java.base/java.nio.charset=ALL-UNNAMED --add-opens java.base/java.nio.file=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.time.chrono=ALL-UNNAMED --add-opens java.base/java.time.format=ALL-UNNAMED --add-opens java.base/java.time.temporal=ALL-UNNAMED --add-opens java.base/java.time.zone=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomics=ALL-UNNAMED --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED --add-opens java.base/java.util.jar=ALL-UNNAMED --add-opens java.base/java.util.zip=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.desktop/com.sun.imageio.plugins.png=ALL-UNNAMED --add-opens java.desktop/sun.awt.image=ALL-UNNAMED --add-opens java.desktop/sun.awt=ALL-UNNAMED --add-opens java.sql.rowset/javax.sql.rowset.serial=ALL-UNNAMED --add-opens jdk.dynalink/jdk.dynalink.beans=ALL-UNNAMED --add-opens jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED,java.naming

(Experimental) Relauncher installation

To install in any client launcher, just drop the lwjgl3ify-VERSION.jar mod jar in the mods/ directory and configure to launch with Java 8 as usual. On startup, a window should appear prompting you to select a modern Java version to relaunch with, and the JVM settings for that Java installation.

Tweaking configs

If you want to tweak your default window size, OpenGL context properties or other more advanced settings, check out the config file in config/lwjgl3ify.cfg after the first startup.

Server

First, install a working 1.7.10 Forge server, from a modpack zip or using forge's installer. Then, install the mod jar (and unimixins, hodgepodge and gtnhlib as needed) in mods/, and download the forgePatches jar. Put the forgePatches jar in the same folder as your forge-universal and minecraft server jars. Thermos/Crucible/Hybrid servers are not supported!. Lwjgl3ify also depends on Unimixins (https://github.com/LegacyModdingMC/UniMixins/), use at least version 0.1.7. Rename the forgePatches jar to just lwjgl3ify-forgePatches.jar. Create a file named java9args.txt with the contents of the file in this repository. You can now launch the server with a command like the following, assuming the first java executable on your PATH is java 11/17/newer:

java -Xmx6G -Xms6G @java9args.txt -jar lwjgl3ify-forgePatches.jar nogui

LiteLoader compatibility

If you install LiteLoader via MultiMC or a fork like PrismLauncher, make sure to move the "LiteLoader" entry above the "LWJGL3ify Launch Args" entry, otherwise the launch will fail.

How does it work?

There are a few components to enabling modern java and lwjgl usage on old minecraft versions, lwjgl3ify implements these for 1.7.10 specifically:

Fixing mods

This mod is not enough to make modpacks work in Java 9+, mods had to be updated. Here's a potentially incomplete list of breaking changes that needed code changes in mods, or mixins to fix non-open-source mods:

(NOT IN lwjgl3ify) System class loader is no longer a URLClassLoader

Lwjgl3ify replaces the system classloader with its own URLClassLoader-derived class that supports addURL and getURLs via public method calls that are compatible with the old reflection access for them.

To get the classpath, simply split System.getProperty("java.class.path") by File.pathSeparator.

Do note that for most mods, their classloader is still a LaunchClassLoader which does extend URLClassLoader.

To modify the URLClassPath you can use the following code:

public static void addUrlToClassloader(ClassLoader loader, URL coreModUrl) {
    try {
        if (loader instanceof URLClassLoader) {
            if (ADDURL == null) {
                ADDURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
                ADDURL.setAccessible(true);
            }
            ADDURL.invoke(loader, coreModUrl);
        } else {
            Field ucpField;
            try {
                // Java 8-11
                ucpField = loader.getClass().getDeclaredField("ucp");
            } catch (NoSuchFieldException e) {
                // Java 17
                ucpField = loader.getClass().getSuperclass().getDeclaredField("ucp");
            }
            ucpField.setAccessible(true);
            // URLClassPath is in different packages in different Java versions, so we use Object.
            final Object ucp = ucpField.get(loader);
            final Method urlAdder = ucp.getClass().getDeclaredMethod("addURL", URL.class);
            urlAdder.invoke(ucp, coreModUrl);
        }
    } catch (ReflectiveOperationException e) {
        throw new RuntimeException("Couldn't add url to classpath in loader " + loader.getClass(), e);
    }
}

java.base/java.lang.reflect classes are protected from reflective access

E.g. Field.class.getDeclaredFields() returns an empty list. The most common use of this was to change the modifiers field to strip final from the Field accessor to trick it into letting you write into static final fields. Instead of that, use an access transformer (public-f class field_srg # field_mcp) to remove the final attribute at class load time. Alternatively, inject a different initializer via Mixins/asm by modifying the class static constructor special method <clinit>.

If that is not feasible (e.g. runtime reflection discovering fields), you can use sun.misc.Unsafe to directly write to the memory location of the final field like so:

final Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
final Unsafe UNSAFE = (Unsafe) theUnsafe.get(null); // Cache this

public static void setField(Field data, Object object, Object value) {
    if (object == null) {
        long offset = UNSAFE.staticFieldOffset(data);
        Object base = UNSAFE.staticFieldBase(data);
        UNSAFE.putObject(base, offset, value);
    } else {
        long offset = UNSAFE.objectFieldOffset(data);
        UNSAFE.putObject(object, offset, value);
    }
}

This is likely to be removed from a 2025 Java release though, I'll probably build a JNI-based alternative built into lwjgl3ify by then.

Beware, the Java JIT compiler will assume that final fields don't change value at runtime, so the value you write there using Unsafe might not actually be read back by some code. This can lead to inconsistent states, where some methods might see the old value while other ones will see the new value, especially if you change the value down a callchain that already read the value earlier.

Development

First, do ./gradlew build and use the zip in ./build/distributions/lwjgl3ify-VERSION-multimc.zip to install lwjgl3ify into a 1.7.10 forge instance in Prism. Install the mod jar from ./build/libs/ as usual.