/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.spongepowered.asm.mixin; import java.io.File; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Resource; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.appender.AbstractAppender; import org.spongepowered.asm.launch.Blackboard; import org.spongepowered.asm.launch.MixinBootstrap; import org.spongepowered.asm.lib.Opcodes; import org.spongepowered.asm.mixin.extensibility.IEnvironmentTokenProvider; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.throwables.MixinException; import org.spongepowered.asm.mixin.transformer.MixinTransformer; import org.spongepowered.asm.obfuscation.RemapperChain; import org.spongepowered.asm.util.ITokenProvider; import org.spongepowered.asm.util.JavaVersion; import org.spongepowered.asm.util.PrettyPrinter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import net.minecraft.launchwrapper.IClassNameTransformer; import net.minecraft.launchwrapper.IClassTransformer; import net.minecraft.launchwrapper.ITweaker; import net.minecraft.launchwrapper.Launch; import net.minecraft.launchwrapper.LaunchClassLoader; /** * The mixin environment manages global state information for the mixin * subsystem. */ public final class MixinEnvironment implements ITokenProvider { /** * Environment phase, deliberately not implemented as an enum */ public static final class Phase { /** * Not initialised phase */ static final Phase NOT_INITIALISED = new Phase(-1, "NOT_INITIALISED"); /** * "Pre initialisation" phase, everything before the tweak system begins * to load the game */ public static final Phase PREINIT = new Phase(0, "PREINIT"); /** * "Initialisation" phase, after FML's deobf transformer has loaded */ public static final Phase INIT = new Phase(1, "INIT"); /** * "Default" phase, during runtime */ public static final Phase DEFAULT = new Phase(2, "DEFAULT"); /** * All phases */ static final List<Phase> phases = ImmutableList.of( Phase.PREINIT, Phase.INIT, Phase.DEFAULT ); /** * Phase ordinal */ final int ordinal; /** * Phase name */ final String name; /** * Environment for this phase */ private MixinEnvironment environment; private Phase(int ordinal, String name) { this.ordinal = ordinal; this.name = name; } @Override public String toString() { return this.name; } /** * Get a phase by name, returns <tt>null</tt> if no phases exist with * the specified name * * @param name phase name to lookup * @return phase object or <tt>null</tt> if non existent */ public static Phase forName(String name) { for (Phase phase : Phase.phases) { if (phase.name.equals(name)) { return phase; } } return null; } MixinEnvironment getEnvironment() { if (this.ordinal < 0) { throw new IllegalArgumentException("Cannot access the NOT_INITIALISED environment"); } if (this.environment == null) { this.environment = new MixinEnvironment(this); } return this.environment; } } /** * Represents a "side", client or dedicated server */ public static enum Side { /** * The environment was unable to determine current side */ UNKNOWN { @Override protected boolean detect() { return false; } }, /** * Client-side environment */ CLIENT { @Override protected boolean detect() { String sideName = this.getSideName(); return "CLIENT".equals(sideName); } }, /** * (Dedicated) Server-side environment */ SERVER { @Override protected boolean detect() { String sideName = this.getSideName(); return "SERVER".equals(sideName) || "DEDICATEDSERVER".equals(sideName); } }; protected abstract boolean detect(); protected final String getSideName() { // Using this method first prevents us from accidentally loading FML classes // too early when using the tweaker in dev for (ITweaker tweaker : Blackboard.<List<ITweaker>>get(Blackboard.Keys.TWEAKS)) { if (tweaker.getClass().getName().endsWith(".common.launcher.FMLServerTweaker")) { return "SERVER"; } else if (tweaker.getClass().getName().endsWith(".common.launcher.FMLTweaker")) { return "CLIENT"; } } String name = this.getSideName("net.minecraftforge.fml.relauncher.FMLLaunchHandler", "side"); if (name != null) { return name; } name = this.getSideName("cpw.mods.fml.relauncher.FMLLaunchHandler", "side"); if (name != null) { return name; } name = this.getSideName("com.mumfrey.liteloader.launch.LiteLoaderTweaker", "getEnvironmentType"); if (name != null) { return name; } return "UNKNOWN"; } private String getSideName(String className, String methodName) { try { Class<?> clazz = Class.forName(className, false, Launch.classLoader); Method method = clazz.getDeclaredMethod(methodName); return ((Enum<?>)method.invoke(null)).name(); } catch (Exception ex) { return null; } } } /** * Mixin options */ public static enum Option { /** * Enable all debugging options */ DEBUG_ALL("debug"), /** * Enable post-mixin class export. This causes all classes to be written * to the .mixin.out directory within the runtime directory * <em>after</em> mixins are applied, for debugging purposes. */ DEBUG_EXPORT(Option.DEBUG_ALL, "export"), /** * Export filter, if omitted allows all transformed classes to be * exported. If specified, acts as a filter for class names to export * and only matching classes will be exported. This is useful when using * Fernflower as exporting can be otherwise very slow. The following * wildcards are allowed: * * <dl> * <dt>*</dt><dd>Matches one or more characters except dot (.)</dd> * <dt>**</dt><dd>Matches any number of characters</dd> * <dt>?</dt><dd>Matches exactly one character</dd> * </dl> */ DEBUG_EXPORT_FILTER(Option.DEBUG_EXPORT, "filter", false), /** * Allow fernflower to be disabled even if it is found on the classpath */ DEBUG_EXPORT_DECOMPILE(Option.DEBUG_EXPORT, Inherit.ALLOW_OVERRIDE, "decompile"), /** * Run fernflower in a separate thread. In general this will allow * export to impact startup time much less (decompiling normally adds * about 20% to load times) with the trade-off that crashes may lead to * undecompiled exports. */ DEBUG_EXPORT_DECOMPILE_THREADED(Option.DEBUG_EXPORT_DECOMPILE, Inherit.ALLOW_OVERRIDE, "async"), /** * Run the CheckClassAdapter on all classes after mixins are applied, * also enables stricter checks on mixins for use at dev-time, promotes * some warning-level messages to exceptions */ DEBUG_VERIFY(Option.DEBUG_ALL, "verify"), /** * Enable verbose mixin logging (elevates all DEBUG level messages to * INFO level) */ DEBUG_VERBOSE(Option.DEBUG_ALL, "verbose"), /** * Elevates failed injections to an error condition, see * {@link Inject#expect} for details */ DEBUG_INJECTORS(Option.DEBUG_ALL, "countInjections"), /** * Enable strict checks */ DEBUG_STRICT(Option.DEBUG_ALL, Inherit.INDEPENDENT, "strict"), /** * If false (default), {@link Unique} public methods merely raise a * warning when encountered and are not merged into the target. If true, * an exception is thrown instead */ DEBUG_UNIQUE(Option.DEBUG_STRICT, "unique"), /** * Enable strict checking for mixin targets */ DEBUG_TARGETS(Option.DEBUG_STRICT, "targets"), /** * Dumps the bytecode for the target class to disk when mixin * application fails */ DUMP_TARGET_ON_FAILURE("dumpTargetOnFailure"), /** * Enable all checks */ CHECK_ALL("checks"), /** * Checks that all declared interface methods are implemented on a class * after mixin application. */ CHECK_IMPLEMENTS(Option.CHECK_ALL, "interfaces"), /** * If interface check is enabled, "strict mode" (default) applies the * implementation check even to abstract target classes. Setting this * option to <tt>false</tt> causes abstract targets to be skipped when * generating the implementation report. */ CHECK_IMPLEMENTS_STRICT(Option.CHECK_IMPLEMENTS, Inherit.ALLOW_OVERRIDE, "strict"), /** * Ignore all constraints on mixin annotations, output warnings instead */ IGNORE_CONSTRAINTS("ignoreConstraints"), /** * Enables the hot-swap agent */ HOT_SWAP("hotSwap"), /** * Parent for environment settings */ ENVIRONMENT(Inherit.ALWAYS_FALSE, "env"), /** * Force refmap obf type when required */ OBFUSCATION_TYPE(Option.ENVIRONMENT, Inherit.ALWAYS_FALSE, "obf"), /** * Disable refmap when required */ DISABLE_REFMAP(Option.ENVIRONMENT, Inherit.INDEPENDENT, "disableRefMap"), /** * Globally ignore the "required" attribute of all configurations */ IGNORE_REQUIRED(Option.ENVIRONMENT, Inherit.INDEPENDENT, "ignoreRequired"), /** * Default compatibility level to operate at */ DEFAULT_COMPATIBILITY_LEVEL(Option.ENVIRONMENT, Inherit.INDEPENDENT, "compatLevel"), /** * Behaviour for initialiser injections, current supported options are * "default" and "safe" */ INITIALISER_INJECTION_MODE("initialiserInjectionMode", "default"); /** * Type of inheritance for options */ private enum Inherit { /** * If the parent is set, this option will be set too. */ INHERIT, /** * If the parent is set, this option will be set too. However * setting the option explicitly to <tt>false</tt> will override the * parent value. */ ALLOW_OVERRIDE, /** * This option ignores the value of the parent option, parent is * only used for grouping. */ INDEPENDENT, /** * This option is always <tt>false</tt>. */ ALWAYS_FALSE; } /** * Prefix for mixin options */ private static final String PREFIX = "mixin"; /** * Parent option to this option, if non-null then this option is enabled * if */ final Option parent; /** * Inheritance behaviour for this option */ final Inherit inheritance; /** * Java property name */ final String property; /** * Default value for string properties */ final String defaultValue; /** * Whether this property is boolean or not */ final boolean isFlag; /** * Number of parents */ final int depth; private Option(String property) { this(null, property, true); } private Option(Inherit inheritance, String property) { this(null, inheritance, property, true); } private Option(String property, boolean flag) { this(null, property, flag); } private Option(String property, String defaultStringValue) { this(null, Inherit.INHERIT, property, false, defaultStringValue); } private Option(Option parent, String property) { this(parent, Inherit.INHERIT, property, true); } private Option(Option parent, Inherit inheritance, String property) { this(parent, inheritance, property, true); } private Option(Option parent, String property, boolean isFlag) { this(parent, Inherit.INHERIT, property, isFlag, null); } private Option(Option parent, Inherit inheritance, String property, boolean isFlag) { this(parent, inheritance, property, isFlag, null); } private Option(Option parent, String property, String defaultStringValue) { this(parent, Inherit.INHERIT, property, false, defaultStringValue); } private Option(Option parent, Inherit inheritance, String property, String defaultStringValue) { this(parent, inheritance, property, false, defaultStringValue); } private Option(Option parent, Inherit inheritance, String property, boolean isFlag, String defaultStringValue) { this.parent = parent; this.inheritance = inheritance; this.property = (parent != null ? parent.property : Option.PREFIX) + "." + property; this.defaultValue = defaultStringValue; this.isFlag = isFlag; int depth = 0; for (; parent != null; depth++) { parent = parent.parent; } this.depth = depth; } Option getParent() { return this.parent; } String getProperty() { return this.property; } @Override public String toString() { return this.isFlag ? String.valueOf(this.getBooleanValue()) : this.getStringValue(); } private boolean getLocalBooleanValue(boolean defaultValue) { return Boolean.parseBoolean(System.getProperty(this.property, Boolean.toString(defaultValue))); } private boolean getInheritedBooleanValue() { return this.parent != null && this.parent.getBooleanValue(); } final boolean getBooleanValue() { if (this.inheritance == Inherit.ALWAYS_FALSE) { return false; } boolean local = this.getLocalBooleanValue(false); if (this.inheritance == Inherit.INDEPENDENT) { return local; } boolean inherited = local || this.getInheritedBooleanValue(); return this.inheritance == Inherit.INHERIT ? inherited : this.getLocalBooleanValue(inherited); } final String getStringValue() { return (this.parent == null || this.parent.getBooleanValue()) ? System.getProperty(this.property, this.defaultValue) : this.defaultValue; } @SuppressWarnings("unchecked") <E extends Enum<E>> E getEnumValue(E defaultValue) { String value = System.getProperty(this.property, defaultValue.name()); try { return (E)Enum.valueOf(defaultValue.getClass(), value.toUpperCase()); } catch (IllegalArgumentException ex) { return defaultValue; } } } /** * Operational compatibility level for the mixin subsystem */ public static enum CompatibilityLevel { /** * Java 6 and above */ JAVA_6(6, Opcodes.V1_6, false), /** * Java 7 and above */ JAVA_7(7, Opcodes.V1_7, false) { @Override boolean isSupported() { return JavaVersion.current() >= 1.7; } }, /** * Java 8 and above */ JAVA_8(8, Opcodes.V1_8, true) { @Override boolean isSupported() { return JavaVersion.current() >= 1.8; } }; private final int ver; private final int classVersion; private final boolean supportsMethodsInInterfaces; private CompatibilityLevel maxCompatibleLevel; private CompatibilityLevel(int ver, int classVersion, boolean resolveMethodsInInterfaces) { this.ver = ver; this.classVersion = classVersion; this.supportsMethodsInInterfaces = resolveMethodsInInterfaces; } @SuppressWarnings("unused") private void setMaxCompatibleLevel(CompatibilityLevel maxCompatibleLevel) { this.maxCompatibleLevel = maxCompatibleLevel; } /** * Get whether this compatibility level is supported in the current * environment */ boolean isSupported() { return true; } /** * Class version expected at this compatibility level */ public int classVersion() { return this.classVersion; } /** * Get whether this environment supports non-abstract methods in * interfaces, true in Java 1.8 and above */ public boolean supportsMethodsInInterfaces() { return this.supportsMethodsInInterfaces; } /** * Get whether this level is the same or greater than the specified * level * * @param level level to compare to * @return true if this level is equal or higher the supplied level */ public boolean isAtLeast(CompatibilityLevel level) { return level == null || this.ver >= level.ver; } /** * Get whether this level can be elevated to the specified level * * @param level desired level * @return true if this level supports elevation */ public boolean canElevateTo(CompatibilityLevel level) { if (level == null || this.maxCompatibleLevel == null) { return true; } return level.ver <= this.maxCompatibleLevel.ver; } /** * True if this level can support the specified level * * @param level desired level * @return true if the other level can be elevated to this level */ public boolean canSupport(CompatibilityLevel level) { if (level == null) { return true; } return level.canElevateTo(this); } } /** * Tweaker used to notify the environment when we transition from preinit to * default */ public static class EnvironmentStateTweaker implements ITweaker { @Override public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile) { } @Override public void injectIntoClassLoader(LaunchClassLoader classLoader) { MixinBootstrap.getPlatform().injectIntoClassLoader(classLoader); } @Override public String getLaunchTarget() { return ""; } @Override public String[] getLaunchArguments() { MixinEnvironment.gotoPhase(Phase.DEFAULT); return new String[0]; } } /** * Wrapper for providing a natural sorting order for providers */ static class TokenProviderWrapper implements Comparable<TokenProviderWrapper> { private static int nextOrder = 0; private final int priority, order; private final IEnvironmentTokenProvider provider; private final MixinEnvironment environment; public TokenProviderWrapper(IEnvironmentTokenProvider provider, MixinEnvironment environment) { this.provider = provider; this.environment = environment; this.order = TokenProviderWrapper.nextOrder++; this.priority = provider.getPriority(); } @Override public int compareTo(TokenProviderWrapper other) { if (other == null) { return 0; } if (other.priority == this.priority) { return other.order - this.order; } return (other.priority - this.priority); } public IEnvironmentTokenProvider getProvider() { return this.provider; } Integer getToken(String token) { return this.provider.getToken(token, this.environment); } } /** * Temporary */ static class MixinLogger { static MixinAppender appender = new MixinAppender("MixinLogger", null, null); public MixinLogger() { org.apache.logging.log4j.core.Logger log = (org.apache.logging.log4j.core.Logger)LogManager.getLogger("FML"); appender.start(); log.addAppender(appender); } /** * Temporary */ static class MixinAppender extends AbstractAppender { protected MixinAppender(String name, Filter filter, Layout<? extends Serializable> layout) { super(name, filter, layout); } @Override public void append(LogEvent event) { if (event.getLevel() == Level.DEBUG && "Validating minecraft".equals(event.getMessage().getFormat())) { // transition to INIT MixinEnvironment.gotoPhase(Phase.INIT); } } } } /** * Known re-entrant transformers, other re-entrant transformers will * detected automatically */ private static final Set<String> excludeTransformers = Sets.<String>newHashSet( "net.minecraftforge.fml.common.asm.transformers.EventSubscriptionTransformer", "cpw.mods.fml.common.asm.transformers.EventSubscriptionTransformer", "net.minecraftforge.fml.common.asm.transformers.TerminalTransformer", "cpw.mods.fml.common.asm.transformers.TerminalTransformer" ); /** * Currently active environment */ private static MixinEnvironment currentEnvironment; /** * Current (active) environment phase, set to NOT_INITIALISED until the * phases have been populated */ private static Phase currentPhase = Phase.NOT_INITIALISED; /** * Current compatibility level */ private static CompatibilityLevel compatibility = Option.DEFAULT_COMPATIBILITY_LEVEL.<CompatibilityLevel>getEnumValue(CompatibilityLevel.JAVA_6); /** * Show debug header info on first environment construction */ private static boolean showHeader = true; /** * Logger */ private static final Logger logger = LogManager.getLogger("mixin"); /** * The phase for this environment */ private final Phase phase; /** * The blackboard key for this environment's configs */ private final String configsKey; /** * This environment's options */ private final boolean[] options; /** * List of token provider classes */ private final Set<String> tokenProviderClasses = new HashSet<String>(); /** * List of token providers in this environment */ private final List<TokenProviderWrapper> tokenProviders = new ArrayList<TokenProviderWrapper>(); /** * Internal tokens defined by this environment */ private final Map<String, Integer> internalTokens = new HashMap<String, Integer>(); /** * Remappers for this environment */ private final RemapperChain remappers = new RemapperChain(); /** * Detected side */ private Side side; /** * Local transformer chain, this consists of all transformers present at the * init phase with the exclusion of the mixin transformer itself and known * re-entrant transformers. Detected re-entrant transformers will be * subsequently removed. */ private List<IClassTransformer> transformers; /** * Class name transformer (if present) */ private IClassNameTransformer nameTransformer; /** * Obfuscation context (refmap key to use in this environment) */ private String obfuscationContext = null; MixinEnvironment(Phase phase) { this.phase = phase; this.configsKey = Blackboard.Keys.CONFIGS + "." + this.phase.name.toLowerCase(); // Sanity check Object version = this.getVersion(); if (version == null || !MixinBootstrap.VERSION.equals(version)) { throw new MixinException("Environment conflict, mismatched versions or you didn't call MixinBootstrap.init()"); } // Also sanity check if (this.getClass().getClassLoader() != Launch.class.getClassLoader()) { throw new MixinException("Attempted to init the mixin environment in the wrong classloader"); } this.options = new boolean[Option.values().length]; for (Option option : Option.values()) { this.options[option.ordinal()] = option.getBooleanValue(); } if (MixinEnvironment.showHeader) { MixinEnvironment.showHeader = false; this.printHeader(version); } } private void printHeader(Object version) { Side side = this.getSide(); String codeSource = this.getCodeSource(); MixinEnvironment.logger.info("SpongePowered MIXIN Subsystem Version={} Source={} Env={}", version, codeSource, side); if (this.getOption(Option.DEBUG_VERBOSE)) { PrettyPrinter printer = new PrettyPrinter(32); printer.add("SpongePowered MIXIN (Verbose debugging enabled)").centre().hr(); printer.kv("Code source", codeSource); printer.kv("Internal Version", version); printer.kv("Java 8 Supported", CompatibilityLevel.JAVA_8.isSupported()).hr(); for (Option option : Option.values()) { StringBuilder indent = new StringBuilder(); for (int i = 0; i < option.depth; i++) { indent.append("- "); } printer.kv(option.property, "%s<%s>", indent, option); } printer.hr().kv("Detected Side", side); printer.print(System.err); } } private String getCodeSource() { try { return this.getClass().getProtectionDomain().getCodeSource().getLocation().toString(); } catch (Throwable th) { return "Unknown"; } } /** * Get the phase for this environment * * @return the phase */ public Phase getPhase() { return this.phase; } /** * Get mixin configurations from the blackboard * * @return list of registered mixin configs * @deprecated no replacement */ @Deprecated public List<String> getMixinConfigs() { List<String> mixinConfigs = Blackboard.<List<String>>get(this.configsKey); if (mixinConfigs == null) { mixinConfigs = new ArrayList<String>(); Launch.blackboard.put(this.configsKey, mixinConfigs); } return mixinConfigs; } /** * Add a mixin configuration to the blackboard * * @param config Name of configuration resource to add * @return fluent interface * @deprecated use Mixins::addConfiguration instead */ @Deprecated public MixinEnvironment addConfiguration(String config) { MixinEnvironment.logger.warn("MixinEnvironment::addConfiguration is deprecated and will be removed. Use Mixins::addConfiguration instead!"); Mixins.addConfiguration(config, this); return this; } void registerConfig(String config) { List<String> configs = this.getMixinConfigs(); if (!configs.contains(config)) { configs.add(config); } } /** * Add a new error handler class to this environment * * @param handlerName Handler class to add * @return fluent interface * @deprecated use Mixins::registerErrorHandlerClass */ @Deprecated public MixinEnvironment registerErrorHandlerClass(String handlerName) { Mixins.registerErrorHandlerClass(handlerName); return this; } /** * Add a new token provider class to this environment * * @param providerName Class name of the token provider to add * @return fluent interface */ public MixinEnvironment registerTokenProviderClass(String providerName) { if (!this.tokenProviderClasses.contains(providerName)) { try { @SuppressWarnings("unchecked") Class<? extends IEnvironmentTokenProvider> providerClass = (Class<? extends IEnvironmentTokenProvider>)Class.forName(providerName, true, Launch.classLoader); IEnvironmentTokenProvider provider = providerClass.newInstance(); this.registerTokenProvider(provider); } catch (Throwable th) { MixinEnvironment.logger.error("Error instantiating " + providerName, th); } } return this; } /** * Add a new token provider to this environment * * @param provider Token provider to add * @return fluent interface */ public MixinEnvironment registerTokenProvider(IEnvironmentTokenProvider provider) { if (provider != null && !this.tokenProviderClasses.contains(provider.getClass().getName())) { String providerName = provider.getClass().getName(); TokenProviderWrapper wrapper = new TokenProviderWrapper(provider, this); MixinEnvironment.logger.info("Adding new token provider {} to {}", providerName, this); this.tokenProviders.add(wrapper); this.tokenProviderClasses.add(providerName); Collections.sort(this.tokenProviders); } return this; } /** * Get a token value from this environment * * @param token Token to fetch * @return token value or null if the token is not present in the * environment */ @Override public Integer getToken(String token) { token = token.toUpperCase(); for (TokenProviderWrapper provider : this.tokenProviders) { Integer value = provider.getToken(token); if (value != null) { return value; } } return this.internalTokens.get(token); } /** * Get all registered error handlers for this environment * * @return set of error handler class names * @deprecated use Mixins::getErrorHandlerClasses */ @Deprecated public Set<String> getErrorHandlerClasses() { return Mixins.getErrorHandlerClasses(); } /** * Get the active mixin transformer instance (if any) * * @return active mixin transformer instance */ public Object getActiveTransformer() { return Blackboard.get(Blackboard.Keys.TRANSFORMER); } /** * Set the mixin transformer instance * * @param transformer Mixin Transformer */ public void setActiveTransformer(IClassTransformer transformer) { if (transformer != null) { Blackboard.put(Blackboard.Keys.TRANSFORMER, transformer); } } /** * Allows a third party to set the side if the side is currently UNKNOWN * * @param side Side to set to * @return fluent interface */ public MixinEnvironment setSide(Side side) { if (side != null && this.getSide() == Side.UNKNOWN && side != Side.UNKNOWN) { this.side = side; } return this; } /** * Get (and detect if necessary) the current side * * @return current side (or UNKNOWN if could not be determined) */ public Side getSide() { if (this.side == null) { for (Side side : Side.values()) { if (side.detect()) { this.side = side; break; } } } return this.side != null ? this.side : Side.UNKNOWN; } /** * Get the current mixin subsystem version * * @return current version */ public String getVersion() { return Blackboard.<String>get(Blackboard.Keys.INIT); } /** * Get the specified option from the current environment * * @param option Option to get * @return Option value */ public boolean getOption(Option option) { return this.options[option.ordinal()]; } /** * Set the specified option for this environment * * @param option Option to set * @param value New option value */ public void setOption(Option option, boolean value) { this.options[option.ordinal()] = value; } /** * Get the specified option from the current environment * * @param option Option to get * @return Option value */ public String getOptionValue(Option option) { return option.getStringValue(); } /** * Set the obfuscation context * * @param context new context */ public void setObfuscationContext(String context) { this.obfuscationContext = context; } /** * Get the current obfuscation context */ public String getObfuscationContext() { return this.obfuscationContext; } /** * Get the current obfuscation context */ public String getRefmapObfuscationContext() { String overrideObfuscationType = Option.OBFUSCATION_TYPE.getStringValue(); if (overrideObfuscationType != null) { return overrideObfuscationType; } return this.obfuscationContext; } /** * Get the remapper chain for this environment */ public RemapperChain getRemappers() { return this.remappers; } /** * Invoke a mixin environment audit process */ public void audit() { Object activeTransformer = this.getActiveTransformer(); if (activeTransformer instanceof MixinTransformer) { MixinTransformer transformer = (MixinTransformer)activeTransformer; transformer.audit(); } } /** * Returns (and generates if necessary) the transformer delegation list for * this environment. * * @return current transformer delegation list (read-only) */ public List<IClassTransformer> getTransformers() { if (this.transformers == null) { this.buildTransformerDelegationList(); } return Collections.unmodifiableList(this.transformers); } /** * Adds a transformer to the transformer exclusions list * * @param name Class transformer exclusion to add */ public void addTransformerExclusion(String name) { MixinEnvironment.excludeTransformers.add(name); // Force rebuild of the list this.transformers = null; } /** * Map a class name back to its obfuscated counterpart * * @param className class name to unmap * @return obfuscated name for the specified deobfuscated reference */ public String unmap(String className) { if (this.transformers == null) { this.buildTransformerDelegationList(); } if (this.nameTransformer != null) { return this.nameTransformer.unmapClassName(className); } return className; } /** * Builds the transformer list to apply to loaded mixin bytecode. Since * generating this list requires inspecting each transformer by name (to * cope with the new wrapper functionality added by FML) we generate the * list just once per environment and cache the result. */ private void buildTransformerDelegationList() { MixinEnvironment.logger.debug("Rebuilding transformer delegation list:"); this.transformers = new ArrayList<IClassTransformer>(); for (IClassTransformer transformer : Launch.classLoader.getTransformers()) { String transformerName = transformer.getClass().getName(); boolean include = true; for (String excludeClass : MixinEnvironment.excludeTransformers) { if (transformerName.contains(excludeClass)) { include = false; break; } } boolean ignoreTransformer = transformer.getClass().getAnnotation(Resource.class) != null; if (include && !ignoreTransformer && !transformerName.contains(MixinTransformer.class.getName())) { MixinEnvironment.logger.debug(" Adding: {}", transformerName); this.transformers.add(transformer); } else { MixinEnvironment.logger.debug(" Excluding: {}", transformerName); } } MixinEnvironment.logger.debug("Transformer delegation list created with {} entries", this.transformers.size()); for (IClassTransformer transformer : Launch.classLoader.getTransformers()) { if (transformer instanceof IClassNameTransformer) { MixinEnvironment.logger.debug("Found name transformer: {}", transformer.getClass().getName()); this.nameTransformer = (IClassNameTransformer) transformer; } } } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s[%s]", this.getClass().getSimpleName(), this.phase); } /** * Get the current phase, triggers initialisation if necessary */ private static Phase getCurrentPhase() { if (MixinEnvironment.currentPhase == Phase.NOT_INITIALISED) { MixinEnvironment.init(Phase.PREINIT); } return MixinEnvironment.currentPhase; } /** * Initialise the mixin environment in the specified phase * * @param phase initial phase */ public static void init(Phase phase) { if (MixinEnvironment.currentPhase == Phase.NOT_INITIALISED) { MixinEnvironment.currentPhase = phase; MixinEnvironment.getEnvironment(phase); @SuppressWarnings("unused") MixinLogger logSpy = new MixinLogger(); } } /** * Get the mixin environment for the specified phase * * @param phase phase to fetch environment for * @return the environment */ public static MixinEnvironment getEnvironment(Phase phase) { if (phase == null) { return Phase.DEFAULT.getEnvironment(); } return phase.getEnvironment(); } /** * Gets the default environment * * @return the {@link Phase#DEFAULT DEFAULT} environment */ public static MixinEnvironment getDefaultEnvironment() { return MixinEnvironment.getEnvironment(Phase.DEFAULT); } /** * Gets the current environment * * @return the currently active environment */ public static MixinEnvironment getCurrentEnvironment() { if (MixinEnvironment.currentEnvironment == null) { MixinEnvironment.currentEnvironment = MixinEnvironment.getEnvironment(MixinEnvironment.getCurrentPhase()); } return MixinEnvironment.currentEnvironment; } /** * Get the current compatibility level */ public static CompatibilityLevel getCompatibilityLevel() { return MixinEnvironment.compatibility; } /** * Set desired compatibility level for the entire environment * * @param level Level to set, ignored if less than the current level * @throws IllegalArgumentException if the specified level is not supported * @deprecated set compatibility level in configuration */ @Deprecated public static void setCompatibilityLevel(CompatibilityLevel level) throws IllegalArgumentException { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); if (!"org.spongepowered.asm.mixin.transformer.MixinConfig".equals(stackTrace[2].getClassName())) { MixinEnvironment.logger.warn("MixinEnvironment::setCompatibilityLevel is deprecated and will be removed. Set level via config instead!"); } if (level != MixinEnvironment.compatibility && level.isAtLeast(MixinEnvironment.compatibility)) { if (!level.isSupported()) { throw new IllegalArgumentException("The requested compatibility level " + level + " could not be set. Level is not supported"); } MixinEnvironment.compatibility = level; MixinEnvironment.logger.info("Compatibility level set to {}", level); } } /** * Internal callback * * @param phase phase to go to */ static void gotoPhase(Phase phase) { if (phase == null || phase.ordinal < 0) { throw new IllegalArgumentException("Cannot go to the specified phase, phase is null or invalid"); } if (phase.ordinal > getCurrentPhase().ordinal) { MixinBootstrap.addProxy(); } if (phase == Phase.DEFAULT) { // remove appender org.apache.logging.log4j.core.Logger log = (org.apache.logging.log4j.core.Logger)LogManager.getLogger("FML"); log.removeAppender(MixinLogger.appender); } MixinEnvironment.currentPhase = phase; MixinEnvironment.currentEnvironment = MixinEnvironment.getEnvironment(MixinEnvironment.getCurrentPhase()); } }