Add clientSideOnly feature to mods.toml (#10085) (backport of #9804 to 1.20.1) (#10368)

Co-authored-by: Paint_Ninja <PaintNinja@users.noreply.github.com>
This commit is contained in:
JonathingandPaint_Ninja authored and GitHub committed 2025-02-18 14:30:31 -08:00
1 parent b018b8f476
commit 764b334c21
5 files changed
+57 -5

No files matched your search

@@ -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().<String>getConfigElement("displayTest").orElse("MATCH_VERSION"); // missing defaults to DEFAULT type
final String displayTestString = info.getConfig().<String>getConfigElement("displayTest")
.orElseGet(() -> (Boolean) info.getOwningFile().getFileProperties().getOrDefault(ModFileInfo.CLIENT_SIDE_ONLY_PROP, Boolean.FALSE) ? "IGNORE_ALL_VERSION" : "MATCH_VERSION");
Supplier<IExtensionPoint.DisplayTest> displayTestSupplier = switch (displayTestString) {
case "MATCH_VERSION" -> // default displaytest checks for version string match
() -> new IExtensionPoint.DisplayTest(() -> this.modInfo.getVersion().toString(),
@@ -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;
}
}
@@ -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<EarlyLoadingException.ExceptionData> 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());
@@ -43,6 +43,10 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
private final String license;
private final List<String> usesServices;
public static final String CLIENT_SIDE_ONLY_PROP = "__FORGE_clientSideOnly";
private static final Map<String, Object> 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.<String>getConfigElement("license")
.orElse("");
this.showAsResourcePack = config.<Boolean>getConfigElement("showAsResourcePack").orElse(false);
boolean clientSideOnly = config.<Boolean>getConfigElement("clientSideOnly").orElse(false);
this.usesServices = config.<List<String>>getConfigElement("services").orElse(List.of());
this.properties = config.<Map<String, Object>>getConfigElement("properties").orElse(Collections.emptyMap());
var maybeProps = config.<Map<String, Object>>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.<String>getConfigElement("issueTrackerURL").map(StringUtils::toURL).orElse(null);
final List<? extends IConfigurable> modConfigs = config.getConfigList("mods");
+4 -1
View File
@@ -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}'''