diff --git a/build.gradle b/build.gradle index e57b9f099b..602be46561 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ ext { MCP_VERSION = '20230612.114412' SNAPSHOT = false - SPI_VERSION = '7.0.0' + SPI_VERSION = '7.0.1' MERGETOOL_VERSION = '1.1.5' ACCESSTRANSFORMERS_VERSION = '8.0.4' COREMODS_VERSION = '5.0.1' diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java index 8fdbbcf39b..e1119e6276 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java @@ -155,12 +155,12 @@ public class ModLoader .toList(); if (!failedBounds.isEmpty()) { - LOGGER.fatal(CORE, "Failed to validate feature bounds for mods"); + LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds); statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); modList.setLoadedMods(Collections.emptyList()); loadingStateValid = false; throw new LoadingFailedException(failedBounds.stream() - .map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb.featureName(), fb.featureBound(), ForgeFeature.featureValue(fb))) + .map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb))) .toList()); } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java index c4d00a602d..1b29f4335d 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java @@ -96,9 +96,7 @@ public class MinecraftLocator extends AbstractModProvider implements IModLocator final NightConfigWrapper configWrapper = new NightConfigWrapper(conf); //final ModFileInfo modFileInfo = new ModFileInfo(modFile, configWrapper, extralangs.build().toList()); - final ModFileInfo modFileInfo = new ModFileInfo(modFile, configWrapper, List.of()); - configWrapper.setFile(modFileInfo); - return modFileInfo; + return new ModFileInfo(modFile, configWrapper, configWrapper::setFile, List.of()); } @Override diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileInfo.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileInfo.java index 449cd58db4..31f3b6c5c7 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileInfo.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileInfo.java @@ -26,7 +26,14 @@ import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -43,31 +50,40 @@ public class ModFileInfo implements IModFileInfo, IConfigurable private final String license; private final List usesServices; - ModFileInfo(final ModFile modFile, final IConfigurable config) + ModFileInfo(final ModFile modFile, final IConfigurable config, Consumer configFileConsumer) { this.modFile = modFile; this.config = config; + configFileConsumer.accept(this); + // modloader is essential var modLoader = config.getConfigElement("modLoader") .orElseThrow(()->new InvalidModFileException("Missing ModLoader in file", this)); + // as is modloader version var modLoaderVersion = config.getConfigElement("loaderVersion") .map(MavenVersionAdapter::createFromVersionSpec) .orElseThrow(()->new InvalidModFileException("Missing ModLoader version in file", this)); this.languageSpecs = new ArrayList<>(List.of(new LanguageSpec(modLoader, modLoaderVersion))); + // the remaining properties are optional with sensible defaults this.license = config.getConfigElement("license") - .orElse(""); - this.showAsResourcePack = config.getConfigElement("showAsResourcePack").orElse(false); - this.usesServices = config.>getConfigElement("services").orElse(List.of()); - this.properties = config.>getConfigElement("properties").orElse(Collections.emptyMap()); + .orElse(""); + this.showAsResourcePack = config.getConfigElement("showAsResourcePack") + .orElse(false); + this.usesServices = config.>getConfigElement("services") + .orElse(List.of()); + this.properties = config.>getConfigElement("properties") + .orElse(Collections.emptyMap()); this.modFile.setFileProperties(this.properties); - this.issueURL = config.getConfigElement("issueTrackerURL").map(StringUtils::toURL).orElse(null); + this.issueURL = config.getConfigElement("issueTrackerURL") + .map(StringUtils::toURL) + .orElse(null); final List modConfigs = config.getConfigList("mods"); if (modConfigs.isEmpty()) { throw new InvalidModFileException("Missing mods list", this); } this.mods = modConfigs.stream() - .map(mi-> new ModInfo(this, mi)) - .collect(Collectors.toList()); + .map(mi-> (IModInfo)new ModInfo(this, mi)) + .toList(); if (LOGGER.isDebugEnabled(LogMarkers.LOADING)) { LOGGER.debug(LogMarkers.LOADING, "Found valid mod file {} with {} mods - versions {}", @@ -77,8 +93,8 @@ public class ModFileInfo implements IModFileInfo, IConfigurable } } - public ModFileInfo(final ModFile file, final IConfigurable config, final List languageSpecs) { - this(file, config); + public ModFileInfo(final ModFile file, final IConfigurable config, Consumer configFileConsumer, final List languageSpecs) { + this(file, config, configFileConsumer); this.languageSpecs.addAll(languageSpecs); } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileParser.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileParser.java index eb1140b982..9d519b5aaa 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileParser.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFileParser.java @@ -44,9 +44,7 @@ public class ModFileParser { fileConfig.load(); fileConfig.close(); final NightConfigWrapper configWrapper = new NightConfigWrapper(fileConfig); - final ModFileInfo modFileInfo = new ModFileInfo(modFile, configWrapper); - configWrapper.setFile(modFileInfo); - return modFileInfo; + return new ModFileInfo(modFile, configWrapper, configWrapper::setFile); } protected static List getCoreMods(final ModFile modFile) { @@ -67,6 +65,6 @@ public class ModFileParser { return coreModPaths.entrySet().stream() .peek(e-> LOGGER.debug(LogMarkers.LOADING,"Found coremod {} with Javascript path {}", e.getKey(), e.getValue())) .map(e -> new CoreModFile(e.getKey(), modFile.findResource(e.getValue()),modFile)) - .collect(Collectors.toList()); + .toList(); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModInfo.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModInfo.java index 3542685ccf..267f302c8a 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModInfo.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModInfo.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class ModInfo implements IModInfo, IConfigurable { @@ -54,55 +53,57 @@ public class ModInfo implements IModInfo, IConfigurable Optional ownFile = Optional.ofNullable(owningFile); this.owningFile = owningFile; this.config = config; + // These are sourced from the mod specific [[mod]] entry this.modId = config.getConfigElement("modId") .orElseThrow(() -> new InvalidModFileException("Missing modId", owningFile)); + // verify we have a valid modid if (!VALID_MODID.matcher(this.modId).matches()) { LOGGER.error(LogUtils.FATAL_MARKER, "Invalid modId found in file {} - {} does not match the standard: {}", this.owningFile.getFile().getFilePath(), this.modId, VALID_MODID.pattern()); throw new InvalidModFileException("Invalid modId found : " + this.modId, owningFile); } - this.namespace = config.getConfigElement("namespace").orElse(this.modId); + this.namespace = config.getConfigElement("namespace") + .orElse(this.modId); + // verify our namespace is valid if (!VALID_NAMESPACE.matcher(this.namespace).matches()) { LOGGER.error(LogUtils.FATAL_MARKER, "Invalid override namespace found in file {} - {} does not match the standard: {}", this.owningFile.getFile().getFilePath(), this.namespace, VALID_NAMESPACE.pattern()); throw new InvalidModFileException("Invalid override namespace found : " + this.namespace, owningFile); } this.version = config.getConfigElement("version") .map(s -> StringSubstitutor.replace(s, ownFile.map(ModFileInfo::getFile).orElse(null))) - .map(DefaultArtifactVersion::new).orElse(DEFAULT_VERSION); - this.displayName = config.getConfigElement("displayName").orElse(this.modId); - this.description = config.getConfigElement("description").orElse("MISSING DESCRIPTION"); - - this.logoFile = Optional.ofNullable(config.getConfigElement("logoFile") - .orElseGet(() -> ownFile.flatMap(mf -> mf.getConfigElement("logoFile")).orElse(null))); - this.logoBlur = config.getConfigElement("logoBlur") - .orElseGet(() -> ownFile.flatMap(f -> f.getConfigElement("logoBlur")) - .orElse(true)); - - this.updateJSONURL = config.getConfigElement("updateJSONURL") - .map(StringUtils::toURL); - - this.dependencies = ownFile.map(mfi -> mfi.getConfigList("dependencies", this.modId) - .stream() - .map(dep -> new ModVersion(this, dep)) - .collect(Collectors.toList())) - .orElse(Collections.emptyList()); - - this.features = ownFile.map(mfi -> mfi.>getConfigElement("features", this.modId) - .stream() - .flatMap(m->m.entrySet().stream()) - .map(e->new ForgeFeature.Bound(e.getKey(), e.getValue(), this)) - .collect(Collectors.toList())).orElse(Collections.emptyList()); - - this.properties = ownFile.map(mfi -> mfi.>getConfigElement("modproperties", this.modId) - .orElse(Collections.emptyMap())) - .orElse(Collections.emptyMap()); - - this.modUrl = config.getConfigElement("modUrl") - .map(StringUtils::toURL); - - // verify we have a valid mod version otherwise throw an exception + .map(DefaultArtifactVersion::new) + .orElse(DEFAULT_VERSION); + // verify we have a valid mod version if (!VALID_VERSION.matcher(this.version.toString()).matches()) { throw new InvalidModFileException("Illegal version number specified "+this.version, this.getOwningFile()); } + // The remaining properties all default to sensible values and are not essential + this.displayName = config.getConfigElement("displayName") + .orElse(this.modId); + this.description = config.getConfigElement("description") + .orElse("MISSING DESCRIPTION"); + this.logoFile = Optional.ofNullable(config.getConfigElement("logoFile") + .orElseGet(() -> ownFile.flatMap(mf -> mf.getConfigElement("logoFile")) + .orElse(null))); + this.logoBlur = config.getConfigElement("logoBlur") + .orElseGet(() -> ownFile.flatMap(f -> f.getConfigElement("logoBlur")) + .orElse(true)); + this.updateJSONURL = config.getConfigElement("updateJSONURL") + .map(StringUtils::toURL); + this.modUrl = config.getConfigElement("modUrl") + .map(StringUtils::toURL); + // These are sourced from the file rather than the mod-specific block, but with a modid tag + this.dependencies = ownFile.map(mfi -> mfi.getConfigList("dependencies", this.modId)) + .orElse(Collections.emptyList()) + .stream() + .map(dep -> new ModVersion(this, dep)) + .toList(); + this.features = ownFile.flatMap(mfi -> mfi.>getConfigElement("features", this.modId)) + .stream() + .flatMap(m->m.entrySet().stream()) + .map(this::makeBound) + .toList(); + this.properties = ownFile.flatMap(mfi -> mfi.>getConfigElement("modproperties", this.modId)) + .orElse(Collections.emptyMap()); } @Override @@ -189,6 +190,14 @@ public class ModInfo implements IModInfo, IConfigurable return modUrl; } + private ForgeFeature.Bound makeBound(Map.Entry e) { + if (e.getValue() instanceof String val) { + return new ForgeFeature.Bound(e.getKey(), val, this); + } else { + throw new InvalidModFileException("Invalid feature bound {" + e.getValue() + "} for key {" + e.getKey() + "} only strings are accepted", this.owningFile); + } + } + class ModVersion implements net.minecraftforge.forgespi.language.IModInfo.ModVersion { private IModInfo owner; private final String modId; diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/NightConfigWrapper.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/NightConfigWrapper.java index 8859aea973..a25b54acbc 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/NightConfigWrapper.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/NightConfigWrapper.java @@ -33,9 +33,12 @@ public class NightConfigWrapper implements IConfigurable { @Override @SuppressWarnings("unchecked") public Optional getConfigElement(final String... key) { - return this.config.getOptional(asList(key)).map(value -> { + var path = asList(key); + return this.config.getOptional(path).map(value -> { if (value instanceof UnmodifiableConfig) { return (T) ((UnmodifiableConfig) value).valueMap(); + } else if (value instanceof ArrayList al && al.size() > 0 && al.get(0) instanceof UnmodifiableConfig) { + throw new InvalidModFileException("The configuration path " + path + " is invalid. I wasn't expecting a multi-object list - remove one of the [[ ]]", file); } return (T) value; }); diff --git a/mdk/src/main/resources/META-INF/mods.toml b/mdk/src/main/resources/META-INF/mods.toml index 3f5f56d7b1..95e8668984 100644 --- a/mdk/src/main/resources/META-INF/mods.toml +++ b/mdk/src/main/resources/META-INF/mods.toml @@ -62,3 +62,9 @@ description='''${mod_description}''' versionRange="${minecraft_version_range}" ordering="NONE" side="BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features.${mod_id}] +#openGLVersion="[3.2,)" \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/common/ForgeI18n.java b/src/main/java/net/minecraftforge/common/ForgeI18n.java index a3941089a9..1501ef29d9 100644 --- a/src/main/java/net/minecraftforge/common/ForgeI18n.java +++ b/src/main/java/net/minecraftforge/common/ForgeI18n.java @@ -50,6 +50,8 @@ public class ForgeI18n { customFactories.put("exc", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> parseException(formatString, stringBuffer, objectToParse))); // {0,vr} -> transform VersionRange into cleartext string using fml.messages.version.restriction.* strings customFactories.put("vr", (name, formatString, locale) -> new CustomReadOnlyFormat(MavenVersionStringHelper::parseVersionRange)); + // {0,featurebound} -> transform feature bound to cleartext string + customFactories.put("featurebound", (name, formatString, locale) -> new CustomReadOnlyFormat(MavenVersionStringHelper::parseFeatureBoundValue)); // {0,i18n,fml.message} -> pass object to i18n string 'fml.message' customFactories.put("i18n", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, o) -> stringBuffer.append(ForgeI18n.parseMessage(formatString, o)))); // {0,ornull,fml.absent} -> append String value of o, or i18n string 'fml.absent' (message format transforms nulls into the string literal "null") diff --git a/src/main/java/net/minecraftforge/common/util/MavenVersionStringHelper.java b/src/main/java/net/minecraftforge/common/util/MavenVersionStringHelper.java index 4ebf0b2060..a17300f57d 100644 --- a/src/main/java/net/minecraftforge/common/util/MavenVersionStringHelper.java +++ b/src/main/java/net/minecraftforge/common/util/MavenVersionStringHelper.java @@ -6,6 +6,7 @@ package net.minecraftforge.common.util; import net.minecraftforge.common.ForgeI18n; +import net.minecraftforge.forgespi.locating.ForgeFeature; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.Restriction; import org.apache.maven.artifact.versioning.VersionRange; @@ -80,4 +81,17 @@ public class MavenVersionStringHelper { public static void parseVersionRange(final StringBuffer stringBuffer, final Object range) { stringBuffer.append(versionRangeToString((VersionRange) range)); } + + public static void parseFeatureBoundValue(final StringBuffer stringBuffer, final Object range) { + if (range instanceof ForgeFeature.Bound bound) { + stringBuffer.append(bound.featureName()); + if (bound.bound() instanceof Boolean b) { + stringBuffer.append("=").append(b); + } else if (bound.bound() instanceof VersionRange vr) { + stringBuffer.append(" ").append(versionRangeToString(vr)); + } else { + stringBuffer.append("=\"").append(bound.featureBound()).append("\""); + } + } + } } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index b1c6aee014..25ce7e46ce 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -14,5 +14,4 @@ license="LGPL v2.1" authors="LexManos,cpw" description=''' Forge, a broad compatibility API. - ''' - \ No newline at end of file + ''' \ No newline at end of file diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index 9554d66910..95be069adf 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -61,7 +61,7 @@ "fml.modloading.missingmetadata": "mods.toml missing metadata for modid {3}", "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.feature.missing": "{0,modinfo,name} ({0,modinfo,id}) is missing a feature it requires to run\n§7It requires {3}=>{4} but {5} is available", + "fml.modloading.feature.missing": "{0,modinfo,name} ({0,modinfo,id}) is missing a feature it requires to run\n§7It requires {3,featurebound} but {4} is available", "fml.modloading.uncaughterror":"An uncaught parallel processing error has occurred.\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",