diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java
index 5b7c804ce..69843adb6 100644
--- a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java
+++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java
@@ -1,5 +1,8 @@
package net.fabricmc.minecraft.test.server_only;
+import org.quiltmc.loader.api.minecraft.DedicatedServerOnly;
+
+@DedicatedServerOnly
public class TestMixinGuiHelper {
public static void help() {
diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java
index 699f8da12..d1573bf03 100644
--- a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java
+++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java
@@ -1,3 +1,4 @@
-// @org.quiltmc.loader.api.minecraft.DedicatedServerOnly
+//@org.quiltmc.loader.api.minecraft.DedicatedServerOnly
+//@org.quiltmc.loader.api.Requires({"trinkets", "quilt_loader", "minecraft", "buildcraft"})
// Uncomment the above line to test out package based stripping.
package net.fabricmc.minecraft.test.server_only;
diff --git a/src/main/java/org/quiltmc/loader/api/Requires.java b/src/main/java/org/quiltmc/loader/api/Requires.java
index c6ddeadc9..6e8e19f8e 100644
--- a/src/main/java/org/quiltmc/loader/api/Requires.java
+++ b/src/main/java/org/quiltmc/loader/api/Requires.java
@@ -25,7 +25,7 @@
/** Applied to declare that the annotated element requires specific mods to exist.
*
- * When applied to mod code this will result in quilt-loader removing that element when running without the specified mods.
+ * When applied to mod code this will result in quilt-loader removing that element when running without all of the specified mods.
*
* When the annotated element is removed, bytecode associated with the element will not be removed. For example, if a
* field is removed, its initializer code will not, and will cause an error on execution.
diff --git a/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java b/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java
index e3f54fa05..2845928ab 100644
--- a/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java
+++ b/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java
@@ -23,6 +23,7 @@
import java.net.URL;
import java.nio.file.Path;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.jar.Manifest;
@@ -40,6 +41,7 @@ public interface QuiltLauncher {
void setAllowedPrefixes(Path path, String... prefixes);
void setTransformCache(URL insideTransformCache);
void setHiddenClasses(Set classes);
+ void setHiddenClasses(Map classes);
void hideParentUrl(URL hidden);
void hideParentPath(Path obf);
void validateGameClassLoader(Object gameInstance);
diff --git a/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java b/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java
index b32e8a17a..9053671cd 100644
--- a/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java
+++ b/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java
@@ -313,6 +313,11 @@ public void setHiddenClasses(Set hiddenClasses) {
classLoader.getDelegate().setHiddenClasses(hiddenClasses);
}
+ @Override
+ public void setHiddenClasses(Map hiddenClasses) {
+ classLoader.getDelegate().setHiddenClasses(hiddenClasses);
+ }
+
@Override
public void hideParentUrl(URL parent) {
classLoader.getDelegate().hideParentUrl(parent);
diff --git a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java
index f63748dbf..853c64421 100644
--- a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java
+++ b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java
@@ -52,6 +52,8 @@
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -97,7 +99,7 @@ public Optional getQuiltMod() {
private IMixinTransformer mixinTransformer;
private boolean transformInitialized = false;
private boolean transformFinishedLoading = false;
- private Set hiddenClasses = Collections.emptySet();
+ private Map hiddenClasses = Collections.emptyMap();
private String transformCacheUrl;
private final Map allowedPrefixes = new ConcurrentHashMap<>();
private final Set parentSourcedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>());
@@ -106,8 +108,8 @@ public Optional getQuiltMod() {
* in this loader. */
private final Set parentHiddenUrls = Collections.newSetFromMap(new ConcurrentHashMap<>());
- /** Map of package to whether we can load it in this environment. */
- private final Map packageSideCache = new ConcurrentHashMap<>();
+ /** Map of package to the reason why it cannot be loaded. If the package can be loaded then the value is the empty string. */
+ private final Map packageLoadDenyCache = new ConcurrentHashMap<>();
KnotClassDelegate(boolean isDevelopment, EnvType envType, KnotClassLoaderInterface itf, GameProvider provider) {
this.isDevelopment = isDevelopment;
@@ -238,6 +240,25 @@ Class> tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound
}
}
+ int pkgDelimiterPos = name.lastIndexOf('.');
+ String pkgString = pkgDelimiterPos > 0 ? name.substring(0, pkgDelimiterPos) : null;
+
+ if (pkgString != null) {
+ final boolean allowFromParentFinal = allowFromParent;
+ String denyReason = packageLoadDenyCache.computeIfAbsent(pkgString, pkgName -> {
+ return computePackageDenyLoadReason(pkgName, allowFromParentFinal);
+ });
+
+ if (denyReason != null && !denyReason.isEmpty()) {
+ throw new RuntimeException("Cannot load package " + pkgString + " " + denyReason);
+ }
+ }
+
+ String hideReason = hiddenClasses.get(name);
+ if (hideReason != null) {
+ throw new RuntimeException("Cannot load " + name + " " + hideReason);
+ }
+
byte[] input = getPostMixinClassByteArray(url, name);
if (input == null) return null;
@@ -247,8 +268,6 @@ Class> tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound
KnotClassDelegate.Metadata metadata = getMetadata(name, url);
- int pkgDelimiterPos = name.lastIndexOf('.');
-
final String modId;
if (metadata.codeSource == null) {
@@ -268,19 +287,8 @@ Class> tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound
return c;
}
- if (pkgDelimiterPos > 0) {
+ if (pkgString != null) {
// TODO: package definition stub
- String pkgString = name.substring(0, pkgDelimiterPos);
-
- final boolean allowFromParentFinal = allowFromParent;
- Boolean permitted = packageSideCache.computeIfAbsent(pkgString, pkgName -> {
- return computeCanLoadPackage(pkgName, allowFromParentFinal);
- });
-
- if (permitted != null && !permitted) {
- throw new RuntimeException("Cannot load package " + pkgString + " in environment type " + envType);
- }
-
Package pkg = itf.getPackage(pkgString);
if (pkg == null) {
@@ -310,28 +318,15 @@ Class> tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound
return c;
}
+
private boolean shouldRerouteToParent(String name) {
return name.startsWith("org.slf4j.") || name.startsWith("org.apache.logging.log4j.");
}
- boolean computeCanLoadPackage(String pkgName, boolean allowFromParent) {
+ private String computePackageDenyLoadReason(String pkgName, boolean allowFromParent) {
String fileName = pkgName + ".package-info";
- try {
- byte[] bytes = getRawClassByteArray(fileName, allowFromParent);
- if (bytes == null) {
- // No package-info class file
- return true;
- }
-
- ClassReader reader = new ClassReader(bytes);
-
- PackageStrippingData strippingData = new PackageStrippingData(QuiltLoaderImpl.ASM_VERSION, envType, modCodeSourceMap);
- reader.accept(strippingData, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
-
- return !strippingData.stripEntirePackage();
- } catch (IOException e) {
- throw new RuntimeException("Unable to load " + fileName, e);
- }
+ String hideReason = hiddenClasses.get(fileName);
+ return hideReason != null ? hideReason : "";
}
Metadata getMetadata(String name, URL resourceURL) {
@@ -508,10 +503,6 @@ public byte[] getRawClassByteArray(String name, boolean allowFromParent) throws
}
public byte[] getRawClassByteArray(URL url, String name) throws IOException {
- if (hiddenClasses.contains(name)) {
- return null;
- }
-
try (InputStream inputStream = (url != null ? url.openStream() : null)) {
if (inputStream == null) {
return null;
@@ -533,6 +524,14 @@ void setTransformCache(URL insideTransformCache) {
}
void setHiddenClasses(Set hiddenClasses) {
+ Map map = new HashMap<>();
+ for (String cl : hiddenClasses) {
+ map.put(cl, "unknown reason");
+ }
+ setHiddenClasses(map);
+ }
+
+ void setHiddenClasses(Map hiddenClasses) {
this.hiddenClasses = hiddenClasses;
}
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/AbstractStripData.java b/src/main/java/org/quiltmc/loader/impl/transformer/AbstractStripData.java
new file mode 100644
index 000000000..ecfcaa2f1
--- /dev/null
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/AbstractStripData.java
@@ -0,0 +1,110 @@
+package org.quiltmc.loader.impl.transformer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.objectweb.asm.ClassVisitor;
+import org.quiltmc.loader.api.Requires;
+import org.quiltmc.loader.impl.util.QuiltLoaderInternal;
+import org.quiltmc.loader.impl.util.QuiltLoaderInternalType;
+
+import net.fabricmc.api.EnvType;
+
+/** Contains string processing for both {@link PackageStrippingData} and {@link ClassStrippingData} */
+@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL)
+public abstract class AbstractStripData extends ClassVisitor {
+
+ protected final EnvType envType;
+ protected final Set mods;
+
+ protected final List denyLoadReasons = new ArrayList<>();
+
+ protected AbstractStripData(int api, EnvType envType, Set mods) {
+ this(api, null, envType, mods);
+ }
+
+ protected AbstractStripData(int api, ClassVisitor classVisitor, EnvType envType, Set mods) {
+ super(api, classVisitor);
+ this.envType = envType;
+ this.mods = mods;
+ }
+
+ /** @return What this represents - generally "package" or "class". */
+ protected abstract String type();
+
+ public List getDenyLoadReasons() {
+ return Collections.unmodifiableList(denyLoadReasons);
+ }
+
+ public String summarizeDenyLoadReasons() {
+ if (denyLoadReasons.isEmpty()) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ if (denyLoadReasons.size() == 1) {
+ sb.append("because ");
+ sb.append(denyLoadReasons.get(0));
+ } else {
+ sb.append("because:");
+ for (String reason : denyLoadReasons) {
+ sb.append("\n- ");
+ sb.append(reason);
+ }
+ }
+ return sb.toString();
+ }
+
+ /** Appends a client-only deny reason to {@link #denyLoadReasons}. */
+ protected void denyClientOnlyLoad() {
+ denyLoadReasons.add("the " + type() + " is annotated with @ClientOnly but we're on the dedicated server");
+ }
+
+ /** Appends a dedicated server only deny reason to {@link #denyLoadReasons}. */
+ protected void denyDediServerOnlyLoad() {
+ denyLoadReasons.add("the " + type() + " is annotated with @DedicatedServerOnly but we're on the client");
+ }
+
+ /** Checks to see if all mods given are in {@link #mods}, and appends a deny reason to {@link #denyLoadReasons} if
+ * any are missing. Assumes the annotation is {@link Requires} in the error message */
+ protected void checkHasAllMods(List requiredMods) {
+ List missingMods = new ArrayList<>();
+ for (String mod : requiredMods) {
+ if (!mods.contains(mod)) {
+ missingMods.add(mod);
+ }
+ }
+
+ checkHasAllMods(requiredMods, missingMods);
+ }
+
+ /** Checks to see if the missing mods list is empty, and appends a deny reason to {@link #denyLoadReasons} if it is
+ * not. Assumes the annotation is {@link Requires} in the error message */
+ protected void checkHasAllMods(List requiredMods, List missingMods) {
+ if (!missingMods.isEmpty()) {
+ StringBuilder all = new StringBuilder();
+ if (requiredMods.size() > 1) {
+ all.append("{");
+ }
+
+ for (String mod : requiredMods) {
+ if (all.length() > 1) {
+ all.append(", ");
+ }
+ all.append("\"");
+ all.append(mod);
+ all.append("\"");
+ }
+
+ if (requiredMods.size() > 1) {
+ all.append("}");
+ }
+
+ denyLoadReasons.add(
+ "the " + type() + " is annotated with @Requires(" + all + ") but the mods " + missingMods
+ + " are not loaded!"
+ );
+ }
+ }
+}
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java b/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java
index 6db35bbb1..3ed98e0bb 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java
@@ -213,7 +213,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
case REMOVED: {
QuiltMetadata qm = this_value_is_actually_nullable(result.getMetadata().get(QuiltMetadata.class));
if (qm != null) {
- cache.hideClass(LoaderUtil.getClassNameFromTransformCache(qm.name));
+ cache.hideClass(LoaderUtil.getClassNameFromTransformCache(qm.name), "it was removed by a chasm transformer");
} else {
throw new UnsupportedOperationException("Cannot remove unknown class");
}
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/ClassStrippingData.java
similarity index 78%
rename from src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java
rename to src/main/java/org/quiltmc/loader/impl/transformer/ClassStrippingData.java
index 8b62d840c..304da8302 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/ClassStrippingData.java
@@ -20,6 +20,7 @@
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
import org.objectweb.asm.TypeReference;
@@ -35,14 +36,16 @@
import net.fabricmc.api.EnvironmentInterface;
import net.fabricmc.api.EnvironmentInterfaces;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
/** Scans a class for Environment, EnvironmentInterface and Requires annotations to figure out what needs to be stripped. */
@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL)
-public class StrippingData extends ClassVisitor {
+public class ClassStrippingData extends AbstractStripData {
// Fabric annotations
private static final String ENVIRONMENT_DESCRIPTOR = Type.getDescriptor(Environment.class);
@@ -54,11 +57,8 @@ public class StrippingData extends ClassVisitor {
private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class);
private static final String REQUIRES_DESCRIPTOR = Type.getDescriptor(Requires.class);
- private final EnvType envType;
private final String envTypeString;
- private final List mods;
- private boolean stripEntireClass = false;
private final Collection stripInterfaces = new HashSet<>();
private final Collection stripFields = new HashSet<>();
private final Collection stripMethods = new HashSet<>();
@@ -66,6 +66,7 @@ public class StrippingData extends ClassVisitor {
/** Every method contained in this will also be contained in {@link #stripMethods}. */
final Collection stripMethodLambdas = new HashSet<>();
+ private String type = "class";
private String[] interfaces;
private class FabricEnvironmentAnnotationVisitor extends AnnotationVisitor {
@@ -142,14 +143,28 @@ public void visitEnd() {
}
}
+ @FunctionalInterface
+ private interface OnModsMissing {
+ void onModsMissing(List required, List missing);
+ }
+
private class QuiltRequiresAnnotationVisitor extends AnnotationVisitor {
+ private final OnModsMissing onModsMissingDetailed;
private final Runnable onModsMismatch;
private final Runnable onModsMismatchLambdas;
private boolean stripLambdas = true;
+ private QuiltRequiresAnnotationVisitor(int api, OnModsMissing onModsMissing) {
+ super(api);
+ this.onModsMissingDetailed = onModsMissing;
+ this.onModsMismatch = null;
+ this.onModsMismatchLambdas = null;
+ }
+
private QuiltRequiresAnnotationVisitor(int api, Runnable onModsMismatch, Runnable onModsMismatchLambdas) {
super(api);
+ this.onModsMissingDetailed = null;
this.onModsMismatch = onModsMismatch;
this.onModsMismatchLambdas = onModsMismatchLambdas;
}
@@ -164,19 +179,51 @@ public void visit(String name, Object value) {
@Override
public AnnotationVisitor visitArray(String name) {
if ("value".equals(name)) {
- return new AnnotationVisitor(api) {
+ if (onModsMissingDetailed == null) {
+ return new AnnotationVisitor(api) {
- @Override
- public void visit(String name, Object value) {
- if (!mods.contains(String.valueOf(value))) {
- onModsMismatch.run();
+ boolean anyMissing = false;
- if (stripLambdas && onModsMismatchLambdas != null) {
- onModsMismatchLambdas.run();
+ @Override
+ public void visit(String name, Object value) {
+ if (!mods.contains(String.valueOf(value))) {
+ anyMissing = true;
}
}
- }
- };
+
+ @Override
+ public void visitEnd() {
+ if (anyMissing) {
+ onModsMismatch.run();
+
+ if (stripLambdas && onModsMismatchLambdas != null) {
+ onModsMismatchLambdas.run();
+ }
+ }
+ }
+ };
+ } else {
+ return new AnnotationVisitor(api) {
+ List requiredMods = new ArrayList<>();
+ List missingMods = new ArrayList<>();
+
+ @Override
+ public void visit(String name, Object value) {
+ String mod = String.valueOf(value);
+ requiredMods.add(mod);
+ if (!mods.contains(mod)) {
+ missingMods.add(mod);
+ }
+ }
+
+ @Override
+ public void visitEnd() {
+ if (!missingMods.isEmpty()) {
+ onModsMissingDetailed.onModsMissing(requiredMods, missingMods);
+ }
+ }
+ };
+ }
}
else {
return null;
@@ -205,32 +252,49 @@ private AnnotationVisitor visitMemberAnnotation(String descriptor, boolean visib
return null;
}
- public StrippingData(int api, EnvType envType, List mods) {
- super(api);
- this.envType = envType;
+ public ClassStrippingData(int api, EnvType envType, List mods) {
+ super(api, envType, mods.stream().map(ModLoadOption::id).collect(Collectors.toSet()));
this.envTypeString = envType.name();
- this.mods = mods.stream().map(ModLoadOption::id).collect(Collectors.toList());
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.interfaces = interfaces;
+
+ if (name.endsWith("/package-info")) {
+ type = "package";
+ } else if ((access & Opcodes.ACC_ENUM) != 0) {
+ type = "enum";
+ } else if ((access & Opcodes.ACC_RECORD) != 0) {
+ type = "record";
+ } else if ((access & Opcodes.ACC_INTERFACE) != 0) {
+ type = "interface";
+ } else if ((access & Opcodes.ACC_ANNOTATION) != 0) {
+ type = "annotation";
+ } else {
+ type = "class";
+ }
+ }
+
+ @Override
+ protected String type() {
+ return type;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) {
if (envType == EnvType.SERVER) {
- stripEntireClass = true;
+ denyClientOnlyLoad();
}
} else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) {
if (envType == EnvType.CLIENT) {
- stripEntireClass = true;
+ denyDediServerOnlyLoad();
}
} else if (REQUIRES_DESCRIPTOR.equals(descriptor)) {
- return new QuiltRequiresAnnotationVisitor(api, () -> stripEntireClass = true, null);
+ return new QuiltRequiresAnnotationVisitor(api, this::checkHasAllMods);
} else if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) {
- return new FabricEnvironmentAnnotationVisitor(api, () -> stripEntireClass = true);
+ return new FabricEnvironmentAnnotationVisitor(api, () -> denyLoadReasons.add("Mismatched @Envrionment"));
} else if (ENVIRONMENT_INTERFACE_DESCRIPTOR.equals(descriptor)) {
return new FabricEnvironmentInterfaceAnnotationVisitor(api);
} else if (ENVIRONMENT_INTERFACES_DESCRIPTOR.equals(descriptor)) {
@@ -316,7 +380,7 @@ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
}
public boolean stripEntireClass() {
- return this.stripEntireClass;
+ return denyLoadReasons.size() > 0;
}
public Collection getStripInterfaces() {
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java
index 32e268bc9..e26559c8d 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java
@@ -28,10 +28,10 @@
import net.fabricmc.api.EnvType;
-/** Deprecated. All stuff were moved to {@link StrippingData}. */
+/** Deprecated. All stuff were moved to {@link ClassStrippingData}. */
@Deprecated
@QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED)
-public class EnvironmentStrippingData extends StrippingData {
+public class EnvironmentStrippingData extends ClassStrippingData {
public EnvironmentStrippingData(int api, EnvType envType) {
super(api, envType, new ArrayList<>());
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java
index 63b459cea..e52dec5ca 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java
@@ -17,11 +17,11 @@
package org.quiltmc.loader.impl.transformer;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.objectweb.asm.AnnotationVisitor;
-import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Type;
import org.quiltmc.loader.api.Requires;
import org.quiltmc.loader.api.minecraft.ClientOnly;
@@ -32,32 +32,25 @@
import net.fabricmc.api.EnvType;
@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL)
-public class PackageStrippingData extends ClassVisitor {
+public class PackageStrippingData extends AbstractStripData {
private static final String CLIENT_ONLY_DESCRIPTOR = Type.getDescriptor(ClientOnly.class);
private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class);
private static final String REQUIRES_DESCRIPTOR = Type.getDescriptor(Requires.class);
- private final EnvType envType;
- private final List mods;
-
- private boolean stripEntirePackage = false;
-
public PackageStrippingData(int api, EnvType envType, Map modCodeSourceMap) {
- super(api);
- this.envType = envType;
- this.mods = new ArrayList<>(modCodeSourceMap.keySet());
+ super(api, envType, new HashSet<>(modCodeSourceMap.keySet()));
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) {
if (envType == EnvType.SERVER) {
- stripEntirePackage = true;
+ denyClientOnlyLoad();
}
} else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) {
if (envType == EnvType.CLIENT) {
- stripEntirePackage = true;
+ denyDediServerOnlyLoad();
}
} else if (REQUIRES_DESCRIPTOR.equals(descriptor)) {
return new AnnotationVisitor(api) {
@@ -65,11 +58,17 @@ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
public AnnotationVisitor visitArray(String name) {
if ("value".equals(name)) {
return new AnnotationVisitor(api) {
+
+ final List requiredMods = new ArrayList<>();
+
@Override
public void visit(String name, Object value) {
- if (!mods.contains(String.valueOf(value))) {
- stripEntirePackage = true;
- }
+ requiredMods.add(String.valueOf(value));
+ }
+
+ @Override
+ public void visitEnd() {
+ checkHasAllMods(requiredMods);
}
};
}
@@ -82,7 +81,12 @@ public void visit(String name, Object value) {
return null;
}
+ @Override
+ protected String type() {
+ return "package";
+ }
+
public boolean stripEntirePackage() {
- return this.stripEntirePackage;
+ return denyLoadReasons.size() > 0;
}
}
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java b/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java
index ede8c5b1c..29acd7c99 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java
@@ -52,11 +52,11 @@ final class QuiltTransformer {
int visitorCount = 0;
if (strip) {
- StrippingData data = new StrippingData(QuiltLoaderImpl.ASM_VERSION, envType, cache.getMods());
+ ClassStrippingData data = new ClassStrippingData(QuiltLoaderImpl.ASM_VERSION, envType, cache.getMods());
classReader.accept(data, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
if (data.stripEntireClass()) {
- cache.hideClass(name);
+ cache.hideClass(name, data.summarizeDenyLoadReasons());
return null;
}
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java b/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java
index 944e810ca..59d534dc2 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java
@@ -170,6 +170,9 @@ private static List getRemapClasspath() throws IOException {
private static boolean requiresMixinRemap(Path inputPath) throws IOException {
final Manifest manifest = ManifestUtil.readManifest(inputPath);
+ if (manifest == null) {
+ return false;
+ }
final Attributes mainAttributes = manifest.getMainAttributes();
return REMAP_TYPE_STATIC.equalsIgnoreCase(mainAttributes.getValue(REMAP_TYPE_MANIFEST_KEY));
}
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java
index f5d6654c9..937fb5a74 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java
@@ -58,7 +58,7 @@ class TransformCache {
private final Path root;
private final Map modRoots = new HashMap<>();
private final List orderedMods;
- private final Set hiddenClasses = new HashSet<>();
+ private final Map hiddenClasses = new HashMap<>();
private static final boolean COPY_ON_WRITE = true;
public TransformCache(Path root, List orderedMods) {
@@ -165,8 +165,8 @@ public List getMods() {
return Collections.unmodifiableList(orderedMods);
}
- public Set getHiddenClasses() {
- return Collections.unmodifiableSet(hiddenClasses);
+ public Map getHiddenClasses() {
+ return Collections.unmodifiableMap(hiddenClasses);
}
public void forEachClassFile(ClassConsumer action)
@@ -176,8 +176,10 @@ public void forEachClassFile(ClassConsumer action)
}
}
- public void hideClass(String className) {
- hiddenClasses.add(className);
+ public void hideClass(String className, String denyReason) {
+ hiddenClasses.merge(className, denyReason, (current, nval) -> {
+ return current + "\n" + nval;
+ });
}
private static void copyFile(Path path, Path modSrc, Path modDst, CopyOption... copyOptions) {
@@ -239,7 +241,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
String fileName = file.getFileName().toString();
if (fileName.endsWith(".class") && couldBeJavaElement(fileName, true)) {
String name = LoaderUtil.getClassNameFromTransformCache(file.toString());
- if (!hiddenClasses.contains(name)) {
+ if (!hiddenClasses.containsKey(name)) {
byte[] result = action.run(mod, name, file);
if (result != null) {
Files.write(file, result);
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java
index 034a1916a..63280c26d 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java
@@ -17,6 +17,7 @@
package org.quiltmc.loader.impl.transformer;
import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOError;
import java.io.IOException;
import java.net.URI;
@@ -28,6 +29,7 @@
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -52,6 +54,9 @@
import org.quiltmc.loader.impl.util.SystemProperties;
import org.quiltmc.loader.impl.util.log.Log;
import org.quiltmc.loader.impl.util.log.LogCategory;
+import org.quiltmc.parsers.json.JsonReader;
+import org.quiltmc.parsers.json.JsonToken;
+import org.quiltmc.parsers.json.JsonWriter;
@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL)
public class TransformCacheManager {
@@ -64,7 +69,7 @@ public class TransformCacheManager {
private static final String CACHE_FILE = "files.zip";
private static final String FILE_TRANSFORM_COMPLETE = "__TRANSFORM_COMPLETE";
- private static final String HIDDEN_CLASSES_PATH = "hidden_classes.txt";
+ private static final String DENY_LOAD_REASONS_PATH = "deny_load_reasons.json";
public static TransformCacheResult populateTransformBundle(Path transformCacheFolder, List modList,
Map modOriginHash, ModSolveResult result) throws ModResolutionException {
@@ -103,7 +108,17 @@ public static TransformCacheResult populateTransformBundle(Path transformCacheFo
FilePreloadHelper.preLoad(transformCacheFolder.resolve(CACHE_FILE));
}
try {
- return new TransformCacheResult(existing, isNewlyGenerated, new HashSet<>(Files.readAllLines(existing.resolve(HIDDEN_CLASSES_PATH))));
+ Map hiddenClasses = new HashMap<>();
+ try (JsonReader reader = JsonReader.json(existing.resolve(DENY_LOAD_REASONS_PATH))) {
+ reader.beginObject();
+ while (reader.peek() == JsonToken.NAME) {
+ String clName = reader.nextName();
+ String clReason = reader.nextString();
+ hiddenClasses.put(clName, clReason);
+ }
+ reader.endObject();
+ }
+ return new TransformCacheResult(existing, isNewlyGenerated, hiddenClasses);
} catch (IOException e) {
throw new ModResolutionException("Failed to read hidden classes in the transform cache file!", e);
}
@@ -287,7 +302,17 @@ private static void writeTransformCache(String options, List modL
TransformCache cache = TransformCacheGenerator.generate(root, modList);
QuiltMapFileSystem.dumpEntries(root.getFileSystem(), "after-populate");
Files.write(root.resolve("options.txt"), options.getBytes(StandardCharsets.UTF_8));
- Files.write(root.resolve(HIDDEN_CLASSES_PATH), cache.getHiddenClasses());
+ try (JsonWriter json = JsonWriter.json(Files.newBufferedWriter(root.resolve(DENY_LOAD_REASONS_PATH)))) {
+ if (true) {
+ json.setIndent(" ");
+ }
+ json.beginObject();
+ for (Map.Entry entry : cache.getHiddenClasses().entrySet()) {
+ json.name(entry.getKey());
+ json.value(entry.getValue());
+ }
+ json.endObject();
+ }
Files.createFile(root.resolve(FILE_TRANSFORM_COMPLETE));
}
diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java
index 02c02b180..de637ce06 100644
--- a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java
+++ b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java
@@ -16,7 +16,7 @@
package org.quiltmc.loader.impl.transformer;
-import java.util.Set;
+import java.util.Map;
import org.quiltmc.loader.impl.filesystem.QuiltZipPath;
import org.quiltmc.loader.impl.util.QuiltLoaderInternal;
@@ -26,9 +26,9 @@
public class TransformCacheResult {
public final QuiltZipPath transformCacheRoot;
public final boolean isNewlyGenerated;
- public final Set hiddenClasses;
+ public final Map hiddenClasses;
- TransformCacheResult(QuiltZipPath transformCacheRoot, boolean isNewlyGenerated, Set hiddenClasses) {
+ TransformCacheResult(QuiltZipPath transformCacheRoot, boolean isNewlyGenerated, Map hiddenClasses) {
this.isNewlyGenerated = isNewlyGenerated;
this.transformCacheRoot = transformCacheRoot;
this.hiddenClasses = hiddenClasses;
diff --git a/src/main/resources/changelog/0.24.1.txt b/src/main/resources/changelog/0.24.1.txt
index e03081868..ad26b9ef3 100644
--- a/src/main/resources/changelog/0.24.1.txt
+++ b/src/main/resources/changelog/0.24.1.txt
@@ -1,5 +1,19 @@
Features:
+- [#414] Add a new `@Requires` annotation.
+ - This is similar to `@ClientOnly` or `@DedicatedServerOnly`, but instead removes an annotated element if the mod(s) specified are not present.
+ For example:
+
+```java
+public class MyOptionalTrinketItem extends Item implements @Requires("trinkets") Trinket {
+ @Override
+ @Requires("trinkets")
+ public Multimap getModifiers(ItemStack stack, SlotReference slot, LivingEntity entity, UUID uuid) {
+ // ...
+ }
+}
+```
+
- [#413] Include the reason in breakage errors (EnnuiL)
- Also check the game provider jar to see if it contains a mod.
- This was added for Cosmic Quilt, it doesn't affect Minecraft, but it may be useful to other game providers.
@@ -7,3 +21,4 @@ Features:
Bug Fixes:
- Fixed the forked error window throwing errors too early if it cannot create the new process
+- Fixed @DedicatedServerOnly and @ClientOnly not working when applied to packages.
diff --git a/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java b/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java
index a9938d63e..95659b154 100644
--- a/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java
+++ b/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java
@@ -34,7 +34,7 @@
import net.fabricmc.api.EnvType;
-import org.quiltmc.loader.impl.transformer.StrippingData;
+import org.quiltmc.loader.impl.transformer.ClassStrippingData;
public class LambdaStripTester {
@@ -65,7 +65,7 @@ public static void main(String[] args) throws IOException {
| ClassReader.SKIP_FRAMES
);
- StrippingData strip = new StrippingData(Opcodes.ASM9, EnvType.SERVER, new ArrayList<>());
+ ClassStrippingData strip = new ClassStrippingData(Opcodes.ASM9, EnvType.SERVER, new ArrayList<>());
reader.accept(strip, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
Collection stripMethods = strip.getStripMethods();