/* * Minecraft Forge * Copyright (c) 2016. * * This library 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 version 2.1 * of the License. * * This library 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 this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package net.minecraftforge.fml.common; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import net.minecraftforge.fml.common.LoaderState.ModState; import net.minecraftforge.fml.common.ProgressManager.ProgressBar; import net.minecraftforge.fml.common.event.FMLEvent; import net.minecraftforge.fml.common.event.FMLLoadEvent; import net.minecraftforge.fml.common.event.FMLModDisabledEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLStateEvent; import net.minecraftforge.fml.common.functions.ArtifactVersionNameFunction; import net.minecraftforge.fml.common.versioning.ArtifactVersion; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.ThreadContext; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.BiMap; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.SubscriberExceptionHandler; import com.google.common.eventbus.SubscriberExceptionContext; public class LoadController { private Loader loader; private EventBus masterChannel; private ImmutableMap eventChannels; private LoaderState state; private Multimap modStates = ArrayListMultimap.create(); private Multimap errors = ArrayListMultimap.create(); private Map modNames = Maps.newHashMap(); private List activeModList = Lists.newArrayList(); private ModContainer activeContainer; private BiMap modObjectList; private ListMultimap packageOwners; public LoadController(Loader loader) { this.loader = loader; this.masterChannel = new EventBus(new SubscriberExceptionHandler() { @Override public void handleException(Throwable exception, SubscriberExceptionContext context) { FMLLog.log("FMLMainChannel", Level.ERROR, exception, "Could not dispatch event: %s to %s", context.getEvent(), context.getSubscriberMethod()); } }); this.masterChannel.register(this); state = LoaderState.NOINIT; packageOwners = ArrayListMultimap.create(); } void disableMod(ModContainer mod) { HashMap temporary = Maps.newHashMap(eventChannels); String modId = mod.getModId(); EventBus bus = temporary.remove(modId); bus.post(new FMLModDisabledEvent()); if (errors.get(modId).isEmpty()) { eventChannels = ImmutableMap.copyOf(temporary); modStates.put(modId, ModState.DISABLED); modObjectList.remove(mod); activeModList.remove(mod); } } @Subscribe public void buildModList(FMLLoadEvent event) { Builder eventBus = ImmutableMap.builder(); for (final ModContainer mod : loader.getModList()) { //Create mod logger, and make the EventBus logger a child of it. EventBus bus = new EventBus(new SubscriberExceptionHandler() { @Override public void handleException(final Throwable exception, final SubscriberExceptionContext context) { LoadController.this.errorOccurred(mod, exception); } }); boolean isActive = mod.registerBus(bus, this); if (isActive) { activeModList.add(mod); modStates.put(mod.getModId(), ModState.UNLOADED); eventBus.put(mod.getModId(), bus); FMLCommonHandler.instance().addModToResourcePack(mod); } else { FMLLog.log(mod.getModId(), Level.WARN, "Mod %s has been disabled through configuration", mod.getModId()); modStates.put(mod.getModId(), ModState.UNLOADED); modStates.put(mod.getModId(), ModState.DISABLED); } modNames.put(mod.getModId(), mod.getName()); } eventChannels = eventBus.build(); } public void distributeStateMessage(LoaderState state, Object... eventData) { if (state.hasEvent()) { masterChannel.post(state.getEvent(eventData)); } } public void transition(LoaderState desiredState, boolean forceState) { LoaderState oldState = state; state = state.transition(!errors.isEmpty()); if (state != desiredState && !forceState) { Entry toThrow = null; FMLLog.severe("Fatal errors were detected during the transition from %s to %s. Loading cannot continue", oldState, desiredState); StringBuilder sb = new StringBuilder(); printModStates(sb); FMLLog.severe("%s", sb.toString()); if (errors.size()>0) { FMLLog.severe("The following problems were captured during this phase"); for (Entry error : errors.entries()) { String modId = error.getKey(); String modName = modNames.get(modId); FMLLog.log(Level.ERROR, error.getValue(), "Caught exception from %s (%s)", modName, modId); if (error.getValue() instanceof IFMLHandledException) { toThrow = error; } else if (toThrow == null) { toThrow = error; } } } else { FMLLog.severe("The ForgeModLoader state engine has become corrupted. Probably, a state was missed by and invalid modification to a base class" + "ForgeModLoader depends on. This is a critical error and not recoverable. Investigate any modifications to base classes outside of" + "ForgeModLoader, especially Optifine, to see if there are fixes available."); throw new RuntimeException("The ForgeModLoader state engine is invalid"); } if (toThrow != null) { String modId = toThrow.getKey(); String modName = modNames.get(modId); String errMsg = String.format("Caught exception from %s (%s)", modName, modId); throw new LoaderExceptionModCrash(errMsg, toThrow.getValue()); } } else if (state != desiredState && forceState) { FMLLog.info("The state engine was in incorrect state %s and forced into state %s. Errors may have been discarded.", state, desiredState); forceState(desiredState); } } public ModContainer activeContainer() { return activeContainer != null ? activeContainer : findActiveContainerFromStack(); } void forceActiveContainer(ModContainer container) { activeContainer = container; } @Subscribe public void propogateStateMessage(FMLEvent stateEvent) { if (stateEvent instanceof FMLPreInitializationEvent) { modObjectList = buildModObjectList(); } ProgressBar bar = ProgressManager.push(stateEvent.description(), activeModList.size(), true); for (ModContainer mc : activeModList) { bar.step(mc.getName()); sendEventToModContainer(stateEvent, mc); } ProgressManager.pop(bar); } private void sendEventToModContainer(FMLEvent stateEvent, ModContainer mc) { String modId = mc.getModId(); Collection requirements = Collections2.transform(mc.getRequirements(),new ArtifactVersionNameFunction()); for (ArtifactVersion av : mc.getDependencies()) { if (av.getLabel()!= null && requirements.contains(av.getLabel()) && modStates.containsEntry(av.getLabel(),ModState.ERRORED)) { FMLLog.log(modId, Level.ERROR, "Skipping event %s and marking errored mod %s since required dependency %s has errored", stateEvent.getEventType(), modId, av.getLabel()); modStates.put(modId, ModState.ERRORED); return; } } activeContainer = mc; stateEvent.applyModContainer(activeContainer()); ThreadContext.put("mod", modId); FMLLog.log(modId, Level.TRACE, "Sending event %s to mod %s", stateEvent.getEventType(), modId); eventChannels.get(modId).post(stateEvent); FMLLog.log(modId, Level.TRACE, "Sent event %s to mod %s", stateEvent.getEventType(), modId); ThreadContext.remove("mod"); activeContainer = null; if (stateEvent instanceof FMLStateEvent) { if (!errors.containsKey(modId)) { modStates.put(modId, ((FMLStateEvent)stateEvent).getModState()); } else { modStates.put(modId, ModState.ERRORED); } } } public ImmutableBiMap buildModObjectList() { ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); for (ModContainer mc : activeModList) { if (!mc.isImmutable() && mc.getMod()!=null) { builder.put(mc, mc.getMod()); List packages = mc.getOwnedPackages(); for (String pkg : packages) { packageOwners.put(pkg, mc); } } if (mc.getMod()==null && !mc.isImmutable() && state!=LoaderState.CONSTRUCTING) { FMLLog.severe("There is a severe problem with %s - it appears not to have constructed correctly", mc.getModId()); if (state != LoaderState.CONSTRUCTING) { this.errorOccurred(mc, new RuntimeException()); } } } return builder.build(); } public void errorOccurred(ModContainer modContainer, Throwable exception) { if (exception instanceof InvocationTargetException) { errors.put(modContainer.getModId(), exception.getCause()); } else { errors.put(modContainer.getModId(), exception); } } public void printModStates(StringBuilder ret) { ret.append("\n\tStates:"); for (ModState state : ModState.values()) ret.append(" '").append(state.getMarker()).append("' = ").append(state.toString()); for (ModContainer mc : loader.getModList()) { ret.append("\n\t"); for (ModState state : modStates.get(mc.getModId())) ret.append(state.getMarker()); ret.append("\t").append(mc.getModId()).append("{").append(mc.getVersion()).append("} [").append(mc.getName()).append("] (").append(mc.getSource().getName()).append(") "); } } public List getActiveModList() { return activeModList; } public ModState getModState(ModContainer selectedMod) { return Iterables.getLast(modStates.get(selectedMod.getModId()), ModState.AVAILABLE); } public void distributeStateMessage(Class customEvent) { try { masterChannel.post(customEvent.newInstance()); } catch (Exception e) { FMLLog.log(Level.ERROR, e, "An unexpected exception"); throw new LoaderException(e); } } public BiMap getModObjectList() { if (modObjectList == null) { FMLLog.severe("Detected an attempt by a mod %s to perform game activity during mod construction. This is a serious programming error.", activeContainer); return buildModObjectList(); } return ImmutableBiMap.copyOf(modObjectList); } public boolean isInState(LoaderState state) { return this.state == state; } boolean hasReachedState(LoaderState state) { return this.state.ordinal()>=state.ordinal() && this.state!=LoaderState.ERRORED; } void forceState(LoaderState newState) { this.state = newState; } private ModContainer findActiveContainerFromStack() { for (Class c : getCallingStack()) { int idx = c.getName().lastIndexOf('.'); if (idx == -1) { continue; } String pkg = c.getName().substring(0,idx); if (packageOwners.containsKey(pkg)) { return packageOwners.get(pkg).get(0); } } return null; } private FMLSecurityManager accessibleManager = new FMLSecurityManager(); class FMLSecurityManager extends SecurityManager { Class[] getStackClasses() { return getClassContext(); } } Class[] getCallingStack() { return accessibleManager.getStackClasses(); } LoaderState getState() { return state; } }