diff --git a/build.gradle b/build.gradle index 4471e1b..d7ff7e8 100644 --- a/build.gradle +++ b/build.gradle @@ -160,7 +160,7 @@ task patchMixinModule { void visitProvide(String service, String... providers) { // clean all mixin services if (service == "cpw/mods/modlauncher/api/ITransformationService") { - providers = ["io/github/lxgaming/mixin/launch/MixinTransformationServiceV2"] + providers = ["io/github/lxgaming/mixin/launch/MixinTransformationService"] } super.visitProvide(service, providers) diff --git a/src/main/java/io/github/lxgaming/mixin/launch/CommonTransformationService.java b/src/main/java/io/github/lxgaming/mixin/launch/CommonTransformationService.java deleted file mode 100644 index a326099..0000000 --- a/src/main/java/io/github/lxgaming/mixin/launch/CommonTransformationService.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2019-2021 Alex Thomson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.github.lxgaming.mixin.launch; - -import cpw.mods.modlauncher.LaunchPluginHandler; -import cpw.mods.modlauncher.Launcher; -import cpw.mods.modlauncher.api.IEnvironment; -import cpw.mods.modlauncher.api.ITransformationService; -import cpw.mods.modlauncher.api.ITransformer; -import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; -import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; -import joptsimple.OptionSpecBuilder; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.lang.reflect.Field; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiFunction; - -abstract class CommonTransformationService implements ITransformationService { - static final String NAME = "mixinbootstrap"; - static final Logger LOGGER = LogManager.getLogger("MixinBootstrap Launch"); - final Map launchPluginServices; - final Set transformationServices; - - public CommonTransformationService() { - if (Launcher.INSTANCE == null) { - throw new IllegalStateException("Launcher has not been initialized!"); - } - - this.launchPluginServices = getLaunchPluginServices(); - this.transformationServices = new HashSet<>(); - } - - @Override - public String name() { - return NAME; - } - - @Override - public void arguments(BiFunction argumentBuilder) { - this.transformationServices.forEach(transformationService -> transformationService.arguments(argumentBuilder)); - } - - @Override - public void argumentValues(OptionResult option) { - this.transformationServices.forEach(transformationService -> transformationService.argumentValues(option)); - } - - @Override - public void initialize(IEnvironment environment) { - this.transformationServices.forEach(transformationService -> transformationService.initialize(environment)); - } - - @Override - public List> runScan(IEnvironment environment) { - List> list = new ArrayList<>(); - this.transformationServices.forEach(transformationService -> list.addAll(transformationService.runScan(environment))); - - return list; - } - - @Override - public void onLoad(IEnvironment env, Set otherServices) throws IncompatibleEnvironmentException { - for (ITransformationService transformationService : this.transformationServices) { - transformationService.onLoad(env, otherServices); - } - } - - @Override - @SuppressWarnings("rawtypes") - public List transformers() { - List list = new ArrayList<>(); - this.transformationServices.forEach(transformationService -> list.addAll(transformationService.transformers())); - - return list; - } - - @SuppressWarnings("unchecked") - private Map getLaunchPluginServices() { - try { - // cpw.mods.modlauncher.Launcher.launchPlugins - Field launchPluginsField = Launcher.class.getDeclaredField("launchPlugins"); - launchPluginsField.setAccessible(true); - LaunchPluginHandler launchPluginHandler = (LaunchPluginHandler) launchPluginsField.get(Launcher.INSTANCE); - - // cpw.mods.modlauncher.LaunchPluginHandler.plugins - Field pluginsField = LaunchPluginHandler.class.getDeclaredField("plugins"); - pluginsField.setAccessible(true); - return (Map) pluginsField.get(launchPluginHandler); - } catch (Exception ex) { - LOGGER.error("Encountered an error while getting LaunchPluginServices", ex); - return null; - } - } - - @SuppressWarnings("unchecked") - void registerLaunchPluginService(String className, ClassLoader classLoader) throws IncompatibleEnvironmentException { - try { - Class launchPluginServiceClass = (Class) Class.forName(className, true, classLoader); - if (isLaunchPluginServicePresent(launchPluginServiceClass)) { - LOGGER.warn("{} is already registered", launchPluginServiceClass.getSimpleName()); - return; - } - - ILaunchPluginService launchPluginService = launchPluginServiceClass.newInstance(); - String pluginName = launchPluginService.name(); - this.launchPluginServices.put(pluginName, launchPluginService); - - List> mods = Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.MODLIST.get()).orElse(null); - if (mods != null) { - Map mod = new HashMap<>(); - mod.put("name", pluginName); - mod.put("type", "PLUGINSERVICE"); - String fileName = launchPluginServiceClass.getProtectionDomain().getCodeSource().getLocation().getFile(); - mod.put("file", fileName.substring(fileName.lastIndexOf('/'))); - mods.add(mod); - } - - LOGGER.debug("Registered {} ({})", launchPluginServiceClass.getSimpleName(), pluginName); - } catch (Throwable ex) { - LOGGER.error("Encountered an error while registering {}", className, ex); - throw new IncompatibleEnvironmentException(String.format("Failed to register %s", className)); - } - } - - @SuppressWarnings("unchecked") - void registerTransformationService(String className, ClassLoader classLoader) throws IncompatibleEnvironmentException { - try { - Class transformationServiceClass = (Class) Class.forName(className, true, classLoader); - if (isTransformationServicePresent(transformationServiceClass)) { - LOGGER.warn("{} is already registered", transformationServiceClass.getSimpleName()); - return; - } - - ITransformationService transformationService = transformationServiceClass.newInstance(); - String name = transformationService.name(); - this.transformationServices.add(transformationService); - LOGGER.debug("Registered {} ({})", transformationServiceClass.getSimpleName(), name); - } catch (Exception ex) { - LOGGER.error("Encountered an error while registering {}", className, ex); - throw new IncompatibleEnvironmentException(String.format("Failed to register %s", className)); - } - } - - private boolean isLaunchPluginServicePresent(Class launchPluginServiceClass) { - for (ILaunchPluginService launchPluginService : this.launchPluginServices.values()) { - if (launchPluginServiceClass.isInstance(launchPluginService)) { - return true; - } - } - - return false; - } - - private boolean isTransformationServicePresent(Class transformationServiceClass) { - for (ITransformationService transformationService : this.transformationServices) { - if (transformationServiceClass.isInstance(transformationService)) { - return true; - } - } - - return false; - } -} diff --git a/src/main/java/io/github/lxgaming/mixin/launch/MixinBootstrap.java b/src/main/java/io/github/lxgaming/mixin/launch/MixinBootstrap.java new file mode 100644 index 0000000..eb38acd --- /dev/null +++ b/src/main/java/io/github/lxgaming/mixin/launch/MixinBootstrap.java @@ -0,0 +1,201 @@ +/* + * Copyright 2021 Alex Thomson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lxgaming.mixin.launch; + +import cpw.mods.modlauncher.Launcher; +import cpw.mods.modlauncher.api.IEnvironment; +import cpw.mods.modlauncher.api.ITransformationService; +import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; +import io.github.lxgaming.classloader.ClassLoaderUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class MixinBootstrap { + + public static final String ID = "mixinbootstrap"; + public static final String NAME = "MixinBootstrap"; + public static final String VERSION = "@version@"; + public static final Logger LOGGER = LogManager.getLogger(NAME + " Launch"); + + static { + LOGGER.info("{} v{}", NAME, VERSION); + LOGGER.info("Mixin v{}", org.spongepowered.asm.launch.MixinBootstrap.VERSION); + LOGGER.info("ModLauncher v{} ({})", IEnvironment.class.getPackage().getImplementationVersion(), IEnvironment.class.getPackage().getSpecificationVersion()); + } + + public static void initialize(IEnvironment environment) { + ensureTransformerExclusion(); + } + + public static void onLoad(IEnvironment environment, MixinTransformationService service) throws IncompatibleEnvironmentException { + if (environment.findLaunchPlugin("mixin").isPresent()) { + LOGGER.debug("MixinLaunchPlugin detected"); + return; + } + + if (IEnvironment.class.getPackage().isCompatibleWith("8.0")) { + setFallbackClassLoader(service.getClass().getClassLoader()); + + // Mixin + // - Plugin Service + service.registerLaunchPluginService("org.spongepowered.asm.launch.MixinLaunchPlugin", MixinBootstrap.class.getClassLoader()); + + // - Transformation Service + // This cannot be loaded by the ServiceLoader as it will load classes under the wrong classloader + service.registerTransformationService("org.spongepowered.asm.launch.MixinTransformationService", MixinBootstrap.class.getClassLoader()); + return; + } + + if (IEnvironment.class.getPackage().isCompatibleWith("4.0")) { + appendToClassPath(); + + // Mixin + // - Plugin Service + service.registerLaunchPluginService("org.spongepowered.asm.launch.MixinLaunchPluginLegacy", Launcher.class.getClassLoader()); + + // - Transformation Service + // This cannot be loaded by the ServiceLoader as it will load classes under the wrong classloader + service.registerTransformationService("org.spongepowered.asm.launch.MixinTransformationServiceLegacy", Thread.currentThread().getContextClassLoader()); + + // MixinBootstrap + // - Plugin Service + service.registerLaunchPluginService("io.github.lxgaming.mixin.launch.MixinLaunchPluginService", Launcher.class.getClassLoader()); + return; + } + + LOGGER.error("-------------------------[ ERROR ]-------------------------"); + LOGGER.error("Mixin is not compatible with ModLauncher v{}", ITransformationService.class.getPackage().getImplementationVersion()); + LOGGER.error("Ensure you are running Forge v28.1.45 or later"); + LOGGER.error("-------------------------[ ERROR ]-------------------------"); + throw new IncompatibleEnvironmentException("Incompatibility with ModLauncher"); + } + + private static void appendToClassPath() throws IncompatibleEnvironmentException { + try { + Map map = new HashMap<>(); + map.put("create", "true"); + FileSystem fileSystem = FileSystems.newFileSystem(URI.create("jar:" + MixinBootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI()), map); + Map libraries = Files.list(fileSystem.getPath("META-INF", "libraries")) + .filter(path -> !Files.isDirectory(path) && path.getFileName().toString().endsWith(".jar")) + .collect(Collectors.toMap(path -> path, path -> { + String fileName = path.getFileName().toString(); + int index = fileName.lastIndexOf('.'); + return index != -1 ? fileName.substring(0, index) : fileName; + })); + + LOGGER.debug("Found {} libraries", libraries.size()); + + for (Map.Entry entry : libraries.entrySet()) { + Path temporaryPath = Files.createTempFile(MixinBootstrap.ID + "-" + entry.getValue() + "-", ".jar"); + + LOGGER.debug("Copying {} -> {}", entry.getKey(), temporaryPath); + Files.copy(entry.getKey(), temporaryPath, StandardCopyOption.REPLACE_EXISTING); + + URL url = temporaryPath.toUri().toURL(); + LOGGER.debug("Loading {}", url); + ClassLoaderUtils.appendToClassPath(Thread.currentThread().getContextClassLoader(), url); + } + + URL url = MixinBootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI().toURL(); + LOGGER.debug("Loading {}", url); + ClassLoaderUtils.appendToClassPath(Thread.currentThread().getContextClassLoader(), url); + } catch (Throwable ex) { + LOGGER.error("Encountered an error while appending to the class path", ex); + throw new IncompatibleEnvironmentException("Failed to append to the class path"); + } + } + + /** + * Mixin requires it can be loaded in context class loader. + * Thanks ZekerZhayard + */ + private static void setFallbackClassLoader(ClassLoader classLoader) throws IncompatibleEnvironmentException { + try { + Class moduleClassLoaderClass = Class.forName("cpw.mods.cl.ModuleClassLoader"); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + if (!moduleClassLoaderClass.isInstance(contextClassLoader) || classLoader.equals(contextClassLoader)) { + throw new IllegalStateException("Unexpected ClassLoader: " + contextClassLoader.getClass().getName()); + } + + Method getPlatformClassLoaderMethod = ClassLoader.class.getMethod("getPlatformClassLoader"); + getPlatformClassLoaderMethod.setAccessible(true); + ClassLoader platformClassLoader = (ClassLoader) getPlatformClassLoaderMethod.invoke(null); + + Method setFallbackClassLoaderMethod = moduleClassLoaderClass.getMethod("setFallbackClassLoader", ClassLoader.class); + setFallbackClassLoaderMethod.setAccessible(true); + setFallbackClassLoaderMethod.invoke(contextClassLoader, new MixinClassLoader(platformClassLoader, classLoader)); + } catch (Throwable ex) { + LOGGER.error("Encountered an error while setting fallback classloader", ex); + throw new IncompatibleEnvironmentException("Failed to set fallback classloader"); + } + } + + /** + * Fixes https://github.com/MinecraftForge/MinecraftForge/pull/6600 + */ + @SuppressWarnings("unchecked") + private static void ensureTransformerExclusion() { + try { + Path path = Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.GAMEDIR.get()) + .map(parentPath -> parentPath.resolve("mods")) + .map(Path::toAbsolutePath) + .map(Path::normalize) + .map(parentPath -> { + // Extract the file name + String file = MixinBootstrap.class.getProtectionDomain().getCodeSource().getLocation().getFile(); + return parentPath.resolve(file.substring(file.lastIndexOf('/') + 1)); + }) + .filter(Files::exists) + .orElse(null); + if (path == null) { + return; + } + + // Check if the path is behind a symbolic link + if (path.equals(path.toRealPath())) { + return; + } + + Class modDirTransformerDiscovererClass = Class.forName("net.minecraftforge.fml.loading.ModDirTransformerDiscoverer", true, Launcher.class.getClassLoader()); + + // net.minecraftforge.fml.loading.ModDirTransformerDiscoverer.transformers + Field transformersField = modDirTransformerDiscovererClass.getDeclaredField("transformers"); + transformersField.setAccessible(true); + List transformers = (List) transformersField.get(null); + + if (transformers != null && !transformers.contains(path)) { + transformers.add(path); + } + } catch (Throwable ex) { + LOGGER.error("Encountered an error while getting ensuring transformer exclusion", ex); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/mixin/launch/MixinClassLoader.java b/src/main/java/io/github/lxgaming/mixin/launch/MixinClassLoader.java new file mode 100644 index 0000000..0af1c82 --- /dev/null +++ b/src/main/java/io/github/lxgaming/mixin/launch/MixinClassLoader.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Alex Thomson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lxgaming.mixin.launch; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class MixinClassLoader extends ClassLoader { + + private final ClassLoader child; + private final Set invalidClasses; + + public MixinClassLoader(ClassLoader parent, ClassLoader child) { + super(parent); + this.child = child; + this.invalidClasses = Collections.newSetFromMap(new ConcurrentHashMap<>()); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (invalidClasses.contains(name)) { + throw new ClassNotFoundException(name); + } + + invalidClasses.add(name); + Class validClass = child.loadClass(name); + invalidClasses.remove(name); + + return validClass; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/mixin/launch/MixinLaunchPluginService.java b/src/main/java/io/github/lxgaming/mixin/launch/MixinLaunchPluginService.java index cbb7a0a..25122c8 100644 --- a/src/main/java/io/github/lxgaming/mixin/launch/MixinLaunchPluginService.java +++ b/src/main/java/io/github/lxgaming/mixin/launch/MixinLaunchPluginService.java @@ -25,12 +25,9 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.List; -import java.util.Map; public class MixinLaunchPluginService implements ILaunchPluginService { - public static final String NAME = "mixinbootstrap"; - private static final List SKIP_PACKAGES = Arrays.asList( "org.objectweb.asm.", "org.spongepowered.asm.launch.", @@ -42,7 +39,7 @@ public class MixinLaunchPluginService implements ILaunchPluginService { @Override public String name() { - return MixinLaunchPluginService.NAME; + return MixinBootstrap.ID; } @Override @@ -65,18 +62,9 @@ public boolean processClass(Phase phase, ClassNode classNode, Type classType, St return false; } - @Override - public void addResources(List> resources) { - } - @Override public void initializeLaunch(ITransformerLoader transformerLoader, Path[] specialPaths) { TransformingClassLoader classLoader = (TransformingClassLoader) Thread.currentThread().getContextClassLoader(); classLoader.addTargetPackageFilter(name -> SKIP_PACKAGES.stream().noneMatch(name::startsWith)); } - - @Override - public T getExtension() { - return null; - } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationService.java b/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationService.java index 951e41a..3a347ce 100644 --- a/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationService.java +++ b/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationService.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Alex Thomson + * Copyright 2021 Alex Thomson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,140 +16,202 @@ package io.github.lxgaming.mixin.launch; +import cpw.mods.modlauncher.LaunchPluginHandler; import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.IEnvironment; import cpw.mods.modlauncher.api.ITransformationService; +import cpw.mods.modlauncher.api.ITransformer; import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; -import io.github.lxgaming.classloader.ClassLoaderUtils; +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import joptsimple.OptionSpecBuilder; import java.lang.reflect.Field; -import java.net.URI; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; +import java.net.URL; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; -public class MixinTransformationService extends CommonTransformationService { +public class MixinTransformationService implements ITransformationService { + + private final Map launchPluginServices; + private final Set transformationServices; + + public MixinTransformationService() { + if (Launcher.INSTANCE == null) { + throw new IllegalStateException("Launcher has not been initialized!"); + } + + this.launchPluginServices = getLaunchPluginServices(); + this.transformationServices = new HashSet<>(); + } + + @Override + public String name() { + return MixinBootstrap.ID; + } @Override public void initialize(IEnvironment environment) { - ensureTransformerExclusion(); + MixinBootstrap.initialize(environment); - super.initialize(environment); + for (ITransformationService transformationService : this.transformationServices) { + transformationService.initialize(environment); + } } - + @Override public void beginScanning(IEnvironment environment) { - this.transformationServices.forEach(transformationService -> transformationService.beginScanning(environment)); + for (ITransformationService transformationService : this.transformationServices) { + transformationService.beginScanning(environment); + } } @Override public void onLoad(IEnvironment env, Set otherServices) throws IncompatibleEnvironmentException { - if (env.findLaunchPlugin("mixin").isPresent()) { - LOGGER.debug("MixinLauncherPlugin detected"); - return; - } + MixinBootstrap.onLoad(env, this); - if (this.launchPluginServices == null) { - throw new IncompatibleEnvironmentException("LaunchPluginServices is unavailable"); + for (ITransformationService transformationService : this.transformationServices) { + transformationService.onLoad(env, otherServices); + } + } + + @Override + @SuppressWarnings("rawtypes") + public List transformers() { + List list = new ArrayList<>(); + for (ITransformationService transformationService : this.transformationServices) { + list.addAll(transformationService.transformers()); } - if (!ITransformationService.class.getPackage().isCompatibleWith("4.0") || ITransformationService.class.getPackage().isCompatibleWith("8.0")) { - LOGGER.error("-------------------------[ ERROR ]-------------------------"); - LOGGER.error("Mixin is not compatible with ModLauncher v" + ITransformationService.class.getPackage().getImplementationVersion()); - LOGGER.error("Ensure you are running Forge v28.1.45 ~ v37.0.0"); - LOGGER.error("-------------------------[ ERROR ]-------------------------"); - throw new IncompatibleEnvironmentException("Incompatibility with ModLauncher"); + return list; + } + + @Override + public void arguments(BiFunction argumentBuilder) { + for (ITransformationService transformationService : this.transformationServices) { + transformationService.arguments(argumentBuilder); } - - ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); - try { - Map map = new HashMap<>(); - map.put("create", "true"); - FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()), map); - Files.list(fs.getPath("META-INF", "libraries")) - .filter(p -> !Files.isDirectory(p) && p.getFileName().toString().endsWith(".jar")) - .forEach(p -> { - try { - ClassLoaderUtils.appendToClassPath(contextCL, Files.copy(p, Files.createTempFile("", ".jar"), StandardCopyOption.REPLACE_EXISTING).toUri().toURL()); - } catch (Throwable e) { - throw new RuntimeException(e); - } - }); - - ClassLoaderUtils.appendToClassPath(contextCL, getClass().getProtectionDomain().getCodeSource().getLocation().toURI().toURL()); - } catch (Throwable ex) { - LOGGER.error("Encountered an error while attempting to append to the class path", ex); - throw new IncompatibleEnvironmentException("Failed to append to the class path"); + } + + @Override + public void argumentValues(OptionResult option) { + for (ITransformationService transformationService : this.transformationServices) { + transformationService.argumentValues(option); + } + } + + @Override + public List> runScan(IEnvironment environment) { + List> list = new ArrayList<>(); + for (ITransformationService transformationService : this.transformationServices) { + list.addAll(transformationService.runScan(environment)); } - // Mixin - // - Plugin Service - registerLaunchPluginService("org.spongepowered.asm.launch.MixinLaunchPluginLegacy", contextCL); - - // - Transformation Service - // This cannot be loaded by the ServiceLoader as it will load classes under the wrong classloader - registerTransformationService("org.spongepowered.asm.launch.MixinTransformationServiceLegacy", contextCL); - - // MixinBootstrap - // - Plugin Service - registerLaunchPluginService("io.github.lxgaming.mixin.launch.MixinLaunchPluginService", contextCL); - - super.onLoad(env, otherServices); + return list; + } + + @Override + public Map.Entry, Supplier>>> additionalClassesLocator() { + return null; + } + + @Override + public Map.Entry, Supplier>>> additionalResourcesLocator() { + return null; } - /** - * Fixes https://github.com/MinecraftForge/MinecraftForge/pull/6600 - */ - private void ensureTransformerExclusion() { + @SuppressWarnings("unchecked") + private Map getLaunchPluginServices() { try { - Path path = Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.GAMEDIR.get()) - .map(parentPath -> parentPath.resolve("mods")) - .map(Path::toAbsolutePath) - .map(Path::normalize) - .map(parentPath -> { - // Extract the file name - String file = getClass().getProtectionDomain().getCodeSource().getLocation().getFile(); - return parentPath.resolve(file.substring(file.lastIndexOf('/') + 1)); - }) - .filter(Files::exists) - .orElse(null); - if (path == null) { - return; - } + // cpw.mods.modlauncher.Launcher.launchPlugins + Field launchPluginsField = Launcher.class.getDeclaredField("launchPlugins"); + launchPluginsField.setAccessible(true); + LaunchPluginHandler launchPluginHandler = (LaunchPluginHandler) launchPluginsField.get(Launcher.INSTANCE); - // Check if the path is behind a symbolic link - if (path.equals(path.toRealPath())) { + // cpw.mods.modlauncher.LaunchPluginHandler.plugins + Field pluginsField = LaunchPluginHandler.class.getDeclaredField("plugins"); + pluginsField.setAccessible(true); + return (Map) pluginsField.get(launchPluginHandler); + } catch (Exception ex) { + MixinBootstrap.LOGGER.error("Encountered an error while getting LaunchPluginServices", ex); + return null; + } + } + + @SuppressWarnings("unchecked") + public void registerLaunchPluginService(String className, ClassLoader classLoader) throws IncompatibleEnvironmentException { + try { + Class launchPluginServiceClass = (Class) Class.forName(className, true, classLoader); + if (isLaunchPluginServicePresent(launchPluginServiceClass)) { + MixinBootstrap.LOGGER.warn("{} is already registered", launchPluginServiceClass.getSimpleName()); return; } - List transformers = getTransformers(); - if (transformers != null && !transformers.contains(path)) { - transformers.add(path); + ILaunchPluginService launchPluginService = launchPluginServiceClass.newInstance(); + String pluginName = launchPluginService.name(); + this.launchPluginServices.put(pluginName, launchPluginService); + + List> mods = Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.MODLIST.get()).orElse(null); + if (mods != null) { + Map mod = new HashMap<>(); + mod.put("name", pluginName); + mod.put("type", "PLUGINSERVICE"); + String fileName = launchPluginServiceClass.getProtectionDomain().getCodeSource().getLocation().getFile(); + mod.put("file", fileName.substring(fileName.lastIndexOf('/'))); + mods.add(mod); } + + MixinBootstrap.LOGGER.debug("Registered {} ({})", launchPluginServiceClass.getSimpleName(), pluginName); } catch (Throwable ex) { - // no-op + MixinBootstrap.LOGGER.error("Encountered an error while registering {}", className, ex); + throw new IncompatibleEnvironmentException(String.format("Failed to register %s", className)); } } - - + @SuppressWarnings("unchecked") - private List getTransformers() { + public void registerTransformationService(String className, ClassLoader classLoader) throws IncompatibleEnvironmentException { try { - Class modDirTransformerDiscovererClass = Class.forName("net.minecraftforge.fml.loading.ModDirTransformerDiscoverer", true, Launcher.class.getClassLoader()); - - // net.minecraftforge.fml.loading.ModDirTransformerDiscoverer.transformers - Field transformersField = modDirTransformerDiscovererClass.getDeclaredField("transformers"); - transformersField.setAccessible(true); - return (List) transformersField.get(null); + Class transformationServiceClass = (Class) Class.forName(className, true, classLoader); + if (isTransformationServicePresent(transformationServiceClass)) { + MixinBootstrap.LOGGER.warn("{} is already registered", transformationServiceClass.getSimpleName()); + return; + } + + ITransformationService transformationService = transformationServiceClass.newInstance(); + String name = transformationService.name(); + this.transformationServices.add(transformationService); + MixinBootstrap.LOGGER.debug("Registered {} ({})", transformationServiceClass.getSimpleName(), name); } catch (Exception ex) { - LOGGER.error("Encountered an error while getting Transformers", ex); - return null; + MixinBootstrap.LOGGER.error("Encountered an error while registering {}", className, ex); + throw new IncompatibleEnvironmentException(String.format("Failed to register %s", className)); } } -} + + private boolean isLaunchPluginServicePresent(Class launchPluginServiceClass) { + for (ILaunchPluginService launchPluginService : this.launchPluginServices.values()) { + if (launchPluginServiceClass.isInstance(launchPluginService)) { + return true; + } + } + + return false; + } + + private boolean isTransformationServicePresent(Class transformationServiceClass) { + for (ITransformationService transformationService : this.transformationServices) { + if (transformationServiceClass.isInstance(transformationService)) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationServiceV2.java b/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationServiceV2.java deleted file mode 100644 index 96fe9be..0000000 --- a/src/main/java/io/github/lxgaming/mixin/launch/MixinTransformationServiceV2.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2019-2021 Alex Thomson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.github.lxgaming.mixin.launch; - -import cpw.mods.modlauncher.api.IEnvironment; -import cpw.mods.modlauncher.api.ITransformationService; -import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; -import sun.misc.Unsafe; - -import java.lang.reflect.Field; -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -public class MixinTransformationServiceV2 extends CommonTransformationService { - - @Override - public void beginScanning(IEnvironment environment) { - - } - - @Override - public void onLoad(IEnvironment env, Set otherServices) throws IncompatibleEnvironmentException { - if (!ITransformationService.class.getPackage().isCompatibleWith("8.0")) { - throw new IncompatibleEnvironmentException("Incompatibility with ModLauncher"); - } - - try { - resetBootstrapFallBackClassLoader(); - } catch (Throwable ex) { - LOGGER.error("Encountered an error while attempting to append to the class path", ex); - throw new IncompatibleEnvironmentException("Failed to append to the class path"); - } - - // Mixin - // - Plugin Service - registerLaunchPluginService("org.spongepowered.asm.launch.MixinLaunchPlugin", this.getClass().getClassLoader()); - - // - Transformation Service - // This cannot be loaded by the ServiceLoader as it will load classes under the wrong classloader - registerTransformationService("org.spongepowered.asm.launch.MixinTransformationService", this.getClass().getClassLoader()); - - super.onLoad(env, otherServices); - } - - // Mixin requires it can be loaded in context class loader. - private static void resetBootstrapFallBackClassLoader() throws Throwable { - Class moduleClassLoaderClass = Class.forName("cpw.mods.cl.ModuleClassLoader"); - ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); - ClassLoader currentCL = MixinTransformationServiceV2.class.getClassLoader(); - if (moduleClassLoaderClass.isInstance(contextCL) && !currentCL.equals(contextCL)) { - Unsafe unsafe = getUnsafe(); - Field fallbackClassLoaderField = moduleClassLoaderClass.getDeclaredField("fallbackClassLoader"); - ClassLoader fallbackClassLoader = (ClassLoader) unsafe.getObject(contextCL, unsafe.objectFieldOffset(fallbackClassLoaderField)); - unsafe.putObject(contextCL, unsafe.objectFieldOffset(fallbackClassLoaderField), new ClassLoader(fallbackClassLoader) { - private final Set invalidClasses = Collections.newSetFromMap(new ConcurrentHashMap<>()); - @Override - protected Class findClass(final String name) throws ClassNotFoundException { - try { - return fallbackClassLoader.loadClass(name); - } catch (ClassNotFoundException cnfe) { - if (invalidClasses.contains(name)) throw cnfe; - invalidClasses.add(name); - Class c = currentCL.loadClass(name); - invalidClasses.remove(name); - return c; - } - } - }); - } - } - - private static Unsafe getUnsafe() throws Throwable { - Field unsafeFiled = Unsafe.class.getDeclaredField("theUnsafe"); - unsafeFiled.setAccessible(true); - return (Unsafe) unsafeFiled.get(null); - } -}