[1.12] Add support for client & server dependencies for mods (#4403)

This commit is contained in:
mezz authored and GitHub committed 2017-10-02 22:13:30 -07:00
1 parent cf39ff18e1
commit f494117453
7 files changed
+577 -108

No files matched your search

@@ -51,6 +51,7 @@ import net.minecraftforge.fml.common.event.FMLFingerprintViolationEvent;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import net.minecraftforge.fml.common.versioning.ArtifactVersion;
import net.minecraftforge.fml.common.versioning.DefaultArtifactVersion;
import net.minecraftforge.fml.common.versioning.DependencyParser;
import net.minecraftforge.fml.common.versioning.VersionParser;
import net.minecraftforge.fml.common.versioning.VersionRange;
import net.minecraftforge.fml.relauncher.Side;
@@ -67,14 +68,11 @@ import java.util.function.Function;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
@@ -140,19 +138,18 @@ public class FMLModContainer implements ModContainer
String modid = (String)this.descriptor.get("modid");
if (Strings.isNullOrEmpty(modid))
{
throw new IllegalArgumentException("Modid cannot be null or empty");
throw new IllegalArgumentException("The modId is null or empty");
}
if (modid.length() > 64)
{
FMLLog.bigWarning("The modid {} is longer than the recommended maximum of 64 characters. Truncation is enforced in 1.11", modid);
throw new IllegalArgumentException(String.format("The modid %s is longer than the recommended maximum of 64 characters. Truncation is enforced in 1.11", modid));
throw new IllegalArgumentException(String.format("The modId %s is longer than the maximum of 64 characters.", modid));
}
if (!modid.equals(modid.toLowerCase(Locale.ENGLISH)))
{
FMLLog.bigWarning("The modid {} is not the same as it's lowercase version. Lowercasing is enforced in 1.11", modid);
throw new IllegalArgumentException(String.format("The modid %s is not the same as it's lowercase version. Lowercasing will be enforced in 1.11", modid));
throw new IllegalArgumentException(String.format("The modId %s must be all lowercase.", modid));
}
}
private ILanguageAdapter getLanguageAdapter()
{
if (languageAdapter == null)
@@ -212,17 +209,15 @@ public class FMLModContainer implements ModContainer
if (overridesMetadata || !modMetadata.useDependencyInformation)
{
Set<ArtifactVersion> requirements = Sets.newHashSet();
List<ArtifactVersion> dependencies = Lists.newArrayList();
List<ArtifactVersion> dependants = Lists.newArrayList();
annotationDependencies = (String)descriptor.get("dependencies");
Loader.instance().computeDependencies(annotationDependencies, requirements, dependencies, dependants);
dependants.addAll(Loader.instance().getInjectedBefore(getModId()));
dependencies.addAll(Loader.instance().getInjectedAfter(getModId()));
modMetadata.requiredMods = requirements;
modMetadata.dependencies = dependencies;
modMetadata.dependants = dependants;
modLog.trace("Parsed dependency info : {} {} {}", requirements, dependencies, dependants);
DependencyParser dependencyParser = new DependencyParser(getModId(), FMLCommonHandler.instance().getSide());
DependencyParser.DependencyInfo info = dependencyParser.parseDependencies(annotationDependencies);
info.dependants.addAll(Loader.instance().getInjectedBefore(getModId()));
info.dependencies.addAll(Loader.instance().getInjectedAfter(getModId()));
modMetadata.requiredMods = info.requirements;
modMetadata.dependencies = info.dependencies;
modMetadata.dependants = info.dependants;
modLog.trace("Parsed dependency info : Requirements: {} After:{} Before:{}", info.requirements, info.dependencies, info.dependants);
}
else
{
@@ -26,7 +26,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -35,6 +34,7 @@ import java.util.Properties;
import java.util.Set;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.ForgeVersion;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.config.ConfigManager;
import net.minecraftforge.common.crafting.CraftingHelper;
@@ -52,6 +52,7 @@ import net.minecraftforge.fml.common.toposort.ModSortingException;
import net.minecraftforge.fml.common.toposort.TopologicalSort;
import net.minecraftforge.fml.common.toposort.ModSortingException.SortingExceptionData;
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.Side;
@@ -62,7 +63,6 @@ import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.Level;
import com.google.common.base.CharMatcher;
import java.util.function.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
@@ -124,9 +124,7 @@ import javax.annotation.Nullable;
@SuppressWarnings("unused")
public class Loader
{
public static final String MC_VERSION = net.minecraftforge.common.ForgeVersion.mcVersion;
private static final Splitter DEPENDENCYPARTSPLITTER = Splitter.on(":").omitEmptyStrings().trimResults();
private static final Splitter DEPENDENCYSPLITTER = Splitter.on(";").omitEmptyStrings().trimResults();
public static final String MC_VERSION = ForgeVersion.mcVersion;
/**
* The singleton instance
*/
@@ -695,77 +693,17 @@ public class Loader
return modClassLoader;
}
/**
* @deprecated use {@link DependencyParser#parseDependencies(String)}
*/
@Deprecated // TODO: remove in 1.13
public void computeDependencies(String dependencyString, Set<ArtifactVersion> requirements, List<ArtifactVersion> dependencies, List<ArtifactVersion> dependants)
{
if (dependencyString == null || dependencyString.length() == 0)
{
return;
}
boolean parseFailure = false;
for (String dep : DEPENDENCYSPLITTER.split(dependencyString))
{
List<String> depparts = Lists.newArrayList(DEPENDENCYPARTSPLITTER.split(dep));
// Need two parts to the string
if (depparts.size() != 2)
{
parseFailure = true;
continue;
}
String instruction = depparts.get(0);
String target = depparts.get(1);
boolean targetIsAll = target.startsWith("*");
// Cannot have an "all" relationship with anything except pure *
if (targetIsAll && target.length() > 1)
{
parseFailure = true;
continue;
}
// If this is a required element, add it to the required list
if ("required-before".equals(instruction) || "required-after".equals(instruction))
{
// You can't require everything
if (!targetIsAll)
{
requirements.add(VersionParser.parseVersionReference(target));
}
else
{
parseFailure = true;
continue;
}
}
// You cannot have a versioned dependency on everything
if (targetIsAll && target.indexOf('@') > -1)
{
parseFailure = true;
continue;
}
// before elements are things we are loaded before (so they are our dependants)
if ("required-before".equals(instruction) || "before".equals(instruction))
{
dependants.add(VersionParser.parseVersionReference(target));
}
// after elements are things that load before we do (so they are out dependencies)
else if ("required-after".equals(instruction) || "after".equals(instruction))
{
dependencies.add(VersionParser.parseVersionReference(target));
}
else
{
parseFailure = true;
}
}
if (parseFailure)
{
FMLLog.log.warn("Unable to parse dependency string {}", dependencyString);
throw new LoaderException(String.format("Unable to parse dependency string %s", dependencyString));
}
DependencyParser dependencyParser = new DependencyParser("unknown", FMLCommonHandler.instance().getSide());
DependencyParser.DependencyInfo info = dependencyParser.parseDependencies(dependencyString);
requirements.addAll(info.requirements);
dependencies.addAll(info.dependencies);
dependants.addAll(info.dependants);
}
public Map<String,ModContainer> getIndexedModList()
@@ -101,24 +101,37 @@ public @interface Mod
/**
* A dependency string for this mod, which specifies which mod(s) it depends on in order to run.
*
* A dependency string can start with the following four prefixes:
* before, after, required-before, required-after
* A dependency string must start with a combination of these prefixes, separated by "-":
* [before, after], [required], [client, server]
* At least one "before", "after", or "required" must be specified.
* Then ":" and the mod id.
*
* Optionally, a version range can be specified for the mod by adding "@" and then the version range.
* The version range format is described in the javadoc here: {@link VersionRange#createFromVersionSpec(java.lang.String)}
* Then a version range should be specified for the mod by adding "@" and the version range.
* The version range format is described in the javadoc here:
* {@link VersionRange#createFromVersionSpec(java.lang.String)}
* Then a ";".
*
* If a "required" mod is missing, or a mod exists with a version outside the specified range,
* the game will not start and an error screen will tell the player which versions are required.
*
* Example:
* Our example mod is a dedicated addon to mod1 and has optional integration with mod2.
* It uses new features that were introduced in forge version 14.21.1.2395 and mod2 version 4.7.0
* Our example mod:
* * depends on Forge and uses new features that were introduced in Forge version 14.21.1.2395
* "required:forge@[14.21.1.2395,);"
* * is a dedicated addon to mod1 and has to have its event handlers run after mod1's are run,
* "required-after:mod1;"
* * has optional integration with mod2 which depends on features introduced in mod2 version 4.7.0,
* "after:mod2@[4.7.0,);"
* * depends on a client-side-only rendering library called rendermod
* "required-client:rendermod;"
*
* the dependencies string = "after:forge@[14.21.1.2395,);required-after:mod1;after:mod2@[4.7.0,);"
* The full dependencies string is all of those combined:
* "required:forge@[14.21.1.2395,);required-after:mod1;after:mod2@[4.7.0,);required-client:rendermod;"
*
* This will stop the game and display an error message if any of these is true:
* The installed forge is too old, mod1 is missing, or an old version of mod2 is present.
* The installed forge is too old,
* mod1 is missing,
* an old version of mod2 is present,
* rendermod is missing on the client.
*/
String dependencies() default "";
@@ -0,0 +1,221 @@
/*
* Minecraft Forge
* Copyright (c) 2016.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.common.versioning;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import net.minecraftforge.fml.common.LoaderException;
import net.minecraftforge.fml.relauncher.Side;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public final class DependencyParser
{
public static class DependencyInfo
{
public final Set<ArtifactVersion> requirements = new HashSet<>();
public final List<ArtifactVersion> dependencies = new ArrayList<>();
public final List<ArtifactVersion> dependants = new ArrayList<>();
}
private static final Logger LOGGER = LogManager.getLogger("FML");
private static final ImmutableList<String> DEPENDENCY_INSTRUCTIONS = ImmutableList.of("client", "server", "required", "before", "after");
private static final Splitter DEPENDENCY_INSTRUCTIONS_SPLITTER = Splitter.on("-").omitEmptyStrings().trimResults();
private static final Splitter DEPENDENCY_PART_SPLITTER = Splitter.on(":").omitEmptyStrings().trimResults();
private static final Splitter DEPENDENCY_SPLITTER = Splitter.on(";").omitEmptyStrings().trimResults();
private final String modId;
private final Side side;
public DependencyParser(String modId, Side side)
{
this.modId = modId;
this.side = side;
}
public DependencyInfo parseDependencies(String dependencyString)
{
DependencyInfo info = new DependencyInfo();
if (dependencyString == null || dependencyString.length() == 0)
{
return info;
}
for (String dep : DEPENDENCY_SPLITTER.split(dependencyString))
{
final List<String> depParts = DEPENDENCY_PART_SPLITTER.splitToList(dep);
if (depParts.size() != 2)
{
throw new DependencyParserException(modId, dep, "Dependency string needs 2 parts.");
}
final List<String> instructions = DEPENDENCY_INSTRUCTIONS_SPLITTER.splitToList(depParts.get(0));
final String target = depParts.get(1);
parseDependency(dep, instructions, target, info);
}
return info;
}
private void parseDependency(String dep, List<String> instructions, String target, DependencyInfo info)
{
final boolean targetIsAll = target.startsWith("*");
final boolean targetIsBounded = target.contains("@");
if (targetIsAll)
{
if (target.length() > 1)
{
throw new DependencyParserException(modId, dep, "Cannot have an \"all\" (*) relationship with anything except pure *");
}
else if (targetIsBounded)
{
throw new DependencyParserException(modId, dep, "You cannot have a versioned dependency on everything (*)");
}
}
Side depSide = null;
String depOrder = null;
boolean depRequired = false;
for (String instruction : instructions)
{
if ("client".equals(instruction))
{
if (depSide != null)
{
throw new DependencyParserException(modId, dep, "Up to one side (client or server) can be specified.");
}
depSide = Side.CLIENT;
}
else if ("server".equals(instruction))
{
if (depSide != null)
{
throw new DependencyParserException(modId, dep, "Up to one side (client or server) can be specified.");
}
depSide = Side.SERVER;
}
else if ("required".equals(instruction))
{
if (depRequired)
{
throw new DependencyParserException(modId, dep, "'required' can only be specified once.");
}
if (targetIsAll)
{
throw new DependencyParserException(modId, dep, "You can't 'require' everything (*)");
}
depRequired = true;
}
else if ("before".equals(instruction) || "after".equals(instruction))
{
if (depOrder != null)
{
throw new DependencyParserException(modId, dep, "'before' or 'after' can only be specified once.");
}
depOrder = instruction;
}
else
{
throw new DependencyParserException(modId, dep, String.format("Found invalid instruction '%s'. Only %s are allowed.", instruction, DEPENDENCY_INSTRUCTIONS));
}
}
ArtifactVersion artifactVersion;
try
{
artifactVersion = VersionParser.parseVersionReference(target);
}
catch (RuntimeException e)
{
throw new DependencyParserException(modId, dep, "Could not parse version string.", e);
}
if (!targetIsAll)
{
String depModId = artifactVersion.getLabel();
sanityCheckModId(modId, dep, depModId);
}
if (!depRequired && depOrder == null)
{
throw new DependencyParserException(modId, dep, "'required', 'client', or 'server' must be specified.");
}
if (depSide == null || depSide == this.side)
{
if (depRequired)
{
info.requirements.add(artifactVersion);
}
if ("before".equals(depOrder))
{
info.dependants.add(artifactVersion);
}
else if ("after".equals(depOrder))
{
info.dependencies.add(artifactVersion);
}
}
}
// TODO 1.13: throw these exceptions instead of logging them
/** Based on {@link net.minecraftforge.fml.common.FMLModContainer#sanityCheckModId()} */
private static void sanityCheckModId(String modId, String dep, String depModId)
{
if (Strings.isNullOrEmpty(depModId))
{
LOGGER.error(new DependencyParserException(modId, dep, "The modId is null or empty").getMessage());
}
else if (depModId.length() > 64)
{
LOGGER.error(new DependencyParserException(modId, dep, String.format("The modId '%s' is longer than the maximum of 64 characters.", depModId)).getMessage());
}
else if (!depModId.equals(depModId.toLowerCase(Locale.ENGLISH)))
{
LOGGER.error(new DependencyParserException(modId, dep, String.format("The modId '%s' must be all lowercase.", depModId)).getMessage());
}
}
private static class DependencyParserException extends LoaderException
{
public DependencyParserException(String modId, String dependencyString, String explanation)
{
super(formatMessage(modId, dependencyString, explanation));
}
public DependencyParserException(String modId, String dependencyString, String explanation, Throwable cause)
{
super(formatMessage(modId, dependencyString, explanation), cause);
}
public static String formatMessage(String modId, String dependencyString, String explanation)
{
return String.format("Unable to parse dependency for mod '%s' with dependency string '%s'. %s", modId, dependencyString, explanation);
}
}
}
@@ -21,11 +21,8 @@ package net.minecraftforge.fml.common.versioning;
import java.util.List;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.LoaderException;
import org.apache.logging.log4j.Level;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
@@ -76,8 +73,7 @@ public class VersionParser
}
catch (InvalidVersionSpecificationException e)
{
FMLLog.log.error("Unable to parse a version range specification successfully {}", range, e);
throw new LoaderException(e);
throw new LoaderException("Unable to parse a version range specification successfully " + range, e);
}
}
}