diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java index e1e7d055c6..8cdc5fceb3 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/ModDiscoverer.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java index 7e0b36abec..fd647dfb97 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 @@ -12,6 +12,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; @@ -20,6 +21,7 @@ import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.locating.IDependencyLocator; import net.minecraftforge.forgespi.locating.IModFile; import net.minecraftforge.forgespi.locating.IModLocator; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import java.util.ArrayList; @@ -67,6 +69,7 @@ public class ModDiscoverer { boolean successfullyLoadedMods = true; List brokenFiles = new ArrayList<>(); + boolean distIsDedicatedServer = FMLLoader.getDist().isDedicatedServer(); //Loop all mod locators to get the prime mods to load from. for (IModLocator locator : modLocatorList) { try { @@ -84,8 +87,24 @@ public class ModDiscoverer { if (!badModFiles.isEmpty()) { LOGGER.error(LogMarkers.SCAN, "Locator {} returned {} files which is are not ModFile instances! They will be skipped!", locator, badModFiles.size()); brokenFiles.addAll(badModFiles.stream().map(IModFile::getModFileInfo).toList()); + locatedFiles.removeAll(badModFiles); } - 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) { 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..72fc543101 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}'''