diff --git a/jsons/1.12.2-dev.json b/jsons/1.12.2-dev.json index 0e0fab8eb0..bd304fddfb 100644 --- a/jsons/1.12.2-dev.json +++ b/jsons/1.12.2-dev.json @@ -87,6 +87,11 @@ }, { "name": "net.sf.trove4j:trove4j:3.0.3" + }, + { + "name": "org.apache.maven:maven-artifact:3.5.3", + "children" : ["sources"], + "url" : "http://repo.maven.apache.org/maven2" } ] } diff --git a/jsons/1.12.2-rel.json b/jsons/1.12.2-rel.json index 2b11e8d59a..26ef1aa8b0 100644 --- a/jsons/1.12.2-rel.json +++ b/jsons/1.12.2-rel.json @@ -138,6 +138,12 @@ "name": "net.sf.trove4j:trove4j:3.0.3", "clientreq":true, "serverreq":true + }, + { + "name": "org.apache.maven:maven-artifact:3.5.3", + "url" : "http://files.minecraftforge.net/maven/", + "serverreq":true, + "clientreq":true } ] }, diff --git a/src/main/java/net/minecraftforge/common/ForgeVersion.java b/src/main/java/net/minecraftforge/common/ForgeVersion.java index 4ae3a409bc..2ed033c43b 100644 --- a/src/main/java/net/minecraftforge/common/ForgeVersion.java +++ b/src/main/java/net/minecraftforge/common/ForgeVersion.java @@ -32,14 +32,12 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; -import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.google.common.io.ByteStreams; import com.google.gson.Gson; -import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.InjectedModContainer; import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.ModContainer; diff --git a/src/main/java/net/minecraftforge/fml/common/Loader.java b/src/main/java/net/minecraftforge/fml/common/Loader.java index 5dd9f2288b..80e50045e2 100644 --- a/src/main/java/net/minecraftforge/fml/common/Loader.java +++ b/src/main/java/net/minecraftforge/fml/common/Loader.java @@ -32,8 +32,10 @@ import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.regex.Matcher; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.classloading.FMLForgePlugin; import net.minecraftforge.common.ForgeVersion; import net.minecraftforge.common.capabilities.CapabilityManager; import net.minecraftforge.common.config.ConfigManager; @@ -42,6 +44,8 @@ import net.minecraftforge.fml.common.LoaderState.ModState; import net.minecraftforge.fml.common.ModContainer.Disableable; import net.minecraftforge.fml.common.ProgressManager.ProgressBar; import net.minecraftforge.fml.common.discovery.ASMDataTable; +import net.minecraftforge.fml.common.discovery.ContainerType; +import net.minecraftforge.fml.common.discovery.ModCandidate; import net.minecraftforge.fml.common.discovery.ModDiscoverer; import net.minecraftforge.fml.common.event.FMLInterModComms; import net.minecraftforge.fml.common.event.FMLLoadEvent; @@ -54,8 +58,11 @@ import net.minecraftforge.fml.common.toposort.ModSortingException.SortingExcepti import net.minecraftforge.fml.common.versioning.ArtifactVersion; import net.minecraftforge.fml.common.versioning.DependencyParser; import net.minecraftforge.fml.common.versioning.VersionParser; -import net.minecraftforge.fml.relauncher.ModListHelper; +import net.minecraftforge.fml.relauncher.CoreModManager; import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.libraries.Artifact; +import net.minecraftforge.fml.relauncher.libraries.LibraryManager; +import net.minecraftforge.fml.relauncher.libraries.Repository; import net.minecraftforge.registries.GameData; import net.minecraftforge.registries.ObjectHolderRegistry; @@ -384,18 +391,42 @@ public class Loader mods.add(new InjectedModContainer(mc,mc.getSource())); } ModDiscoverer discoverer = new ModDiscoverer(); - FMLLog.log.debug("Attempting to load mods contained in the minecraft jar file and associated classes"); - discoverer.findClasspathMods(modClassLoader); - FMLLog.log.debug("Minecraft jar mods loaded successfully"); - FMLLog.log.info("Found {} mods from the command line. Injecting into mod discoverer", ModListHelper.additionalMods.size()); - FMLLog.log.info("Searching {} for mods", canonicalModsDir.getAbsolutePath()); - discoverer.findModDirMods(canonicalModsDir, ModListHelper.additionalMods.values().toArray(new File[0])); - File versionSpecificModsDir = new File(canonicalModsDir,mccversion); - if (versionSpecificModsDir.isDirectory()) + if (!FMLForgePlugin.RUNTIME_DEOBF) //Only descover mods in the classpath if we're in the dev env. + { //TODO: Move this to GradleStart? + FMLLog.log.debug("Attempting to load mods contained in the minecraft jar file and associated classes"); + discoverer.findClasspathMods(modClassLoader); + FMLLog.log.debug("Minecraft jar mods loaded successfully"); + } + + List maven_canidates = LibraryManager.flattenLists(minecraftDir); + List file_canidates = LibraryManager.gatherLegacyCanidates(minecraftDir); + + for (Artifact artifact : maven_canidates) { - FMLLog.log.info("Also searching {} for mods", versionSpecificModsDir); - discoverer.findModDirMods(versionSpecificModsDir); + artifact = Repository.resolveAll(artifact); + if (artifact != null) + { + File target = artifact.getFile(); + if (!file_canidates.contains(target)) + file_canidates.add(target); + } + } + //Do we want to sort the full list after resolving artifacts? + //TODO: Add dependency gathering? + + for (File mod : file_canidates) + { + // skip loaded coremods + if (CoreModManager.getIgnoredMods().contains(mod.getName())) + { + FMLLog.log.trace("Skipping already parsed coremod or tweaker {}", mod.getName()); + } + else + { + FMLLog.log.debug("Found a candidate zip or jar file {}", mod.getName()); + discoverer.addCandidate(new ModCandidate(mod, mod, ContainerType.JAR)); + } } mods.addAll(discoverer.identifyMods()); diff --git a/src/main/java/net/minecraftforge/fml/common/asm/transformers/ModAccessTransformer.java b/src/main/java/net/minecraftforge/fml/common/asm/transformers/ModAccessTransformer.java index 5c113f0e2d..7eb99f549d 100644 --- a/src/main/java/net/minecraftforge/fml/common/asm/transformers/ModAccessTransformer.java +++ b/src/main/java/net/minecraftforge/fml/common/asm/transformers/ModAccessTransformer.java @@ -24,20 +24,21 @@ import java.io.InputStream; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.jar.Manifest; - import net.minecraftforge.fml.common.FMLLog; import com.google.common.collect.Maps; import com.google.common.io.ByteSource; import com.google.common.io.CharSource; -public class ModAccessTransformer extends AccessTransformer { +public class ModAccessTransformer extends AccessTransformer +{ + public static final Attributes.Name FMLAT = new Attributes.Name("FMLAT"); private static Map embedded = Maps.newHashMap(); //Needs to be primitive so that both classloaders get the same class. @SuppressWarnings("unchecked") - public ModAccessTransformer() throws Exception + public ModAccessTransformer() throws Exception { super(ModAccessTransformer.class); //We are in the new ClassLoader here, so we need to get the static field from the other ClassLoader. @@ -59,11 +60,8 @@ public class ModAccessTransformer extends AccessTransformer { } } - public static void addJar(JarFile jar) throws IOException + public static void addJar(JarFile jar, String atList) throws IOException { - Manifest manifest = jar.getManifest(); - String atList = manifest.getMainAttributes().getValue("FMLAT"); - if (atList == null) return; for (String at : atList.split(" ")) { JarEntry jarEntry = jar.getJarEntry("META-INF/"+at); diff --git a/src/main/java/net/minecraftforge/fml/common/discovery/ModDiscoverer.java b/src/main/java/net/minecraftforge/fml/common/discovery/ModDiscoverer.java index 0720f3b188..fd742c20e9 100644 --- a/src/main/java/net/minecraftforge/fml/common/discovery/ModDiscoverer.java +++ b/src/main/java/net/minecraftforge/fml/common/discovery/ModDiscoverer.java @@ -21,24 +21,17 @@ package net.minecraftforge.fml.common.discovery; import java.io.File; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.LoaderException; import net.minecraftforge.fml.common.ModClassLoader; import net.minecraftforge.fml.common.ModContainer; import net.minecraftforge.fml.relauncher.CoreModManager; -import net.minecraftforge.fml.relauncher.FileListHelper; - import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; -import com.google.common.collect.ObjectArrays; public class ModDiscoverer { - private static Pattern zipJar = Pattern.compile("(.+).(zip|jar)$"); - private List candidates = Lists.newArrayList(); private ASMDataTable dataTable = new ASMDataTable(); @@ -89,45 +82,6 @@ public class ModDiscoverer } - public void findModDirMods(File modsDir) - { - findModDirMods(modsDir, new File[0]); - } - - public void findModDirMods(File modsDir, File[] supplementalModFileCandidates) - { - File[] modList = FileListHelper.sortFileList(modsDir, null); - modList = FileListHelper.sortFileList(ObjectArrays.concat(modList, supplementalModFileCandidates, File.class)); - for (File modFile : modList) - { - // skip loaded coremods - if (CoreModManager.getIgnoredMods().contains(modFile.getName())) - { - FMLLog.log.trace("Skipping already parsed coremod or tweaker {}", modFile.getName()); - } - else if (modFile.isDirectory()) - { - //TODO Remove in 1.13+ Mods should never be directory based anymore. - FMLLog.log.debug("Found a candidate mod directory {}", modFile.getName()); - addCandidate(new ModCandidate(modFile, modFile, ContainerType.DIR)); - } - else - { - Matcher matcher = zipJar.matcher(modFile.getName()); - - if (matcher.matches()) - { - FMLLog.log.debug("Found a candidate zip or jar file {}", matcher.group(0)); - addCandidate(new ModCandidate(modFile, modFile, ContainerType.JAR)); - } - else - { - FMLLog.log.debug("Ignoring unknown file {} in mods directory", modFile.getName()); - } - } - } - } - public List identifyMods() { List modList = Lists.newArrayList(); @@ -165,7 +119,7 @@ public class ModDiscoverer return nonModLibs; } - private void addCandidate(ModCandidate candidate) + public void addCandidate(ModCandidate candidate) { for (ModCandidate c : candidates) { diff --git a/src/main/java/net/minecraftforge/fml/common/launcher/FMLTweaker.java b/src/main/java/net/minecraftforge/fml/common/launcher/FMLTweaker.java index 9c446655c0..a6f133351d 100644 --- a/src/main/java/net/minecraftforge/fml/common/launcher/FMLTweaker.java +++ b/src/main/java/net/minecraftforge/fml/common/launcher/FMLTweaker.java @@ -32,7 +32,6 @@ import net.minecraft.launchwrapper.LaunchClassLoader; import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import net.minecraftforge.fml.relauncher.FMLSecurityManager; -import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import com.google.common.collect.Lists; diff --git a/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java b/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java index 86bdf3ff1d..b673a1ac4e 100644 --- a/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java +++ b/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java @@ -22,10 +22,8 @@ package net.minecraftforge.fml.relauncher; import java.io.Closeable; import java.io.File; import java.io.FileFilter; -import java.io.FileOutputStream; -import java.io.FilenameFilter; +import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; @@ -34,17 +32,15 @@ import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.ToIntFunction; import java.util.jar.Attributes; -import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.Manifest; -import com.google.common.io.ByteStreams; -import com.google.common.io.Files; import net.minecraft.launchwrapper.ITweaker; import net.minecraft.launchwrapper.Launch; import net.minecraft.launchwrapper.LaunchClassLoader; @@ -59,6 +55,9 @@ import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.MCVersion; import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.Name; import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.SortingIndex; import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions; +import net.minecraftforge.fml.relauncher.libraries.Artifact; +import net.minecraftforge.fml.relauncher.libraries.LibraryManager; +import net.minecraftforge.fml.relauncher.libraries.Repository; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -71,8 +70,6 @@ import com.google.common.primitives.Ints; public class CoreModManager { private static final Attributes.Name COREMODCONTAINSFMLMOD = new Attributes.Name("FMLCorePluginContainsFMLMod"); private static final Attributes.Name MODTYPE = new Attributes.Name("ModType"); - private static final Attributes.Name MODSIDE = new Attributes.Name("ModSide"); - private static final Attributes.Name MODCONTAINSDEPS = new Attributes.Name("ContainedDeps"); private static String[] rootPlugins = { "net.minecraftforge.fml.relauncher.FMLCorePlugin", "net.minecraftforge.classloading.FMLForgePlugin" }; private static List ignoredModFiles = Lists.newArrayList(); private static Map> transformers = Maps.newHashMap(); @@ -83,7 +80,6 @@ public class CoreModManager { private static List candidateModFiles = Lists.newArrayList(); private static List accessTransformers = Lists.newArrayList(); private static Set rootNames = Sets.newHashSet(); - private static final List skipContainedDeps = Arrays.asList(System.getProperty("fml.skipContainedDeps","").split(",")); static { @@ -197,6 +193,8 @@ public class CoreModManager { { CoreModManager.mcDir = mcDir; CoreModManager.tweaker = tweaker; + + //TODO: Detect this better? Support install time deobfusication? Decouple deobf from dev? Add dev flag in GradleStart? try { // Are we in a 'decompiled' environment? @@ -257,7 +255,7 @@ public class CoreModManager { private static void findDerpMods(LaunchClassLoader classLoader, File modDir, File modDirVer) { - File[] derplist = listFiles((dir, name) -> name.endsWith(".jar.zip"), modDir, modDirVer); + File[] derplist = listFiles(path -> path.getName().endsWith(".jar.zip"), modDir, modDirVer); if (derplist != null && derplist.length > 0) { FMLLog.log.fatal("FML has detected several badly downloaded jar files, which have been named as zip files. You probably need to download them again, or they may not work properly"); @@ -297,20 +295,6 @@ public class CoreModManager { } } - private static File[] listFiles(FilenameFilter filter, File ... dirs) - { - File[] ret = null; - for (File dir : dirs) - { - if (!dir.isDirectory() || !dir.exists()) - continue; - if (ret == null) - ret = dir.listFiles(filter); - else - ret = ObjectArrays.concat(ret, dir.listFiles(filter), File.class); - } - return ret == null ? new File[0] : ret; - } private static File[] listFiles(FileFilter filter, File ... dirs) { File[] ret = null; @@ -334,18 +318,27 @@ public class CoreModManager { findDerpMods(classLoader, modsDir, modsDirVer); - extractPackedJars(modsDir, modsDirVer); - - ModListHelper.parseModList(mcDir); + //By the time we get here, all bundeled jars should be extracted to the proper repos. + //As well as the mods folders being cleaned up {any files that have maven info being moved to maven folder} FMLLog.log.debug("Discovering coremods"); - File[] coreModList = listFiles((dir, name) -> name.endsWith(".jar"), modsDir, modsDirVer); + List maven_canidates = LibraryManager.flattenLists(mcDir); + List file_canidates = LibraryManager.gatherLegacyCanidates(mcDir); - coreModList = ObjectArrays.concat(coreModList, ModListHelper.additionalMods.values().toArray(new File[0]), File.class); + for (Artifact artifact : maven_canidates) + { + artifact = Repository.resolveAll(artifact); + if (artifact != null) + { + File target = artifact.getFile(); + if (!file_canidates.contains(target)) + file_canidates.add(target); + } + } + //Do we want to sort the full list after resolving artifacts? + //TODO: Add dependency gathering? - coreModList = FileListHelper.sortFileList(coreModList); - - for (File coreMod : coreModList) + for (File coreMod : file_canidates) { FMLLog.log.debug("Examining for coremod candidacy {}", coreMod.getName()); JarFile jar = null; @@ -353,14 +346,39 @@ public class CoreModManager { String fmlCorePlugin; try { - jar = new JarFile(coreMod); - if (jar.getManifest() == null) + File manifest = new File(coreMod.getAbsolutePath() + ".meta"); + + if (LibraryManager.DISABLE_EXTERNAL_MANIFEST || !manifest.exists()) { - // Not a coremod and no access transformer list + jar = new JarFile(coreMod); + mfAttributes = jar.getManifest() == null ? null : jar.getManifest().getMainAttributes(); + } + else + { + FileInputStream fis = new FileInputStream(manifest); + mfAttributes = new Manifest(fis).getMainAttributes(); + fis.close(); + } + + if (mfAttributes == null) // Not a coremod and no access transformer list + continue; + + String modSide = mfAttributes.getValue(LibraryManager.MODSIDE); + if (!"BOTH".equals(modSide) || FMLLaunchHandler.side().name().equals(modSide)) + { + FMLLog.log.debug("Mod {} has ModSide meta-inf value {}, and we're {} It will be ignored", coreMod.getName(), modSide, FMLLaunchHandler.side.name()); + ignoredModFiles.add(coreMod.getName()); continue; } - ModAccessTransformer.addJar(jar); - mfAttributes = jar.getManifest().getMainAttributes(); + + String ats = mfAttributes.getValue(ModAccessTransformer.FMLAT); + if (ats != null && !ats.isEmpty()) + { + if (jar == null) //We could of loaded the external manifest earlier, if so the jar isn't loaded. + jar = new JarFile(coreMod); + ModAccessTransformer.addJar(jar, ats); + } + String cascadedTweaker = mfAttributes.getValue("TweakClass"); if (cascadedTweaker != null) { @@ -379,13 +397,6 @@ public class CoreModManager { ignoredModFiles.add(coreMod.getName()); continue; } - String modSide = mfAttributes.containsKey(MODSIDE) ? mfAttributes.getValue(MODSIDE) : "BOTH"; - if (! ("BOTH".equals(modSide) || FMLLaunchHandler.side.name().equals(modSide))) - { - FMLLog.log.debug("Mod {} has ModSide meta-inf value {}, and we're {} It will be ignored", coreMod.getName(), modSide, FMLLaunchHandler.side.name()); - ignoredModFiles.add(coreMod.getName()); - continue; - } fmlCorePlugin = mfAttributes.getValue("FMLCorePlugin"); if (fmlCorePlugin == null) { @@ -428,95 +439,6 @@ public class CoreModManager { } } - private static void extractPackedJars(File modsDir, File modsDirVer) - { - for (File dir : new File[]{modsDir, modsDirVer}) - { - for (File file : listFiles((d, name) -> name.endsWith(".jar"), dir)) - { - JarFile jar = null; - Attributes mfAttributes; - try - { - jar = new JarFile(file); - if (jar.getManifest() == null) - continue; - - mfAttributes = jar.getManifest().getMainAttributes(); - String modSide = mfAttributes.containsKey(MODSIDE) ? mfAttributes.getValue(MODSIDE) : "BOTH"; - if (! ("BOTH".equals(modSide) || FMLLaunchHandler.side.name().equals(modSide))) - continue; - - extractContainedDepJars(jar, dir == modsDir ? modsDir : modsDirVer, dir == modsDir ? modsDirVer : modsDir); - } - catch (IOException ioe) - { - FMLLog.log.error("Unable to read the jar file {} - ignoring", file.getName(), ioe); - continue; - } - finally - { - closeQuietly(jar); - } - } - } - } - - private static void extractContainedDepJars(JarFile jar, File ... modsDirs) throws IOException - { - if (!jar.getManifest().getMainAttributes().containsKey(MODCONTAINSDEPS)) return; - - String deps = jar.getManifest().getMainAttributes().getValue(MODCONTAINSDEPS); - String[] depList = deps.split(" "); - for (String dep : depList) - { - String depEndName = new File(dep).getName(); // extract last part of name - if (skipContainedDeps.contains(dep) || skipContainedDeps.contains(depEndName)) - { - FMLLog.log.error("Skipping dep at request: {}", dep); - continue; - } - final JarEntry jarEntry = jar.getJarEntry(dep); - if (jarEntry == null) - { - FMLLog.log.error("Found invalid ContainsDeps declaration {} in {}", dep, jar.getName()); - continue; - } - - boolean exit = false; - for (File f : modsDirs) - { - File tmp = new File(f, depEndName); - if (tmp.exists()) - { - FMLLog.log.debug("Found existing ContainsDep extracted to {}, skipping extraction", tmp.getCanonicalPath()); - exit = true; - break; - } - } - if (exit) - continue; - - File target = new File(modsDirs[0], depEndName); - FMLLog.log.debug("Extracting ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath()); - try - { - Files.createParentDirs(target); - try ( - FileOutputStream targetOutputStream = new FileOutputStream(target); - InputStream jarInputStream = jar.getInputStream(jarEntry); - ){ - ByteStreams.copy(jarInputStream, targetOutputStream); - } - FMLLog.log.debug("Extracted ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath()); - } catch (IOException e) - { - FMLLog.log.error("An error occurred extracting dependency", e); - } - } - return; - } - private static Method ADDURL; private static void handleCascadingTweak(File coreMod, JarFile jar, String cascadedTweaker, LaunchClassLoader classLoader, Integer sortingOrder) @@ -719,51 +641,8 @@ public class CoreModManager { // Basically a copy of Collections.sort pre 8u20, optimized as we know we're an array list. // Thanks unhelpful fixer of http://bugs.java.com/view_bug.do?bug_id=8032636 ITweaker[] toSort = tweakers.toArray(new ITweaker[tweakers.size()]); - Arrays.sort(toSort, new Comparator() { - @Override - public int compare(ITweaker o1, ITweaker o2) - { - Integer first = null; - Integer second = null; - if (o1 instanceof FMLInjectionAndSortingTweaker) - { - first = Integer.MIN_VALUE; - } - if (o2 instanceof FMLInjectionAndSortingTweaker) - { - second = Integer.MIN_VALUE; - } - - if (o1 instanceof FMLPluginWrapper) - { - first = ((FMLPluginWrapper) o1).sortIndex; - } - else if (first == null) - { - first = tweakSorting.get(o1.getClass().getName()); - } - if (o2 instanceof FMLPluginWrapper) - { - second = ((FMLPluginWrapper) o2).sortIndex; - } - else if (second == null) - { - second = tweakSorting.get(o2.getClass().getName()); - } - if (first == null) - { - first = 0; - } - if (second == null) - { - second = 0; - } - - return Ints.saturatedCast((long)first - (long)second); - } - }); - // Basically a copy of Collections.sort, optimized as we know we're an array list. - // Thanks unhelpful fixer of http://bugs.java.com/view_bug.do?bug_id=8032636 + ToIntFunction getOrder = o -> o instanceof FMLInjectionAndSortingTweaker ? Integer.MIN_VALUE : o instanceof FMLPluginWrapper ? ((FMLPluginWrapper)o).sortIndex : tweakSorting.getOrDefault(o.getClass().getName(), 0); + Arrays.sort(toSort, (o1, o2) -> Ints.saturatedCast((long)getOrder.applyAsInt(o1) - (long)getOrder.applyAsInt(o2))); for (int j = 0; j < toSort.length; j++) { tweakers.set(j, toSort[j]); } diff --git a/src/main/java/net/minecraftforge/fml/relauncher/FMLLaunchHandler.java b/src/main/java/net/minecraftforge/fml/relauncher/FMLLaunchHandler.java index af5946884b..f39fb885de 100644 --- a/src/main/java/net/minecraftforge/fml/relauncher/FMLLaunchHandler.java +++ b/src/main/java/net/minecraftforge/fml/relauncher/FMLLaunchHandler.java @@ -25,6 +25,8 @@ import net.minecraft.launchwrapper.LaunchClassLoader; import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.TracingPrintStream; import net.minecraftforge.fml.common.launcher.FMLTweaker; +import net.minecraftforge.fml.relauncher.libraries.LibraryManager; + import org.apache.logging.log4j.LogManager; public class FMLLaunchHandler @@ -99,6 +101,7 @@ public class FMLLaunchHandler try { + LibraryManager.setup(minecraftHome); CoreModManager.handleLaunch(minecraftHome, classLoader, tweaker); } catch (Throwable t) @@ -129,4 +132,6 @@ public class FMLLaunchHandler { INSTANCE.injectPostfixTransformers(); } + + } diff --git a/src/main/java/net/minecraftforge/fml/relauncher/ModListHelper.java b/src/main/java/net/minecraftforge/fml/relauncher/ModListHelper.java deleted file mode 100644 index d6cabcdf90..0000000000 --- a/src/main/java/net/minecraftforge/fml/relauncher/ModListHelper.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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.relauncher; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; -import java.util.Set; -import net.minecraft.launchwrapper.Launch; -import net.minecraftforge.fml.common.FMLLog; - -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.common.io.Files; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -import javax.annotation.Nullable; - -public class ModListHelper { - public static class JsonModList { - public String repositoryRoot; - public List modRef; - public String parentList; - } - private static File mcDirectory; - private static Set visitedFiles = Sets.newHashSet(); - public static final Map additionalMods = Maps.newLinkedHashMap(); - static void parseModList(File minecraftDirectory) - { - FMLLog.log.debug("Attempting to load commandline specified mods, relative to {}", minecraftDirectory.getAbsolutePath()); - mcDirectory = minecraftDirectory; - @SuppressWarnings("unchecked") - Map args = (Map) Launch.blackboard.get("launchArgs"); - String listFile = args.get("--modListFile"); - if (listFile != null) - { - parseListFile(listFile); - } - String extraMods = args.get("--mods"); - if (extraMods != null) - { - String[] split = extraMods.split(","); - for (String modFile : split) - { - tryAddFile(modFile, null, modFile); - } - } - - String[] extras = new String[] - { - "mods/mod_list.json", - "mods/" + FMLInjectionData.mccversion + "/mod_list.json" - }; - - for (String extra : extras) - { - if ((new File(mcDirectory, extra)).exists()) - parseListFile(extra); - } - - } - private static void parseListFile(String listFile) { - File f; - try - { - if (listFile.startsWith("absolute:")) - f = new File(listFile.substring(9)).getCanonicalFile(); - else - f = new File(mcDirectory, listFile).getCanonicalFile(); - } catch (IOException e2) - { - FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Unable to canonicalize path {} relative to {}", listFile, mcDirectory.getAbsolutePath()), e2); - return; - } - if (!f.exists()) - { - FMLLog.log.info("Failed to find modList file {}", f.getAbsolutePath()); - return; - } - if (visitedFiles.contains(f)) - { - FMLLog.log.fatal("There appears to be a loop in the modListFile hierarchy. You shouldn't do this!"); - throw new RuntimeException("Loop detected, impossible to load modlistfile"); - } - String json; - try { - json = Files.asCharSource(f, StandardCharsets.UTF_8).read(); - } catch (IOException e1) { - FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to read modList json file {}.", listFile), e1); - return; - } - Gson gsonParser = new Gson(); - JsonModList modList; - try { - modList = gsonParser.fromJson(json, JsonModList.class); - } catch (JsonSyntaxException e) { - FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to parse modList json file {}.", listFile), e); - return; - } - visitedFiles.add(f); - // We visit parents before children, so the additionalMods list is sorted from parent to child - if (modList.parentList != null) - { - parseListFile(modList.parentList); - } - File repoRoot = new File(modList.repositoryRoot); - if (!repoRoot.exists()) - { - FMLLog.log.info("Failed to find the specified repository root {}", modList.repositoryRoot); - return; - } - - for (String s : modList.modRef) - { - StringBuilder fileName = new StringBuilder(); - StringBuilder genericName = new StringBuilder(); - String[] parts = s.split(":"); - fileName.append(parts[0].replace('.', File.separatorChar)); - genericName.append(parts[0]); - fileName.append(File.separatorChar); - fileName.append(parts[1]).append(File.separatorChar); - genericName.append(":").append(parts[1]); - fileName.append(parts[2]).append(File.separatorChar); - fileName.append(parts[1]).append('-').append(parts[2]); - if (parts.length == 4) - { - fileName.append('-').append(parts[3]); - genericName.append(":").append(parts[3]); - } - fileName.append(".jar"); - tryAddFile(fileName.toString(), repoRoot, genericName.toString()); - } - } - private static void tryAddFile(String modFileName, @Nullable File repoRoot, String descriptor) { - File modFile = repoRoot != null ? new File(repoRoot,modFileName) : new File(mcDirectory, modFileName); - if (!modFile.exists()) - { - FMLLog.log.info("Failed to find mod file {} ({})", descriptor, modFile.getAbsolutePath()); - } - else - { - FMLLog.log.debug("Adding {} ({}) to the mod list", descriptor, modFile.getAbsolutePath()); - additionalMods.put(descriptor, modFile); - } - } -} diff --git a/src/main/java/net/minecraftforge/fml/relauncher/libraries/Artifact.java b/src/main/java/net/minecraftforge/fml/relauncher/libraries/Artifact.java new file mode 100644 index 0000000000..80475b9697 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/relauncher/libraries/Artifact.java @@ -0,0 +1,200 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * 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.relauncher.libraries; + +import java.io.File; +import java.text.ParseException; +import java.util.Date; +import java.util.Locale; + +import org.apache.maven.artifact.versioning.ComparableVersion; + +public class Artifact implements Comparable +{ + private final Repository repo; + private final String group; + private final String artifact; + private final String classifier; + private final String extension; + private final String value; + private final ComparableVersion version; + private final String timestamp; + private final Date date; + + private String filename; + private String folder; + + public Artifact(Repository repo, String value, String timestamp) + { + this.repo = repo; + this.value = value; + + int idx = value.indexOf('@'); + if (idx > 0) + { + this.extension = value.substring(idx + 1); + value = value.substring(0, idx); + } + else + this.extension = "jar"; + + String[] parts = value.split(":"); + this.group = parts[0]; + this.artifact = parts[1]; + this.version = new ComparableVersion(parts[2]); + this.classifier = parts.length > 3 ? parts[3] : null; + this.timestamp = isSnapshot() ? timestamp : null; + try + { + this.date = this.timestamp == null ? null : SnapshotJson.TIMESTAMP.parse(this.timestamp); + } + catch (ParseException e) + { + throw new RuntimeException(e); //TODO: Better logging? + } + } + + public Artifact(Artifact other, Repository repo, String timestamp) + { + this.repo = repo; + this.group = other.group; + this.artifact = other.artifact; + this.classifier = other.classifier; + this.extension = other.extension; + this.value = other.value; + this.version = other.version; + this.timestamp = timestamp; + try + { + this.date = this.timestamp == null ? null : SnapshotJson.TIMESTAMP.parse(this.timestamp); + } + catch (ParseException e) + { + throw new RuntimeException(e); //TODO: Better logging? + } + } + + @Override + public String toString() + { + return this.value; + } + + @Override + public int hashCode() + { + return this.value.hashCode(); + } + + public String getFilename() + { + if (this.filename == null) + { + StringBuilder sb = new StringBuilder(); + sb.append(artifact).append('-').append(version); + if (isSnapshot() && timestamp != null) + sb.append('-').append(getTimestamp()); + if (classifier != null) + sb.append('-').append(classifier); + sb.append('.').append(extension); + this.filename = sb.toString(); + } + return this.filename; + } + + public String getFolder() + { + if (folder == null) + { + StringBuilder sb = new StringBuilder(); + for (String part : group.split(".")) + sb.append(part).append(File.separatorChar); + sb.append(artifact).append(File.separatorChar); + sb.append(version); + folder = sb.toString(); + } + return folder; + } + + public String getPath() + { + return getFolder() + File.separatorChar + getFilename(); + } + + public File getFile() + { + return (repo != null ? repo : LibraryManager.getDefaultRepo()).getFile(getPath()); + } + + public File getSnapshotMeta() + { + if (!isSnapshot()) + throw new IllegalStateException("Attempted to call date suffix on non-snapshot"); + return (repo != null ? repo : LibraryManager.getDefaultRepo()).getFile(getFolder() + File.separatorChar + SnapshotJson.META_JSON_FILE); + } + + public boolean isSnapshot() + { + return version.toString().toLowerCase(Locale.ENGLISH).endsWith("-snapshot"); + } + + public String getTimestamp() + { + if (!isSnapshot()) + throw new IllegalStateException("Attempted to call date suffix on non-snapshot"); + + return timestamp; + } + + public ComparableVersion getVersion() + { + return this.version; + } + + public Repository getRepository() + { + return this.repo; + } + + public boolean matchesID(Artifact o) + { + if (o == null) + return false; + return group.equals(o.group) && artifact.equals(o.artifact) && (o.classifier == null ? classifier == null : o.classifier.equals(classifier)); //TODO: Case sensitive? + } + + public int compareVersion(Artifact o) + { + int ver = version.compareTo(o.version); + if (ver != 0 || !isSnapshot()) return ver; + return timestamp == null ? (o.timestamp == null ? 0 : -1) : o.timestamp == null ? 1 : date.compareTo(o.date); + } + + @Override + public int compareTo(Artifact o) + { + if (o == null) return 1; + if (!group.equals(o.group)) return group.compareTo(o.group); + if (!artifact.equals(o.artifact)) return artifact.compareTo(o.artifact); + if (classifier == null && o.classifier != null) return -1; + if (classifier != null && o.classifier == null) return 1; + if (classifier != null && !classifier.equals(o.classifier)) return classifier.compareTo(o.classifier); + return compareVersion(o); + } +} diff --git a/src/main/java/net/minecraftforge/fml/relauncher/libraries/LibraryManager.java b/src/main/java/net/minecraftforge/fml/relauncher/libraries/LibraryManager.java new file mode 100644 index 0000000000..1359ff01ed --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/relauncher/libraries/LibraryManager.java @@ -0,0 +1,466 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * 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.relauncher.libraries; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.security.CodeSource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.maven.artifact.versioning.ArtifactVersion; + +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import net.minecraft.launchwrapper.Launch; +import net.minecraftforge.common.ForgeVersion; +import net.minecraftforge.fml.common.FMLLog; +import net.minecraftforge.fml.relauncher.FMLLaunchHandler; + +public class LibraryManager +{ + public static final boolean DISABLE_EXTERNAL_MANIFEST = Boolean.parseBoolean(System.getProperty("forge.disable_external_manifest", "false")); + public static final boolean ENABLE_AUTO_MOD_MOVEMENT = Boolean.parseBoolean(System.getProperty("forge.enable_auto_mod_movement", "false")); + private static final String LIBRARY_DIRECTORY_OVERRIDE = System.getProperty("forge.lib_folder", null); + private static final List skipContainedDeps = Arrays.asList(System.getProperty("fml.skipContainedDeps","").split(",")); //TODO: Is this used by anyone in the real world? TODO: Remove in 1.13. + private static final FilenameFilter MOD_FILENAME_FILTER = (dir, name) -> name.endsWith(".jar") || name.endsWith(".zip"); //TODO: Disable support for zip in 1.13 + private static final Comparator FILE_NAME_SORTER_INSENSITVE = (o1, o2) -> o1.getName().toLowerCase(Locale.ENGLISH).compareTo(o2.getName().toLowerCase(Locale.ENGLISH)); + + public static final Attributes.Name MODSIDE = new Attributes.Name("ModSide"); + private static final Attributes.Name MODCONTAINSDEPS = new Attributes.Name("ContainedDeps"); + private static final Attributes.Name MAVEN_ARTIFACT = new Attributes.Name("Maven-Artifact"); + private static final Attributes.Name TIMESTAMP = new Attributes.Name("Timestamp"); + private static final Attributes.Name MD5 = new Attributes.Name("MD5"); + private static Repository libraries_dir = null; + private static Set processed = new HashSet(); + + public static void setup(File minecraftHome) + { + File libDir = findLibraryFolder(minecraftHome); + FMLLog.log.debug("Determined Minecraft Libraries Root: {}", libDir); + Repository old = Repository.replace(libDir, "libraries"); + if (old != null) + FMLLog.log.debug(" Overwriting Previous: {}", old); + libraries_dir = Repository.get("libraries"); + + File mods = new File(minecraftHome, "mods"); + File mods_ver = new File(mods, ForgeVersion.mcVersion); + + ModList memory = null; + if (!ENABLE_AUTO_MOD_MOVEMENT) + { + Repository repo = new LinkRepository(new File(mods, "memory_repo")); + memory = new MemoryModList(repo); + ModList.cache.put("MEMORY", memory); + Repository.cache.put("MEMORY", repo); + } + + for (File dir : new File[]{mods, mods_ver}) + cleanDirectory(dir, ENABLE_AUTO_MOD_MOVEMENT ? ModList.create(new File(dir, "mod_list.json"), minecraftHome) : memory, mods_ver, mods); + + for (ModList list : ModList.getKnownLists(minecraftHome)) + { + Repository repo = list.getRepository() == null ? libraries_dir : list.getRepository(); + for (Artifact artifact : list.getArtifacts()) + { + Artifact resolved = repo.resolve(artifact); + if (resolved != null) + { + File target = repo.getFile(resolved.getPath()); + if (target.exists()) + extractPacked(target, list, mods_ver, mods); + } + } + } + } + + private static File findLibraryFolder(File minecraftHome) + { + if (LIBRARY_DIRECTORY_OVERRIDE != null) + { + FMLLog.log.error("System variable set to override Library Directory: {}", LIBRARY_DIRECTORY_OVERRIDE); + return new File(LIBRARY_DIRECTORY_OVERRIDE); + } + + CodeSource source = ArtifactVersion.class.getProtectionDomain().getCodeSource(); + if (source == null) + { + FMLLog.log.error("Unable to determine codesource for {}. Using default libraries directory.", ArtifactVersion.class.getName()); + return new File(minecraftHome, "libraries"); + } + + try + { + File apache = new File(source.getLocation().toURI()); + if (apache.isFile()) + apache = apache.getParentFile(); //Get to a directory, this *should* always be the case... + apache = apache.getParentFile(); //Skip the version folder. In case we ever update the version, I don't want to edit this code again. + + if (!apache.getAbsolutePath().toLowerCase(Locale.ENGLISH).replace(File.separatorChar, '/').endsWith("/org/apache/maven/maven/")) + { + FMLLog.log.error("Apache Maven library folder was not in the format expected: {}. Using default libraries directory.", new File(source.getLocation().toURI())); + return new File(minecraftHome, "libraries"); + } + // maven /maven /apache /org /libraries + return apache.getParentFile().getParentFile().getParentFile().getParentFile(); + } + catch (URISyntaxException e) + { + FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Unable to determine file for {}. Using default libraries directory.", ArtifactVersion.class.getName()), e); + } + + return new File(minecraftHome, "libraries"); //Everything else failed, return the default. + } + + private static void cleanDirectory(File dir, ModList modlist, File... modDirs) + { + if (!dir.exists()) + return; + + FMLLog.log.debug("Cleaning up mods folder: {}", dir); + for (File file : dir.listFiles(f -> f.isFile() && f.getName().endsWith(".jar"))) + { + Pair ret = extractPacked(file, modlist, modDirs); + if (ret != null) + { + Artifact artifact = ret.getLeft(); + Repository repo = modlist.getRepository() == null ? libraries_dir : modlist.getRepository(); + File moved = repo.archive(artifact, file, ret.getRight()); + modlist.add(artifact); + processed.add(moved); + } + } + + try + { + if (modlist.changed()) + modlist.save(); + } + catch (IOException e) + { + FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Error updating modlist file {}", modlist.getName()), e); + } + } + + private static Pair extractPacked(File file, ModList modlist, File... modDirs) + { + if (processed.contains(file)) + { + FMLLog.log.debug("File already proccessed {}, Skipping", file.getAbsolutePath()); + return null; + } + JarFile jar = null; + try + { + jar = new JarFile(file); + FMLLog.log.debug("Examining file: {}", file.getName()); + processed.add(file); + return extractPacked(jar, modlist, modDirs); + } + catch (IOException ioe) + { + FMLLog.log.error("Unable to read the jar file {} - ignoring", file.getName(), ioe); + } + finally + { + try { + if (jar != null) + jar.close(); + } catch (IOException e) {} + } + return null; + } + private static Pair extractPacked(JarFile jar, ModList modlist, File... modDirs) throws IOException + { + Attributes attrs; + if (jar.getManifest() == null) + return null; + + JarEntry manfest_entry = jar.getJarEntry(JarFile.MANIFEST_NAME); + if (manfest_entry == null) + manfest_entry = jar.stream().filter(e -> JarFile.MANIFEST_NAME.equals(e.getName().toUpperCase(Locale.ENGLISH))).findFirst().get(); //We know that getManifest returned non-null so we know there is *some* entry that matches the manifest file. So we dont need to empty check. + + attrs = jar.getManifest().getMainAttributes(); + + String modSide = attrs.getValue(LibraryManager.MODSIDE); + if (!"BOTH".equals(modSide) || FMLLaunchHandler.side().name().equals(modSide)) + return null; + + if (attrs.containsKey(MODCONTAINSDEPS)) + { + for (String dep : attrs.getValue(MODCONTAINSDEPS).split(" ")) + { + if (!dep.endsWith(".jar")) + { + FMLLog.log.error("Contained Dep is not a jar file: {}", dep); + throw new IllegalStateException("Invalid contained dep, Must be jar: " + dep); + } + + if (jar.getJarEntry(dep) == null && jar.getJarEntry("META-INF/libraries/" + dep) != null) + dep = "META-INF/libraries/" + dep; + + JarEntry depEntry = jar.getJarEntry(dep); + if (depEntry == null) + { + FMLLog.log.error("Contained Dep is not in the jar: {}", dep); + throw new IllegalStateException("Invalid contained dep, Missing from jar: " + dep); + } + + String depEndName = new File(dep).getName(); // extract last part of name + if (skipContainedDeps.contains(dep) || skipContainedDeps.contains(depEndName)) + { + FMLLog.log.error("Skipping dep at request: {}", dep); + continue; + } + + Attributes meta = null; + byte[] data = null; + byte[] manifest_data = null; + + JarEntry metaEntry = jar.getJarEntry(dep + ".meta"); + if (metaEntry != null) + { + manifest_data = readAll(jar.getInputStream(metaEntry)); + meta = new Manifest(new ByteArrayInputStream(manifest_data)).getMainAttributes(); + } + else + { + data = readAll(jar.getInputStream(depEntry)); + try (ZipInputStream zi = new ZipInputStream(new ByteArrayInputStream(data))) //We use zip input stream directly, as the current Oracle implementation of JarInputStream only works when the manifest is the First/Second entry in the jar... + { + ZipEntry ze = null; + while ((ze = zi.getNextEntry()) != null) + { + if (ze.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME)) + { + manifest_data = readAll(zi); + meta = new Manifest(new ByteArrayInputStream(manifest_data)).getMainAttributes(); + break; + } + } + } + } + + if (meta == null || !meta.containsKey(MAVEN_ARTIFACT)) //Ugh I really don't want to do backwards compatibility here, I want to force modders to provide information... TODO: Remove in 1.13? + { + boolean found = false; + for (File dir : modDirs) + { + File target = new File(dir, depEndName); + if (target.exists()) + { + FMLLog.log.debug("Found existing ContainDep extracted to {}, skipping extraction", target.getCanonicalPath()); + found = true; + } + } + if (!found) + { + File target = new File(modDirs[0], depEndName); + FMLLog.log.debug("Extracting ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath()); + try + { + Files.createParentDirs(target); + try + ( + FileOutputStream out = new FileOutputStream(target); + InputStream in = data == null ? jar.getInputStream(depEntry) : new ByteArrayInputStream(data) + ) + { + ByteStreams.copy(in, out); + } + FMLLog.log.debug("Extracted ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath()); + extractPacked(target, modlist, modDirs); + } + catch (IOException e) + { + FMLLog.log.error("An error occurred extracting dependency", e); + } + } + } + else + { + try + { + String timestamp = meta.getValue(TIMESTAMP); + if (timestamp != null) + timestamp = SnapshotJson.TIMESTAMP.format(new Date(Integer.parseInt(timestamp))); + + Artifact artifact = new Artifact(modlist.getRepository(), meta.getValue(MAVEN_ARTIFACT), timestamp); + File target = artifact.getFile(); + if (target.exists()) + FMLLog.log.debug("Found existing ContainedDep {}({}) from {} extracted to {}, skipping extraction", dep, artifact.toString(), target.getCanonicalPath(), jar.getName()); + else + { + FMLLog.log.debug("Extracting ContainedDep {}({}) from {} to {}", dep, artifact.toString(), jar.getName(), target.getCanonicalPath()); + Files.createParentDirs(target); + try + ( + FileOutputStream out = new FileOutputStream(target); + InputStream in = data == null ? jar.getInputStream(depEntry) : new ByteArrayInputStream(data) + ) + { + ByteStreams.copy(in, out); + } + FMLLog.log.debug("Extracted ContainedDep {}({}) from {} to {}", dep, artifact.toString(), jar.getName(), target.getCanonicalPath()); + + if (artifact.isSnapshot()) + { + SnapshotJson json = SnapshotJson.create(artifact.getSnapshotMeta()); + json.add(new SnapshotJson.Entry(artifact.getTimestamp(), meta.getValue(MD5))); + json.write(artifact.getSnapshotMeta()); + } + + if (!DISABLE_EXTERNAL_MANIFEST) + { + File meta_target = new File(target.getAbsolutePath() + ".meta"); + Files.write(manifest_data, meta_target); + } + extractPacked(target, modlist, modDirs); + } + } + catch (NumberFormatException nfe) + { + FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("An error occurred extracting dependency. Invalid Timestamp: {}", meta.getValue(TIMESTAMP)), nfe); + } + catch (IOException e) + { + FMLLog.log.error("An error occurred extracting dependency", e); + } + } + } + } + + return attrs.containsKey(MAVEN_ARTIFACT) ? Pair.of(new Artifact(modlist.getRepository(), attrs.getValue(MAVEN_ARTIFACT), attrs.getValue(TIMESTAMP)), readAll(jar.getInputStream(manfest_entry))) : null; + } + + private static byte[] readAll(InputStream in) throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int read = -1; + byte[] data = new byte[1024 * 16]; + + while ((read = in.read(data, 0, data.length)) != -1) + out.write(data, 0, read); + + out.flush(); + return out.toByteArray(); + } + + public static List flattenLists(File mcDir) + { + List merged = new ArrayList<>(); + for (ModList list : ModList.getBasicLists(mcDir)) + { + for (Artifact art : list.flatten()) + { + Optional old = merged.stream().filter(art::matchesID).findFirst(); + if (!old.isPresent()) + { + merged.add(art); + } + else if (old.get().getVersion().compareTo(art.getVersion()) < 0) + { + merged.add(merged.indexOf(old.get()), art); + merged.remove(old.get()); + } + } + } + return merged; + } + + public static List gatherLegacyCanidates(File mcDir) + { + List list = new ArrayList<>(); + + @SuppressWarnings("unchecked") + Map args = (Map)Launch.blackboard.get("launchArgs"); + String extraMods = args.get("--mods"); + if (extraMods != null) + { + FMLLog.log.info("Found mods from the command line:"); + for (String mod : extraMods.split(",")) + { + File file = new File(mcDir, mod); + if (!file.exists()) + { + FMLLog.log.info(" Failed to find mod file {} ({})", mod, file.getAbsolutePath()); + } + else if (!list.contains(file)) + { + FMLLog.log.debug(" Adding {} ({}) to the mod list", mod, file.getAbsolutePath()); + list.add(file); + } + else if (!list.contains(file)) + { + FMLLog.log.debug(" Duplicte command line mod detected {} ({})", mod, file.getAbsolutePath()); + } + } + } + + for (String dir : new String[]{"mods", "mods" + File.separatorChar + ForgeVersion.mcVersion}) + { + File base = new File(mcDir, dir); + if (!base.isDirectory() || !base.exists()) + continue; + + FMLLog.log.info("Searching {} for mods", base.getAbsolutePath()); + for (File f : base.listFiles(MOD_FILENAME_FILTER)) + { + if (!list.contains(f)) + { + FMLLog.log.debug(" Adding {} to the mod list", f.getName()); + list.add(f); + } + } + } + + ModList memory = ModList.cache.get("MEMORY"); + if (!ENABLE_AUTO_MOD_MOVEMENT && memory != null && memory.getRepository() != null) + memory.getRepository().filterLegacy(list); + + list.sort(FILE_NAME_SORTER_INSENSITVE); + return list; + } + + public static Repository getDefaultRepo() + { + return libraries_dir; + } +} diff --git a/src/main/java/net/minecraftforge/fml/relauncher/libraries/LinkRepository.java b/src/main/java/net/minecraftforge/fml/relauncher/libraries/LinkRepository.java new file mode 100644 index 0000000000..13718dc592 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/relauncher/libraries/LinkRepository.java @@ -0,0 +1,102 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * 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.relauncher.libraries; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.minecraftforge.fml.common.FMLLog; + +public class LinkRepository extends Repository +{ + private Map artifact_to_file = new HashMap<>(); + private Map filesystem = new HashMap<>(); + private Map snapshots = new HashMap<>(); + private Set known = new HashSet<>(); + + LinkRepository(File root) + { + super(root, "MEMORY"); + } + + public File archive(Artifact artifact, File file, byte[] manifest) + { + String key = artifact.toString(); + known.add(file); + if (artifact_to_file.containsKey(key)) + { + FMLLog.log.debug("Maven file already exists for {}({}) at {}, ignoring duplicate.", file.getName(), artifact.toString(), artifact_to_file.get(key).getAbsolutePath()); + + if (artifact.isSnapshot()) + { + Artifact old = snapshots.get(key); + if (old == null || old.compareVersion(artifact) < 0) + { + FMLLog.log.debug("Overriding Snapshot {} -> {}", old == null ? "null" : old.getTimestamp(), artifact.getTimestamp()); + snapshots.put(key, artifact); + artifact_to_file.put(key, file); + filesystem.put(artifact.getPath(), file); + } + } + } + else + { + FMLLog.log.debug("Making maven link for {} in memory to {}.", key, file.getAbsolutePath()); + artifact_to_file.put(key, file); + filesystem.put(artifact.getPath(), file); + + if (artifact.isSnapshot()) + snapshots.put(key, artifact); + + /* Support external meta? screw it. + if (!LibraryManager.DISABLE_EXTERNAL_MANIFEST) + { + File meta_target = new File(target.getAbsolutePath() + ".meta"); + Files.write(manifest, meta_target); + } + */ + } + return file; + } + + @Override + public void filterLegacy(List list) + { + list.removeIf(e -> known.contains(e)); + } + + public Artifact resolve(Artifact artifact) + { + String key = artifact.toString(); + File file = artifact_to_file.get(key); + if (file == null || !file.exists()) + return null; + return new Artifact(artifact, this, artifact.isSnapshot() ? artifact.getTimestamp() : null); + } + + @Override + public File getFile(String path) + { + return filesystem.containsKey(path) ? super.getFile(path) : filesystem.get(path); + } +} diff --git a/src/main/java/net/minecraftforge/fml/relauncher/libraries/MemoryModList.java b/src/main/java/net/minecraftforge/fml/relauncher/libraries/MemoryModList.java new file mode 100644 index 0000000000..033f41f8f6 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/relauncher/libraries/MemoryModList.java @@ -0,0 +1,38 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * 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.relauncher.libraries; + +import java.io.IOException; + +public class MemoryModList extends ModList +{ + MemoryModList(Repository repo) + { + super(repo); + } + + @Override + public void save() throws IOException {} + + @Override + public String getName() + { + return "MEMORY"; + } +} diff --git a/src/main/java/net/minecraftforge/fml/relauncher/libraries/ModList.java b/src/main/java/net/minecraftforge/fml/relauncher/libraries/ModList.java new file mode 100644 index 0000000000..e463dc4ac2 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/relauncher/libraries/ModList.java @@ -0,0 +1,243 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * 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.relauncher.libraries; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +import net.minecraft.launchwrapper.Launch; +import net.minecraftforge.common.ForgeVersion; +import net.minecraftforge.fml.common.FMLLog; + +public class ModList +{ + private static final Gson GSON = new GsonBuilder().setLenient().create(); + static final Map cache = new HashMap<>(); + + public static ModList create(File json, File mcdir) + { + try + { + String key = json.getCanonicalFile().getAbsolutePath(); + if (cache.containsKey(key)) + return cache.get(key); + } + catch (IOException e) + { + FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Unable to load ModList json at {}.", json.getAbsoluteFile()), e); + } + + return new ModList(json, mcdir); + } + + @SuppressWarnings("unchecked") + public static List getKnownLists(File mcdir) + { + for (String list : new String[] {"mods/mod_list.json", "mods/" + ForgeVersion.mcVersion + "/mod_list.json", ((Map)Launch.blackboard.get("launchArgs")).get("--modListFile")}) + { + if (list != null) + { + File listFile = getFile(mcdir, list); + if (listFile != null && listFile.exists()) + create(listFile, mcdir); //Create will recursively create parent lists, so after this run everything should be populated. + } + } + return ImmutableList.copyOf(cache.values()); //TODO: Do we care about order? I dont think so as we resolve all libs in a flat map. + } + + @SuppressWarnings("unchecked") + public static List getBasicLists(File mcdir) + { + List lst = new ArrayList<>(); + + ModList memory = cache.get("MEMORY"); + if (memory != null) + lst.add(memory); + + for (String list : new String[] {"mods/mod_list.json", "mods/" + ForgeVersion.mcVersion + "/mod_list.json", ((Map)Launch.blackboard.get("launchArgs")).get("--modListFile")}) + { + if (list != null) + { + File listFile = getFile(mcdir, list); + if (listFile != null && listFile.exists()) + lst.add(create(listFile, mcdir)); + } + } + return lst; + } + + private final File path; + private final JsonModList mod_list; + private final Repository repo; + private final ModList parent; + private final List artifacts = new ArrayList<>(); + private final List artifacts_imm = Collections.unmodifiableList(artifacts); + private final Map art_map = new HashMap<>(); + + private boolean changed = false; + + protected ModList(Repository repo) + { + this.path = null; + this.mod_list = new JsonModList(); + this.repo = repo; + this.parent = null; + } + + private ModList(File path, File mcdir) + { + this.path = path; + JsonModList temp_list = null; + if (this.path.exists()) + { + try + { + String json = Files.asCharSource(path, StandardCharsets.UTF_8).read(); + temp_list = GSON.fromJson(json, JsonModList.class); + } + catch (JsonSyntaxException jse) + { + FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to parse modList json file {}.", path), jse); + } + catch (IOException ioe) + { + FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to read modList json file {}.", path), ioe); + } + } + this.mod_list = temp_list == null ? new JsonModList() : temp_list; + Repository temp = null; + if (mod_list.repositoryRoot != null) + { + try + { + temp = Repository.create(getFile(mcdir, this.mod_list.repositoryRoot)); + } + catch (IOException e) + { + FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to create repository for modlist at {}.", mod_list.repositoryRoot), e); + } + } + this.repo = temp; + File parent_path = this.mod_list.parentList == null ? null : getFile(mcdir, this.mod_list.parentList); + this.parent = parent_path == null || !parent_path.exists() ? null : ModList.create(parent_path, mcdir); + + if (mod_list.modRef != null) + { + for (String ref : mod_list.modRef) + add(new Artifact(this.getRepository(), ref, null)); + changed = false; + } + } + + public Repository getRepository() + { + return this.repo; + } + + public void add(Artifact artifact) + { + Artifact old = art_map.get(artifact.toString()); + if (old != null) + { + artifacts.add(artifacts.indexOf(old), artifact); + artifacts.remove(old); + } + else + artifacts.add(artifact); + changed = true; + } + + public List getArtifacts() + { + return artifacts_imm; + } + + public boolean changed() + { + return changed; + } + + public void save() throws IOException + { + mod_list.modRef = artifacts.stream().map(a -> a.toString()).collect(Collectors.toList()); + Files.write(GSON.toJson(mod_list), path, StandardCharsets.UTF_8); + } + + private static File getFile(File root, String path) + { + try + { + if (path.startsWith("absolute:")) + return new File(path.substring(9)).getCanonicalFile(); + else + return new File(root, path).getCanonicalFile(); + } + catch (IOException ioe) + { + FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Unable to canonicalize path {} relative to {}", path, root.getAbsolutePath()), ioe); + } + return null; + } + + private static class JsonModList + { + public String repositoryRoot; + public List modRef; + public String parentList; + } + + //TODO: Some form of caching? + public List flatten() + { + List lst = parent == null ? new ArrayList<>() : parent.flatten(); + for (Artifact art : artifacts) + { + Optional old = lst.stream().filter(art::matchesID).findFirst(); + if (!old.isPresent()) + { + lst.add(art); + } + else if (old.get().getVersion().compareTo(art.getVersion()) < 0) + { + lst.add(lst.indexOf(old.get()), art); + lst.remove(old.get()); + } + } + return lst; + } + + public Object getName() + { + return path.getAbsolutePath(); + } +} diff --git a/src/main/java/net/minecraftforge/fml/relauncher/libraries/Repository.java b/src/main/java/net/minecraftforge/fml/relauncher/libraries/Repository.java new file mode 100644 index 0000000000..87078606fd --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/relauncher/libraries/Repository.java @@ -0,0 +1,162 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * 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.relauncher.libraries; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.hash.Hashing; +import com.google.common.io.Files; + +import net.minecraftforge.fml.common.FMLLog; + +public class Repository +{ + static final Map cache = new LinkedHashMap<>(); + + public static Repository create(File root) throws IOException + { + return create(root, root.getCanonicalPath()); + } + public static Repository create(File root, String name) + { + return cache.computeIfAbsent(name, f -> new Repository(root, name)); + } + public static Repository replace(File root, String name) + { + return cache.put(name, new Repository(root, name)); + } + public static Repository get(String name) + { + return cache.get(name); + } + public static Artifact resolveAll(Artifact artifact) + { + Artifact ret = null; + for (Repository repo : cache.values()) + { + Artifact tmp = repo.resolve(artifact); + if (tmp == null) + continue; + if (!artifact.isSnapshot()) + return tmp; //If its a concrete version *assume* any resolved one in any repo will work. As they shouldn't release overriding versions. + ret = ret == null || ret.compareTo(tmp) < 0 ? tmp : ret; + } + return ret; + } + + + private final String name; + private final File root; + + protected Repository(File root) throws IOException + { + this(root, root.getCanonicalPath()); + } + protected Repository(File root, String name) + { + this.root = root; + this.name = name; + if (name == null) + throw new IllegalArgumentException("Invalid Repository Name, for " + root); + } + + @Override + public int hashCode() + { + return this.name.hashCode(); + } + + @Override + public boolean equals(Object o) + { + return o instanceof Repository && ((Repository)o).name.equals(name); + } + + public Artifact resolve(Artifact artifact) + { + if (!artifact.isSnapshot()) + return getFile(artifact.getPath()).exists() ? artifact : null; + + File meta = getFile(artifact.getFolder() + File.separatorChar + SnapshotJson.META_JSON_FILE); + if (!meta.exists()) + return null; + + SnapshotJson json = SnapshotJson.create(getFile(artifact.getFolder() + File.separatorChar + SnapshotJson.META_JSON_FILE)); + if (json.getLatest() == null) + return null; + + Artifact ret = new Artifact(artifact, this, json.getLatest()); + while (json.getLatest() != null && !getFile(ret.getPath()).exists()) + { + if (!json.remove(json.getLatest())) + throw new IllegalStateException("Something went wrong, Latest (" + json.getLatest() + ") did not point to an entry in the json list: " + meta.getAbsolutePath()); + ret = new Artifact(artifact, this, json.getLatest()); + } + + return getFile(ret.getPath()).exists() ? ret : null; + } + + public File getFile(String path) + { + return new File(root, path); + } + + public File archive(Artifact artifact, File file, byte[] manifest) + { + File target = artifact.getFile(); + try + { + if (target.exists()) + { + FMLLog.log.debug("Maven file already exists for {}({}) at {}, deleting duplicate.", file.getName(), artifact.toString(), target.getAbsolutePath()); + file.delete(); + } + else + { + FMLLog.log.debug("Moving file {}({}) to maven repo at {}.", file.getName(), artifact.toString(), target.getAbsolutePath()); + Files.move(file, target); + + if (artifact.isSnapshot()) + { + SnapshotJson json = SnapshotJson.create(artifact.getSnapshotMeta()); + json.add(new SnapshotJson.Entry(artifact.getTimestamp(), Files.hash(target, Hashing.md5()).toString())); + json.write(artifact.getSnapshotMeta()); + } + + if (!LibraryManager.DISABLE_EXTERNAL_MANIFEST) + { + File meta_target = new File(target.getAbsolutePath() + ".meta"); + Files.write(manifest, meta_target); + } + } + return target; + } + catch (IOException e) + { + FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Error moving file {} to {}", file, target.getAbsolutePath()), e); + } + return file; + } + + public void filterLegacy(List list){} +} diff --git a/src/main/java/net/minecraftforge/fml/relauncher/libraries/SnapshotJson.java b/src/main/java/net/minecraftforge/fml/relauncher/libraries/SnapshotJson.java new file mode 100644 index 0000000000..b9f6beeb93 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/relauncher/libraries/SnapshotJson.java @@ -0,0 +1,163 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * 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.relauncher.libraries; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import com.google.common.io.Files; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +import net.minecraftforge.fml.common.FMLLog; +/** + * This is different from the standard maven snapshot metadata. + * Because none of that data is exposed to us as a user of gradle/maven/whatever. + * So we JUST use the timestamp. + * + * { + * "latest": "yyyymmdd.hhmmss", + * "versions": [ + * { + * "md5": "md5 in hex lowercase", + * "timestamp": "yyyymmdd.hhmmss" + * ] + * } + * + */ +public class SnapshotJson implements Comparable +{ + public static final DateFormat TIMESTAMP = new SimpleDateFormat("yyyymmdd.hhmmss"); + public static final String META_JSON_FILE = "maven-metadata.json"; + private static final Gson GSON = new GsonBuilder().create(); + private static final Comparator SORTER = (o1, o2) -> o2.timestamp.compareTo(o2.timestamp); + + public static SnapshotJson create(File target) + { + if (!target.exists()) + return new SnapshotJson(); + + try + { + String json = Files.asCharSource(target, StandardCharsets.UTF_8).read(); + SnapshotJson obj = GSON.fromJson(json, SnapshotJson.class); + obj.updateLatest(); + return obj; + } + catch (JsonSyntaxException jse) + { + FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to parse snapshot json file {}.", target), jse); + } + catch (IOException ioe) + { + FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to read snapshot json file {}.", target), ioe); + } + + return new SnapshotJson(); + } + + private String latest; + private List versions; + + public String getLatest() + { + return this.latest; + } + + public void add(Entry data) + { + if (versions == null) + versions = new ArrayList<>(); + versions.add(data); + updateLatest(); + } + + public void merge(SnapshotJson o) + { + if (o.versions != null) + { + if (versions == null) + versions = new ArrayList<>(o.versions); + else + o.versions.stream().filter(e -> versions.stream().anyMatch(e2 -> e.timestamp.equals(e2.timestamp))).forEach(e -> versions.add(e)); + updateLatest(); + } + } + + public boolean remove(String timestamp) + { + if (versions == null) + return false; + if (versions.removeIf(e -> e.timestamp.equals(timestamp))) + updateLatest(); + return false; + } + + public String updateLatest() + { + if (versions == null) + { + latest = null; + return null; + } + Collections.sort(versions, SORTER); + return latest = versions.isEmpty() ? null : versions.get(0).timestamp; + } + + public void write(File target) throws IOException + { + Files.write(GSON.toJson(this), target, StandardCharsets.UTF_8); + } + + @Override + public int compareTo(SnapshotJson o) + { + return o == null ? 1 : o.latest == null ? latest == null ? 0 : 1 : latest == null ? -1 : o.latest.compareTo(latest); + } + + public static class Entry + { + private String timestamp; + private String md5; + + public Entry(String timestamp, String md5) + { + this.timestamp = timestamp; + this.md5 = md5; + } + + public String getTimestamp() + { + return this.timestamp; + } + + public String getMd5() + { + return this.md5; + } + } +}