diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java index a82ebea87d..938a5dcd14 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java @@ -9,6 +9,7 @@ import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.config.IConfigEvent; import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.forgespi.language.IModInfo; import java.util.EnumMap; @@ -58,7 +59,8 @@ public abstract class ModContainer this.modInfo = info; this.modLoadingStage = ModLoadingStage.CONSTRUCT; - final String displayTestString = info.getConfig().getConfigElement("displayTest").orElse("MATCH_VERSION"); // missing defaults to DEFAULT type + final String displayTestString = info.getConfig().getConfigElement("displayTest") + .orElseGet(() -> (Boolean) info.getOwningFile().getFileProperties().getOrDefault(ModFileInfo.CLIENT_SIDE_ONLY_PROP, Boolean.FALSE) ? "IGNORE_ALL_VERSION" : "MATCH_VERSION"); Supplier displayTestSupplier = switch (displayTestString) { case "MATCH_VERSION" -> // default displaytest checks for version string match () -> new IExtensionPoint.DisplayTest(() -> this.modInfo.getVersion().toString(), diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/InvalidModFileException.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/InvalidModFileException.java index 3c7e67d92c..3a03c6f108 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/InvalidModFileException.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/InvalidModFileException.java @@ -18,4 +18,8 @@ public class InvalidModFileException extends RuntimeException super(String.format(Locale.ENGLISH, "%s (%s)", message, ((ModFileInfo)modFileInfo).getFile().getFileName())); this.modFileInfo = modFileInfo; } + + public IModFileInfo getBrokenFile() { + return this.modFileInfo; + } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java index d3c457002c..5fefc1e90c 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java @@ -11,6 +11,7 @@ import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.util.ServiceLoaderUtils; import net.minecraftforge.fml.loading.EarlyLoadingException; +import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.LogMarkers; import net.minecraftforge.fml.loading.UniqueModListBuilder; import net.minecraftforge.fml.loading.progress.StartupMessageManager; @@ -19,6 +20,8 @@ import net.minecraftforge.forgespi.locating.IModFile; import net.minecraftforge.forgespi.locating.IModLocator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.List; @@ -52,17 +55,40 @@ public class ModDiscoverer { List discoveryErrorData = new ArrayList<>(); boolean successfullyLoadedMods = true; + boolean distIsDedicatedServer = FMLLoader.getDist().isDedicatedServer(); //Loop all mod locators to get the prime mods to load from. for (IModLocator locator : modLocatorList) { try { LOGGER.debug(LogMarkers.SCAN,"Trying locator {}", locator); var locatedFiles = locator.scanMods(); - if (locatedFiles.stream().anyMatch(file -> !(file instanceof ModFile))) { - LOGGER.error(LogMarkers.SCAN, "A mod locator returned a file which is not a ModFile instance!. They will be skipped!"); + // Backported from 1.19.2 so we don't accidentally process any bad mod files when getting client-only mods + var badModFiles = locatedFiles.stream().filter(file -> !(file instanceof ModFile)).toList(); + if (!badModFiles.isEmpty()) { + LOGGER.error(LogMarkers.SCAN, "Locator {} returned {} files which is are not ModFile instances! They will be skipped!", locator, badModFiles.size()); + locatedFiles.removeAll(badModFiles); } + if (distIsDedicatedServer) { + var clientOnlyModFiles = locatedFiles.stream() + .filter(file -> { + // some mod files can have null infos, like javafml, mclanguage, lowcode, and fmlcore + @Nullable var info = file.getModFileInfo(); + + return info != null && (Boolean) info.getFileProperties().getOrDefault(ModFileInfo.CLIENT_SIDE_ONLY_PROP, Boolean.FALSE); + }) + .toList(); + if (!clientOnlyModFiles.isEmpty()) { + LOGGER.warn(LogMarkers.SCAN, "Locator {} returned {} files which are client-side-only mods, but we're on a dedicated server. They will be skipped!", locator, clientOnlyModFiles.size()); + locatedFiles.removeAll(clientOnlyModFiles); + } + } + + LOGGER.debug(LogMarkers.SCAN, "Locator {} found {} valid mod files", locator, locatedFiles.size()); handleLocatedFiles(loadedFiles, locatedFiles); + } catch (InvalidModFileException imfe) { + // We don't generally expect this exception, since it should come from the candidates stream above and be handled in the Locator, but just in case. + LOGGER.error(LogMarkers.SCAN, "Locator {} found an invalid mod file {}", locator, imfe.getBrokenFile(), imfe); } catch (EarlyLoadingException exception) { LOGGER.error(LogMarkers.SCAN, "Failed to load mods with locator {}", locator, exception); discoveryErrorData.addAll(exception.getAllData()); 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 74b4644841..53e94c58fb 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 @@ -43,6 +43,10 @@ public class ModFileInfo implements IModFileInfo, IConfigurable private final String license; private final List usesServices; + public static final String CLIENT_SIDE_ONLY_PROP = "__FORGE_clientSideOnly"; + + private static final Map CLIENT_SIDE_ONLY_MAP = Map.of(CLIENT_SIDE_ONLY_PROP, Boolean.TRUE); + ModFileInfo(final ModFile modFile, final IConfigurable config) { this.modFile = modFile; @@ -56,8 +60,21 @@ public class ModFileInfo implements IModFileInfo, IConfigurable this.license = config.getConfigElement("license") .orElse(""); this.showAsResourcePack = config.getConfigElement("showAsResourcePack").orElse(false); + boolean clientSideOnly = config.getConfigElement("clientSideOnly").orElse(false); this.usesServices = config.>getConfigElement("services").orElse(List.of()); - this.properties = config.>getConfigElement("properties").orElse(Collections.emptyMap()); + + var maybeProps = config.>getConfigElement("properties"); + if (clientSideOnly) { + if (maybeProps.isPresent()) { + this.properties = new HashMap<>(maybeProps.get()); + this.properties.put(CLIENT_SIDE_ONLY_PROP, Boolean.TRUE); + } else { + this.properties = CLIENT_SIDE_ONLY_MAP; + } + } else { + this.properties = maybeProps.orElse(Collections.emptyMap()); + } + this.modFile.setFileProperties(this.properties); this.issueURL = config.getConfigElement("issueTrackerURL").map(StringUtils::toURL).orElse(null); final List modConfigs = config.getConfigList("mods"); diff --git a/mdk/src/main/resources/META-INF/mods.toml b/mdk/src/main/resources/META-INF/mods.toml index 8cf3e73f64..6faf96ded9 100644 --- a/mdk/src/main/resources/META-INF/mods.toml +++ b/mdk/src/main/resources/META-INF/mods.toml @@ -12,6 +12,9 @@ loaderVersion="${loader_version_range}" #mandatory This is typically bumped ever license="${mod_license}" # A URL to refer people to when problems occur with this mod #issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# If your mod is purely client-side and has no multiplayer functionality (be it dedicated servers or Open to LAN), +# set this to true, and Forge will set the correct displayTest for you and skip loading your mod on dedicated servers. +#clientSideOnly=true #optional - defaults to false if absent # A list of mods - how many allowed here is determined by the individual mod loader [[mods]] #mandatory # The modid of the mod @@ -36,7 +39,7 @@ authors="${mod_authors}" #optional # IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. # NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. # IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. -#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) +#displayTest="MATCH_VERSION" # if nothing is specified, MATCH_VERSION is the default when clientSideOnly=false, otherwise IGNORE_ALL_VERSION when clientSideOnly=true (#optional) # The description text for the mod (multi line!) (#mandatory) description='''${mod_description}'''