Skip to content

Commit

Permalink
Add Java Core Mods (#79)
Browse files Browse the repository at this point in the history
Mods must ship a separate Jar-file (for example via jar-in-jar) and mark it as FMLModType: LIBRARY to make it load above the GAME layer. They then must provide an implementation of ICoreMod via the Java ServiceLoader to contribute their transformers.

---------

Co-authored-by: Matyrobbrt <[email protected]>
  • Loading branch information
shartte and Matyrobbrt authored Jun 2, 2024
1 parent df670d9 commit 69b5811
Show file tree
Hide file tree
Showing 22 changed files with 668 additions and 185 deletions.
2 changes: 1 addition & 1 deletion loader/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
api "net.neoforged:JarJarSelector:${jarjar_version}"
api "net.neoforged:JarJarMetadata:${jarjar_version}"
api("net.neoforged:bus:${eventbus_version}")
implementation("net.neoforged:JarJarFileSystems:$jarjar_version")

implementation("net.sf.jopt-simple:jopt-simple:${jopt_simple_version}")
implementation("cpw.mods:securejarhandler:${securejarhandler_version}")
Expand All @@ -44,7 +45,6 @@ dependencies {
testCompileOnly("org.jetbrains:annotations:${jetbrains_annotations_version}")
testRuntimeOnly("cpw.mods:bootstraplauncher:${bootstraplauncher_version}")
testRuntimeOnly("org.apache.logging.log4j:log4j-core:$log4j_version")
testRuntimeOnly("net.neoforged:JarJarFileSystems:$jarjar_version")
testImplementation("org.junit.jupiter:junit-jupiter-api:$jupiter_version")
testImplementation("org.junit.jupiter:junit-jupiter-params:$jupiter_version")
testImplementation("org.mockito:mockito-junit-jupiter:$mockito_version")
Expand Down
9 changes: 8 additions & 1 deletion loader/src/main/java/net/neoforged/fml/ModLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.VisibleForTesting;

/**
* Contains the logic to load mods, i.e. turn the {@link LoadingModList} into the {@link ModList},
Expand Down Expand Up @@ -91,7 +92,6 @@ private static String computeModLauncherServiceList() {
*/
public static void gatherAndInitializeMods(final Executor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
var loadingModList = FMLLoader.getLoadingModList();
loadingIssues.clear();
loadingIssues.addAll(loadingModList.getModLoadingIssues());

ForgeFeature.registerFeature("javaVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.BOTH, System.getProperty("java.version")));
Expand Down Expand Up @@ -410,6 +410,13 @@ public static List<ModLoadingIssue> getLoadingIssues() {
return List.copyOf(loadingIssues);
}

@VisibleForTesting
@ApiStatus.Internal
public static void clearLoadingIssues() {
LOGGER.info("Clearing {} loading issues", loadingIssues.size());
loadingIssues.clear();
}

@ApiStatus.Internal
public static void addLoadingIssue(ModLoadingIssue issue) {
loadingIssues.add(issue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,12 @@ private static Object[] getTranslationArgs(ModLoadingIssue issue) {
var args = new ArrayList<>(3 + issue.translationArgs().size());

var modInfo = issue.affectedMod();
if (modInfo == null && issue.affectedModFile() != null) {
if (!issue.affectedModFile().getModInfos().isEmpty()) {
modInfo = issue.affectedModFile().getModInfos().getFirst();
var file = issue.affectedModFile();
while (modInfo == null && file != null) {
if (!file.getModInfos().isEmpty()) {
modInfo = file.getModInfos().getFirst();
}
file = file.getDiscoveryAttributes().parent();
}
args.add(modInfo);
args.add(null); // Previously mod-loading phase
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.loading;

import cpw.mods.modlauncher.api.ITransformer;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.util.List;
import net.neoforged.coremod.CoreModScriptingEngine;
import net.neoforged.coremod.ICoreModScriptSource;
import net.neoforged.fml.loading.moddiscovery.CoreModFile;
import net.neoforged.fml.loading.moddiscovery.ModFileInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Uses the coremod scripting engine from https://github.com/neoforged/CoreMods
* to load JS-based coremods. Avoids loading any of the classes unless a mod
* contains a JS-based coremod.
*/
class CoreModScriptLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(CoreModScriptLoader.class);

private CoreModScriptLoader() {}

/**
* Enumerate script-based coremods.
*/
public static List<ITransformer<?>> loadCoreModScripts(List<ModFileInfo> modFileInfos) {
CoreModScriptingEngine engine;
try {
engine = new CoreModScriptingEngine();
} catch (NoClassDefFoundError e) {
// Fail for all mods that require a coremod scripting engine to be present
throw new IllegalStateException("Could not find the coremod script-engine, but the following mods require it: " + modFileInfos, e);
}

LOGGER.debug(LogMarkers.CORE, "Loading coremod scripts");
for (var modFile : modFileInfos) {
for (var coreMod : modFile.getFile().getCoreMods()) {
engine.loadCoreMod(new ScriptSourceAdapter(coreMod));
}
}

return engine.initializeCoreMods();
}

private record ScriptSourceAdapter(CoreModFile coreMod) implements ICoreModScriptSource {
@Override
public Reader readCoreMod() throws IOException {
return Files.newBufferedReader(coreMod.path());
}

@Override
public String getDebugSource() {
return coreMod.path().toString();
}

@Override
public Reader getAdditionalFile(final String fileName) throws IOException {
return Files.newBufferedReader(coreMod.file().findResource(fileName));
}

@Override
public String getOwnerId() {
return this.coreMod.file().getModInfos().getFirst().getModId();
}

@Override
public String toString() {
return "{Name: " + coreMod.name() + ", Owner: " + getOwnerId() + " @ " + getDebugSource() + "}";
}
}
}
9 changes: 0 additions & 9 deletions loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import net.neoforged.accesstransformer.api.AccessTransformerEngine;
import net.neoforged.accesstransformer.ml.AccessTransformerService;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.coremod.CoreModScriptingEngine;
import net.neoforged.fml.common.asm.RuntimeDistCleaner;
import net.neoforged.fml.loading.mixin.DeferredMixinConfigRegistration;
import net.neoforged.fml.loading.moddiscovery.ModDiscoverer;
Expand All @@ -38,7 +37,6 @@
public class FMLLoader {
private static final Logger LOGGER = LogUtils.getLogger();
private static AccessTransformerEngine accessTransformer;
private static CoreModScriptingEngine coreModEngine;
private static LanguageProviderLoader languageProviderLoader;
private static Dist dist;
private static LoadingModList loadingModList;
Expand Down Expand Up @@ -90,9 +88,6 @@ static void onInitialLoad(IEnvironment environment) throws IncompatibleEnvironme
});
LOGGER.debug(LogMarkers.CORE, "Found Runtime Dist Cleaner");

coreModEngine = new CoreModScriptingEngine();
LOGGER.debug(LogMarkers.CORE, "FML found CoreMods version : {}", coreModEngine.getClass().getPackage().getImplementationVersion());

try {
Class.forName("com.electronwill.nightconfig.core.Config", false, environment.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.toml.TomlFormat", false, environment.getClass().getClassLoader());
Expand Down Expand Up @@ -147,10 +142,6 @@ public static List<ITransformationService.Resource> completeScan(ILaunchContext
return List.of(modValidator.getModResources());
}

static CoreModScriptingEngine getCoreModEngine() {
return coreModEngine;
}

public static LanguageProviderLoader getLanguageLoadingProvider() {
return languageProviderLoader;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
package net.neoforged.fml.loading;

import static net.neoforged.fml.loading.LogMarkers.CORE;
import static net.neoforged.fml.loading.LogMarkers.LOADING;

import com.mojang.logging.LogUtils;
import cpw.mods.modlauncher.api.IEnvironment;
import cpw.mods.modlauncher.api.IModuleLayerManager;
import cpw.mods.modlauncher.api.ITransformationService;
import cpw.mods.modlauncher.api.ITransformer;
import cpw.mods.modlauncher.api.IncompatibleEnvironmentException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
Expand All @@ -21,8 +23,13 @@
import java.util.function.Supplier;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionSpecBuilder;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.ModLoadingIssue;
import net.neoforged.fml.util.ServiceLoaderUtil;
import net.neoforged.neoforgespi.Environment;
import net.neoforged.neoforgespi.ILaunchContext;
import net.neoforged.neoforgespi.coremod.ICoreMod;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;

public class FMLServiceProvider implements ITransformationService {
Expand All @@ -40,7 +47,8 @@ public class FMLServiceProvider implements ITransformationService {
private List<String> mavenRootsArgumentList;
private List<String> mixinConfigsArgumentList;
private VersionInfo versionInfo;
private ILaunchContext launchContext;
@VisibleForTesting
ILaunchContext launchContext;

public FMLServiceProvider() {
final String markerselection = System.getProperty("forge.logging.markers", "");
Expand Down Expand Up @@ -115,7 +123,50 @@ public void argumentValues(OptionResult option) {

@Override
public List<? extends ITransformer<?>> transformers() {
LOGGER.debug(CORE, "Loading coremod transformers");
return FMLLoader.getCoreModEngine().initializeCoreMods();
LOGGER.debug(LOADING, "Loading coremod transformers");

var result = new ArrayList<>(loadCoreModScripts());

// Find all Java core mods
for (var coreMod : ServiceLoaderUtil.loadServices(launchContext, ICoreMod.class)) {
// Try to identify the mod-file this is from
var sourceFile = ServiceLoaderUtil.identifySourcePath(launchContext, coreMod);

try {
for (var transformer : coreMod.getTransformers()) {
LOGGER.debug(CORE, "Adding {} transformer from core-mod {} in {}", transformer.targets(), coreMod, sourceFile);
result.add(transformer);
}
} catch (Exception e) {
// Throwing here would cause the game to immediately crash without a proper error screen,
// since this method is called by ModLauncher directly.
ModLoader.addLoadingIssue(
ModLoadingIssue.error("fml.modloading.coremod_error", coreMod.getClass().getName(), sourceFile).withCause(e));
}
}

return result;
}

private List<ITransformer<?>> loadCoreModScripts() {
var filesWithCoreModScripts = LoadingModList.get().getModFiles()
.stream()
.filter(mf -> !mf.getFile().getCoreMods().isEmpty())
.toList();

if (filesWithCoreModScripts.isEmpty()) {
// Don't even bother starting the scripting engine if no mod contains scripting core mods
LOGGER.debug(LogMarkers.CORE, "Not loading coremod script-engine since no mod requested it");
return List.of();
}

LOGGER.info(LogMarkers.CORE, "Loading coremod script-engine for {}", filesWithCoreModScripts);
try {
return CoreModScriptLoader.loadCoreModScripts(filesWithCoreModScripts);
} catch (NoClassDefFoundError e) {
var message = "Could not find the coremod script-engine, but the following mods require it: " + filesWithCoreModScripts;
ImmediateWindowHandler.crash(message);
throw new IllegalStateException(message, e);
}
}
}
23 changes: 12 additions & 11 deletions loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import net.neoforged.fml.loading.moddiscovery.ModFileInfo;
import net.neoforged.fml.loading.moddiscovery.ModInfo;
import net.neoforged.fml.loading.modscan.BackgroundScanHandler;
import net.neoforged.neoforgespi.language.IModFileInfo;
import net.neoforged.neoforgespi.language.IModInfo;

/**
Expand All @@ -31,13 +32,17 @@
*/
public class LoadingModList {
private static LoadingModList INSTANCE;
private final List<IModFileInfo> plugins;
private final List<ModFileInfo> modFiles;
private final List<ModInfo> sortedList;
private final Map<ModInfo, List<ModInfo>> modDependencies;
private final Map<String, ModFileInfo> fileById;
private final List<ModLoadingIssue> modLoadingIssues;

private LoadingModList(final List<ModFile> modFiles, final List<ModInfo> sortedList, Map<ModInfo, List<ModInfo>> modDependencies) {
private LoadingModList(final List<ModFile> plugins, final List<ModFile> modFiles, final List<ModInfo> sortedList, Map<ModInfo, List<ModInfo>> modDependencies) {
this.plugins = plugins.stream()
.map(ModFile::getModFileInfo)
.collect(Collectors.toList());
this.modFiles = modFiles.stream()
.map(ModFile::getModFileInfo)
.map(ModFileInfo.class::cast)
Expand All @@ -54,8 +59,8 @@ private LoadingModList(final List<ModFile> modFiles, final List<ModInfo> sortedL
this.modLoadingIssues = new ArrayList<>();
}

public static LoadingModList of(List<ModFile> modFiles, List<ModInfo> sortedList, List<ModLoadingIssue> issues, Map<ModInfo, List<ModInfo>> modDependencies) {
INSTANCE = new LoadingModList(modFiles, sortedList, modDependencies);
public static LoadingModList of(List<ModFile> plugins, List<ModFile> modFiles, List<ModInfo> sortedList, List<ModLoadingIssue> issues, Map<ModInfo, List<ModInfo>> modDependencies) {
INSTANCE = new LoadingModList(plugins, modFiles, sortedList, modDependencies);
INSTANCE.modLoadingIssues.addAll(issues);
return INSTANCE;
}
Expand All @@ -64,14 +69,6 @@ public static LoadingModList get() {
return INSTANCE;
}

public void addCoreMods() {
modFiles.stream()
.map(ModFileInfo::getFile)
.map(ModFile::getCoreMods)
.flatMap(List::stream)
.forEach(FMLLoader.getCoreModEngine()::loadCoreMod);
}

public void addMixinConfigs() {
modFiles.stream()
.map(ModFileInfo::getFile)
Expand All @@ -94,6 +91,10 @@ public void addForScanning(BackgroundScanHandler backgroundScanHandler) {
.forEach(backgroundScanHandler::submitForScanning);
}

public List<IModFileInfo> getPlugins() {
return plugins;
}

public List<ModFileInfo> getModFiles() {
return modFiles;
}
Expand Down
10 changes: 5 additions & 5 deletions loader/src/main/java/net/neoforged/fml/loading/ModSorter.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ private ModSorter(final List<ModFile> modFiles) {
this.uniqueModListBuilder = new UniqueModListBuilder(modFiles);
}

public static LoadingModList sort(List<ModFile> mods, final List<ModLoadingIssue> issues) {
public static LoadingModList sort(List<ModFile> plugins, List<ModFile> mods, final List<ModLoadingIssue> issues) {
final ModSorter ms = new ModSorter(mods);
try {
ms.buildUniqueList();
} catch (ModLoadingException e) {
// We cannot build any list with duped mods. We have to abort immediately and report it
return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf -> (ModInfo) mf.getModInfos().get(0)).collect(toList()), e.getIssues(), Map.of());
return LoadingModList.of(plugins, ms.systemMods, ms.systemMods.stream().map(mf -> (ModInfo) mf.getModInfos().get(0)).collect(toList()), concat(issues, e.getIssues()), Map.of());
}

// try and validate dependencies
Expand All @@ -70,7 +70,7 @@ public static LoadingModList sort(List<ModFile> mods, final List<ModLoadingIssue

// if we miss a dependency or detect an incompatibility, we abort now
if (!resolutionResult.versionResolution.isEmpty() || !resolutionResult.incompatibilities.isEmpty()) {
list = LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf -> (ModInfo) mf.getModInfos().get(0)).collect(toList()), concat(issues, resolutionResult.buildErrorMessages()), Map.of());
list = LoadingModList.of(plugins, ms.systemMods, ms.systemMods.stream().map(mf -> (ModInfo) mf.getModInfos().get(0)).collect(toList()), concat(issues, resolutionResult.buildErrorMessages()), Map.of());
} else {
// Otherwise, lets try and sort the modlist and proceed
ModLoadingException modLoadingException = null;
Expand All @@ -80,9 +80,9 @@ public static LoadingModList sort(List<ModFile> mods, final List<ModLoadingIssue
modLoadingException = e;
}
if (modLoadingException == null) {
list = LoadingModList.of(ms.modFiles, ms.sortedList, issues, ms.modDependencies);
list = LoadingModList.of(plugins, ms.modFiles, ms.sortedList, issues, ms.modDependencies);
} else {
list = LoadingModList.of(ms.modFiles, ms.sortedList, concat(issues, modLoadingException.getIssues()), Map.of());
list = LoadingModList.of(plugins, ms.modFiles, ms.sortedList, concat(issues, modLoadingException.getIssues()), Map.of());
}
}

Expand Down
Loading

0 comments on commit 69b5811

Please sign in to comment.