diff --git a/build.gradle b/build.gradle index e324da750..b126b9e60 100644 --- a/build.gradle +++ b/build.gradle @@ -122,7 +122,7 @@ project(':forge') { api 'org.ow2.asm:asm:6.2' api 'org.ow2.asm:asm-commons:6.2' api 'org.ow2.asm:asm-tree:6.2' - api 'cpw.mods:modlauncher:0.1.0-rc.+' + api 'cpw.mods:modlauncher:0.1.0' api 'net.minecraftforge:accesstransformers:0.10+:shadowed' api 'net.minecraftforge:eventbus:0.1+:service' api 'net.minecraftforge:forgespi:0.1+' diff --git a/src/main/java/net/minecraftforge/common/config/Configuration.java b/src/main/java/net/minecraftforge/common/config/Configuration.java index 8935913a8..3ea4e0c77 100644 --- a/src/main/java/net/minecraftforge/common/config/Configuration.java +++ b/src/main/java/net/minecraftforge/common/config/Configuration.java @@ -35,9 +35,7 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PushbackInputStream; import java.io.Reader; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; @@ -51,7 +49,7 @@ import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Floats; -import net.minecraftforge.fml.common.FMLPaths; +import net.minecraftforge.fml.loading.FMLPaths; import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/java/net/minecraftforge/fml/BrandingControl.java b/src/main/java/net/minecraftforge/fml/BrandingControl.java index 579e62ab9..5e972b4c7 100644 --- a/src/main/java/net/minecraftforge/fml/BrandingControl.java +++ b/src/main/java/net/minecraftforge/fml/BrandingControl.java @@ -45,8 +45,7 @@ public class BrandingControl brd.add("MCP " + ForgeVersion.mcpVersion); brd.add("Forge " + ForgeVersion.getVersion()); int tModCount = ModList.get().size(); - - brd.add(MessageFormat.format(ForgeI18n.getPattern("fml.menu.loadingmods"), tModCount)); + brd.add(ForgeI18n.parseMessage("fml.menu.loadingmods", tModCount)); brandings = brd.build(); brandingsNoMC = brandings.subList(1, brandings.size()); } @@ -68,11 +67,11 @@ public class BrandingControl } private static final List defaultClientBranding = Stream.of("fml", "forge").collect(Collectors.toList()); public static String getClientBranding() { - return defaultClientBranding.stream().collect(Collectors.joining(",")); + return String.join(",", defaultClientBranding); } public static final List defaultServerBranding = Arrays.asList("fml", "forge"); public static String getServerBranding() { - return defaultServerBranding.stream().collect(Collectors.joining(",")); + return String.join(",", defaultServerBranding); } } diff --git a/src/main/java/net/minecraftforge/fml/ForgeI18n.java b/src/main/java/net/minecraftforge/fml/ForgeI18n.java index bda421c3c..581936563 100644 --- a/src/main/java/net/minecraftforge/fml/ForgeI18n.java +++ b/src/main/java/net/minecraftforge/fml/ForgeI18n.java @@ -19,16 +19,83 @@ package net.minecraftforge.fml; +import net.minecraftforge.fml.loading.StringUtils; +import net.minecraftforge.fml.loading.moddiscovery.ModInfo; +import org.apache.commons.lang3.text.ExtendedMessageFormat; +import org.apache.commons.lang3.text.FormatFactory; + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; public class ForgeI18n { private static Map i18n; + private static Map customFactories; - static String getPattern(final String patternName) { - return i18n.get(patternName); + static { + customFactories = new HashMap<>(); + customFactories.put("modinfo", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> parseModInfo(formatString, stringBuffer, objectToParse))); + customFactories.put("lower", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> stringBuffer.append(StringUtils.toLowerCase((String)objectToParse)))); + customFactories.put("upper", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> stringBuffer.append(StringUtils.toUpperCase((String)objectToParse)))); + customFactories.put("exc", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> parseException(formatString, stringBuffer, objectToParse))); + } + + private static void parseException(final String formatString, final StringBuffer stringBuffer, final Object objectToParse) { + Throwable t = (Throwable) objectToParse; + if (Objects.equals(formatString, "msg")) { + stringBuffer.append(t.getMessage()); + } else if (Objects.equals(formatString, "cls")) { + stringBuffer.append(t.getClass()); + } + } + + private static void parseModInfo(final String formatString, final StringBuffer stringBuffer, final Object modInfo) { + final ModInfo info = (ModInfo) modInfo; + if (Objects.equals(formatString, "id")) { + stringBuffer.append(info.getModId()); + } else if (Objects.equals(formatString, "name")) { + stringBuffer.append(info.getDisplayName()); + } + } + + public static String getPattern(final String patternName) { + return i18n.getOrDefault(patternName, patternName); } public static void loadLanguageData(final Map properties) { i18n = properties; } -} + + public static String parseMessage(final String i18nMessage, Object... args) { + final String pattern = getPattern(i18nMessage); + return parseFormat(pattern, args); + } + + public static String parseFormat(final String format, final Object... args) { + final ExtendedMessageFormat extendedMessageFormat = new ExtendedMessageFormat(format, customFactories); + return extendedMessageFormat.format(args); + } + + public static class CustomReadOnlyFormat extends Format { + private final BiConsumer formatter; + + CustomReadOnlyFormat(final BiConsumer formatter) { + this.formatter = formatter; + } + + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + formatter.accept(toAppendTo, obj); + return toAppendTo; + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + throw new UnsupportedOperationException("Parsing is not supported"); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/fml/LifecycleEventProvider.java b/src/main/java/net/minecraftforge/fml/LifecycleEventProvider.java index 858c9039c..046af40ef 100644 --- a/src/main/java/net/minecraftforge/fml/LifecycleEventProvider.java +++ b/src/main/java/net/minecraftforge/fml/LifecycleEventProvider.java @@ -22,6 +22,8 @@ package net.minecraftforge.fml; import net.minecraftforge.fml.common.event.ModLifecycleEvent; import net.minecraftforge.fml.javafmlmod.FMLModContainer; +import java.util.List; +import java.util.function.Consumer; import java.util.function.Supplier; public enum LifecycleEventProvider @@ -33,8 +35,8 @@ public enum LifecycleEventProvider POSTINIT(()->new LifecycleEvent(ModLoadingStage.POSTINIT)), COMPLETE(()->new LifecycleEvent(ModLoadingStage.COMPLETE)); - public void dispatch() { - ModList.get().dispatchLifeCycleEvent(this.event.get()); + public void dispatch(Consumer> errorHandler) { + ModList.get().dispatchLifeCycleEvent(this.event.get(), errorHandler); } private final Supplier event; diff --git a/src/main/java/net/minecraftforge/fml/LoadingFailedException.java b/src/main/java/net/minecraftforge/fml/LoadingFailedException.java new file mode 100644 index 000000000..51a0b06f5 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/LoadingFailedException.java @@ -0,0 +1,15 @@ +package net.minecraftforge.fml; + +import java.util.List; + +public class LoadingFailedException extends RuntimeException { + private final List loadingExceptions; + + public LoadingFailedException(final List loadingExceptions) { + this.loadingExceptions = loadingExceptions; + } + + public List getErrors() { + return this.loadingExceptions; + } +} diff --git a/src/main/java/net/minecraftforge/fml/ModContainer.java b/src/main/java/net/minecraftforge/fml/ModContainer.java index a9b0e438a..40bfa46d2 100644 --- a/src/main/java/net/minecraftforge/fml/ModContainer.java +++ b/src/main/java/net/minecraftforge/fml/ModContainer.java @@ -21,7 +21,7 @@ package net.minecraftforge.fml; import net.minecraftforge.fml.language.IModInfo; -import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; @@ -50,13 +50,11 @@ public abstract class ModContainer protected ModLoadingStage modLoadingStage; protected final Map> triggerMap; protected final Map> extensionPoints = new IdentityHashMap<>(); - protected final List modLoadingError; public ModContainer(IModInfo info) { this.modId = info.getModId(); this.modInfo = info; this.triggerMap = new HashMap<>(); - this.modLoadingError = new ArrayList<>(); this.modLoadingStage = ModLoadingStage.CONSTRUCT; } @@ -88,19 +86,18 @@ public abstract class ModContainer * Transition the mod to this event if possible. * @param event to transition to */ - public final void transitionState(LifecycleEventProvider.LifecycleEvent event) + public final void transitionState(LifecycleEventProvider.LifecycleEvent event, Consumer> errorHandler) { if (modLoadingStage == event.fromStage()) { try { triggerMap.getOrDefault(modLoadingStage, e->{}).accept(event); - modLoadingStage = event.toStage(); } - catch (RuntimeException e) + catch (ModLoadingException e) { - modLoadingError.add(e); modLoadingStage = ModLoadingStage.ERROR; + errorHandler.accept(Collections.singletonList(e)); } } } diff --git a/src/main/java/net/minecraftforge/fml/ModList.java b/src/main/java/net/minecraftforge/fml/ModList.java index c5f898746..1b5a9cbcd 100644 --- a/src/main/java/net/minecraftforge/fml/ModList.java +++ b/src/main/java/net/minecraftforge/fml/ModList.java @@ -62,7 +62,7 @@ public class ModList private ModList(final List modFiles, final List sortedList) { this.modFiles = modFiles.stream().map(ModFile::getModFileInfo).map(ModFileInfo.class::cast).collect(Collectors.toList()); - this.sortedList = Streams.concat(DefaultModInfos.getModInfos().stream(), sortedList.stream()). + this.sortedList = sortedList.stream(). map(ModInfo.class::cast). collect(Collectors.toList()); this.fileById = this.modFiles.stream().map(ModFileInfo::getMods).flatMap(Collection::stream). @@ -91,23 +91,21 @@ public class ModList return this.fileById.get(modid); } - public void dispatchLifeCycleEvent(LifecycleEventProvider.LifecycleEvent lifecycleEvent) { + public void dispatchLifeCycleEvent(LifecycleEventProvider.LifecycleEvent lifecycleEvent, final Consumer> errorHandler) { FMLLoader.getLanguageLoadingProvider().forEach(lp->lp.preLifecycleEvent(lifecycleEvent)); DeferredWorkQueue.deferredWorkQueue.clear(); try { - modLoadingThreadPool.submit(()->this.mods.parallelStream().forEach(m->m.transitionState(lifecycleEvent))).get(); + modLoadingThreadPool.submit(()->this.mods.parallelStream().forEach(m->m.transitionState(lifecycleEvent, errorHandler))).get(); } catch (InterruptedException | ExecutionException e) { LOGGER.error(LOADING, "Encountered an exception during parallel processing", e); } + LOGGER.debug(LOADING, "Dispatching synchronous work, {} jobs", DeferredWorkQueue.deferredWorkQueue.size()); DeferredWorkQueue.deferredWorkQueue.forEach(FutureTask::run); + LOGGER.debug(LOADING, "Synchronous work queue complete"); FMLLoader.getLanguageLoadingProvider().forEach(lp->lp.postLifecycleEvent(lifecycleEvent)); - final List erroredContainers = this.mods.stream().filter(m -> m.getCurrentState() == ModLoadingStage.ERROR).collect(Collectors.toList()); - if (!erroredContainers.isEmpty()) { - throw new RuntimeException("Errored containers found!", erroredContainers.get(0).modLoadingError.get(0)); - } } public void setLoadedMods(final List modContainers) diff --git a/src/main/java/net/minecraftforge/fml/ModLoader.java b/src/main/java/net/minecraftforge/fml/ModLoader.java index f786c3ac9..de738ce91 100644 --- a/src/main/java/net/minecraftforge/fml/ModLoader.java +++ b/src/main/java/net/minecraftforge/fml/ModLoader.java @@ -25,6 +25,8 @@ import net.minecraftforge.client.event.ModelRegistryEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.capabilities.CapabilityManager; import net.minecraftforge.fml.language.IModInfo; +import net.minecraftforge.fml.loading.DefaultModInfos; +import net.minecraftforge.fml.loading.EarlyLoadingException; import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.LoadingModList; import net.minecraftforge.fml.loading.moddiscovery.ModFile; @@ -34,11 +36,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.Callable; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -53,12 +54,15 @@ public class ModLoader private final LoadingModList loadingModList; private final ModLoadingClassLoader modClassLoader; + private final List loadingExceptions; private ModLoader() { INSTANCE = this; this.launchClassLoader = FMLLoader.getLaunchClassLoader(); this.loadingModList = FMLLoader.getLoadingModList(); this.modClassLoader = new ModLoadingClassLoader(this.launchClassLoader); + this.loadingExceptions = FMLLoader.getLoadingModList(). + getErrors().stream().map(ModLoadingException::fromEarlyException).collect(Collectors.toList()); Thread.currentThread().setContextClassLoader(this.modClassLoader); } @@ -67,14 +71,17 @@ public class ModLoader return INSTANCE == null ? INSTANCE = new ModLoader() : INSTANCE; } - private static Callable fireClientEvents() + private static Runnable fireClientEvents() { return ()->MinecraftForge.EVENT_BUS.post(new ModelRegistryEvent()); } public void loadMods() { + if (!this.loadingExceptions.isEmpty()) { + throw new LoadingFailedException(loadingExceptions); + } final ModList modList = ModList.of(loadingModList.getModFiles().stream().map(ModFileInfo::getFile).collect(Collectors.toList()), loadingModList.getMods()); - final ModContainer forgeModContainer; + ModContainer forgeModContainer; try { forgeModContainer = (ModContainer)Class.forName("net.minecraftforge.common.ForgeModContainer", true, modClassLoader). @@ -83,21 +90,41 @@ public class ModLoader catch (ClassNotFoundException | IllegalAccessException | NoSuchMethodException | InstantiationException | InvocationTargetException e) { LOGGER.error(CORE,"Unable to load the Forge Mod Container", e); - throw new RuntimeException(e); + loadingExceptions.add(new ModLoadingException(DefaultModInfos.forgeModInfo, ModLoadingStage.CONSTRUCT, "fml.modloading.failedtoloadforge", e)); + forgeModContainer = null; } final Stream modContainerStream = loadingModList.getModFiles().stream(). map(ModFileInfo::getFile). map(mf -> buildMods(mf, modClassLoader)). flatMap(Collection::stream); + if (!loadingExceptions.isEmpty()) { + LOGGER.error(CORE, "Failed to initialize mod containers"); + throw new LoadingFailedException(loadingExceptions); + } modList.setLoadedMods(Streams.concat(Stream.of(forgeModContainer), modContainerStream).collect(Collectors.toList())); - LifecycleEventProvider.CONSTRUCT.dispatch(); + dispatchAndHandleError(LifecycleEventProvider.CONSTRUCT); WorldPersistenceHooks.addHook(new FMLWorldPersistenceHook()); WorldPersistenceHooks.addHook((WorldPersistenceHooks.WorldPersistenceHook)forgeModContainer.getMod()); GameData.fireCreateRegistryEvents(); CapabilityManager.INSTANCE.injectCapabilities(modList.getAllScanData()); - LifecycleEventProvider.PREINIT.dispatch(); - Boolean result = DistExecutor.callWhenOn(Dist.CLIENT, ModLoader::fireClientEvents); - LifecycleEventProvider.SIDEDINIT.dispatch(); + dispatchAndHandleError(LifecycleEventProvider.PREINIT); + DistExecutor.runWhenOn(Dist.CLIENT, ModLoader::fireClientEvents); + dispatchAndHandleError(LifecycleEventProvider.SIDEDINIT); + } + + private void dispatchAndHandleError(LifecycleEventProvider event) { + if (!loadingExceptions.isEmpty()) { + LOGGER.error("Skipping lifecycle event {}, {} errors found.", event, loadingExceptions.size()); + } else { + event.dispatch(this::accumulateErrors); + } + if (!loadingExceptions.isEmpty()) { + LOGGER.error("Failed to complete lifecycle event {}, {} errors found", event, loadingExceptions.size()); + throw new LoadingFailedException(loadingExceptions); + } + } + private void accumulateErrors(List errors) { + loadingExceptions.addAll(errors); } private List buildMods(final ModFile modFile, final ModLoadingClassLoader modClassLoader) @@ -105,16 +132,22 @@ public class ModLoader final Map modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity())); return modFile.getScanResult().getTargets().entrySet().stream(). - map(e->e.getValue().loadMod(modInfoMap.get(e.getKey()), modClassLoader, modFile.getScanResult())). - collect(Collectors.toList()); + map(e-> { + try { + return e.getValue().loadMod(modInfoMap.get(e.getKey()), modClassLoader, modFile.getScanResult()); + } catch (ModLoadingException mle) { + loadingExceptions.add(mle); + return null; + } + }).collect(Collectors.toList()); } public void finishMods() { - LifecycleEventProvider.INIT.dispatch(); - LifecycleEventProvider.POSTINIT.dispatch(); - LifecycleEventProvider.COMPLETE.dispatch(); + dispatchAndHandleError(LifecycleEventProvider.INIT); + dispatchAndHandleError(LifecycleEventProvider.POSTINIT); + dispatchAndHandleError(LifecycleEventProvider.COMPLETE); GameData.freezeData(); } diff --git a/src/main/java/net/minecraftforge/fml/ModLoadingException.java b/src/main/java/net/minecraftforge/fml/ModLoadingException.java index a9b03f711..f42bb30e6 100644 --- a/src/main/java/net/minecraftforge/fml/ModLoadingException.java +++ b/src/main/java/net/minecraftforge/fml/ModLoadingException.java @@ -19,16 +19,60 @@ package net.minecraftforge.fml; -import java.util.ArrayList; +import com.google.common.collect.ObjectArrays; +import com.google.common.collect.Streams; +import net.minecraftforge.fml.language.IModInfo; +import net.minecraftforge.fml.loading.EarlyLoadingException; + +import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; /** - * Accumulates errors during loading for compact handling + * General purpose mod loading error message */ public class ModLoadingException extends RuntimeException { - private List errorEvents = new ArrayList<>(); - public static class ErrorEvent { + /** + * Mod Info for mod with issue + */ + private final IModInfo modInfo; + /** + * The stage where this error was encountered + */ + private final ModLoadingStage errorStage; + /** + * I18N message to use for display + */ + private final String i18nMessage; + + /** + * Context for message display + */ + private final List context; + + public ModLoadingException(final IModInfo modInfo, final ModLoadingStage errorStage, final String i18nMessage, final Throwable originalException, Object... context) { + super(ForgeI18n.getPattern(i18nMessage), originalException); + this.modInfo = modInfo; + this.errorStage = errorStage; + this.i18nMessage = i18nMessage; + this.context = Arrays.asList(context); + } + + static ModLoadingException fromEarlyException(final EarlyLoadingException e) { + return new ModLoadingException(null, ModLoadingStage.VALIDATE, e.getI18NMessage(), e, e.getContext().toArray()); + } + + public String getI18NMessage() { + return i18nMessage; + } + + public Object[] getContext() { + return context.toArray(); + } + + public String formatToString() { + return ForgeI18n.parseMessage(i18nMessage, Streams.concat(Stream.of(modInfo, errorStage, getCause()), context.stream()).toArray()); } } diff --git a/src/main/java/net/minecraftforge/fml/ModLoadingStage.java b/src/main/java/net/minecraftforge/fml/ModLoadingStage.java index b277c8107..7b9494a8d 100644 --- a/src/main/java/net/minecraftforge/fml/ModLoadingStage.java +++ b/src/main/java/net/minecraftforge/fml/ModLoadingStage.java @@ -31,6 +31,7 @@ import java.util.function.Supplier; public enum ModLoadingStage { ERROR(null), + VALIDATE(null), CONSTRUCT(null), PREINIT(()->FMLPreInitializationEvent::new), SIDEDINIT(SidedProvider.SIDEDINIT::get), diff --git a/src/main/java/net/minecraftforge/fml/client/ClientModLoader.java b/src/main/java/net/minecraftforge/fml/client/ClientModLoader.java index 3787cf605..966963608 100644 --- a/src/main/java/net/minecraftforge/fml/client/ClientModLoader.java +++ b/src/main/java/net/minecraftforge/fml/client/ClientModLoader.java @@ -29,11 +29,13 @@ import net.minecraft.resources.ResourcePackList; import net.minecraft.resources.data.IMetadataSectionSerializer; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.LoadingFailedException; import net.minecraftforge.fml.LogicalSidedProvider; +import net.minecraftforge.fml.ModLoader; import net.minecraftforge.fml.SidedProvider; import net.minecraftforge.fml.VersionChecker; -import net.minecraftforge.fml.ModLoader; import net.minecraftforge.fml.client.gui.GuiNotification; +import net.minecraftforge.fml.client.gui.LoadingErrorScreen; import net.minecraftforge.fml.client.registry.RenderingRegistry; import java.io.IOException; @@ -44,6 +46,7 @@ public class ClientModLoader { private static boolean loading; private static Minecraft mc; + private static LoadingFailedException error; public static void begin(final Minecraft minecraft, final ResourcePackList defaultResourcePacks, final IReloadableResourceManager mcResourceManager, DownloadingPackFinder metadataSerializer) { @@ -51,13 +54,21 @@ public class ClientModLoader ClientModLoader.mc = minecraft; SidedProvider.setClient(()->minecraft); LogicalSidedProvider.setClient(()->minecraft); - ModLoader.get().loadMods(); + try { + ModLoader.get().loadMods(); + } catch (LoadingFailedException e) { + error = e; + } ResourcePackLoader.loadResourcePacks(defaultResourcePacks); } public static void end() { - ModLoader.get().finishMods(); + try { + ModLoader.get().finishMods(); + } catch (LoadingFailedException e) { + if (error == null) error = e; + } loading = false; mc.gameSettings.loadOptions(); } @@ -71,11 +82,9 @@ public class ClientModLoader { GlStateManager.disableTexture2D(); GlStateManager.enableTexture2D(); - } - - public static boolean isErrored() - { - return false; + if (error != null) { + mc.displayGuiScreen(new LoadingErrorScreen(error)); + } } public static boolean isLoading() diff --git a/src/main/java/net/minecraftforge/fml/client/ResourcePackLoader.java b/src/main/java/net/minecraftforge/fml/client/ResourcePackLoader.java index 25d04f815..576276c34 100644 --- a/src/main/java/net/minecraftforge/fml/client/ResourcePackLoader.java +++ b/src/main/java/net/minecraftforge/fml/client/ResourcePackLoader.java @@ -19,12 +19,9 @@ package net.minecraftforge.fml.client; -import net.minecraft.client.resources.ResourcePackInfoClient; import net.minecraft.resources.AbstractResourcePack; import net.minecraft.resources.FilePack; import net.minecraft.resources.FolderPack; -import net.minecraft.resources.IPackFinder; -import net.minecraft.resources.IReloadableResourceManager; import net.minecraft.resources.IResourcePack; import net.minecraft.resources.ResourcePackInfo; import net.minecraft.resources.ResourcePackList; @@ -33,7 +30,6 @@ import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.moddiscovery.ModFile; import java.nio.file.Files; -import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; diff --git a/src/main/java/net/minecraftforge/fml/client/gui/GuiButtonClickConsumer.java b/src/main/java/net/minecraftforge/fml/client/gui/GuiButtonClickConsumer.java new file mode 100644 index 000000000..025e136c3 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/client/gui/GuiButtonClickConsumer.java @@ -0,0 +1,19 @@ +package net.minecraftforge.fml.client.gui; + +import net.minecraft.client.gui.GuiButton; + +import java.util.function.DoubleBinaryOperator; + +public class GuiButtonClickConsumer extends GuiButton { + private final DoubleBinaryOperator onClickAction; + + public GuiButtonClickConsumer(final int buttonId, final int x, final int y, final int widthIn, final int heightIn, final String buttonText, DoubleBinaryOperator onClick) { + super(buttonId, x, y, widthIn, heightIn, buttonText); + this.onClickAction = onClick; + } + + @Override + public void onClick(final double mouseX, final double mouseY) { + onClickAction.applyAsDouble(mouseX, mouseY); + } +} diff --git a/src/main/java/net/minecraftforge/fml/client/gui/GuiErrorBase.java b/src/main/java/net/minecraftforge/fml/client/gui/GuiErrorBase.java index b749f7ede..79a16f36e 100644 --- a/src/main/java/net/minecraftforge/fml/client/gui/GuiErrorBase.java +++ b/src/main/java/net/minecraftforge/fml/client/gui/GuiErrorBase.java @@ -27,7 +27,7 @@ import net.minecraft.client.gui.GuiErrorScreen; import net.minecraft.client.resources.I18n; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.fml.common.FMLPaths; +import net.minecraftforge.fml.loading.FMLPaths; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/java/net/minecraftforge/fml/client/gui/LoadingErrorScreen.java b/src/main/java/net/minecraftforge/fml/client/gui/LoadingErrorScreen.java new file mode 100644 index 000000000..65086ef3a --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/client/gui/LoadingErrorScreen.java @@ -0,0 +1,132 @@ +package net.minecraftforge.fml.client.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiErrorScreen; +import net.minecraft.client.gui.GuiListExtended; +import net.minecraft.client.renderer.Tessellator; +import net.minecraftforge.fml.ForgeI18n; +import net.minecraftforge.fml.LoadingFailedException; +import net.minecraftforge.fml.ModLoadingException; +import net.minecraftforge.fml.VersionChecker; +import net.minecraftforge.fml.loading.FMLPaths; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import static net.minecraft.util.StringUtils.stripControlCodes; + +public class LoadingErrorScreen extends GuiErrorScreen { + private static final Logger LOGGER = LogManager.getLogger(); + private final Path modsDir; + private final Path logFile; + private final LoadingFailedException loadingFailedException; + private LoadingErrorList errorList; + public LoadingErrorScreen(LoadingFailedException loadingException) + { + super(null, null); + this.loadingFailedException = loadingException; + this.modsDir = FMLPaths.MODSDIR.get(); + this.logFile = FMLPaths.GAMEDIR.get().resolve(Paths.get("logs","latest.log")); + } + + private double openModsDir(double mouseX, double mouseY) + { + try + { + Desktop.getDesktop().open(modsDir.toFile()); + } + catch (Exception e) + { + LOGGER.error("Problem opening mods folder", e); + } + return 0.0; + } + + private double openLogFile(double mouseX, double mouseY) + { + try + { + Desktop.getDesktop().open(logFile.toFile()); + } + catch (Exception e) + { + LOGGER.error("Problem opening log file {}", logFile, e); + } + return 0.0; + } + + @Override + public void initGui() + { + super.initGui(); + this.buttons.clear(); + this.children.clear(); + this.addButton(new GuiButtonClickConsumer(10, 50, this.height - 38, this.width / 2 - 55, 20, + ForgeI18n.parseMessage("fml.button.open.mods.folder"), this::openModsDir)); + this.addButton(new GuiButtonClickConsumer(11, this.width / 2 + 5, this.height - 38, this.width / 2 - 55, 20, + ForgeI18n.parseMessage("fml.button.open.file", logFile.getFileName()), this::openLogFile)); + this.errorList = new LoadingErrorList(this, this.loadingFailedException.getErrors()); + } + + @Override + public void render(int mouseX, int mouseY, float partialTicks) + { + this.drawDefaultBackground(); + this.errorList.drawScreen(mouseX, mouseY, partialTicks); + drawMultiLineCenteredString(fontRenderer, ForgeI18n.parseMessage("fml.loadingerrorscreen.header", this.loadingFailedException.getErrors().size()), this.width / 2, 10); + this.buttons.forEach(button -> button.render(mouseX, mouseY, partialTicks)); + } + + private void drawMultiLineCenteredString(FontRenderer fr, String str, int x, int y) { + for (String s : fr.listFormattedStringToWidth(str, this.width)) { + fr.drawStringWithShadow(s, (float) (x - fr.getStringWidth(s) / 2.0), y, 0xFFFFFF); + y+=fr.FONT_HEIGHT; + } + } + public static class LoadingErrorList extends GuiListExtended { + LoadingErrorList(final LoadingErrorScreen parent, final List errors) { + super(parent.mc,parent.width,parent.height,35,parent.height - 50, 2 * parent.mc.fontRenderer.FONT_HEIGHT + 8); + errors.forEach(e->addEntry(new ErrorEntry(e))); + } + + @Override + protected int getScrollBarX() + { + return this.right - 6; + } + + @Override + public int getListWidth() + { + return this.width; + } + + public class ErrorEntry extends GuiListExtended.IGuiListEntry { + private final ModLoadingException error; + + ErrorEntry(final ModLoadingException e) { + this.error = e; + } + + @Override + public void drawEntry(final int entryWidth, final int entryHeight, final int mouseX, final int mouseY, final boolean p_194999_5_, final float partialTicks) { + int top = this.getY(); + int left = this.getX(); + FontRenderer font = Minecraft.getInstance().fontRenderer; + final List strings = font.listFormattedStringToWidth(error.formatToString(), LoadingErrorList.this.width); + float f = (float)top + 2; + for (int i = 0; i < Math.min(strings.size(), 2); i++) { + font.drawString(strings.get(i), left + 5, f, 0xFFFFFFFF); + f += font.FONT_HEIGHT; + } + } + } + + } +} diff --git a/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java b/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java index 489d815a1..0230f021f 100644 --- a/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java +++ b/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java @@ -23,12 +23,13 @@ import net.minecraftforge.eventbus.EventBusErrorMessage; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.IEventListener; +import net.minecraftforge.fml.AutomaticEventSubscriber; import net.minecraftforge.fml.LifecycleEventProvider; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModLoadingException; +import net.minecraftforge.fml.ModLoadingStage; import net.minecraftforge.fml.ModThreadContext; import net.minecraftforge.fml.common.event.ModLifecycleEvent; -import net.minecraftforge.fml.AutomaticEventSubscriber; -import net.minecraftforge.fml.ModLoadingStage; -import net.minecraftforge.fml.ModContainer; import net.minecraftforge.fml.language.IModInfo; import net.minecraftforge.fml.language.ModFileScanData; @@ -67,7 +68,7 @@ public class FMLModContainer extends ModContainer catch (Throwable e) { LOGGER.error(LOADING, "Failed to load class {}", className, e); - throw new RuntimeException(e); + throw new ModLoadingException(info, ModLoadingStage.CONSTRUCT, "fml.modloading.failedtoloadmodclass", e); } } @@ -86,7 +87,7 @@ public class FMLModContainer extends ModContainer private void onEventFailed(IEventBus iEventBus, Event event, IEventListener[] iEventListeners, int i, Throwable throwable) { LOGGER.error(new EventBusErrorMessage(event, i, iEventListeners, throwable)); - modLoadingError.add(throwable); + } private void beforeEvent(LifecycleEventProvider.LifecycleEvent lifecycleEvent) { @@ -105,8 +106,7 @@ public class FMLModContainer extends ModContainer catch (Throwable e) { LOGGER.error(LOADING,"Caught exception during event {} dispatch for modid {}", event, this.getModId(), e); - modLoadingStage = ModLoadingStage.ERROR; - modLoadingError.add(e); + throw new ModLoadingException(modInfo, lifecycleEvent.fromStage(), "fml.modloading.errorduringevent", e); } } @@ -120,7 +120,9 @@ public class FMLModContainer extends ModContainer private void preinitMod(LifecycleEventProvider.LifecycleEvent lifecycleEvent) { + LOGGER.debug(LOADING, "Injecting Automatic event subscribers for {}", getModId()); AutomaticEventSubscriber.inject(this, this.scanResults, this.modClass.getClassLoader()); + LOGGER.debug(LOADING, "Completed Automatic event subscribers for {}", getModId()); } private void constructMod(LifecycleEventProvider.LifecycleEvent event) @@ -134,8 +136,7 @@ public class FMLModContainer extends ModContainer catch (Throwable e) { LOGGER.error(LOADING,"Failed to create mod instance. ModID: {}, class {}", getModId(), modClass.getName(), e); - modLoadingStage = ModLoadingStage.ERROR; - modLoadingError.add(e); + throw new ModLoadingException(modInfo, event.fromStage(), "fml.modloading.failedtoloadmod", e, modClass); } } diff --git a/src/main/java/net/minecraftforge/fml/loading/EarlyLoadingException.java b/src/main/java/net/minecraftforge/fml/loading/EarlyLoadingException.java new file mode 100644 index 000000000..560ecd38f --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/loading/EarlyLoadingException.java @@ -0,0 +1,27 @@ +package net.minecraftforge.fml.loading; + +import java.util.Arrays; +import java.util.List; + +/** + * Thrown during early loading phase, and collected by the LoadingModList for handoff to the client + * or server. + */ +public class EarlyLoadingException extends RuntimeException { + private final String i18nMessage; + private final List context; + + public EarlyLoadingException(final String message, final String i18nMessage, final Throwable originalException, Object... context) { + super(message, originalException); + this.i18nMessage = i18nMessage; + this.context = Arrays.asList(context); + } + + public String getI18NMessage() { + return this.i18nMessage; + } + + public List getContext() { + return this.context; + } +} diff --git a/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java b/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java index dd2893a63..d3633c99b 100644 --- a/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java +++ b/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java @@ -23,7 +23,6 @@ import com.electronwill.nightconfig.core.ConfigSpec; import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.core.io.WritingMode; import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.fml.common.FMLPaths; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; diff --git a/src/main/java/net/minecraftforge/fml/common/FMLPaths.java b/src/main/java/net/minecraftforge/fml/loading/FMLPaths.java similarity index 88% rename from src/main/java/net/minecraftforge/fml/common/FMLPaths.java rename to src/main/java/net/minecraftforge/fml/loading/FMLPaths.java index 029e4d267..e3a0c5ea1 100644 --- a/src/main/java/net/minecraftforge/fml/common/FMLPaths.java +++ b/src/main/java/net/minecraftforge/fml/loading/FMLPaths.java @@ -17,13 +17,14 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package net.minecraftforge.fml.common; +package net.minecraftforge.fml.loading; import cpw.mods.modlauncher.api.IEnvironment; import net.minecraftforge.fml.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; @@ -72,6 +73,12 @@ public enum FMLPaths for (FMLPaths path : FMLPaths.values()) { path.absolutePath = rootPath.resolve(path.relativePath).toAbsolutePath(); + try { + path.absolutePath = path.absolutePath.toRealPath(); + } catch (IOException e) { + LOGGER.error("Unable to resolve path {}", path.absolutePath, e); + throw new RuntimeException(e); + } LOGGER.debug(CORE,"Path {} is {}", ()-> path, ()-> path.absolutePath); if (path.isDirectory) { diff --git a/src/main/java/net/minecraftforge/fml/loading/FMLServiceProvider.java b/src/main/java/net/minecraftforge/fml/loading/FMLServiceProvider.java index a389d432a..451e61e2d 100644 --- a/src/main/java/net/minecraftforge/fml/loading/FMLServiceProvider.java +++ b/src/main/java/net/minecraftforge/fml/loading/FMLServiceProvider.java @@ -25,7 +25,6 @@ import cpw.mods.modlauncher.api.ITransformer; import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionSpecBuilder; -import net.minecraftforge.fml.common.FMLPaths; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -37,8 +36,6 @@ import java.util.function.BiFunction; import static net.minecraftforge.fml.Logging.CORE; -import cpw.mods.modlauncher.api.ITransformationService.OptionResult; - public class FMLServiceProvider implements ITransformationService { diff --git a/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java b/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java index 577d4b74a..14b496686 100644 --- a/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java +++ b/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java @@ -30,10 +30,10 @@ import net.minecraftforge.fml.loading.moddiscovery.ModInfo; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -48,6 +48,7 @@ public class LoadingModList private final List sortedList; private final Map fileById; private BackgroundScanHandler scanner; + private final List preLoadErrors; private LoadingModList(final List modFiles, final List sortedList) { @@ -58,11 +59,16 @@ public class LoadingModList this.fileById = this.modFiles.stream().map(ModFileInfo::getMods).flatMap(Collection::stream). map(ModInfo.class::cast). collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile)); + this.preLoadErrors = new ArrayList<>(); } - public static LoadingModList of(List modFiles, List sortedList) + public static LoadingModList of(List modFiles, List sortedList, final EarlyLoadingException earlyLoadingException) { INSTANCE = new LoadingModList(modFiles, sortedList); + if (earlyLoadingException != null) + { + INSTANCE.preLoadErrors.add(earlyLoadingException); + } return INSTANCE; } @@ -109,4 +115,8 @@ public class LoadingModList { return this.sortedList; } + + public List getErrors() { + return preLoadErrors; + } } diff --git a/src/main/java/net/minecraftforge/fml/loading/ModSorter.java b/src/main/java/net/minecraftforge/fml/loading/ModSorter.java index b47d7409f..60d7366f9 100644 --- a/src/main/java/net/minecraftforge/fml/loading/ModSorter.java +++ b/src/main/java/net/minecraftforge/fml/loading/ModSorter.java @@ -56,10 +56,15 @@ public class ModSorter public static LoadingModList sort(List mods) { final ModSorter ms = new ModSorter(mods); - ms.buildUniqueList(); - ms.verifyDependencyVersions(); - ms.sort(); - return LoadingModList.of(ms.modFiles, ms.sortedList); + EarlyLoadingException earlyLoadingException = null; + try { + ms.buildUniqueList(); + ms.verifyDependencyVersions(); + ms.sort(); + } catch (EarlyLoadingException ele) { + earlyLoadingException = ele; + } + return LoadingModList.of(ms.modFiles, ms.sortedList, earlyLoadingException); } private void sort() @@ -77,7 +82,7 @@ public class ModSorter { TopologicalSort.TopoSortException.TopoSortExceptionData> data = e.getData(); LOGGER.error(LOADING, ()-> data); - throw e; + throw new EarlyLoadingException("Sorting error", "fml.modloading.sortingerror", e, e.getData()); } this.sortedList = sorted.stream().map(Supplier::get).map(ModFileInfo::getMods). flatMap(Collection::stream).map(ModInfo.class::cast).collect(Collectors.toList()); @@ -107,7 +112,7 @@ public class ModSorter final List>> dupedMods = modIds.entrySet().stream().filter(e -> e.getValue().size() > 1).collect(Collectors.toList()); if (!dupedMods.isEmpty()) { - throw new DuplicateModsFoundException(null); + throw new EarlyLoadingException("Duplicate mods found", "fml.modloading.dupesfound", null, dupedMods); } modIdNameLookup = modIds.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); @@ -127,7 +132,7 @@ public class ModSorter LOGGER.debug(LOADING, "Found {} mandatory mod requirements missing", missingVersions.size()); if (!missingVersions.isEmpty()) { - throw new RuntimeException("Missing mods"); + throw new EarlyLoadingException("Missing mods", "fml.modloading.missingmods", null, missingVersions); } } diff --git a/src/main/java/net/minecraftforge/fml/loading/StringUtils.java b/src/main/java/net/minecraftforge/fml/loading/StringUtils.java index 7d9d8d61b..dfc449a1f 100644 --- a/src/main/java/net/minecraftforge/fml/loading/StringUtils.java +++ b/src/main/java/net/minecraftforge/fml/loading/StringUtils.java @@ -23,6 +23,7 @@ import org.apache.commons.lang3.text.StrSubstitutor; import java.net.MalformedURLException; import java.net.URL; +import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -32,7 +33,11 @@ import java.util.Optional; public class StringUtils { public static String toLowerCase(final String str) { - return str.toLowerCase(java.util.Locale.ROOT); + return str.toLowerCase(Locale.ROOT); + } + + public static String toUpperCase(final String str) { + return str.toUpperCase(Locale.ROOT); } public static boolean endsWith(final String search, final String... endings) { diff --git a/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java b/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java index 265a64019..1bf1b42b8 100644 --- a/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java +++ b/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java @@ -20,7 +20,7 @@ package net.minecraftforge.fml.loading.moddiscovery; import net.minecraftforge.fml.loading.StringUtils; -import net.minecraftforge.fml.common.FMLPaths; +import net.minecraftforge.fml.loading.FMLPaths; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index f3d77939b..c6794fbfb 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -7,6 +7,13 @@ "fml.menu.mods.config": "Config", "fml.menu.modoptions": "Mod Options...", "fml.menu.loadingmods": "{0,choice,0#No mods|1#1 mod|1<{0} mods} loaded", + "fml.button.open.file": "Open {0}", + "fml.button.open.mods.folder": "Open Mods Folder", + "fml.loadingerrorscreen.header": "Error loading mods\n{0,choice,1#1 error has|1<{0} errors have} occured during loading", + "fml.modloading.failedtoloadmodclass":"{0,modinfo,name} has class loading errors\n\u00a77{2,exc,msg}", + "fml.modloading.failedtoloadmod":"{0,modinfo,name} ({0,modinfo,id}) has failed to load correctly\n\u00a77{2,exc,msg}", + "fml.modloading.errorduringevent":"{0,modinfo,name} ({0,modinfo,id}) encountered an error during the {1,lower} event phase\n\u00a77{2,exc,msg}", + "fml.modloading.failedtoloadforge": "Failed to load forge", "commands.forge.dimensions.list": "Currently registered dimensions by type:", "commands.forge.entity.list.invalid": "Invalid filter, does not match any entities. Use /forge entity list for a proper list",