Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;

import java.nio.file.Path;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -36,7 +37,7 @@ public class SortedLanguageLoadingProvider extends LanguageLoadingProvider {
@Override
public void addAdditionalLanguages(List<ModFile> modFiles) {
if (modFiles == null) {
return;
modFiles = Collections.emptyList();
}

Set<String> visited = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import cpw.mods.modlauncher.api.*;
import gg.essential.loader.stage2.modlauncher.CompatibilityLayer;
import gg.essential.loader.stage2.modlauncher.EssentialModLocator;
import gg.essential.loader.stage2.util.AugmentedJarOrPathList;
import gg.essential.loader.stage2.util.KFFMerger;
import gg.essential.loader.stage2.util.SortedJarOrPathList;
import org.apache.logging.log4j.LogManager;
Expand All @@ -19,6 +20,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import static gg.essential.loader.stage2.Utils.hasClass;

Expand Down Expand Up @@ -160,12 +162,25 @@ public void onLoad(IEnvironment env, Set<String> otherServices) {
* By default, if there are multiple jars declaring the same module in a layer, ModLauncher will simply pick
* whichever was registered first (see JarModuleFinder). Registration order is effectively random (HashMap iteration
* order), so it effectively picks a random version, which is no good.
* To work around this behavior, we replace the list which holds all jars in a layer with one that automatically
* sorts by version.
* To work around this behavior, we sort all jars in a layer by their version before the layer is loaded.
* This may fail if ModLauncher internals change but there isn't much we can do about it. In such case, we will
* simply fall back to the old, unstable behavior.
*/
private void configureLayerToBeSortedByVersion(IModuleLayerManager.Layer layer) {
private Consumer<List<Object>> sortedByVersion() {
return new SortedJarOrPathList(compatibilityLayer);
}

/**
* Configures the given layer to have the give augmentations applied to its classpath before it is loaded.
*/
@SafeVarargs
private void configureLayerAugmentation(IModuleLayerManager.Layer layer, Consumer<List<Object>>...augmentations) {
Consumer<List<Object>> combinedAugmentations = list -> {
for (Consumer<List<Object>> augmentation : augmentations) {
augmentation.accept(list);
}
};

try {
IModuleLayerManager layerManager = Launcher.INSTANCE.findLayerManager().orElseThrow();
Field layersField = layerManager.getClass().getDeclaredField("layers");
Expand All @@ -175,14 +190,13 @@ private void configureLayerToBeSortedByVersion(IModuleLayerManager.Layer layer)
(Map<IModuleLayerManager.Layer, List<Object>>) layersField.get(layerManager);

layers.compute(layer, (__, list) -> {
SortedJarOrPathList sortedList = new SortedJarOrPathList(compatibilityLayer, kffMerger::maybeMergeInto);
if (list != null) {
sortedList.addAll(list);
if (list == null) {
list = new ArrayList<>();
}
return sortedList;
return new AugmentedJarOrPathList(list, combinedAugmentations);
});
} catch (Throwable t) {
LOGGER.error("Failed to replace mod list of " + layer + " with sorted list:", t);
LOGGER.error("Failed to replace mod list of " + layer + " with augmented list:", t);
}
}

Expand Down Expand Up @@ -244,7 +258,7 @@ public List<Resource> beginScanning(IEnvironment environment) {
if (injectMods()) {
modsInjected = true;
}
configureLayerToBeSortedByVersion(IModuleLayerManager.Layer.PLUGIN);
configureLayerAugmentation(IModuleLayerManager.Layer.PLUGIN, kffMerger::apply, sortedByVersion());
return List.of(new Resource(IModuleLayerManager.Layer.PLUGIN, this.pluginJars));
}

Expand All @@ -256,7 +270,7 @@ public List<Resource> completeScan(IModuleLayerManager layerManager) {
if (!modsInjected) {
LOGGER.error("Failed to inject Essential into Forge mod list, falling back to Mixin-only operation. " +
"Mod will not be listed in Forge's mod list.");
configureLayerToBeSortedByVersion(IModuleLayerManager.Layer.GAME);
configureLayerAugmentation(IModuleLayerManager.Layer.GAME, sortedByVersion());
return Collections.singletonList(new Resource(IModuleLayerManager.Layer.GAME, this.gameJars));
}
return List.of();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import gg.essential.loader.stage2.modlauncher.CompatibilityLayer;
import gg.essential.loader.stage2.util.DelegatingJarMetadata;
import gg.essential.loader.stage2.util.Lazy;
import gg.essential.loader.stage2.util.SortedJarOrPathList;
import gg.essential.loader.stage2.util.PathOrJarAccessor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand Down Expand Up @@ -102,7 +102,7 @@ private List<Object> getLayerElements() throws Throwable {
private List<SecureJar> getLayerJars() throws Throwable {
List<SecureJar> jars = new ArrayList<>();
for (Object pathOrJar : getLayerElements()) {
SecureJar jar = SortedJarOrPathList.getJar(pathOrJar);
SecureJar jar = PathOrJarAccessor.getJar(pathOrJar);
if (jar != null) {
jars.add(jar);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package gg.essential.loader.stage2.util;

import org.jetbrains.annotations.NotNull;

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;

public class AugmentedJarOrPathList extends DelegatingList<Object> {
private final Consumer<List<Object>> augmentation;

public AugmentedJarOrPathList(List<Object> inner, Consumer<List<Object>> augmentation) {
super(inner);
this.augmentation = augmentation;
}

// Called by ModLauncher when finalizing the layer
@Override
public @NotNull Stream<Object> stream() {
augmentation.accept(this);
return super.stream();
}

// MinecraftForge's ModLauncher fork uses this instead:
// https://github.com/MinecraftForge/ModLauncher/commit/1c5695789fd26bef41328900bb82898c14760629#diff-19fd94ffbddfd7c1d3967bc8152bf96d12ce47c23e7f9e83f734198175c428e1
@Override
public <T1> T1 @NotNull [] toArray(T1 @NotNull [] a) {
augmentation.accept(this);
return super.toArray(a);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package gg.essential.loader.stage2.util;

import org.jetbrains.annotations.NotNull;

import java.util.AbstractList;
import java.util.List;
import java.util.stream.Stream;

public class DelegatingList<T> extends AbstractList<T> {
private final List<T> delegate;

public DelegatingList(List<T> delegate) {
this.delegate = delegate;
}

@Override
public T get(int index) {
return delegate.get(index);
}

@Override
public int size() {
return delegate.size();
}

@Override
public T set(int index, T element) {
return delegate.set(index, element);
}

@Override
public void add(int index, T element) {
delegate.add(index, element);
}

@Override
public T remove(int index) {
return delegate.remove(index);
}

//
// These methods are called by ModLauncher, so we'll forward them directly in case
// another mod also replaces the list and relies on them.
//

@Override
public boolean add(T t) {
return delegate.add(t);
}

@Override
public @NotNull Stream<T> stream() {
return delegate.stream();
}

// Used by MinecraftForge's ModLauncher fork:
// https://github.com/MinecraftForge/ModLauncher/commit/1c5695789fd26bef41328900bb82898c14760629#diff-19fd94ffbddfd7c1d3967bc8152bf96d12ce47c23e7f9e83f734198175c428e1
@Override
public <T1> T1 @NotNull [] toArray(T1 @NotNull [] a) {
return delegate.toArray(a);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gg.essential.loader.stage2.util;

import cpw.mods.jarhandling.JarMetadata;
import cpw.mods.jarhandling.SecureJar;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

public class JarMetadataAccessor {
private static final Logger LOGGER = LogManager.getLogger();

private static final Map<Class<?>, Function<SecureJar, JarMetadata>> metadataGetters = new HashMap<>();

public static JarMetadata getMetadata(SecureJar jar) {
Class<? extends SecureJar> implClass = jar.getClass();
Function<SecureJar, JarMetadata> metadataGetter = metadataGetters.get(implClass);
if (metadataGetter == null) {
try {
String implName = implClass.getName();
switch (implName) {
case "org.sinytra.connector.service.DummyVirtualJar":
// Used for the dummy fabric-loader mod as well as for code generated by ManninghamMills
metadataGetter = __ -> null;
break;
default: // probably the main ModLauncher-internal impl
metadataGetter = UnsafeHacks.makeGetter(implClass.getDeclaredField("metadata"));
}
} catch (Throwable t) {
LOGGER.error("Failed to get metadata from " + implClass + ":", t);
metadataGetter = __ -> null;
}
metadataGetters.put(implClass, metadataGetter);
}
return metadataGetter.apply(jar);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -108,25 +109,35 @@ public boolean addKotlinJar(Path sourceFile, SecureJar secureJar) {
return true;
}

public List<SecureJar> maybeMergeInto(SecureJar secureJar) {
public void apply(List<Object> pathOrJarList) {
// Nothing to merge (older Essential version), nothing to do
if (ourCoreJars.jars.isEmpty()) {
return null;
return;
}

boolean didMergeIntoExistingMod = false;
ListIterator<Object> iter = pathOrJarList.listIterator();
while (iter.hasNext()) {
SecureJar jar = PathOrJarAccessor.getJar(iter.next());
if (jar == null) continue;

SecureJar newJar = maybeMergeInto(jar);
if (newJar != null) {
iter.set(PathOrJarAccessor.from(newJar));
didMergeIntoExistingMod = true;
// continue, in case there's multiple KFF versions
}
}

// If this is a KotlinForForge version which uses JarJar to bundle Kotlin (KFF 5+, potentially 4.12),
// we no longer need to merge the Kotlin jars into it, we merely need to add our Kotlin jars to the to-be-loaded
// list too.
if (isJarJarKff(secureJar)) {
LOGGER.info("Looks like KotlinForForge is using JarJar now, all good to go: {}", secureJar);
List<SecureJar> allJars = new ArrayList<>();
allJars.add(secureJar); // keep user-installed jar
allJars.addAll(ourCoreJars.jars);
allJars.addAll(ourCoroutinesJars.jars);
allJars.addAll(ourSerializationJars.jars);
return allJars;
if (didMergeIntoExistingMod) {
return;
}

// No KFF (or KFF 5+ which is using JarJar), so we can simply add all our Kotlin libs to the classpath directly.
addKotlinLibsToPathOrJarList(pathOrJarList);
}

public SecureJar maybeMergeInto(SecureJar secureJar) {
// Only care about a jar if it contains a Kotlin we can overwrite
if (!compatibilityLayer.getPackages(secureJar).contains("kotlin")) {
return null;
Expand All @@ -147,7 +158,7 @@ public List<SecureJar> maybeMergeInto(SecureJar secureJar) {
// If the jar contains only core but not coroutine libs, then it's not the fat KFF jar but rather KFF is
// using JarJar, and we should be able to find that one later.
if (theirCoreVersion != 0 && theirCoroutinesVersion == 0) {
LOGGER.info("Looks like a standalone Kotlin jar. Keeping as is, we should be finding a JarJar KFF jar.");
LOGGER.info("Looks like a standalone Kotlin jar. Keeping as is.");
return null;
}

Expand Down Expand Up @@ -200,19 +211,33 @@ public List<SecureJar> maybeMergeInto(SecureJar secureJar) {
}
}

return List.of(compatibilityLayer.newSecureJarWithCustomMetadata((__, newMeta) -> new DescriptorRewritingJarMetadata(orgMeta, newMeta) {
return compatibilityLayer.newSecureJarWithCustomMetadata((__, newMeta) -> new DescriptorRewritingJarMetadata(orgMeta, newMeta) {
@Override
public String name() {
// Call the original name from the original SecureJar to allow SelfRenamingJarMetadata to function
return secureJar.name();
}
}, tmpFile));
}, tmpFile);
} catch (Throwable t) {
LOGGER.fatal("Failed to merge updated Kotlin into " + secureJar + ":", t);
return null; // oh well, guess we'll give it a try as is
}
}

private void addKotlinLibsToPathOrJarList(List<Object> pathOrJarList) {
addToPathOrJarList(pathOrJarList, ourCoreJars.jars);
addToPathOrJarList(pathOrJarList, ourCoroutinesJars.jars);
addToPathOrJarList(pathOrJarList, ourSerializationJars.jars);
}

private void addToPathOrJarList(List<Object> pathOrJarList, List<SecureJar> jarsToBeAdded) {
for (SecureJar jar : jarsToBeAdded) {
Object pathOrJar = PathOrJarAccessor.from(jar);
if (pathOrJar == null) continue;
pathOrJarList.add(pathOrJar);
}
}

public static boolean isJarJarKff(SecureJar jar) {
try {
Path jarjarPath = jar.getRootPath().resolve("META-INF").resolve("jarjar").resolve("metadata.json");
Expand Down Expand Up @@ -264,6 +289,13 @@ private int detectKotlinCoroutinesVersion(SecureJar jar, Path root) {
try {
Path versionFile = root.resolve("META-INF").resolve("kotlinx_coroutines_core.version");
if (Files.notExists(versionFile)) {
// Coroutines prior to 1.6.0 (e.g. as in KFF 2.2.0) did not yet have the version file:
// https://github.com/Kotlin/kotlinx.coroutines/commit/5c75e7ad58337dcce68b00f5f7e40bfc0108dce7
// There's no way to know their exact version, but we don't really need to either because it'll always
// be older than what we ship, so any old non-zero version will do.
if (Files.exists(root.resolve("kotlinx").resolve("coroutines").resolve("Job.class"))) {
return version(1, 0, 0);
}
return 0; // this is one of our slim jars, always consider it outdated
}
return version(Files.readString(versionFile));
Expand Down
Loading
Loading