From a17412e1f1c829dd6d085b4849dae4ca571fd16b Mon Sep 17 00:00:00 2001 From: Roman / Linnea Gräf Date: Tue, 22 Nov 2022 20:22:20 +0100 Subject: Terrible Kotlin! (#435) * Terrible Kotlin! You are unloved and everybody dies * Load Kotlin via a Tweaker * Fix version comparison * Allow user to prevent loading of Kotlin libraries using system properties * Remove testing files --- .../commands/dev/DevTestCommand.java | 3 +- .../notenoughupdates/envcheck/EnvironmentScan.java | 2 +- .../loader/KotlinLoadingTweaker.java | 193 +++++++++++++++++++++ .../notenoughupdates/loader/ModLoadingTweaker.java | 67 +++++++ .../loader/NEUDelegatingTweaker.java | 100 +++++++++++ 5 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/loader/KotlinLoadingTweaker.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/loader/ModLoadingTweaker.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/loader/NEUDelegatingTweaker.java (limited to 'src') diff --git a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java index 5f1fe191..5ee31aa9 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.java @@ -166,8 +166,7 @@ public class DevTestCommand extends ClientCommandBase { } if (args.length == 2 && args[0].equalsIgnoreCase("pt")) { - EnumParticleTypes t = EnumParticleTypes.valueOf(args[1]); - FishingHelper.type = t; + FishingHelper.type = EnumParticleTypes.valueOf(args[1]); return; } if (args.length == 1 && args[0].equalsIgnoreCase("dev")) { diff --git a/src/main/java/io/github/moulberry/notenoughupdates/envcheck/EnvironmentScan.java b/src/main/java/io/github/moulberry/notenoughupdates/envcheck/EnvironmentScan.java index 8e0f24b9..1ae5199a 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/envcheck/EnvironmentScan.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/envcheck/EnvironmentScan.java @@ -34,7 +34,7 @@ public class EnvironmentScan { static boolean shouldCheckOnce = true; - static void checkEnvironmentOnce() { + public static void checkEnvironmentOnce() { if (shouldCheckOnce) checkEnvironment(); } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/loader/KotlinLoadingTweaker.java b/src/main/java/io/github/moulberry/notenoughupdates/loader/KotlinLoadingTweaker.java new file mode 100644 index 00000000..68aa8adb --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/loader/KotlinLoadingTweaker.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2022 Linnea Gräf + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.loader; + +import net.minecraft.launchwrapper.ITweaker; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.launchwrapper.LaunchClassLoader; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; + +/** + *

TO OTHER MOD AUTHORS THAT WANT TO BUNDLE KOTLIN AND ARE RUNNING INTO CONFLICTS WITH NEU:

+ * + *

+ * NEU allows you to stop it from loading Kotlin by specifying one of two properties in the + * {@link Launch#blackboard}. This needs to be done before {@link #injectIntoClassLoader}, so do it in your Tweakers + * constructor. Most likely you will not need to do this, since different Kotlin versions rarely contain breaking changes + * and most people do not use any of the newer features. + *

+ * + * + * + *

+ * Sadly the Kotlin stdlib cannot be relocated (and we wouldn't want to do that either, because it would prevent us + * from interoperating with other Kotlin mods in some ways (for example: reflection)), so in order to minimize conflicts, + * we allow other mods to stop us from loading Kotlin. Usually this would be handled with a common library provider, but + * since those come bundled with literal Remote Code Execution, Cosmetics, and unskippable Updates that can just not be + * considered a good thing by any developer with a conscience nowadays, those are not an option for us. Another option + * would be a dependency resolution algorithm like Jar-in-Jar, but sadly we have to mod on a 7 year old modding platform, + * so we have to make due with our crude hack. + *

+ */ +public class KotlinLoadingTweaker implements ITweaker { + + /** + * Full version format: [1, 7, 20] (1.7.20) + * RC version format: [1, 7, 20, 1] (1.7.20-rc1) + */ + public static final int[] BUNDLED_KOTLIN_VERSION = new int[]{1, 7, 20}; + + @Override + public void acceptOptions(List args, File gameDir, File assetsDir, String profile) { + + } + + public boolean areWeBundlingAKotlinVersionHigherThan(int[] x) { + for (int i = 0; ; i++) { + boolean doWeHaveMoreVersionIdsLeft = i < BUNDLED_KOTLIN_VERSION.length; + boolean doTheyHaveMoreVersionIdsLeft = i < x.length; + if (doWeHaveMoreVersionIdsLeft && !doTheyHaveMoreVersionIdsLeft) return false; + if (doTheyHaveMoreVersionIdsLeft && !doWeHaveMoreVersionIdsLeft) return true; + if (!doTheyHaveMoreVersionIdsLeft) return true; + if (x[i] > BUNDLED_KOTLIN_VERSION[i]) return false; + } + } + + @Override + public void injectIntoClassLoader(LaunchClassLoader classLoader) { + FileSystem fs = null; + try { + if ("1".equals(System.getProperty("neu.relinquishkotlin"))) { + System.out.println("NEU is forced to relinquish Kotlin by user configuration."); + return; + } + if (Launch.blackboard.get("fml.deobfuscatedEnvironment") == Boolean.TRUE) { + System.out.println("Skipping NEU Kotlin loading in development environment."); + return; + } + Object relinquishAlways = Launch.blackboard.get("neu.relinquishkotlin.always"); + if (relinquishAlways == Boolean.TRUE) { + System.err.println( + "NEU is forced to blanket relinquish loading Kotlin. This is probably a bad judgement call by another developer."); + return; + } + Object relinquishIfBelow = Launch.blackboard.get("neu.relinquishkotlin.ifbelow"); + if ((relinquishIfBelow instanceof int[])) { + int[] requiredVersion = (int[]) relinquishIfBelow; + if (!areWeBundlingAKotlinVersionHigherThan(requiredVersion)) { + System.err.println( + "NEU is relinquishing loading Kotlin because a higher version is requested. This may lead to errors if the advertised Kotlin version is not found. (" + + Arrays.toString(requiredVersion) + " required, " + Arrays.toString(BUNDLED_KOTLIN_VERSION) + " available)"); + return; + } + } + + System.out.println("Attempting to load Kotlin from NEU wrapped libraries."); + + URI uri = getClass().getResource("/neu-kotlin-libraries-wrapped").toURI(); + Path p; + if ("jar".equals(uri.getScheme())) { + fs = FileSystems.newFileSystem(uri, Collections.emptyMap()); + p = fs.getPath( + "/neu-kotlin-libraries-wrapped"); + } else { + p = Paths.get(uri); + } + System.out.println("Loading NEU Kotlin from " + p.toAbsolutePath()); + Path tempDirectory = Files.createTempDirectory("notenoughupdates-extracted-kotlin"); + System.out.println("Using temporary directory " + tempDirectory + " to store extracted kotlin."); + tempDirectory.toFile().deleteOnExit(); + try (Stream libraries = Files.walk(p, 1)) { + libraries.filter(it -> it.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".jar")) + .forEach(it -> { + try { + Path extractedPath = tempDirectory.resolve(it.getFileName().toString()); + extractedPath.toFile().deleteOnExit(); + Files.copy(it, extractedPath); + addClassSourceTwice(classLoader, extractedPath.toUri().toURL()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + classLoader.loadClass("kotlin.KotlinVersion"); + System.out.println("Could successfully load a Kotlin class."); + } catch (Exception e) { + System.err.println("Failed to load Kotlin into NEU. This is most likely a bad thing."); + e.printStackTrace(); + } finally { + if (fs != null) { + try { + fs.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private void addClassSourceTwice(LaunchClassLoader classLoader, URL url) throws Exception { + classLoader.addURL(url); + ClassLoader classLoaderYetAgain = classLoader.getClass().getClassLoader(); + Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURL.setAccessible(true); + addURL.invoke(classLoaderYetAgain, url); + } + + @Override + public String getLaunchTarget() { + return null; + } + + @Override + public String[] getLaunchArguments() { + return new String[0]; + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/loader/ModLoadingTweaker.java b/src/main/java/io/github/moulberry/notenoughupdates/loader/ModLoadingTweaker.java new file mode 100644 index 00000000..c3428046 --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/loader/ModLoadingTweaker.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Linnea Gräf + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.loader; + +import net.minecraft.launchwrapper.ITweaker; +import net.minecraft.launchwrapper.LaunchClassLoader; +import net.minecraftforge.fml.relauncher.CoreModManager; +import org.spongepowered.asm.launch.MixinBootstrap; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; + +/** + * The mod loading tweaker makes sure that we are recognized as a Forge Mod, despite having a Tweaker. + * We also add ourselves as a mixin container for integration with other mixin loaders. + */ +public class ModLoadingTweaker implements ITweaker { + @Override + public void acceptOptions(List args, File gameDir, File assetsDir, String profile) { + URL location = ModLoadingTweaker.class.getProtectionDomain().getCodeSource().getLocation(); + if (location == null) return; + if (!"file".equals(location.getProtocol())) return; + try { + MixinBootstrap.getPlatform().addContainer(location.toURI()); + String file = new File(location.toURI()).getName(); + CoreModManager.getIgnoredMods().remove(file); + CoreModManager.getReparseableCoremods().add(file); + } catch (URISyntaxException e) { + System.err.println("NEU could not re-add itself as mod."); + e.printStackTrace(); + } + } + + @Override + public void injectIntoClassLoader(LaunchClassLoader classLoader) { + + } + + @Override + public String getLaunchTarget() { + return null; + } + + @Override + public String[] getLaunchArguments() { + return new String[0]; + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/loader/NEUDelegatingTweaker.java b/src/main/java/io/github/moulberry/notenoughupdates/loader/NEUDelegatingTweaker.java new file mode 100644 index 00000000..1d91ca76 --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/loader/NEUDelegatingTweaker.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 Linnea Gräf + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.loader; + +import io.github.moulberry.notenoughupdates.envcheck.EnvironmentScan; +import net.minecraft.launchwrapper.ITweaker; +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.spongepowered.asm.launch.MixinTweaker; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Tweaker used by NEU to allow delegating to multiple tweakers. The following Tweakers are currently delegated to: + * + *
    + *
  • {@link KotlinLoadingTweaker} for late loading Kotlin
  • + *
  • {@link MixinTweaker} for loading Mixins
  • + *
  • {@link ModLoadingTweaker} to ensure we are recognized as a forge mod
  • + *
+ * + *

We also run an environment check, to make sure we are running on the correct Forge and Minecraft version.

+ * + * @see EnvironmentScan + */ +@SuppressWarnings("unused") +public class NEUDelegatingTweaker implements ITweaker { + static { + EnvironmentScan.checkEnvironmentOnce(); + } + + List delegates = new ArrayList<>(); + + public NEUDelegatingTweaker() { + discoverTweakers(); + System.out.println("NEU Delegating Tweaker is loaded with: " + delegates); + } + + private void discoverTweakers() { + delegates.add(new MixinTweaker()); + delegates.add(new ModLoadingTweaker()); + delegates.add(new KotlinLoadingTweaker()); + } + + @Override + public void acceptOptions(List args, File gameDir, File assetsDir, String profile) { + for (ITweaker delegate : delegates) { + delegate.acceptOptions(args, gameDir, assetsDir, profile); + } + } + + @Override + public void injectIntoClassLoader(LaunchClassLoader classLoader) { + for (ITweaker delegate : delegates) { + delegate.injectIntoClassLoader(classLoader); + } + } + + @Override + public String getLaunchTarget() { + String target = null; + for (ITweaker delegate : delegates) { + String launchTarget = delegate.getLaunchTarget(); + if (launchTarget != null) + target = launchTarget; + } + return target; + } + + @Override + public String[] getLaunchArguments() { + List launchArguments = new ArrayList<>(); + for (ITweaker delegate : delegates) { + String[] delegateLaunchArguments = delegate.getLaunchArguments(); + if (delegateLaunchArguments != null) + launchArguments.addAll(Arrays.asList(delegateLaunchArguments)); + } + return launchArguments.toArray(new String[0]); + } + +} -- cgit