diff --git a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/McVersionLookup.java b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/McVersionLookup.java index 2f4fb40e4..3cec07e4c 100644 --- a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/McVersionLookup.java +++ b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/McVersionLookup.java @@ -136,16 +136,24 @@ public static void fillVersionFromJar(SimpleClassPath cp, McVersion.Builder buil return; } - CpEntry entry = cp.getEntry("net/minecraft/client/Minecraft.class"); + CpEntry minecraft = cp.getEntry("net/minecraft/client/Minecraft.class"); + CpEntry sharedConstants = cp.getEntry("net/minecraft/SharedConstants.class"); - if (entry != null) { + if (minecraft != null) { // version-like constant return value of a Minecraft method (obfuscated/unknown name) - if (fromAnalyzer(entry.getInputStream(), new MethodConstantRetVisitor(null), builder)) { + if (fromAnalyzer(minecraft.getInputStream(), new MethodConstantRetVisitor(null), builder)) { return; } // version-like constant passed into Display.setTitle in a Minecraft method (obfuscated/unknown name) - if (fromAnalyzer(entry.getInputStream(), new MethodStringConstantContainsVisitor("org/lwjgl/opengl/Display", "setTitle"), builder)) { + if (fromAnalyzer(minecraft.getInputStream(), new MethodStringConstantContainsVisitor("org/lwjgl/opengl/Display", "setTitle"), builder)) { + return; + } + } + + if (sharedConstants != null) { + // version constant set in SharedConstant's VERSION_STRING field (obfuscated/unknown name) + if (fromAnalyzer(sharedConstants.getInputStream(), new FieldNameVisitor("VERSION_STRING"), builder)) { return; } } @@ -684,6 +692,32 @@ protected void visitAnyInsn() { private String result; } + private static final class FieldNameVisitor extends ClassVisitor implements Analyzer { + FieldNameVisitor(String fieldName) { + super(FabricLoaderImpl.ASM_VERSION); + + this.fieldName = fieldName; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if (name.equals(fieldName)) { + this.result = (String) value; + } + + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public String getResult() { + return this.result; + } + + + private final String fieldName; + private String result; + } + private static final class MethodConstantRetVisitor extends ClassVisitor implements Analyzer { MethodConstantRetVisitor(String methodName) { super(FabricLoaderImpl.ASM_VERSION); diff --git a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java index 572855006..249f832f5 100644 --- a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java +++ b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java @@ -36,6 +36,7 @@ import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.ObjectShare; import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.extension.ModMetadataBuilder.ModDependencyBuilder; import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.impl.FabricLoaderImpl; import net.fabricmc.loader.impl.FormattedException; @@ -47,8 +48,8 @@ import net.fabricmc.loader.impl.game.minecraft.patch.EntrypointPatchFML125; import net.fabricmc.loader.impl.game.patch.GameTransformer; import net.fabricmc.loader.impl.launch.FabricLauncher; +import net.fabricmc.loader.impl.launch.MappingConfiguration; import net.fabricmc.loader.impl.metadata.BuiltinModMetadata; -import net.fabricmc.loader.impl.metadata.ModDependencyImpl; import net.fabricmc.loader.impl.util.Arguments; import net.fabricmc.loader.impl.util.ExceptionUtil; import net.fabricmc.loader.impl.util.LoaderUtil; @@ -119,7 +120,9 @@ public Collection getBuiltinMods() { int version = versionData.getClassVersion().getAsInt() - 44; try { - metadata.addDependency(new ModDependencyImpl(ModDependency.Kind.DEPENDS, "java", Collections.singletonList(String.format(Locale.ENGLISH, ">=%d", version)))); + metadata.addDependency(ModDependencyBuilder.create(ModDependency.Kind.DEPENDS, "java") + .addVersion(String.format(Locale.ENGLISH, ">=%d", version)) + .build()); } catch (VersionParsingException e) { throw new RuntimeException(e); } @@ -146,11 +149,6 @@ public Path getLaunchDirectory() { return getLaunchDirectory(arguments); } - @Override - public boolean isObfuscated() { - return true; // generally yes... - } - @Override public boolean requiresUrlClassLoader() { return hasModLoader; @@ -158,7 +156,7 @@ public boolean requiresUrlClassLoader() { @Override public boolean isEnabled() { - return System.getProperty(SystemProperties.SKIP_MC_PROVIDER) == null; + return !SystemProperties.isSet(SystemProperties.SKIP_MC_PROVIDER); } @Override @@ -287,7 +285,10 @@ private static Path getLaunchDirectory(Arguments argMap) { public void initialize(FabricLauncher launcher) { launcher.setValidParentClassPath(validParentClassPath); - if (isObfuscated()) { + String gameNs = System.getProperty(SystemProperties.GAME_MAPPING_NAMESPACE); + if (gameNs == null) gameNs = launcher.isDevelopment() ? MappingConfiguration.NAMED_NAMESPACE : MappingConfiguration.OFFICIAL_NAMESPACE; + + if (!gameNs.equals(launcher.getMappingConfiguration().getRuntimeNamespace())) { // game is obfuscated / in another namespace -> remap Map obfJars = new HashMap<>(3); String[] names = new String[gameJars.size()]; @@ -311,6 +312,7 @@ public void initialize(FabricLauncher launcher) { } obfJars = GameProviderHelper.deobfuscate(obfJars, + gameNs, getGameId(), getNormalizedGameVersion(), getLaunchDirectory(), launcher); diff --git a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java index 44ca87194..d955a64d9 100644 --- a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java +++ b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java @@ -66,7 +66,6 @@ public abstract class FabricTweaker extends FabricLauncherBase implements ITweak protected Arguments arguments; private LaunchClassLoader launchClassLoader; private final List classPath = new ArrayList<>(); - private boolean isDevelopment; @SuppressWarnings("unchecked") private final boolean isPrimaryTweaker = ((List) Launch.blackboard.get("Tweaks")).isEmpty(); @@ -76,12 +75,6 @@ public String getEntrypoint() { return getLaunchTarget(); } - @Override - public String getTargetNamespace() { - // TODO: Won't work outside of Yarn - return isDevelopment ? "named" : "intermediary"; - } - @Override public void acceptOptions(List localArgs, File gameDir, File assetsDir, String profile) { arguments = new Arguments(); @@ -98,7 +91,6 @@ public void acceptOptions(List localArgs, File gameDir, File assetsDir, @Override public void injectIntoClassLoader(LaunchClassLoader launchClassLoader) { - isDevelopment = Boolean.parseBoolean(System.getProperty(SystemProperties.DEVELOPMENT, "false")); Launch.blackboard.put(SystemProperties.DEVELOPMENT, isDevelopment); setProperties(Launch.blackboard); @@ -143,15 +135,14 @@ private void init() { arguments = null; - provider.initialize(this); - FabricLoaderImpl loader = FabricLoaderImpl.INSTANCE; loader.setGameProvider(provider); + provider.initialize(this); loader.load(); loader.freeze(); launchClassLoader.registerTransformer(FabricClassTransformer.class.getName()); - FabricLoaderImpl.INSTANCE.loadAccessWideners(); + FabricLoaderImpl.INSTANCE.loadClassTweakers(); // Setup Mixin environment MixinBootstrap.init(); @@ -303,9 +294,4 @@ private byte[] toByteArray(InputStream inputStream) throws IOException { return outputStream.toByteArray(); } - - @Override - public boolean isDevelopment() { - return isDevelopment; - } } diff --git a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/patch/EntrypointPatch.java b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/patch/EntrypointPatch.java index 3340efa60..8a23ea4c3 100644 --- a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/patch/EntrypointPatch.java +++ b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/patch/EntrypointPatch.java @@ -98,7 +98,7 @@ public void process(FabricLauncher launcher, Function class throw new RuntimeException("Could not find main method in " + entrypoint + "!"); } - if (type == EnvType.CLIENT && mainMethod.instructions.size() < 10) { + if (type == EnvType.CLIENT && mainMethod.instructions.size() < 18) { // 22w24+ forwards to another method in the same class instead of processing in main() directly, use that other method instead if that's the case MethodInsnNode invocation = null; @@ -108,13 +108,8 @@ public void process(FabricLauncher launcher, Function class if (invocation == null && insn.getType() == AbstractInsnNode.METHOD_INSN && (methodInsn = (MethodInsnNode) insn).owner.equals(mainClass.name)) { - // capture first method insn to the same class + // capture last method insn to the same class invocation = methodInsn; - } else if (insn.getOpcode() > Opcodes.ALOAD // ignore constant and variable loads as well as NOP, labels and line numbers - && insn.getOpcode() != Opcodes.RETURN) { // and RETURN - // found unexpected insn for a simple forwarding method - invocation = null; - break; } } diff --git a/src/main/java/net/fabricmc/loader/api/extension/LoaderExtensionApi.java b/src/main/java/net/fabricmc/loader/api/extension/LoaderExtensionApi.java new file mode 100644 index 000000000..1c7968994 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/extension/LoaderExtensionApi.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.api.extension; + +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.tree.ClassNode; + +import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModMetadata; + +public interface LoaderExtensionApi { // one instance per extension, binding the caller mod id + void addPathToCacheKey(Path path); + void setExternalModSource(); // referenced loader extension must run every time, even if all cache keys match + + ModCandidate readMod(Path path, /*@Nullable*/ String namespace); + ModCandidate readMod(List paths, /*@Nullable*/ String namespace); + ModCandidate createMod(List paths, ModMetadata metadata, Collection nestedMods); + + Collection getMods(String modId); + Collection getMods(); + boolean addMod(ModCandidate mod); + boolean removeMod(ModCandidate mod); + + void addModSource(Function source); + + void addToClassPath(Path path); + // TODO: add a way to add virtual resources (name + content) and classes + + void addMixinConfig(ModCandidate mod, String location); + + void addClassByteBufferTransformer(ClassTransformer transformer, String phase); + void addClassVisitorProvider(ClassTransformer provider, String phase); + void addClassNodeTransformer(ClassTransformer transformer, String phase); + + interface ClassTransformer { + String getName(); // name further identifying the transformer within the context mod + boolean appliesTo(String internalName, /*@Nullable*/ URL source); + /*@Nullable*/ T apply(String internalName, /*@Nullable*/ T input); // may reuse input! + } + + // TODO: resource transformers +} diff --git a/src/main/java/net/fabricmc/loader/api/extension/LoaderExtensionEntrypoint.java b/src/main/java/net/fabricmc/loader/api/extension/LoaderExtensionEntrypoint.java new file mode 100644 index 000000000..d65488e32 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/extension/LoaderExtensionEntrypoint.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.api.extension; + +@FunctionalInterface +public interface LoaderExtensionEntrypoint { + void initExtension(LoaderExtensionApi api); +} diff --git a/src/main/java/net/fabricmc/loader/api/extension/ModCandidate.java b/src/main/java/net/fabricmc/loader/api/extension/ModCandidate.java new file mode 100644 index 000000000..65e28beef --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/extension/ModCandidate.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.api.extension; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.metadata.ModMetadata; + +/** + * Representation of a mod that might get chosen to be loaded. + * + *

The data exposed here is read only, mutating it is not supported! + */ +public interface ModCandidate { + ModMetadata getMetadata(); + String getId(); + Version getVersion(); + + boolean hasPath(); + List getPaths(); + String getLocalPath(); + + boolean isRoot(); + Collection getContainingMods(); + Collection getContainedMods(); +} diff --git a/src/main/java/net/fabricmc/loader/api/extension/ModMetadataBuilder.java b/src/main/java/net/fabricmc/loader/api/extension/ModMetadataBuilder.java new file mode 100644 index 000000000..55f7074f2 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/extension/ModMetadataBuilder.java @@ -0,0 +1,126 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.api.extension; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.Collection; + +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.ContactInformation; +import net.fabricmc.loader.api.metadata.CustomValue; +import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.api.metadata.ModLoadCondition; +import net.fabricmc.loader.api.metadata.ModMetadata; +import net.fabricmc.loader.api.metadata.Person; +import net.fabricmc.loader.api.metadata.version.VersionPredicate; +import net.fabricmc.loader.impl.metadata.ModMetadataBuilderImpl; +import net.fabricmc.loader.impl.metadata.ModMetadataBuilderImpl.ContactInformationBuilderImpl; +import net.fabricmc.loader.impl.metadata.ModMetadataBuilderImpl.ModDependencyBuilderImpl; +import net.fabricmc.loader.impl.metadata.ModMetadataBuilderImpl.ModDependencyMetadataBuilderImpl; + +public interface ModMetadataBuilder extends ModMetadata { + static ModMetadataBuilder create() { + return new ModMetadataBuilderImpl(); + } + + ModMetadataBuilder setId(String modId); + ModMetadataBuilder setVersion(String version) throws VersionParsingException; + ModMetadataBuilder setVersion(Version version); + + ModMetadataBuilder addProvidedMod(String modId, /* @Nullable */ Version version, boolean exclusive); + + ModMetadataBuilder setEnvironment(ModEnvironment environment); + ModMetadataBuilder setLoadCondition(/* @Nullable */ ModLoadCondition loadCondition); + ModMetadataBuilder setLoadPhase(/* @Nullable */ String loadPhase); + + ModMetadataBuilder addEntrypoint(String key, String value, /* @Nullable */ String adapter); + ModMetadataBuilder addNestedMod(String location); + ModMetadataBuilder addMixinConfig(String location, /* @Nullable */ ModEnvironment environment); + ModMetadataBuilder addClassTweaker(String location); + + ModMetadataBuilder addDependency(ModDependency dependenc); + + ModMetadataBuilder setName(String name); + ModMetadataBuilder setDescription(String description); + ModMetadataBuilder addAuthor(String name, /* @Nullable */ ContactInformation contact); + ModMetadataBuilder addAuthor(Person person); + ModMetadataBuilder addContributor(String name, /* @Nullable */ ContactInformation contact); + ModMetadataBuilder addContributor(Person person); + ModMetadataBuilder setContact(/* @Nullable */ ContactInformation contact); + ModMetadataBuilder addLicense(String name); + ModMetadataBuilder setIcon(String location); + ModMetadataBuilder addIcon(int size, String location); + + ModMetadataBuilder addLanguageAdapter(String name, String cls); + + ModMetadataBuilder addCustomValue(String key, CustomValue value); + + void fromJson(Reader reader) throws IOException; + void toJson(Writer writer) throws IOException; + String toJson(); + + ModMetadata build(); + + interface ModDependencyBuilder { + static ModDependencyBuilder create() { + return new ModDependencyBuilderImpl(); + } + + static ModDependencyBuilder create(ModDependency.Kind kind, String modId) { + return new ModDependencyBuilderImpl().setKind(kind).setModId(modId); + } + + ModDependencyBuilder setKind(ModDependency.Kind kind); + ModDependencyBuilder setModId(String modId); + ModDependencyBuilder addVersion(String predicate) throws VersionParsingException; + ModDependencyBuilder addVersion(VersionPredicate predicate); + ModDependencyBuilder addVersions(Collection predicates); + ModDependencyBuilder setEnvironment(ModEnvironment environment); + ModDependencyBuilder setReason(/* @Nullable */ String reason); + ModDependencyBuilder setMetadata(/* @Nullable */ ModDependency.Metadata metadata); + ModDependencyBuilder setRootMetadata(/* @Nullable */ ModDependency.Metadata metadata); + + ModDependency build(); + } + + interface ModDependencyMetadataBuilder { + static ModDependencyMetadataBuilder create() { + return new ModDependencyMetadataBuilderImpl(); + } + + ModDependencyMetadataBuilder setModId(/* @Nullable */ String modId); + ModDependencyMetadataBuilder setName(/* @Nullable */ String name); + ModDependencyMetadataBuilder setDescription(/* @Nullable */ String description); + ModDependencyMetadataBuilder setContact(/* @Nullable */ ContactInformation contact); + + ModDependency.Metadata build(); + } + + interface ContactInformationBuilder { + static ContactInformationBuilder create() { + return new ContactInformationBuilderImpl(); + } + + ContactInformationBuilder set(String key, String value); + + ContactInformation build(); + } +} diff --git a/src/main/java/net/fabricmc/loader/api/metadata/CustomValue.java b/src/main/java/net/fabricmc/loader/api/metadata/CustomValue.java index 4afa26d35..17263e04a 100644 --- a/src/main/java/net/fabricmc/loader/api/metadata/CustomValue.java +++ b/src/main/java/net/fabricmc/loader/api/metadata/CustomValue.java @@ -16,8 +16,11 @@ package net.fabricmc.loader.api.metadata; +import java.util.List; import java.util.Map; +import net.fabricmc.loader.impl.metadata.CustomValueImpl; + /** * Represents a custom value in the {@code fabric.mod.json}. */ @@ -44,7 +47,7 @@ public interface CustomValue { CvArray getAsArray(); /** - * Returns this value as a {@link CvType#STRING}. + * Returns this value as a {@link String}. * * @return this value * @throws ClassCastException if this value is not a string @@ -52,13 +55,21 @@ public interface CustomValue { String getAsString(); /** - * Returns this value as a {@link CvType#NUMBER}. + * Returns this value as a {@link Number}. * * @return this value * @throws ClassCastException if this value is not a number */ Number getAsNumber(); + /** + * Returns this value as an integer. + * + * @return this value + * @throws ClassCastException if this value is not an integer or out of range + */ + int getAsInteger(); + /** * Returns this value as a {@link CvType#BOOLEAN}. * @@ -91,6 +102,15 @@ interface CvObject extends CustomValue, Iterable> * @return the value associated, or {@code null} if no such value is present */ CustomValue get(String key); + + /** + * Gets the value associated with a {@code key} within this object value or the default value if there is none. + * + * @param key the key to check + * @param defaultValue the default value to return if there is no value for the key + * @return the value associated, or {@code defaultValue} if no such value is present + */ + CustomValue getOrDefault(String key, CustomValue defaultValue); } /** @@ -118,4 +138,28 @@ interface CvArray extends CustomValue, Iterable { enum CvType { OBJECT, ARRAY, STRING, NUMBER, BOOLEAN, NULL; } + + static CvObject of(Map map) { + return CustomValueImpl.of(map); + } + + static CvArray of(List list) { + return CustomValueImpl.of(list); + } + + static CustomValue of(String value) { + return CustomValueImpl.of(value); + } + + static CustomValue of(Number value) { + return CustomValueImpl.of(value); + } + + static CustomValue of(boolean value) { + return CustomValueImpl.of(value); + } + + static CustomValue ofNull() { + return CustomValueImpl.ofNull(); + } } diff --git a/src/main/java/net/fabricmc/loader/api/metadata/ModDependency.java b/src/main/java/net/fabricmc/loader/api/metadata/ModDependency.java index a4d34b72f..0d16cf356 100644 --- a/src/main/java/net/fabricmc/loader/api/metadata/ModDependency.java +++ b/src/main/java/net/fabricmc/loader/api/metadata/ModDependency.java @@ -117,4 +117,11 @@ private static Map createMap() { return ret; } } + + public interface Metadata { + String getId(); + String getName(); + String getDescription(); + ContactInformation getContact(); + } } diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ModLoadCondition.java b/src/main/java/net/fabricmc/loader/api/metadata/ModLoadCondition.java similarity index 97% rename from src/main/java/net/fabricmc/loader/impl/discovery/ModLoadCondition.java rename to src/main/java/net/fabricmc/loader/api/metadata/ModLoadCondition.java index 126edcb82..cdcf2ca7a 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/ModLoadCondition.java +++ b/src/main/java/net/fabricmc/loader/api/metadata/ModLoadCondition.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.loader.impl.discovery; +package net.fabricmc.loader.api.metadata; /** * Conditions for whether to load a mod. diff --git a/src/main/java/net/fabricmc/loader/api/metadata/ModMetadata.java b/src/main/java/net/fabricmc/loader/api/metadata/ModMetadata.java index 7f4478892..25949842a 100644 --- a/src/main/java/net/fabricmc/loader/api/metadata/ModMetadata.java +++ b/src/main/java/net/fabricmc/loader/api/metadata/ModMetadata.java @@ -16,7 +16,10 @@ package net.fabricmc.loader.api.metadata; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -48,14 +51,29 @@ public interface ModMetadata { */ String getId(); + @Deprecated + default Collection getProvides() { + Collection mods = getAdditionallyProvidedMods(); + if (mods.isEmpty()) return Collections.emptyList(); + + List ret = new ArrayList<>(mods.size()); + + for (ProvidedMod mod : mods) { + ret.add(mod.getId()); + } + + return ret; + } + /** - * Returns the mod's ID provides. + * Return mods additionally provided by this mod as declared by the {@code provides} directive. * - *

The aliases follow the same rules as ID

+ *

This does not relate to nested (JIJ) mods, provided mods are supposed to be represented by this mod itself. + * Typical implementations include offered APIs, emulation or compatibility layers. * - * @return the mod's ID provides + * @return additionally provided mods (excluding this mod) */ - Collection getProvides(); + Collection getAdditionallyProvidedMods(); /** * Returns the mod's version. @@ -67,10 +85,12 @@ public interface ModMetadata { */ ModEnvironment getEnvironment(); + ModLoadCondition getLoadCondition(); + /** * Returns all of the mod's dependencies. */ - Collection getDependencies(); + Collection getDependencies(); /** * Returns the mod's required dependencies, without which the Loader will terminate loading. @@ -199,5 +219,7 @@ default Collection getBreaks() { * @deprecated Use {@link #containsCustomValue} instead, this will be removed (can't expose GSON types)! */ @Deprecated - boolean containsCustomElement(String key); + default boolean containsCustomElement(String key) { + return containsCustomValue(key); + } } diff --git a/src/main/java/net/fabricmc/loader/api/metadata/ProvidedMod.java b/src/main/java/net/fabricmc/loader/api/metadata/ProvidedMod.java new file mode 100644 index 000000000..f949abf86 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/metadata/ProvidedMod.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.api.metadata; + +import net.fabricmc.loader.api.Version; + +public interface ProvidedMod { + /** + * Returns the provided mod ID. + * + * @return provided mod id + */ + String getId(); + + /** + * Returns the provided mod version. + * + * @return provided mod version + */ + Version getVersion(); + + /** + * Returns whether the provided mod is exclusive and can't share its id with any mod provided by another mod. + * + * @return true if this provided mod id must be exclusively loaded, false otherwise + */ + boolean isExclusive(); +} diff --git a/src/main/java/net/fabricmc/loader/api/metadata/version/VersionPredicate.java b/src/main/java/net/fabricmc/loader/api/metadata/version/VersionPredicate.java index 16b7ae13d..c14ab1b5e 100644 --- a/src/main/java/net/fabricmc/loader/api/metadata/version/VersionPredicate.java +++ b/src/main/java/net/fabricmc/loader/api/metadata/version/VersionPredicate.java @@ -43,6 +43,10 @@ interface PredicateTerm { Version getReferenceVersion(); } + static VersionPredicate any() { + return VersionPredicateParser.any(); + } + static VersionPredicate parse(String predicate) throws VersionParsingException { return VersionPredicateParser.parse(predicate); } diff --git a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java index e633fd55d..61b454f52 100644 --- a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java +++ b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java @@ -24,12 +24,14 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import org.objectweb.asm.Opcodes; @@ -41,17 +43,22 @@ import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.ObjectShare; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; +import net.fabricmc.loader.api.extension.LoaderExtensionApi; +import net.fabricmc.loader.api.extension.LoaderExtensionEntrypoint; +import net.fabricmc.loader.api.metadata.ProvidedMod; import net.fabricmc.loader.impl.discovery.ArgumentModCandidateFinder; import net.fabricmc.loader.impl.discovery.ClasspathModCandidateFinder; import net.fabricmc.loader.impl.discovery.DirectoryModCandidateFinder; -import net.fabricmc.loader.impl.discovery.ModCandidate; +import net.fabricmc.loader.impl.discovery.ModCandidateImpl; import net.fabricmc.loader.impl.discovery.ModDiscoverer; import net.fabricmc.loader.impl.discovery.ModResolutionException; import net.fabricmc.loader.impl.discovery.ModResolver; +import net.fabricmc.loader.impl.discovery.ModResolver.ResolutionContext; import net.fabricmc.loader.impl.discovery.RuntimeModRemapper; import net.fabricmc.loader.impl.entrypoint.EntrypointStorage; import net.fabricmc.loader.impl.game.GameProvider; import net.fabricmc.loader.impl.launch.FabricLauncherBase; +import net.fabricmc.loader.impl.launch.MappingConfiguration; import net.fabricmc.loader.impl.launch.knot.Knot; import net.fabricmc.loader.impl.metadata.DependencyOverrides; import net.fabricmc.loader.impl.metadata.EntrypointMetadata; @@ -77,8 +84,9 @@ public final class FabricLoaderImpl extends net.fabricmc.loader.FabricLoader { public static final String REMAPPED_JARS_DIR_NAME = "remappedJars"; // relative to cache dir private static final String TMP_DIR_NAME = "tmp"; // relative to cache dir + private static final String EXTENSION_ENTRY_POINT = "extension"; + protected final Map modMap = new HashMap<>(); - private List modCandidates; protected List mods = new ArrayList<>(); private final Map adapterMap = new HashMap<>(); @@ -96,8 +104,14 @@ public final class FabricLoaderImpl extends net.fabricmc.loader.FabricLoader { private Path gameDir; private Path configDir; + private ModDiscoverer discoverer; + private FabricLoaderImpl() { } + boolean isFrozen() { + return frozen; + } + /** * Freeze the FabricLoader, preventing additional mods from being loaded. */ @@ -107,6 +121,7 @@ public void freeze() { } frozen = true; + discoverer = null; finishModLoading(); } @@ -127,7 +142,7 @@ public void setGameProvider(GameProvider provider) { } private void setGameDir(Path gameDir) { - this.gameDir = gameDir; + this.gameDir = gameDir.toAbsolutePath().normalize(); this.configDir = gameDir.resolve("config"); } @@ -179,6 +194,10 @@ public File getConfigDirectory() { return getConfigDir().toFile(); } + ModDiscoverer getDiscoverer() { + return discoverer; + } + public void load() { if (provider == null) throw new IllegalStateException("game provider not set"); if (frozen) throw new IllegalStateException("Frozen - cannot load additional mods!"); @@ -195,19 +214,21 @@ public void load() { } private void setup() throws ModResolutionException { + adapterMap.put("default", DefaultLanguageAdapter.INSTANCE); + boolean remapRegularMods = isDevelopmentEnvironment(); VersionOverrides versionOverrides = new VersionOverrides(); DependencyOverrides depOverrides = new DependencyOverrides(configDir); // discover mods - ModDiscoverer discoverer = new ModDiscoverer(versionOverrides, depOverrides); + discoverer = new ModDiscoverer(versionOverrides, depOverrides); discoverer.addCandidateFinder(new ClasspathModCandidateFinder()); discoverer.addCandidateFinder(new DirectoryModCandidateFinder(gameDir.resolve("mods"), remapRegularMods)); discoverer.addCandidateFinder(new ArgumentModCandidateFinder(remapRegularMods)); - Map> envDisabledMods = new HashMap<>(); - modCandidates = discoverer.discoverMods(this, envDisabledMods); + Map> envDisabledMods = new HashMap<>(); + List candidates = discoverer.discoverMods(this, envDisabledMods); // dump version and dependency overrides info @@ -221,92 +242,139 @@ private void setup() throws ModResolutionException { // resolve mods - modCandidates = ModResolver.resolve(modCandidates, getEnvironmentType(), envDisabledMods); + ResolutionContext context = new ResolutionContext(candidates, getEnvironmentType(), envDisabledMods, this::addMods); + ModResolver.resolve(context); + + // sort mods alphabetical + + mods.sort(Comparator.comparing(ModContainerImpl::getId)); // dump mod list StringBuilder modListText = new StringBuilder(); - for (ModCandidate mod : modCandidates) { + for (ModContainerImpl mod : mods) { if (modListText.length() > 0) modListText.append('\n'); modListText.append("\t- "); modListText.append(mod.getId()); modListText.append(' '); - modListText.append(mod.getVersion().getFriendlyString()); + modListText.append(mod.getMetadata().getVersion().getFriendlyString()); + + ModContainer parent = mod.getContainingMod().orElse(null); - if (!mod.getParentMods().isEmpty()) { + if (parent != null) { modListText.append(" via "); - modListText.append(mod.getParentMods().iterator().next().getId()); + modListText.append(parent.getMetadata().getId()); } } - int count = modCandidates.size(); + int count = mods.size(); Log.info(LogCategory.GENERAL, "Loading %d mod%s:%n%s", count, count != 1 ? "s" : "", modListText); + // final sort (may be non-alphabetic) + + sortMods(mods, ModContainerImpl::getId); + } + + private void addMods(List mods, String phase, ResolutionContext context) { Path cacheDir = gameDir.resolve(CACHE_DIR_NAME); Path outputdir = cacheDir.resolve(PROCESSED_MODS_DIR_NAME); // runtime mod remapping - if (remapRegularMods) { + if (isDevelopmentEnvironment()) { if (System.getProperty(SystemProperties.REMAP_CLASSPATH_FILE) == null) { Log.warn(LogCategory.MOD_REMAP, "Runtime mod remapping disabled due to no fabric.remapClasspathFile being specified. You may need to update loom."); } else { - RuntimeModRemapper.remap(modCandidates, cacheDir.resolve(TMP_DIR_NAME), outputdir); + RuntimeModRemapper.remap(mods, context.getMods(), cacheDir.resolve(TMP_DIR_NAME), outputdir); } } - // shuffle mods in-dev to reduce the risk of false order reliance, apply late load requests + sortMods(mods, ModCandidateImpl::getId); + + // add mods - if (isDevelopmentEnvironment() && System.getProperty(SystemProperties.DEBUG_DISABLE_MOD_SHUFFLE) == null) { - Collections.shuffle(modCandidates); + List createdMods = new ArrayList<>(mods.size()); + + for (ModCandidateImpl candidate : mods) { + // prepare nio compatible path + if (!candidate.hasPath() && !candidate.isBuiltin()) { + try { + candidate.setPaths(Collections.singletonList(candidate.copyToDir(outputdir, false))); + } catch (IOException e) { + throw new RuntimeException("Error extracting mod "+candidate, e); + } + } + + // create mod container + ModContainerImpl mod = addMod(candidate); + createdMods.add(mod); + + // add mod to classpath + if (!mod.getId().equals(MOD_ID) && !mod.getMetadata().getType().equals("builtin")) { + for (Path path : mod.getCodeSourcePaths()) { + FabricLauncherBase.getLauncher().addToClassPath(path); + } + } } - String modsToLoadLate = System.getProperty(SystemProperties.DEBUG_LOAD_LATE); + // run extension entrypoints - if (modsToLoadLate != null) { - for (String modId : modsToLoadLate.split(",")) { - for (Iterator it = modCandidates.iterator(); it.hasNext(); ) { - ModCandidate mod = it.next(); + boolean foundEntrypoints = false; - if (mod.getId().equals(modId)) { - it.remove(); - modCandidates.add(mod); - break; - } + for (ModContainerImpl mod : createdMods) { + try { + for (EntrypointMetadata in : mod.getMetadata().getEntrypoints(EXTENSION_ENTRY_POINT)) { + foundEntrypoints = true; + entrypointStorage.add(mod, EXTENSION_ENTRY_POINT, in, adapterMap); } + } catch (Throwable t) { + throw new RuntimeException("Error initializing extension entrypoint for mod "+mod.getId(), t); } } - // add mods + if (foundEntrypoints) { + for (EntrypointContainer entrypoint : getEntrypointContainers(EXTENSION_ENTRY_POINT, LoaderExtensionEntrypoint.class)) { + ModContainer mod = entrypoint.getProvider(); + if (!createdMods.contains(mod)) continue; + + LoaderExtensionApi api = new LoaderExtensionApiImpl(mod.getMetadata().getId(), context); - for (ModCandidate mod : modCandidates) { - if (!mod.hasPath() && !mod.isBuiltin()) { try { - mod.setPaths(Collections.singletonList(mod.copyToDir(outputdir, false))); - } catch (IOException e) { - throw new RuntimeException("Error extracting mod "+mod, e); + entrypoint.getEntrypoint().initExtension(api); + } catch (Throwable t) { + throw new RuntimeException("Error invoking extension entrypoint for mod "+mod.getMetadata().getId(), t); } } + } + } - addMod(mod); + private void sortMods(List mods, Function idGetter) { + // shuffle mods in-dev to reduce the risk of false order reliance, apply late load requests + + if (isDevelopmentEnvironment() && !SystemProperties.isSet(SystemProperties.DEBUG_DISABLE_MOD_SHUFFLE)) { + Collections.shuffle(mods); } - modCandidates = null; - } + String modsToLoadLate = System.getProperty(SystemProperties.DEBUG_LOAD_LATE); - private void finishModLoading() { - // add mods to classpath - // TODO: This can probably be made safer, but that's a long-term goal - for (ModContainerImpl mod : mods) { - if (!mod.getMetadata().getId().equals(MOD_ID) && !mod.getMetadata().getType().equals("builtin")) { - for (Path path : mod.getCodeSourcePaths()) { - FabricLauncherBase.getLauncher().addToClassPath(path); + if (modsToLoadLate != null) { + for (String modId : modsToLoadLate.split(",")) { + for (Iterator it = mods.iterator(); it.hasNext(); ) { + T mod = it.next(); + + if (idGetter.apply(mod).equals(modId)) { + it.remove(); + mods.add(mod); + break; + } } } } + } + private void finishModLoading() { setupLanguageAdapters(); setupMods(); } @@ -328,10 +396,8 @@ public List> getEntrypointContainers(String key, Clas @Override public MappingResolver getMappingResolver() { if (mappingResolver == null) { - mappingResolver = new MappingResolverImpl( - FabricLauncherBase.getLauncher().getMappingConfiguration()::getMappings, - FabricLauncherBase.getLauncher().getTargetNamespace() - ); + MappingConfiguration config = FabricLauncherBase.getLauncher().getMappingConfiguration(); + mappingResolver = new MappingResolverImpl(config::getMappings, config.getRuntimeNamespace()); } return mappingResolver; @@ -342,21 +408,15 @@ public ObjectShare getObjectShare() { return objectShare; } - public ModCandidate getModCandidate(String id) { - if (modCandidates == null) return null; - - for (ModCandidate mod : modCandidates) { - if (mod.getId().equals(id)) return mod; - } - - return null; - } - @Override - public Optional getModContainer(String id) { + public Optional getModContainer(String id) { return Optional.ofNullable(modMap.get(id)); } + public ModContainerImpl getModInternal(String id) { + return modMap.get(id); + } + @Override public Collection getAllMods() { return Collections.unmodifiableList(mods); @@ -376,22 +436,26 @@ public boolean isDevelopmentEnvironment() { return FabricLauncherBase.getLauncher().isDevelopment(); } - private void addMod(ModCandidate candidate) throws ModResolutionException { + private ModContainerImpl addMod(ModCandidateImpl candidate) { ModContainerImpl container = new ModContainerImpl(candidate); mods.add(container); modMap.put(candidate.getId(), container); - for (String provides : candidate.getProvides()) { - modMap.put(provides, container); + for (ProvidedMod mod : candidate.getAdditionallyProvidedMods()) { + if (mod.isExclusive()) { + modMap.put(mod.getId(), container); + } else { + modMap.putIfAbsent(mod.getId(), container); + } } + + return container; } private void setupLanguageAdapters() { - adapterMap.put("default", DefaultLanguageAdapter.INSTANCE); - for (ModContainerImpl mod : mods) { // add language adapters - for (Map.Entry laEntry : mod.getInfo().getLanguageAdapterDefinitions().entrySet()) { + for (Map.Entry laEntry : mod.getMetadata().getLanguageAdapterDefinitions().entrySet()) { if (adapterMap.containsKey(laEntry.getKey())) { throw new RuntimeException("Duplicate language adapter key: " + laEntry.getKey() + "! (" + laEntry.getValue() + ", " + adapterMap.get(laEntry.getKey()).getClass().getName() + ")"); } @@ -408,37 +472,39 @@ private void setupLanguageAdapters() { private void setupMods() { for (ModContainerImpl mod : mods) { try { - for (String in : mod.getInfo().getOldInitializers()) { - String adapter = mod.getInfo().getOldStyleLanguageAdapter(); + for (String in : mod.getMetadata().getOldInitializers()) { + String adapter = mod.getMetadata().getOldStyleLanguageAdapter(); entrypointStorage.addDeprecated(mod, adapter, in); } - for (String key : mod.getInfo().getEntrypointKeys()) { - for (EntrypointMetadata in : mod.getInfo().getEntrypoints(key)) { + for (String key : mod.getMetadata().getEntrypointKeys()) { + for (EntrypointMetadata in : mod.getMetadata().getEntrypoints(key)) { entrypointStorage.add(mod, key, in, adapterMap); } } } catch (Exception e) { - throw new RuntimeException(String.format("Failed to setup mod %s (%s)", mod.getInfo().getName(), mod.getOrigin()), e); + throw new RuntimeException(String.format("Failed to setup mod %s (%s)", mod.getMetadata().getName(), mod.getOrigin()), e); } } } - public void loadAccessWideners() { + public void loadClassTweakers() { AccessWidenerReader accessWidenerReader = new AccessWidenerReader(accessWidener); - for (net.fabricmc.loader.api.ModContainer modContainer : getAllMods()) { + for (ModContainer modContainer : mods) { LoaderModMetadata modMetadata = (LoaderModMetadata) modContainer.getMetadata(); - String accessWidener = modMetadata.getAccessWidener(); - if (accessWidener == null) continue; + Collection classTweakers = modMetadata.getClassTweakers(); + if (classTweakers.isEmpty()) continue; - Path path = modContainer.findPath(accessWidener).orElse(null); - if (path == null) throw new RuntimeException(String.format("Missing accessWidener file %s from mod %s", accessWidener, modContainer.getMetadata().getId())); + for (String loc : classTweakers) { + Path path = modContainer.findPath(loc).orElse(null); + if (path == null) throw new RuntimeException(String.format("Missing classTweaker (accessWidener) file %s from mod %s", loc, modContainer.getMetadata().getId())); - try (BufferedReader reader = Files.newBufferedReader(path)) { - accessWidenerReader.read(reader, getMappingResolver().getCurrentRuntimeNamespace()); - } catch (Exception e) { - throw new RuntimeException("Failed to read accessWidener file from mod " + modMetadata.getId(), e); + try (BufferedReader reader = Files.newBufferedReader(path)) { + accessWidenerReader.read(reader, getMappingResolver().getCurrentRuntimeNamespace()); + } catch (Exception e) { + throw new RuntimeException("Failed to read classTweaker (accessWidener) file from mod " + modMetadata.getId(), e); + } } } } diff --git a/src/main/java/net/fabricmc/loader/impl/LoaderExtensionApiImpl.java b/src/main/java/net/fabricmc/loader/impl/LoaderExtensionApiImpl.java new file mode 100644 index 000000000..d0507535a --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/LoaderExtensionApiImpl.java @@ -0,0 +1,263 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl; + +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.tree.ClassNode; + +import net.fabricmc.loader.api.extension.LoaderExtensionApi; +import net.fabricmc.loader.api.extension.ModCandidate; +import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModMetadata; +import net.fabricmc.loader.impl.discovery.ModCandidateImpl; +import net.fabricmc.loader.impl.discovery.ModDiscoverer; +import net.fabricmc.loader.impl.discovery.ModResolver.ResolutionContext; +import net.fabricmc.loader.impl.launch.FabricLauncherBase; +import net.fabricmc.loader.impl.metadata.LoaderModMetadata; +import net.fabricmc.loader.impl.metadata.ModMetadataBuilderImpl; + +public final class LoaderExtensionApiImpl implements LoaderExtensionApi { + static final List> modSources = new ArrayList<>(); // TODO: use this + static final List mixinConfigs = new ArrayList<>(); + // TODO: use these: + static final List> byteBufferTransformers = new ArrayList<>(); + static final List> classVisitorProviders = new ArrayList<>(); + static final List> classNodeTransformers = new ArrayList<>(); + + private final String pluginModId; + private final ResolutionContext context; + + public LoaderExtensionApiImpl(String pluginModId, ResolutionContext context) { + this.pluginModId = pluginModId; + this.context = context; + } + + @Override + public void addPathToCacheKey(Path path) { + checkFrozen(); + Objects.requireNonNull(path, "null path"); + + // TODO Auto-generated method stub + } + + @Override + public void setExternalModSource() { + checkFrozen(); + + // TODO Auto-generated method stub + } + + @Override + public ModCandidate readMod(Path path, /*@Nullable*/ String namespace) { + Objects.requireNonNull(path, "null path"); + + return readMod(Collections.singletonList(path), namespace); + } + + @Override + public ModCandidate readMod(List paths, /*@Nullable*/ String namespace) { + checkFrozen(); + if (paths.isEmpty()) throw new IllegalArgumentException("empty paths"); + + ModDiscoverer discoverer = FabricLoaderImpl.INSTANCE.getDiscoverer(); + if (discoverer == null) throw new IllegalStateException("createMod is only available during mod discovery"); + + boolean remap = namespace != null && !namespace.equals(FabricLauncherBase.getLauncher().getMappingConfiguration().getRuntimeNamespace()); + + return discoverer.scan(normalizePaths(paths), remap); + } + + @Override + public ModCandidate createMod(List paths, ModMetadata metadata, Collection nestedMods) { + checkFrozen(); + if (paths.isEmpty()) throw new IllegalArgumentException("empty paths"); + Objects.requireNonNull(metadata, "null metadata"); + Objects.requireNonNull(nestedMods, "null nestedMods"); + + LoaderModMetadata loaderMeta; + + if (metadata instanceof LoaderModMetadata) { + loaderMeta = (LoaderModMetadata) metadata; + } else if (metadata instanceof ModMetadataBuilderImpl) { + loaderMeta = ((ModMetadataBuilderImpl) metadata).build(); + } else { // TODO: wrap other types + throw new IllegalArgumentException("invalid ModMetadata class: "+metadata.getClass()); + } + + loaderMeta.applyEnvironment(context.envType); + + Collection nestedModsCopy; + + if (nestedMods.isEmpty()) { + nestedModsCopy = Collections.emptyList(); + } else { + nestedModsCopy = new ArrayList<>(nestedMods.size()); + + for (ModCandidate mod : nestedMods) { + if (!(mod instanceof ModCandidateImpl)) throw new IllegalArgumentException("invalid ModCandidate class: "+mod.getClass()); + nestedModsCopy.add((ModCandidateImpl) mod); + } + } + + return ModCandidateImpl.createPlain(normalizePaths(paths), loaderMeta, false, nestedModsCopy); + } + + private static List normalizePaths(List paths) { + List ret = new ArrayList<>(paths.size()); + + for (Path p : paths) { + ret.add(p.toAbsolutePath().normalize()); + } + + return ret; + } + + @Override + public Collection getMods(String modId) { + checkFrozen(); + Objects.requireNonNull(modId, "null modId"); + + return Collections.unmodifiableCollection(context.getMods(modId)); + } + + @Override + public Collection getMods() { + checkFrozen(); + + return Collections.unmodifiableCollection(context.getMods()); + } + + @Override + public boolean addMod(ModCandidate mod) { + checkFrozen(); + Objects.requireNonNull(mod, "null mod"); + if (!(mod instanceof ModCandidateImpl)) throw new IllegalArgumentException("invalid ModCandidate class: "+mod.getClass()); + + if (!context.addMod((ModCandidateImpl) mod)) return false; + + for (ModCandidate m : mod.getContainedMods()) { + addMod(m); + } + + return true; + } + + @Override + public boolean removeMod(ModCandidate mod) { + checkFrozen(); + Objects.requireNonNull(mod, "null mod"); + if (!(mod instanceof ModCandidateImpl)) throw new IllegalArgumentException("invalid ModCandidate class: "+mod.getClass()); + + return context.removeMod((ModCandidateImpl) mod); + } + + @Override + public void addModSource(Function source) { + checkFrozen(); + Objects.requireNonNull(source, "null source"); + + modSources.add(source); + } + + @Override + public void addToClassPath(Path path) { + checkFrozen(); + Objects.requireNonNull(path, "null path"); + + FabricLauncherBase.getLauncher().addToClassPath(path); + } + + @Override + public void addMixinConfig(ModCandidate mod, String location) { + checkFrozen(); + Objects.requireNonNull(mod, "null mod"); + Objects.requireNonNull(location, "null location"); + + mixinConfigs.add(new MixinConfigEntry(pluginModId, mod.getId(), location)); + } + + @Override + public void addClassByteBufferTransformer(ClassTransformer transformer, String phase) { + checkFrozen(); + Objects.requireNonNull(transformer, "null transformer"); + Objects.requireNonNull(phase, "null phase"); + if (transformer.getName().isEmpty()) throw new IllegalArgumentException("transformer without name"); + + byteBufferTransformers.add(new TransformerEntry<>(pluginModId, phase, transformer)); + } + + @Override + public void addClassVisitorProvider(ClassTransformer provider, String phase) { + checkFrozen(); + Objects.requireNonNull(provider, "null provider"); + Objects.requireNonNull(phase, "null phase"); + if (provider.getName().isEmpty()) throw new IllegalArgumentException("provider without name"); + + classVisitorProviders.add(new TransformerEntry<>(pluginModId, phase, provider)); + } + + @Override + public void addClassNodeTransformer(ClassTransformer transformer, String phase) { + checkFrozen(); + Objects.requireNonNull(transformer, "null transformer"); + Objects.requireNonNull(phase, "null phase"); + if (transformer.getName().isEmpty()) throw new IllegalArgumentException("transformer without name"); + + classNodeTransformers.add(new TransformerEntry<>(pluginModId, phase, transformer)); + } + + private static void checkFrozen() { + if (FabricLoaderImpl.INSTANCE.isFrozen()) throw new IllegalStateException("loading progress advanced beyond where loader plugins may act"); + } + + public static List getMixinConfigs() { + return mixinConfigs; + } + + public static final class MixinConfigEntry { + public final String extensionModId; + public final String modId; + public final String location; + + MixinConfigEntry(String extensionModId, String modId, String location) { + this.extensionModId = extensionModId; + this.modId = modId; + this.location = location; + } + } + + static final class TransformerEntry { + final String extensionModId; + final String phase; + final ClassTransformer transformer; + + TransformerEntry(String extensionModId, String phase, ClassTransformer transformer) { + this.extensionModId = extensionModId; + this.phase = phase; + this.transformer = transformer; + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/ModContainerImpl.java b/src/main/java/net/fabricmc/loader/impl/ModContainerImpl.java index 660af4046..0dd62a75b 100644 --- a/src/main/java/net/fabricmc/loader/impl/ModContainerImpl.java +++ b/src/main/java/net/fabricmc/loader/impl/ModContainerImpl.java @@ -30,7 +30,7 @@ import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.metadata.ModOrigin; -import net.fabricmc.loader.impl.discovery.ModCandidate; +import net.fabricmc.loader.impl.discovery.ModCandidateImpl; import net.fabricmc.loader.impl.metadata.LoaderModMetadata; import net.fabricmc.loader.impl.metadata.ModOriginImpl; import net.fabricmc.loader.impl.util.FileSystemUtil; @@ -47,14 +47,14 @@ public class ModContainerImpl extends net.fabricmc.loader.ModContainer { private volatile List roots; - public ModContainerImpl(ModCandidate candidate) { + public ModContainerImpl(ModCandidateImpl candidate) { this.info = candidate.getMetadata(); this.codeSourcePaths = candidate.getPaths(); - this.parentModId = candidate.getParentMods().isEmpty() ? null : candidate.getParentMods().iterator().next().getId(); - this.childModIds = candidate.getNestedMods().isEmpty() ? Collections.emptyList() : new ArrayList<>(candidate.getNestedMods().size()); + this.parentModId = candidate.getContainingMods().isEmpty() ? null : candidate.getContainingMods().iterator().next().getId(); + this.childModIds = candidate.getContainedMods().isEmpty() ? Collections.emptyList() : new ArrayList<>(candidate.getContainedMods().size()); - for (ModCandidate c : candidate.getNestedMods()) { - if (c.getParentMods().size() <= 1 || c.getParentMods().iterator().next() == candidate) { + for (ModCandidateImpl c : candidate.getContainedMods()) { + if (c.getContainingMods().size() <= 1 || c.getContainingMods().iterator().next() == candidate) { childModIds.add(c.getId()); } } @@ -63,6 +63,10 @@ public ModContainerImpl(ModCandidate candidate) { this.origin = paths != null ? new ModOriginImpl(paths) : new ModOriginImpl(parentModId, candidate.getLocalPath()); } + public String getId() { + return info.getId(); + } + @Override public LoaderModMetadata getMetadata() { return info; diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/BuiltinMetadataWrapper.java b/src/main/java/net/fabricmc/loader/impl/discovery/BuiltinMetadataWrapper.java index 971b80078..a7919ba13 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/BuiltinMetadataWrapper.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/BuiltinMetadataWrapper.java @@ -26,25 +26,28 @@ import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.metadata.ContactInformation; import net.fabricmc.loader.api.metadata.CustomValue; -import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.api.metadata.ModLoadCondition; import net.fabricmc.loader.api.metadata.ModMetadata; import net.fabricmc.loader.api.metadata.Person; +import net.fabricmc.loader.api.metadata.ProvidedMod; import net.fabricmc.loader.impl.metadata.AbstractModMetadata; import net.fabricmc.loader.impl.metadata.EntrypointMetadata; import net.fabricmc.loader.impl.metadata.LoaderModMetadata; +import net.fabricmc.loader.impl.metadata.ModDependencyImpl; import net.fabricmc.loader.impl.metadata.NestedJarEntry; class BuiltinMetadataWrapper extends AbstractModMetadata implements LoaderModMetadata { private final ModMetadata parent; private Version version; - private Collection dependencies; + private Collection dependencies; + @SuppressWarnings("unchecked") BuiltinMetadataWrapper(ModMetadata parent) { this.parent = parent; version = parent.getVersion(); - dependencies = parent.getDependencies(); + dependencies = (Collection) parent.getDependencies(); } @Override @@ -58,8 +61,8 @@ public String getId() { } @Override - public Collection getProvides() { - return parent.getProvides(); + public Collection getAdditionallyProvidedMods() { + return parent.getAdditionallyProvidedMods(); } @Override @@ -78,12 +81,22 @@ public ModEnvironment getEnvironment() { } @Override - public Collection getDependencies() { + public ModLoadCondition getLoadCondition() { + return parent.getLoadCondition(); + } + + @Override + public String getLoadPhase() { + return LoadPhases.DEFAULT; + } + + @Override + public Collection getDependencies() { return dependencies; } @Override - public void setDependencies(Collection dependencies) { + public void setDependencies(Collection dependencies) { this.dependencies = Collections.unmodifiableCollection(dependencies); } @@ -158,8 +171,8 @@ public Collection getMixinConfigs(EnvType type) { } @Override - public String getAccessWidener() { - return null; + public Collection getClassTweakers() { + return Collections.emptyList(); } @Override @@ -181,7 +194,4 @@ public List getEntrypoints(String type) { public Collection getEntrypointKeys() { return Collections.emptyList(); } - - @Override - public void emitFormatWarnings() { } } diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/Explanation.java b/src/main/java/net/fabricmc/loader/impl/discovery/Explanation.java index 43470b47f..a528a29fd 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/Explanation.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/Explanation.java @@ -22,16 +22,16 @@ class Explanation implements Comparable { private static int nextCmpId; final ErrorKind error; - final ModCandidate mod; + final ModCandidateImpl mod; final ModDependency dep; final String data; private final int cmpId; - Explanation(ErrorKind error, ModCandidate mod) { + Explanation(ErrorKind error, ModCandidateImpl mod) { this(error, mod, null, null); } - Explanation(ErrorKind error, ModCandidate mod, ModDependency dep) { + Explanation(ErrorKind error, ModCandidateImpl mod, ModDependency dep) { this(error, mod, dep, null); } @@ -39,11 +39,11 @@ class Explanation implements Comparable { this(error, null, data); } - Explanation(ErrorKind error, ModCandidate mod, String data) { + Explanation(ErrorKind error, ModCandidateImpl mod, String data) { this(error, mod, null, data); } - private Explanation(ErrorKind error, ModCandidate mod, ModDependency dep, String data) { + private Explanation(ErrorKind error, ModCandidateImpl mod, ModDependency dep, String data) { this.error = error; this.mod = mod; this.dep = dep; @@ -123,7 +123,11 @@ enum ErrorKind { /** * Requirement to load at most one mod per id (including provides). */ - UNIQUE_ID(false); + UNIQUE_ID(false), + /** + * Requirement to load at most one mod per id (including provides) while another mod with the id is preselected. + */ + UNIQUE_ID_OTHER_PRESELECTD(false); final boolean isDependencyError; diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/LoadPhases.java b/src/main/java/net/fabricmc/loader/impl/discovery/LoadPhases.java new file mode 100644 index 000000000..9fab4091c --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/discovery/LoadPhases.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.discovery; + +import net.fabricmc.loader.impl.util.PhaseSorting; + +public final class LoadPhases { + public static final String DEFAULT = "default"; + + public static void setDefaultOrder(PhaseSorting sorting) { + String[] phases = { "updater", "adapter", DEFAULT }; + + for (int i = 0; i < phases.length - 1; i++) { + sorting.addPhaseOrdering(phases[i], phases[i + 1]); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ModCandidate.java b/src/main/java/net/fabricmc/loader/impl/discovery/ModCandidateImpl.java similarity index 80% rename from src/main/java/net/fabricmc/loader/impl/discovery/ModCandidate.java rename to src/main/java/net/fabricmc/loader/impl/discovery/ModCandidateImpl.java index 4bc5a52cd..07158682a 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/ModCandidate.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/ModCandidateImpl.java @@ -34,17 +34,20 @@ import java.util.zip.ZipInputStream; import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.extension.ModCandidate; import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModLoadCondition; +import net.fabricmc.loader.api.metadata.ProvidedMod; import net.fabricmc.loader.impl.game.GameProvider.BuiltinMod; import net.fabricmc.loader.impl.metadata.AbstractModMetadata; import net.fabricmc.loader.impl.metadata.DependencyOverrides; import net.fabricmc.loader.impl.metadata.LoaderModMetadata; import net.fabricmc.loader.impl.metadata.VersionOverrides; -public final class ModCandidate implements DomainObject.Mod { - static final Comparator ID_VERSION_COMPARATOR = new Comparator() { +public final class ModCandidateImpl implements ModCandidate, DomainObject.Mod { + static final Comparator ID_VERSION_COMPARATOR = new Comparator() { @Override - public int compare(ModCandidate a, ModCandidate b) { + public int compare(ModCandidateImpl a, ModCandidateImpl b) { int cmp = a.getId().compareTo(b.getId()); return cmp != 0 ? cmp : a.getVersion().compareTo(b.getVersion()); @@ -57,25 +60,26 @@ public int compare(ModCandidate a, ModCandidate b) { private final long hash; private final LoaderModMetadata metadata; private final boolean requiresRemap; - private final Collection nestedMods; - private final Collection parentMods; + private final Collection nestedMods; + private final Collection parentMods; private int minNestLevel; private SoftReference dataRef; + boolean enableGreedyLoad; - static ModCandidate createBuiltin(BuiltinMod mod, VersionOverrides versionOverrides, DependencyOverrides depOverrides) { + static ModCandidateImpl createBuiltin(BuiltinMod mod, VersionOverrides versionOverrides, DependencyOverrides depOverrides) { LoaderModMetadata metadata = new BuiltinMetadataWrapper(mod.metadata); versionOverrides.apply(metadata); depOverrides.apply(metadata); - return new ModCandidate(mod.paths, null, -1, metadata, false, Collections.emptyList()); + return new ModCandidateImpl(mod.paths, null, -1, metadata, false, Collections.emptyList()); } - static ModCandidate createPlain(List paths, LoaderModMetadata metadata, boolean requiresRemap, Collection nestedMods) { - return new ModCandidate(paths, null, -1, metadata, requiresRemap, nestedMods); + public static ModCandidateImpl createPlain(List paths, LoaderModMetadata metadata, boolean requiresRemap, Collection nestedMods) { + return new ModCandidateImpl(paths, null, -1, metadata, requiresRemap, nestedMods); } - static ModCandidate createNested(String localPath, long hash, LoaderModMetadata metadata, boolean requiresRemap, Collection nestedMods) { - return new ModCandidate(null, localPath, hash, metadata, requiresRemap, nestedMods); + static ModCandidateImpl createNested(String localPath, long hash, LoaderModMetadata metadata, boolean requiresRemap, Collection nestedMods) { + return new ModCandidateImpl(null, localPath, hash, metadata, requiresRemap, nestedMods); } static long hash(ZipEntry entry) { @@ -88,7 +92,7 @@ private static long getSize(long hash) { return hash & 0xffffffffL; } - private ModCandidate(List paths, String localPath, long hash, LoaderModMetadata metadata, boolean requiresRemap, Collection nestedMods) { + private ModCandidateImpl(List paths, String localPath, long hash, LoaderModMetadata metadata, boolean requiresRemap, Collection nestedMods) { this.originPaths = paths; this.paths = paths; this.localPath = localPath; @@ -104,10 +108,12 @@ public List getOriginPaths() { return originPaths; } + @Override public boolean hasPath() { return paths != null; } + @Override public List getPaths() { if (paths == null) throw new IllegalStateException("no path set"); @@ -121,6 +127,7 @@ public void setPaths(List paths) { clearCachedData(); } + @Override public String getLocalPath() { if (localPath != null) { return localPath; @@ -131,6 +138,7 @@ public String getLocalPath() { } } + @Override public LoaderModMetadata getMetadata() { return metadata; } @@ -145,8 +153,8 @@ public Version getVersion() { return metadata.getVersion(); } - public Collection getProvides() { - return metadata.getProvides(); + public Collection getAdditionallyProvidedMods() { + return metadata.getAdditionallyProvidedMods(); } public boolean isBuiltin() { @@ -154,10 +162,12 @@ public boolean isBuiltin() { } public ModLoadCondition getLoadCondition() { + if (metadata.getLoadCondition() != null) return metadata.getLoadCondition(); + return minNestLevel == 0 ? ModLoadCondition.ALWAYS : ModLoadCondition.IF_POSSIBLE; } - public Collection getDependencies() { + public Collection getDependencies() { return metadata.getDependencies(); } @@ -165,15 +175,17 @@ public boolean getRequiresRemap() { return requiresRemap; } - public Collection getNestedMods() { - return nestedMods; + @Override + public Collection getContainingMods() { + return parentMods; } - public Collection getParentMods() { - return parentMods; + @Override + public Collection getContainedMods() { + return nestedMods; } - boolean addParent(ModCandidate parent) { + boolean addParent(ModCandidateImpl parent) { if (minNestLevel == 0) return false; if (parentMods.contains(parent)) return false; @@ -196,7 +208,7 @@ boolean resetMinNestLevel() { } } - boolean updateMinNestLevel(ModCandidate parent) { + boolean updateMinNestLevel(ModCandidateImpl parent) { if (minNestLevel <= parent.minNestLevel) return false; this.minNestLevel = parent.minNestLevel + 1; @@ -204,6 +216,7 @@ boolean updateMinNestLevel(ModCandidate parent) { return true; } + @Override public boolean isRoot() { return minNestLevel == 0; } @@ -290,7 +303,7 @@ private void copyToFile(Path out) throws IOException { return; } - ModCandidate parent = getBestSourcingParent(); + ModCandidateImpl parent = getBestSourcingParent(); if (parent.paths != null) { if (parent.paths.size() != 1) throw new UnsupportedOperationException("multiple parent paths for "+this); @@ -334,7 +347,7 @@ private ByteBuffer getData() throws IOException { ret = ByteBuffer.wrap(Files.readAllBytes(paths.get(0))); } else { - ModCandidate parent = getBestSourcingParent(); + ModCandidateImpl parent = getBestSourcingParent(); if (parent.paths != null) { if (parent.paths.size() != 1) throw new UnsupportedOperationException("multiple parent paths for "+this); @@ -369,12 +382,12 @@ private ByteBuffer getData() throws IOException { return ret; } - private ModCandidate getBestSourcingParent() { + private ModCandidateImpl getBestSourcingParent() { if (parentMods.isEmpty()) return null; - ModCandidate ret = null; + ModCandidateImpl ret = null; - for (ModCandidate parent : parentMods) { + for (ModCandidateImpl parent : parentMods) { if (parent.minNestLevel >= minNestLevel) continue; if (parent.paths != null && parent.paths.size() == 1 diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java b/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java index 3071fdf8f..53eca7b65 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/ModDiscoverer.java @@ -82,11 +82,11 @@ public void addCandidateFinder(ModCandidateFinder f) { candidateFinders.add(f); } - public List discoverMods(FabricLoaderImpl loader, Map> envDisabledModsOut) throws ModResolutionException { + public List discoverMods(FabricLoaderImpl loader, Map> envDisabledModsOut) throws ModResolutionException { long startTime = System.nanoTime(); ForkJoinPool pool = new ForkJoinPool(); Set processedPaths = new HashSet<>(); // suppresses duplicate paths - List> futures = new ArrayList<>(); + List> futures = new ArrayList<>(); ModCandidateConsumer taskSubmitter = (paths, requiresRemap) -> { if (paths.size() == 1) { @@ -113,11 +113,11 @@ public List discoverMods(FabricLoaderImpl loader, Map candidates = new ArrayList<>(); + List candidates = new ArrayList<>(); // add builtin mods for (BuiltinMod mod : loader.getGameProvider().getBuiltinMods()) { - ModCandidate candidate = ModCandidate.createBuiltin(mod, versionOverrides, depOverrides); + ModCandidateImpl candidate = ModCandidateImpl.createBuiltin(mod, versionOverrides, depOverrides); candidates.add(MetadataVerifier.verifyIndev(candidate)); } @@ -134,13 +134,13 @@ public List discoverMods(FabricLoaderImpl loader, Map future : futures) { + for (Future future : futures) { if (!future.isDone()) { throw new TimeoutException(); } try { - ModCandidate candidate = future.get(); + ModCandidateImpl candidate = future.get(); if (candidate != null) candidates.add(candidate); } catch (ExecutionException e) { exception = ExceptionUtil.gatherExceptions(e, exception, exc -> new ModResolutionException("Mod discovery failed!", exc)); @@ -148,19 +148,21 @@ public List discoverMods(FabricLoaderImpl loader, Map future : data.futures) { + for (Future future : data.futures) { if (!future.isDone()) { throw new TimeoutException(); } try { - ModCandidate candidate = future.get(); + ModCandidateImpl candidate = future.get(); if (candidate != null) data.target.add(candidate); } catch (ExecutionException e) { exception = ExceptionUtil.gatherExceptions(e, exception, exc -> new ModResolutionException("Mod discovery failed!", exc)); } } } + + nestedModInitDatas.clear(); } catch (TimeoutException e) { throw new FormattedException("Mod discovery took too long!", "Analyzing the mod folder contents took longer than %d seconds. This may be caused by unusually slow hardware, pathological antivirus interference or other issues. The timeout can be changed with the system property %s (-D%).", @@ -175,21 +177,22 @@ public List discoverMods(FabricLoaderImpl loader, Map ret = Collections.newSetFromMap(new IdentityHashMap<>(candidates.size() * 2)); - Queue queue = new ArrayDeque<>(candidates); - ModCandidate mod; + Set ret = Collections.newSetFromMap(new IdentityHashMap<>(candidates.size() * 2)); + Queue queue = new ArrayDeque<>(candidates); + ModCandidateImpl mod; - while ((mod = queue.poll()) != null) { + while ((mod = queue.poll()) != null) { // TODO: merge this with similar logic in scan and similar needs in net.fabricmc.loader.impl.LoaderPluginApiImpl.createMod(List, ModMetadata, Collection) if (mod.getMetadata().loadsInEnvironment(envType)) { if (!ret.add(mod)) continue; - for (ModCandidate child : mod.getNestedMods()) { + for (ModCandidateImpl child : mod.getContainedMods()) { if (child.addParent(mod)) { queue.add(child); } } } else { envDisabledModsOut.computeIfAbsent(mod.getId(), ignore -> Collections.newSetFromMap(new IdentityHashMap<>())).add(mod); + // TODO: add now-unlinked child mods to envDisabledModsOut as well } } @@ -200,17 +203,50 @@ public List discoverMods(FabricLoaderImpl loader, Map(ret); } - private ModCandidate createJavaMod() { + private ModCandidateImpl createJavaMod() { ModMetadata metadata = new BuiltinModMetadata.Builder("java", System.getProperty("java.specification.version").replaceFirst("^1\\.", "")) .setName(System.getProperty("java.vm.name")) .build(); BuiltinMod builtinMod = new BuiltinMod(Collections.singletonList(Paths.get(System.getProperty("java.home"))), metadata); - return ModCandidate.createBuiltin(builtinMod, versionOverrides, depOverrides); + return ModCandidateImpl.createBuiltin(builtinMod, versionOverrides, depOverrides); + } + + public ModCandidateImpl scan(List paths, boolean requiresRemap) { + ModCandidateImpl ret = new ModScanTask(paths, requiresRemap).compute(); + + for (NestedModInitData data : nestedModInitDatas) { + for (Future future : data.futures) { + try { + data.target.add(future.get()); + } catch (ExecutionException | InterruptedException e) { + throw ExceptionUtil.wrap(e); + } + } + } + + nestedModInitDatas.clear(); + + if (!ret.getMetadata().loadsInEnvironment(envType)) return null; + + Queue queue = new ArrayDeque<>(); + ModCandidateImpl mod = ret; + + do { + if (mod.getMetadata().loadsInEnvironment(envType)) { + for (ModCandidateImpl child : mod.getContainedMods()) { + if (child.addParent(mod)) { + queue.add(child); + } + } + } + } while ((mod = queue.poll()) != null); + + return ret; } @SuppressWarnings("serial") - final class ModScanTask extends RecursiveTask { + final class ModScanTask extends RecursiveTask { private final List paths; private final String localPath; private final RewindableInputStream is; @@ -233,7 +269,7 @@ private ModScanTask(List paths, String localPath, RewindableInputStream is } @Override - protected ModCandidate compute() { + protected ModCandidateImpl compute() { if (is != null) { // nested jar try { return computeJarStream(); @@ -257,7 +293,7 @@ protected ModCandidate compute() { } } - private ModCandidate computeDir() throws IOException, ParseMetadataException { + private ModCandidateImpl computeDir() throws IOException, ParseMetadataException { for (Path path : paths) { Path modJson = path.resolve("fabric.mod.json"); if (!Files.exists(modJson)) continue; @@ -268,13 +304,13 @@ private ModCandidate computeDir() throws IOException, ParseMetadataException { metadata = parseMetadata(is, path.toString()); } - return ModCandidate.createPlain(paths, metadata, requiresRemap, Collections.emptyList()); + return ModCandidateImpl.createPlain(paths, metadata, requiresRemap, Collections.emptyList()); } return null; } - private ModCandidate computeJarFile() throws IOException, ParseMetadataException { + private ModCandidateImpl computeJarFile() throws IOException, ParseMetadataException { assert paths.size() == 1; try (ZipFile zf = new ZipFile(paths.get(0).toFile())) { @@ -288,7 +324,7 @@ private ModCandidate computeJarFile() throws IOException, ParseMetadataException } if (!metadata.loadsInEnvironment(envType)) { - return ModCandidate.createPlain(paths, metadata, requiresRemap, Collections.emptyList()); + return ModCandidateImpl.createPlain(paths, metadata, requiresRemap, Collections.emptyList()); } List nestedModTasks; @@ -332,7 +368,7 @@ public RewindableInputStream getInputStream() throws IOException { } } - List nestedMods; + List nestedMods; if (nestedModTasks.isEmpty()) { nestedMods = Collections.emptyList(); @@ -341,11 +377,11 @@ public RewindableInputStream getInputStream() throws IOException { nestedModInitDatas.add(new NestedModInitData(nestedModTasks, nestedMods)); } - return ModCandidate.createPlain(paths, metadata, requiresRemap, nestedMods); + return ModCandidateImpl.createPlain(paths, metadata, requiresRemap, nestedMods); } } - private ModCandidate computeJarStream() throws IOException, ParseMetadataException { + private ModCandidateImpl computeJarStream() throws IOException, ParseMetadataException { LoaderModMetadata metadata = null; ZipEntry entry; @@ -361,7 +397,7 @@ private ModCandidate computeJarStream() throws IOException, ParseMetadataExcepti if (metadata == null) return null; if (!metadata.loadsInEnvironment(envType)) { - return ModCandidate.createNested(localPath, hash, metadata, requiresRemap, Collections.emptyList()); + return ModCandidateImpl.createNested(localPath, hash, metadata, requiresRemap, Collections.emptyList()); } Collection nestedJars = metadata.getJars(); @@ -410,7 +446,7 @@ public RewindableInputStream getInputStream() throws IOException { } } - List nestedMods; + List nestedMods; if (nestedModTasks.isEmpty()) { nestedMods = Collections.emptyList(); @@ -419,7 +455,7 @@ public RewindableInputStream getInputStream() throws IOException { nestedModInitDatas.add(new NestedModInitData(nestedModTasks, nestedMods)); } - ModCandidate ret = ModCandidate.createNested(localPath, hash, metadata, requiresRemap, nestedMods); + ModCandidateImpl ret = ModCandidateImpl.createNested(localPath, hash, metadata, requiresRemap, nestedMods); ret.setData(is.getBuffer()); return ret; @@ -435,7 +471,7 @@ private List computeNestedMods(ZipEntrySource entrySource) throws I ZipEntry entry; while ((entry = entrySource.getNextEntry()) != null) { - long hash = ModCandidate.hash(entry); + long hash = ModCandidateImpl.hash(entry); ModScanTask task = jijDedupMap.get(hash); if (task == null) { @@ -462,7 +498,10 @@ private List computeNestedMods(ZipEntrySource entrySource) throws I } private LoaderModMetadata parseMetadata(InputStream is, String localPath) throws ParseMetadataException { - return ModMetadataParser.parseMetadata(is, localPath, parentPaths, versionOverrides, depOverrides); + LoaderModMetadata ret = ModMetadataParser.parseMetadata(is, localPath, parentPaths, versionOverrides, depOverrides); + ret.applyEnvironment(envType); + + return ret; } } @@ -546,10 +585,10 @@ static ByteBuffer readMod(InputStream is) throws IOException { } private static class NestedModInitData { - final List> futures; - final List target; + final List> futures; + final List target; - NestedModInitData(List> futures, List target) { + NestedModInitData(List> futures, List target) { this.futures = futures; this.target = target; } diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ModResolver.java b/src/main/java/net/fabricmc/loader/impl/discovery/ModResolver.java index 4ded637bb..0696082a7 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/ModResolver.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/ModResolver.java @@ -19,8 +19,12 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -32,17 +36,25 @@ import org.sat4j.specs.TimeoutException; import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.extension.ModCandidate; import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.api.metadata.ModDependency.Kind; +import net.fabricmc.loader.api.metadata.ProvidedMod; import net.fabricmc.loader.impl.discovery.ModSolver.InactiveReason; import net.fabricmc.loader.impl.metadata.ModDependencyImpl; +import net.fabricmc.loader.impl.util.PhaseSorting; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; +import net.fabricmc.loader.impl.util.log.LogLevel; public class ModResolver { - public static List resolve(Collection candidates, EnvType envType, Map> envDisabledMods) throws ModResolutionException { + private static final boolean LOG_VERBOSE = Log.shouldLog(LogLevel.DEBUG, LogCategory.RESOLUTION); + + public static List resolve(ResolutionContext context) throws ModResolutionException { long startTime = System.nanoTime(); - List result = findCompatibleSet(candidates, envType, envDisabledMods); + + List result = findCompatibleSet(context); long endTime = System.nanoTime(); Log.debug(LogCategory.RESOLUTION, "Mod resolution time: %.1f ms", (endTime - startTime) * 1e-6); @@ -50,56 +62,19 @@ public static List resolve(Collection candidates, En return result; } - private static List findCompatibleSet(Collection candidates, EnvType envType, Map> envDisabledMods) throws ModResolutionException { - // sort all mods by priority - - List allModsSorted = new ArrayList<>(candidates); - - allModsSorted.sort(modPrioComparator); - - // group/index all mods by id - - Map> modsById = new LinkedHashMap<>(); // linked to ensure consistent execution - - for (ModCandidate mod : allModsSorted) { - modsById.computeIfAbsent(mod.getId(), ignore -> new ArrayList<>()).add(mod); - - for (String provided : mod.getProvides()) { - modsById.computeIfAbsent(provided, ignore -> new ArrayList<>()).add(mod); - } - } - - // soften positive deps from schema 0 or 1 mods on mods that are present but disabled for the current env - // this is a workaround necessary due to many mods declaring deps that are unsatisfiable in some envs and loader before 0.12x not verifying them properly - - for (ModCandidate mod : allModsSorted) { - if (mod.getMetadata().getSchemaVersion() >= 2) continue; - - for (ModDependency dep : mod.getMetadata().getDependencies()) { - if (!dep.getKind().isPositive() || dep.getKind() == Kind.SUGGESTS) continue; // no positive dep or already suggests - if (!(dep instanceof ModDependencyImpl)) continue; // can't modify dep kind - if (modsById.containsKey(dep.getModId())) continue; // non-disabled match available - - Collection disabledMatches = envDisabledMods.get(dep.getModId()); - if (disabledMatches == null) continue; // no disabled id matches + private static List findCompatibleSet(ResolutionContext context) throws ModResolutionException { + if (LOG_VERBOSE) Log.debug(LogCategory.RESOLUTION, "Starting resolution with %d mods: %s", context.initialMods.size(), context.initialMods); - for (ModCandidate m : disabledMatches) { - if (dep.matches(m.getVersion())) { // disabled version match -> remove dep - ((ModDependencyImpl) dep).setKind(Kind.SUGGESTS); - break; - } - } - } - } + addMods(context.initialMods, context); // preselect mods, check for builtin mod collisions - List preselectedMods = new ArrayList<>(); + List preselectedMods = new ArrayList<>(); - for (List mods : modsById.values()) { - ModCandidate builtinMod = null; + for (List mods : context.modsById.values()) { + ModCandidateImpl builtinMod = null; - for (ModCandidate mod : mods) { + for (ModCandidateImpl mod : mods) { if (mod.isBuiltin()) { builtinMod = mod; break; @@ -116,69 +91,148 @@ private static List findCompatibleSet(Collection can preselectedMods.add(builtinMod); } - Map selectedMods = new HashMap<>(allModsSorted.size()); - List uniqueSelectedMods = new ArrayList<>(allModsSorted.size()); + for (ModCandidateImpl mod : preselectedMods) { + selectMod(mod, context); + } + + if (LOG_VERBOSE) Log.debug(LogCategory.RESOLUTION, "Preselected %d mods: %s", preselectedMods.size(), preselectedMods); - for (ModCandidate mod : preselectedMods) { - preselectMod(mod, allModsSorted, modsById, selectedMods, uniqueSelectedMods); + // phase sorting + + PhaseSorting sorting = new PhaseSorting<>(); + LoadPhases.setDefaultOrder(sorting); + + for (ModCandidateImpl mod : context.allModsSorted) { + sorting.add(mod.getMetadata().getLoadPhase(), mod); } // solve - ModSolver.Result result; + Iterator phaseIterator = sorting.getUsedPhases().iterator(); + boolean advancePhase = true; + String phase = null; - try { - result = ModSolver.solve(allModsSorted, modsById, - selectedMods, uniqueSelectedMods); - } catch (ContradictionException | TimeoutException e) { - throw new ModResolutionException("Solving failed", e); - } + while (!context.allModsSorted.isEmpty()) { + if (advancePhase) { + if (!phaseIterator.hasNext()) break; - if (!result.success) { - Log.warn(LogCategory.RESOLUTION, "Mod resolution failed"); - Log.info(LogCategory.RESOLUTION, "Immediate reason: %s%n", result.immediateReason); - Log.info(LogCategory.RESOLUTION, "Reason: %s%n", result.reason); - if (!envDisabledMods.isEmpty()) Log.info(LogCategory.RESOLUTION, "%s environment disabled: %s%n", envType.name(), envDisabledMods.keySet()); + phase = phaseIterator.next(); - if (result.fix == null) { - Log.info(LogCategory.RESOLUTION, "No fix?"); - } else { - Log.info(LogCategory.RESOLUTION, "Fix: add %s, remove %s, replace [%s]%n", - result.fix.modsToAdd, - result.fix.modsToRemove, - result.fix.modReplacements.entrySet().stream().map(e -> String.format("%s -> %s", e.getValue(), e.getKey())).collect(Collectors.joining(", "))); - - for (Collection mods : envDisabledMods.values()) { - for (ModCandidate m : mods) { - result.fix.inactiveMods.put(m, InactiveReason.WRONG_ENVIRONMENT); + for (ModCandidateImpl mod : sorting.get(phase)) { + mod.enableGreedyLoad = true; + } + } + + if (LOG_VERBOSE) { + List mod = context.allModsSorted.stream().filter(m -> m.enableGreedyLoad).collect(Collectors.toList()); + Log.debug(LogCategory.RESOLUTION, "Phase %s: %d mods: %s", phase, mod.size(), mod); + } + + ModSolver.Result result; + + try { + result = ModSolver.solve(context); + } catch (ContradictionException | TimeoutException e) { + throw new ModResolutionException("Solving failed", e); + } + + if (!result.success) { + Log.warn(LogCategory.RESOLUTION, "Mod resolution failed"); + Log.info(LogCategory.RESOLUTION, "Immediate reason: %s%n", result.immediateReason); + Log.info(LogCategory.RESOLUTION, "Reason: %s%n", result.reason); + if (!context.envDisabledMods.isEmpty()) Log.info(LogCategory.RESOLUTION, "%s environment disabled: %s%n", context.envType.name(), context.envDisabledMods.keySet()); + + if (result.fix == null) { + Log.info(LogCategory.RESOLUTION, "No fix?"); + } else { + Log.info(LogCategory.RESOLUTION, "Fix: add %s, remove %s, replace [%s]%n", + result.fix.modsToAdd, + result.fix.modsToRemove, + result.fix.modReplacements.entrySet().stream().map(e -> String.format("%s -> %s", e.getValue(), e.getKey())).collect(Collectors.joining(", "))); + + for (Collection mods : context.envDisabledMods.values()) { + for (ModCandidateImpl m : mods) { + result.fix.inactiveMods.put(m, InactiveReason.WRONG_ENVIRONMENT); + } } } + + throw new ModResolutionException("Mod resolution encountered an incompatible mod set!%s", + ResultAnalyzer.gatherErrors(result, context)); } - throw new ModResolutionException("Mod resolution encountered an incompatible mod set!%s", - ResultAnalyzer.gatherErrors(result, selectedMods, modsById, envDisabledMods, envType)); + if (!context.currentSelectedMods.isEmpty()) { + if (LOG_VERBOSE) Log.debug(LogCategory.RESOLUTION, "selected %d mods: %s", context.currentSelectedMods.size(), context.currentSelectedMods); + if (context.phaseSelectHandler != null) context.phaseSelectHandler.onSelect(context.currentSelectedMods, phase, context); + context.currentSelectedMods.clear(); + } else { + if (LOG_VERBOSE) Log.debug(LogCategory.RESOLUTION, "no mods selected"); + } + + if (context.addedMods.isEmpty()) { + advancePhase = true; + } else { + addMods(context.addedMods, context); + + Set addedPhases = new HashSet<>(); + + for (ModCandidateImpl mod : context.addedMods) { + String modPhase = mod.getMetadata().getLoadPhase(); + sorting.add(modPhase, mod); + addedPhases.add(modPhase); + } + + int addedPhaseCount = addedPhases.size(); + + // remove phases that aren't beyond the current phase + addedPhases.remove(phase); + + if (!addedPhases.isEmpty()) { + int idx = sorting.getPhaseIndex(phase); + + for (Iterator it = addedPhases.iterator(); it.hasNext(); ) { + if (sorting.getPhaseIndex(it.next()) <= idx) it.remove(); + } + } + + advancePhase = addedPhases.size() == addedPhaseCount; // all added mods use phases beyond the current phase + + if (!advancePhase) { + for (ModCandidateImpl mod : context.addedMods) { + if (!addedPhases.contains(mod.getMetadata().getLoadPhase())) { + mod.enableGreedyLoad = true; + } + } + } + + // reset iterator since it's likely invalid + phaseIterator = sorting.getUsedPhases().iterator(); + while (!phaseIterator.next().equals(phase)) { } + + context.addedMods.clear(); + } } - uniqueSelectedMods.sort(Comparator.comparing(ModCandidate::getId)); + context.uniqueSelectedMods.sort(Comparator.comparing(ModCandidateImpl::getId)); // clear cached data and inbound refs for unused mods, set minNestLevel for used non-root mods to max, queue root mods - Queue queue = new ArrayDeque<>(); + Queue queue = new ArrayDeque<>(); - for (ModCandidate mod : allModsSorted) { - if (selectedMods.get(mod.getId()) == mod) { // mod is selected + for (ModCandidateImpl mod : context.allModsSorted) { + if (context.selectedMods.get(mod.getId()) == mod) { // mod is selected if (!mod.resetMinNestLevel()) { // -> is root queue.add(mod); } } else { mod.clearCachedData(); - for (ModCandidate m : mod.getNestedMods()) { - m.getParentMods().remove(mod); + for (ModCandidateImpl m : mod.getContainedMods()) { + m.getContainingMods().remove(mod); } - for (ModCandidate m : mod.getParentMods()) { - m.getNestedMods().remove(mod); + for (ModCandidateImpl m : mod.getContainingMods()) { + m.getContainedMods().remove(mod); } } } @@ -186,10 +240,10 @@ private static List findCompatibleSet(Collection can // recompute minNestLevel (may have changed due to parent associations having been dropped by the above step) { - ModCandidate mod; + ModCandidateImpl mod; while ((mod = queue.poll()) != null) { - for (ModCandidate child : mod.getNestedMods()) { + for (ModCandidateImpl child : mod.getContainedMods()) { if (child.updateMinNestLevel(mod)) { queue.add(child); } @@ -197,19 +251,59 @@ private static List findCompatibleSet(Collection can } } - String warnings = ResultAnalyzer.gatherWarnings(uniqueSelectedMods, selectedMods, - envDisabledMods, envType); + String warnings = ResultAnalyzer.gatherWarnings(context); if (warnings != null) { Log.warn(LogCategory.RESOLUTION, "Warnings were found!%s", warnings); } - return uniqueSelectedMods; + return context.uniqueSelectedMods; + } + + private static void addMods(Collection mods, ResolutionContext context) { + context.allModsSorted.addAll(mods); + + // sort all mods by priority + + context.allModsSorted.sort(modPrioComparator); + + // group/index all mods by id + + for (ModCandidateImpl mod : mods) { + context.modsById.computeIfAbsent(mod.getId(), ignore -> new ArrayList<>()).add(mod); + + for (ProvidedMod provided : mod.getAdditionallyProvidedMods()) { + context.modsById.computeIfAbsent(provided.getId(), ignore -> new ArrayList<>()).add(mod); + } + } + + // soften positive deps from schema 0 or 1 mods on mods that are present but disabled for the current env + // this is a workaround necessary due to many mods declaring deps that are unsatisfiable in some envs and loader before 0.12x not verifying them properly + + for (ModCandidateImpl mod : mods) { + if (mod.getMetadata().getSchemaVersion() >= 2) continue; + + for (ModDependency dep : mod.getDependencies()) { + if (!dep.getKind().isPositive() || dep.getKind() == Kind.SUGGESTS) continue; // no positive dep or already suggests + if (!(dep instanceof ModDependencyImpl)) continue; // can't modify dep kind + if (context.modsById.containsKey(dep.getModId())) continue; // non-disabled match available + + Collection disabledMatches = context.envDisabledMods.get(dep.getModId()); + if (disabledMatches == null) continue; // no disabled id matches + + for (ModCandidateImpl m : disabledMatches) { + if (depMatches(dep, m)) { // disabled version match -> remove dep + ((ModDependencyImpl) dep).setKind(Kind.SUGGESTS); + break; + } + } + } + } } - private static final Comparator modPrioComparator = new Comparator() { + private static final Comparator modPrioComparator = new Comparator() { @Override - public int compare(ModCandidate a, ModCandidate b) { + public int compare(ModCandidateImpl a, ModCandidateImpl b) { // descending sort prio (less/earlier is higher prio): // root mods first, lower id first, higher version first, less nesting first, parent cmp @@ -235,13 +329,13 @@ public int compare(ModCandidate a, ModCandidate b) { if (a.isRoot()) return 0; // both root - List parents = new ArrayList<>(a.getParentMods().size() + b.getParentMods().size()); - parents.addAll(a.getParentMods()); - parents.addAll(b.getParentMods()); + List parents = new ArrayList<>(a.getContainingMods().size() + b.getContainingMods().size()); + parents.addAll(a.getContainingMods()); + parents.addAll(b.getContainingMods()); parents.sort(this); - if (a.getParentMods().contains(parents.get(0))) { - if (b.getParentMods().contains(parents.get(0))) { + if (a.getContainingMods().contains(parents.get(0))) { + if (b.getContainingMods().contains(parents.get(0))) { return 0; } else { return -1; @@ -252,26 +346,203 @@ public int compare(ModCandidate a, ModCandidate b) { } }; - static void preselectMod(ModCandidate mod, List allModsSorted, Map> modsById, - Map selectedMods, List uniqueSelectedMods) throws ModResolutionException { - selectMod(mod, selectedMods, uniqueSelectedMods); + static boolean depMatches(ModDependency dep, ModCandidateImpl mod) { + String id = dep.getModId(); + Version version; + + if (id.equals(mod.getId())) { + version = mod.getVersion(); + } else { + version = null; + + for (ProvidedMod m : mod.getAdditionallyProvidedMods()) { + if (id.equals(m.getId())) { + version = m.getVersion(); + break; + } + } + + if (version == null) return false; + } + + return dep.matches(version); + } + + static void selectMod(ModCandidateImpl mod, ResolutionContext context) throws ModResolutionException { + ModCandidateImpl prev = context.selectedMods.put(mod.getId(), mod); + if (prev != null && hasExclusiveId(prev, mod.getId())) throw new ModResolutionException("duplicate mod %s", mod.getId()); + + for (ProvidedMod provided : mod.getAdditionallyProvidedMods()) { + String id = provided.getId(); + + if (provided.isExclusive()) { + prev = context.selectedMods.put(id, mod); + if (prev != null && hasExclusiveId(prev, id)) throw new ModResolutionException("duplicate provided mod %s by %s and %s", id, mod, prev); + } else { + prev = context.selectedMods.putIfAbsent(id, mod); + } + } + + context.uniqueSelectedMods.add(mod); + context.currentSelectedMods.add(mod); - allModsSorted.removeAll(modsById.remove(mod.getId())); + // remove from allModsSorted and modsById - for (String provided : mod.getProvides()) { - allModsSorted.removeAll(modsById.remove(provided)); + context.allModsSorted.removeAll(context.modsById.remove(mod.getId())); + + for (ProvidedMod provided : mod.getAdditionallyProvidedMods()) { + String id = provided.getId(); + + if (provided.isExclusive()) { + context.allModsSorted.removeAll(context.modsById.remove(id)); + } else { + List mods = context.modsById.get(id); + mods.remove(mod); + context.allModsSorted.remove(mod); + + for (Iterator it = mods.iterator(); it.hasNext(); ) { + ModCandidateImpl m = it.next(); + + if (!hasExclusiveId(m, id)) { + it.remove(); + context.allModsSorted.remove(m); + } + } + + if (mods.isEmpty()) context.modsById.remove(id); + } } } - static void selectMod(ModCandidate mod, Map selectedMods, List uniqueSelectedMods) throws ModResolutionException { - ModCandidate prev = selectedMods.put(mod.getId(), mod); - if (prev != null) throw new ModResolutionException("duplicate mod %s", mod.getId()); + static boolean hasExclusiveId(ModCandidateImpl mod, String id) { + if (mod.getId().equals(id)) return true; - for (String provided : mod.getProvides()) { - prev = selectedMods.put(provided, mod); - if (prev != null) throw new ModResolutionException("duplicate mod %s", provided); + for (ProvidedMod provided : mod.getAdditionallyProvidedMods()) { + if (provided.isExclusive() && provided.getId().equals(id)) return true; } - uniqueSelectedMods.add(mod); + return false; + } + + public static final class ResolutionContext { + final Collection initialMods; + public final EnvType envType; + final Map> envDisabledMods; + final PhaseSelectHandler phaseSelectHandler; + + final List allModsSorted; + final Map> modsById = new LinkedHashMap<>(); // linked to ensure consistent execution + final Map selectedMods; + final List uniqueSelectedMods; + + final List addedMods = new ArrayList<>(); + final List currentSelectedMods = new ArrayList<>(); + + public ResolutionContext(Collection candidates, EnvType envType, Map> envDisabledMods, + PhaseSelectHandler phaseSelectHandler) { + this.initialMods = candidates; + this.envType = envType; + this.envDisabledMods = envDisabledMods; + this.phaseSelectHandler = phaseSelectHandler; + + this.allModsSorted = new ArrayList<>(candidates.size()); + this.selectedMods = new HashMap<>(candidates.size()); + this.uniqueSelectedMods = new ArrayList<>(candidates.size()); + } + + public Collection getMods(String id) { + List ret = new ArrayList<>(); + ret.addAll(modsById.getOrDefault(id, Collections.emptyList())); + ModCandidateImpl mod = selectedMods.get(id); + if (mod != null) ret.add(mod); + + for (ModCandidateImpl m : addedMods) { + if (m.getId().equals(id)) ret.add(m); + } + + return ret; + } + + public Collection getMods() { + List ret = new ArrayList<>(allModsSorted.size() + uniqueSelectedMods.size() + addedMods.size()); + ret.addAll(allModsSorted); + ret.addAll(uniqueSelectedMods); + ret.addAll(addedMods); + + return ret; + } + + public boolean addMod(ModCandidateImpl mod) { + for (ModCandidateImpl m : modsById.getOrDefault(mod.getId(), Collections.emptyList())) { + if (m == mod || m.getVersion().equals(mod.getVersion())) { + return false; + } + } + + if (selectedMods.containsKey(mod.getId())) return false; + + for (ModCandidateImpl m : addedMods) { + if (m == mod || m.getId().equals(mod.getId()) && m.getVersion().equals(mod.getVersion())) { + return false; + } + } + + addedMods.add(mod); + + for (ModCandidate m : mod.getContainedMods()) { + addMod((ModCandidateImpl) m); + } + + return true; + } + + public boolean removeMod(ModCandidateImpl mod) { + if (selectedMods.get(mod.getId()) == mod) return false; // already loaded + + if (!mod.getContainedMods().isEmpty()) { // also remove all mods that'd become orphaned (check if possible first, then apply) + Set modsToRemove = Collections.newSetFromMap(new IdentityHashMap<>()); + modsToRemove.add(mod); + Queue queue = new ArrayDeque<>(); + ModCandidateImpl parent = mod; + + do { + for (ModCandidateImpl m : parent.getContainedMods()) { + if ((m.getContainingMods().size() == 1 || modsToRemove.containsAll(m.getContainingMods())) // orphaned + && modsToRemove.add(m)) { + if (selectedMods.get(m.getId()) == m) return false; + queue.add(m); + } + } + } while ((parent = queue.poll()) != null); + + for (ModCandidateImpl m : modsToRemove) { + if (m != mod) removeMod0(m); + } + } + + return removeMod0(mod); + } + + private boolean removeMod0(ModCandidateImpl mod) { + List mods = modsById.get(mod.getId()); + boolean removed = mods != null && mods.remove(mod) + || addedMods.remove(mod); + + if (removed) { + for (ModCandidateImpl m : mod.getContainingMods()) { + m.getContainedMods().remove(mod); + } + + for (ModCandidateImpl m : mod.getContainedMods()) { + m.getContainingMods().remove(mod); + } + } + + return removed; + } + } + + public interface PhaseSelectHandler { + void onSelect(List mods, String phase, ResolutionContext context); } } diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ModSolver.java b/src/main/java/net/fabricmc/loader/impl/discovery/ModSolver.java index e24156f4c..68c89e566 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/ModSolver.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/ModSolver.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,9 +43,11 @@ import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModLoadCondition; import net.fabricmc.loader.api.metadata.version.VersionInterval; import net.fabricmc.loader.api.metadata.version.VersionPredicate; import net.fabricmc.loader.impl.discovery.Explanation.ErrorKind; +import net.fabricmc.loader.impl.discovery.ModResolver.ResolutionContext; import net.fabricmc.loader.impl.util.SystemProperties; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; @@ -52,14 +55,13 @@ import net.fabricmc.loader.impl.util.version.VersionPredicateParser; final class ModSolver { - static Result solve(List allModsSorted, Map> modsById, - Map selectedMods, List uniqueSelectedMods) throws ContradictionException, TimeoutException, ModResolutionException { + static Result solve(ResolutionContext context) throws ContradictionException, TimeoutException, ModResolutionException { // build priority index - Map priorities = new IdentityHashMap<>(allModsSorted.size()); + Map priorities = new IdentityHashMap<>(context.allModsSorted.size()); - for (int i = 0; i < allModsSorted.size(); i++) { - priorities.put(allModsSorted.get(i), i); + for (int i = 0; i < context.allModsSorted.size(); i++) { + priorities.put(context.allModsSorted.get(i), i); } // create and configure solver @@ -73,8 +75,7 @@ static Result solve(List allModsSorted, Map dependencyHelper = createDepHelper(solver); - setupSolver(allModsSorted, modsById, - priorities, selectedMods, uniqueSelectedMods, + setupSolver(context, priorities, false, null, false, dependencyHelper); @@ -94,8 +95,8 @@ static Result solve(List allModsSorted, Map allModsSorted, Map failedDeps = Collections.newSetFromMap(new IdentityHashMap<>()); List failedExplanations = new ArrayList<>(); - computeFailureCausesOptional(allModsSorted, modsById, - priorities, selectedMods, uniqueSelectedMods, + computeFailureCausesOptional(context, priorities, reason, dependencyHelper, failedDeps, failedExplanations); @@ -121,8 +121,7 @@ static Result solve(List allModsSorted, Map immediateReason, Collect } } - private static void computeFailureCausesOptional(List allModsSorted, Map> modsById, - Map priorities, Map selectedMods, List uniqueSelectedMods, + private static void computeFailureCausesOptional(ResolutionContext context, Map priorities, Set reason, DependencyHelper dependencyHelper, Set failedDeps, List failedExplanations) throws ContradictionException, TimeoutException { dependencyHelper.reset(); dependencyHelper = createDepHelper(dependencyHelper.getSolver()); // dependencyHelper.reset doesn't fully reset the dep helper - setupSolver(allModsSorted, modsById, - priorities, selectedMods, uniqueSelectedMods, + setupSolver(context, priorities, true, null, false, dependencyHelper); @@ -179,7 +176,7 @@ private static void computeFailureCausesOptional(List allModsSorte if (obj instanceof DisableDepVar) { disabledDeps.add(((DisableDepVar) obj).dep); } else { - assert obj instanceof ModCandidate; + assert obj instanceof ModCandidateImpl; } } @@ -187,9 +184,9 @@ private static void computeFailureCausesOptional(List allModsSorte // record explanation for failed deps that capture the depending mod for (DomainObject obj : solution) { - if (!(obj instanceof ModCandidate)) continue; + if (!(obj instanceof ModCandidateImpl)) continue; - ModCandidate mod = (ModCandidate) obj; + ModCandidateImpl mod = (ModCandidateImpl) obj; for (ModDependency dep : mod.getDependencies()) { if (disabledDeps.contains(dep)) { @@ -203,8 +200,7 @@ private static void computeFailureCausesOptional(List allModsSorte } } - private static Fix computeFix(List uniqueSelectedMods, List allModsSorted, Map> modsById, - Map priorities, Map selectedMods, + private static Fix computeFix(ResolutionContext context, Map priorities, Set failedDeps, DependencyHelper dependencyHelper) throws ContradictionException, TimeoutException { // group positive deps by mod id Map>> depsById = new HashMap<>(); @@ -219,7 +215,7 @@ private static Fix computeFix(List uniqueSelectedMods, List modsWithOnlyOutboundDepFailures = new HashSet<>(); - for (ModCandidate mod : allModsSorted) { + for (ModCandidateImpl mod : context.allModsSorted) { if (!mod.getDependencies().isEmpty() && !depsById.containsKey(mod.getId()) && !Collections.disjoint(mod.getDependencies(), failedDeps)) { // mod has unsatisfied deps @@ -230,7 +226,7 @@ private static Fix computeFix(List uniqueSelectedMods, List uniqueSelectedMods, List uniqueSelectedMods, List activeMods = new HashMap<>(); - Map inactiveMods = new IdentityHashMap<>(allModsSorted.size()); + Map activeMods = new HashMap<>(); + Map inactiveMods = new IdentityHashMap<>(context.allModsSorted.size()); List modsToAdd = new ArrayList<>(); - List modsToRemove = new ArrayList<>(); - Map> modReplacements = new HashMap<>(); + List modsToRemove = new ArrayList<>(); + Map> modReplacements = new HashMap<>(); - for (ModCandidate mod : allModsSorted) { + for (ModCandidateImpl mod : context.allModsSorted) { inactiveMods.put(mod, InactiveReason.UNKNOWN); } for (DomainObject obj : dependencyHelper.getASolution()) { - if (obj instanceof ModCandidate) { - ModCandidate mod = (ModCandidate) obj; + if (obj instanceof ModCandidateImpl) { + ModCandidateImpl mod = (ModCandidateImpl) obj; activeMods.put(mod.getId(), mod); inactiveMods.remove(mod); } else if (obj instanceof AddModVar) { AddModVar mod = (AddModVar) obj; - List replaced = new ArrayList<>(); + List replaced = new ArrayList<>(); - ModCandidate selectedMod = selectedMods.get(obj.getId()); + ModCandidateImpl selectedMod = context.selectedMods.get(obj.getId()); if (selectedMod != null) replaced.add(selectedMod); - List mods = modsById.get(obj.getId()); + List mods = context.modsById.get(obj.getId()); if (mods != null) replaced.addAll(mods); if (replaced.isEmpty()) { @@ -346,14 +341,14 @@ private static Fix computeFix(List uniqueSelectedMods, List replacement modReplacements.put(mod, replaced); - for (ModCandidate m : replaced) { + for (ModCandidateImpl m : replaced) { inactiveMods.put(m, InactiveReason.TO_REPLACE); } } } else if (obj instanceof RemoveModVar) { boolean found = false; - ModCandidate mod = selectedMods.get(obj.getId()); + ModCandidateImpl mod = context.selectedMods.get(obj.getId()); if (mod != null) { modsToRemove.add(mod); @@ -361,10 +356,10 @@ private static Fix computeFix(List uniqueSelectedMods, List mods = modsById.get(obj.getId()); + List mods = context.modsById.get(obj.getId()); if (mods != null) { - for (ModCandidate m : mods) { + for (ModCandidateImpl m : mods) { if (m.isRoot()) { modsToRemove.add(m); inactiveMods.put(m, InactiveReason.TO_REMOVE); @@ -385,7 +380,7 @@ private static Fix computeFix(List uniqueSelectedMods, List intervals = Collections.singletonList(VersionInterval.INFINITE); - for (ModCandidate m : activeMods.values()) { + for (ModCandidateImpl m : activeMods.values()) { for (ModDependency dep : m.getDependencies()) { if (!dep.getModId().equals(mod.getId()) || dep.getKind().isSoft()) continue; @@ -403,14 +398,14 @@ private static Fix computeFix(List uniqueSelectedMods, List entry : inactiveMods.entrySet()) { + inactiveModLoop: for (Map.Entry entry : inactiveMods.entrySet()) { if (entry.getValue() != InactiveReason.UNKNOWN) continue; - ModCandidate mod = entry.getKey(); - ModCandidate active = activeMods.get(mod.getId()); + ModCandidateImpl mod = entry.getKey(); + ModCandidateImpl active = activeMods.get(mod.getId()); if (active != null) { - if (allModsSorted.indexOf(mod) > allModsSorted.indexOf(active)) { // entry has lower prio (=higher index) than active + if (context.allModsSorted.indexOf(mod) > context.allModsSorted.indexOf(active)) { // entry has lower prio (=higher index) than active if (mod.getVersion().equals(active.getVersion())) { entry.setValue(InactiveReason.SAME_ACTIVE); } else { @@ -424,10 +419,10 @@ private static Fix computeFix(List uniqueSelectedMods, List modsToAdd; - final Collection modsToRemove; - final Map> modReplacements; - final Map activeMods; - final Map inactiveMods; + final Collection modsToRemove; + final Map> modReplacements; + final Map activeMods; + final Map inactiveMods; - Fix(Collection modsToAdd, Collection modsToRemove, Map> modReplacements, - Map activeMods, Map inactiveMods) { + Fix(Collection modsToAdd, Collection modsToRemove, Map> modReplacements, + Map activeMods, Map inactiveMods) { this.modsToAdd = modsToAdd; this.modsToRemove = modsToRemove; this.modReplacements = modReplacements; @@ -561,22 +556,19 @@ enum InactiveReason { } } - private static void setupSolver(List allModsSorted, Map> modsById, - Map priorities, Map selectedMods, List uniqueSelectedMods, + private static void setupSolver(ResolutionContext context, Map priorities, boolean depDisableSim, Map> installableMods, boolean removalSim, DependencyHelper dependencyHelper) throws ContradictionException { Map dummies = new HashMap<>(); Map> disabledDeps = depDisableSim ? new HashMap<>() : null; List> weightedObjects = new ArrayList<>(); - generatePreselectConstraints(uniqueSelectedMods, modsById, - priorities, selectedMods, + generatePreselectConstraints(context, priorities, depDisableSim, installableMods, removalSim, dummies, disabledDeps, dependencyHelper, weightedObjects); - generateMainConstraints(allModsSorted, modsById, - priorities, selectedMods, + generateMainConstraints(context, priorities, depDisableSim, installableMods, removalSim, dummies, disabledDeps, dependencyHelper, weightedObjects); @@ -591,34 +583,33 @@ private static void setupSolver(List allModsSorted, Map uniqueSelectedMods, Map> modsById, - Map priorities, Map selectedMods, + private static void generatePreselectConstraints(ResolutionContext context, Map priorities, boolean depDisableSim, Map> installableMods, boolean removalSim, Map dummyMods, Map> disabledDeps, DependencyHelper dependencyHelper, List> weightedObjects) throws ContradictionException { boolean enableOptional = !depDisableSim && installableMods == null && !removalSim; // whether to enable optional mods (regular solve only, not for failure handling) List suitableMods = new ArrayList<>(); - for (ModCandidate mod : uniqueSelectedMods) { + for (ModCandidateImpl mod : context.uniqueSelectedMods) { // add constraints for dependencies (skips deps that are already preselected outside depDisableSim) for (ModDependency dep : mod.getDependencies()) { if (!enableOptional && dep.getKind().isSoft()) continue; - if (selectedMods.containsKey(dep.getModId())) continue; + if (context.selectedMods.containsKey(dep.getModId())) continue; - List availableMods = modsById.get(dep.getModId()); + List available = context.modsById.get(dep.getModId()); - if (availableMods != null) { - for (DomainObject.Mod m : availableMods) { - if (dep.matches(m.getVersion())) suitableMods.add(m); + if (available != null) { + for (ModCandidateImpl m : available) { + if (ModResolver.depMatches(dep, m)) suitableMods.add(m); } } if (installableMods != null) { - availableMods = installableMods.get(dep.getModId()); + List installable = installableMods.get(dep.getModId()); - if (availableMods != null) { - for (DomainObject.Mod m : availableMods) { + if (installable != null) { + for (DomainObject.Mod m : installable) { if (dep.matches(m.getVersion())) suitableMods.add(m); } } @@ -638,7 +629,13 @@ private static void generatePreselectConstraints(List uniqueSelect // this will prioritize greedy over non-greedy loaded mods, regardless of modPrioComparator due to the objective weights // only pull IF_RECOMMENDED or encompassing in - suitableMods.removeIf(m -> ((ModCandidate) m).getLoadCondition().ordinal() > ModLoadCondition.IF_RECOMMENDED.ordinal()); + for (Iterator it = suitableMods.iterator(); it.hasNext(); ) { + ModCandidateImpl m = (ModCandidateImpl) it.next(); + + if (!m.enableGreedyLoad || m.getLoadCondition().ordinal() > ModLoadCondition.IF_RECOMMENDED.ordinal()) { + it.remove(); + } + } if (!suitableMods.isEmpty()) { suitableMods.add(getCreateDummy(dep.getModId(), OptionalDepVar::new, dummyMods, priorities.size(), weightedObjects)); @@ -685,26 +682,25 @@ private static void generatePreselectConstraints(List uniqueSelect } } - private static void generateMainConstraints(List allModsSorted, Map> modsById, - Map priorities, Map selectedMods, + private static void generateMainConstraints(ResolutionContext context, Map priorities, boolean depDisableSim, Map> installableMods, boolean removalSim, Map dummyMods, Map> disabledDeps, DependencyHelper dependencyHelper, List> weightedObjects) throws ContradictionException { boolean enableOptional = !depDisableSim && installableMods == null && !removalSim; // whether to enable optional mods (regular solve only, not for failure handling) List suitableMods = new ArrayList<>(); - for (ModCandidate mod : allModsSorted) { + for (ModCandidateImpl mod : context.allModsSorted) { // add constraints for dependencies for (ModDependency dep : mod.getDependencies()) { if (!enableOptional && dep.getKind().isSoft()) continue; - ModCandidate selectedMod = selectedMods.get(dep.getModId()); + ModCandidateImpl selectedMod = context.selectedMods.get(dep.getModId()); if (selectedMod != null) { // dep is already selected = present if (!removalSim) { if (!dep.getKind().isSoft() // .. and is a hard dep - && dep.matches(selectedMod.getVersion()) != dep.getKind().isPositive()) { // ..but isn't suitable (DEPENDS without match or BREAKS with match) + && ModResolver.depMatches(dep, selectedMod) != dep.getKind().isPositive()) { // ..but isn't suitable (DEPENDS without match or BREAKS with match) if (depDisableSim) { dependencyHelper.setTrue(getCreateDisableDepVar(dep, disabledDeps), new Explanation(ErrorKind.HARD_DEP, mod, dep)); } else { @@ -713,24 +709,24 @@ private static void generateMainConstraints(List allModsSorted, Ma } continue; - } else if (dep.matches(selectedMod.getVersion())) { + } else if (ModResolver.depMatches(dep, selectedMod)) { suitableMods.add(selectedMod); } } - List availableMods = modsById.get(dep.getModId()); + List available = context.modsById.get(dep.getModId()); - if (availableMods != null) { - for (DomainObject.Mod m : availableMods) { - if (dep.matches(m.getVersion())) suitableMods.add(m); + if (available != null) { + for (ModCandidateImpl m : available) { + if (ModResolver.depMatches(dep, m)) suitableMods.add(m); } } if (installableMods != null) { - availableMods = installableMods.get(dep.getModId()); + List installable = installableMods.get(dep.getModId()); - if (availableMods != null) { - for (DomainObject.Mod m : availableMods) { + if (installable != null) { + for (DomainObject.Mod m : installable) { if (dep.matches(m.getVersion())) suitableMods.add(m); } } @@ -753,7 +749,13 @@ private static void generateMainConstraints(List allModsSorted, Ma // this will prioritize greedy over non-greedy loaded mods, regardless of modPrioComparator due to the objective weights // only pull IF_RECOMMENDED or encompassing in - suitableMods.removeIf(m -> ((ModCandidate) m).getLoadCondition().ordinal() > ModLoadCondition.IF_RECOMMENDED.ordinal()); + for (Iterator it = suitableMods.iterator(); it.hasNext(); ) { + ModCandidateImpl m = (ModCandidateImpl) it.next(); + + if (!m.enableGreedyLoad || m.getLoadCondition().ordinal() > ModLoadCondition.IF_RECOMMENDED.ordinal()) { + it.remove(); + } + } if (!suitableMods.isEmpty()) { suitableMods.add(getCreateDummy(dep.getModId(), OptionalDepVar::new, dummyMods, priorities.size(), weightedObjects)); @@ -791,16 +793,14 @@ private static void generateMainConstraints(List allModsSorted, Ma // add constraints to restrict nested mods to selected parents if (!mod.isRoot()) { // nested mod - ModLoadCondition loadCondition = mod.getLoadCondition(); + if (mod.enableGreedyLoad && mod.getLoadCondition() == ModLoadCondition.ALWAYS) { // required with parent + Explanation explanation = new Explanation(ErrorKind.NESTED_FORCELOAD, mod.getContainingMods().iterator().next(), mod.getId()); // FIXME: this applies to all parents + DomainObject[] siblings = context.modsById.get(mod.getId()).toArray(new DomainObject[0]); - if (loadCondition == ModLoadCondition.ALWAYS) { // required with parent - Explanation explanation = new Explanation(ErrorKind.NESTED_FORCELOAD, mod.getParentMods().iterator().next(), mod.getId()); // FIXME: this applies to all parents - DomainObject[] siblings = modsById.get(mod.getId()).toArray(new DomainObject[0]); - - if (isAnyParentSelected(mod, selectedMods)) { + if (isAnyParentSelected(mod, context.selectedMods)) { dependencyHelper.clause(explanation, siblings); } else { - for (ModCandidate parent : mod.getParentMods()) { + for (ModCandidateImpl parent : mod.getContainingMods()) { dependencyHelper.implication(parent).implies(siblings).named(explanation); } } @@ -808,21 +808,21 @@ private static void generateMainConstraints(List allModsSorted, Ma // require parent to be selected with the nested mod - if (!isAnyParentSelected(mod, selectedMods)) { - dependencyHelper.implication(mod).implies(mod.getParentMods().toArray(new DomainObject[0])).named(new Explanation(ErrorKind.NESTED_REQ_PARENT, mod)); + if (!isAnyParentSelected(mod, context.selectedMods)) { + dependencyHelper.implication(mod).implies(mod.getContainingMods().toArray(new DomainObject[0])).named(new Explanation(ErrorKind.NESTED_REQ_PARENT, mod)); } } // add weights if potentially needed (choice between multiple mods or dummies) - if (!mod.isRoot() || mod.getLoadCondition() != ModLoadCondition.ALWAYS || modsById.get(mod.getId()).size() > 1) { + if (!mod.isRoot() || mod.getLoadCondition() != ModLoadCondition.ALWAYS || context.modsById.get(mod.getId()).size() > 1) { int prio = priorities.get(mod); BigInteger weight; - if (mod.getLoadCondition().ordinal() >= ModLoadCondition.IF_RECOMMENDED.ordinal()) { // non-greedy (optional) + if (!mod.enableGreedyLoad || mod.getLoadCondition().ordinal() > ModLoadCondition.IF_POSSIBLE.ordinal()) { // non-greedy (optional) weight = TWO.pow(prio + 1); } else { // greedy - weight = TWO.pow(allModsSorted.size() - prio).negate(); + weight = TWO.pow(context.allModsSorted.size() - prio).negate(); } weightedObjects.add(WeightedObject.newWO(mod, weight)); @@ -832,21 +832,48 @@ private static void generateMainConstraints(List allModsSorted, Ma // add constraints to force-load root mods (ALWAYS only, IF_POSSIBLE is being handled through negative weight later) // add single mod per id constraints - for (List variants : modsById.values()) { - ModCandidate firstMod = variants.get(0); - String id = firstMod.getId(); + for (Map.Entry> entry : context.modsById.entrySet()) { + String id = entry.getKey(); + List variants = entry.getValue(); + + // check for already selected mod with same id (usually involves provided mods) + + ModCandidateImpl selMod = context.selectedMods.get(id); + + if (selMod != null) { + List newVariants = new ArrayList<>(variants.size() + 1); + + if (ModResolver.hasExclusiveId(selMod, id)) { + for (ModCandidateImpl mod : variants) { + if (ModResolver.hasExclusiveId(mod, id)) { + dependencyHelper.setFalse(mod, new Explanation(ErrorKind.UNIQUE_ID_OTHER_PRESELECTD, id)); + } else { + newVariants.add(mod); + } + } + + if (newVariants.isEmpty()) continue; + } else { + newVariants.addAll(variants); + } + + newVariants.add(selMod); + variants = newVariants; + } + + ModCandidateImpl firstMod = variants.get(0); // force-load root mod if (variants.size() == 1 && !removalSim) { // trivial case, others are handled by multi-variant impl - if (firstMod.isRoot() && firstMod.getLoadCondition() == ModLoadCondition.ALWAYS) { + if (firstMod.isRoot() && firstMod.enableGreedyLoad && firstMod.getLoadCondition() == ModLoadCondition.ALWAYS) { dependencyHelper.setTrue(firstMod, new Explanation(ErrorKind.ROOT_FORCELOAD_SINGLE, firstMod)); } } else { // complex case, potentially multiple variants boolean isRequired = false; - for (ModCandidate mod : variants) { - if (mod.isRoot() && mod.getLoadCondition() == ModLoadCondition.ALWAYS) { + for (ModCandidateImpl mod : variants) { + if (mod.isRoot() && mod.enableGreedyLoad && mod.getLoadCondition() == ModLoadCondition.ALWAYS) { isRequired = true; break; } @@ -874,7 +901,9 @@ private static void generateMainConstraints(List allModsSorted, Ma // single mod per id constraint - suitableMods.addAll(variants); + for (ModCandidateImpl mod : variants) { + if (ModResolver.hasExclusiveId(mod, id)) suitableMods.add(mod); // id needs to be unique for the mod + } if (installableMods != null) { List installable = installableMods.get(id); @@ -882,13 +911,13 @@ private static void generateMainConstraints(List allModsSorted, Ma if (installable != null && !installable.isEmpty()) { suitableMods.addAll(installable); - ModCandidate mod = selectedMods.get(id); + ModCandidateImpl mod = context.selectedMods.get(id); if (mod != null) suitableMods.add(mod); } } if (suitableMods.size() > 1 // multiple options - || enableOptional && firstMod.getLoadCondition() == ModLoadCondition.IF_POSSIBLE) { // optional greedy loading + || enableOptional && firstMod.enableGreedyLoad && firstMod.getLoadCondition() == ModLoadCondition.IF_POSSIBLE && firstMod.getId().equals(id)) { // optional greedy loading (actual mod only, not for extra provides entries) dependencyHelper.atMost(1, suitableMods.toArray(new DomainObject[0])).named(new Explanation(ErrorKind.UNIQUE_ID, id)); } @@ -900,12 +929,12 @@ private static void generateMainConstraints(List allModsSorted, Ma if (installableMods != null) { for (List variants : installableMods.values()) { String id = variants.get(0).getId(); - boolean isReplacement = modsById.containsKey(id); + boolean isReplacement = context.modsById.containsKey(id); if (!isReplacement) { // no single mod per id constraint created yet suitableMods.addAll(variants); - ModCandidate selectedMod = selectedMods.get(id); + ModCandidateImpl selectedMod = context.selectedMods.get(id); if (selectedMod != null) suitableMods.add(selectedMod); if (suitableMods.size() > 1) { @@ -1083,22 +1112,22 @@ public boolean isNegated(Object thing) { } }; - static boolean isAnyParentSelected(ModCandidate mod, Map selectedMods) { - for (ModCandidate parentMod : mod.getParentMods()) { + static boolean isAnyParentSelected(ModCandidateImpl mod, Map selectedMods) { + for (ModCandidateImpl parentMod : mod.getContainingMods()) { if (selectedMods.get(parentMod.getId()) == parentMod) return true; } return false; } - static boolean hasAllDepsSatisfied(ModCandidate mod, Map mods) { + static boolean hasAllDepsSatisfied(ModCandidateImpl mod, Map mods) { for (ModDependency dep : mod.getDependencies()) { if (dep.getKind() == ModDependency.Kind.DEPENDS) { - ModCandidate m = mods.get(dep.getModId()); - if (m == null || !dep.matches(m.getVersion())) return false; + ModCandidateImpl m = mods.get(dep.getModId()); + if (m == null || !ModResolver.depMatches(dep, m)) return false; } else if (dep.getKind() == ModDependency.Kind.BREAKS) { - ModCandidate m = mods.get(dep.getModId()); - if (m != null && dep.matches(m.getVersion())) return false; + ModCandidateImpl m = mods.get(dep.getModId()); + if (m != null && ModResolver.depMatches(dep, m)) return false; } } diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/ResultAnalyzer.java b/src/main/java/net/fabricmc/loader/impl/discovery/ResultAnalyzer.java index b530cd32a..d47985e88 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/ResultAnalyzer.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/ResultAnalyzer.java @@ -31,10 +31,10 @@ import java.util.Set; import java.util.stream.Collectors; -import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.api.metadata.version.VersionInterval; +import net.fabricmc.loader.impl.discovery.ModResolver.ResolutionContext; import net.fabricmc.loader.impl.discovery.ModSolver.AddModVar; import net.fabricmc.loader.impl.discovery.ModSolver.InactiveReason; import net.fabricmc.loader.impl.metadata.AbstractModMetadata; @@ -47,8 +47,7 @@ final class ResultAnalyzer { private static final boolean SHOW_INACTIVE = false; @SuppressWarnings("unused") - static String gatherErrors(ModSolver.Result result, Map selectedMods, Map> modsById, - Map> envDisabledMods, EnvType envType) { + static String gatherErrors(ModSolver.Result result, ResolutionContext context) { StringWriter sw = new StringWriter(); try (PrintWriter pw = new PrintWriter(sw)) { @@ -58,54 +57,54 @@ static String gatherErrors(ModSolver.Result result, Map se if (result.fix != null) { pw.printf("\n%s", Localization.format("resolution.solutionHeader")); - formatFix(result.fix, result, selectedMods, modsById, envDisabledMods, envType, pw); + formatFix(result.fix, result, context, pw); pw.printf("\n%s", Localization.format("resolution.depListHeader")); prefix = "\t"; suggestFix = false; } - List matches = new ArrayList<>(); + List matches = new ArrayList<>(); for (Explanation explanation : result.reason) { assert explanation.error.isDependencyError; ModDependency dep = explanation.dep; - ModCandidate selected = selectedMods.get(dep.getModId()); + ModCandidateImpl selected = context.selectedMods.get(dep.getModId()); if (selected != null) { matches.add(selected); } else { - List candidates = modsById.get(dep.getModId()); + List candidates = context.modsById.get(dep.getModId()); if (candidates != null) matches.addAll(candidates); } - addErrorToList(explanation.mod, explanation.dep, matches, envDisabledMods.containsKey(dep.getModId()), suggestFix, prefix, pw); + addErrorToList(explanation.mod, explanation.dep, matches, context.envDisabledMods.containsKey(dep.getModId()), suggestFix, prefix, pw); matches.clear(); } if (SHOW_INACTIVE && result.fix != null && !result.fix.inactiveMods.isEmpty()) { pw.printf("\n%s", Localization.format("resolution.inactiveMods")); - List> entries = new ArrayList<>(result.fix.inactiveMods.entrySet()); + List> entries = new ArrayList<>(result.fix.inactiveMods.entrySet()); // sort by root, id, version - entries.sort(new Comparator>() { + entries.sort(new Comparator>() { @Override - public int compare(Entry o1, Entry o2) { - ModCandidate a = o1.getKey(); - ModCandidate b = o2.getKey(); + public int compare(Entry o1, Entry o2) { + ModCandidateImpl a = o1.getKey(); + ModCandidateImpl b = o2.getKey(); if (a.isRoot() != b.isRoot()) { return a.isRoot() ? -1 : 1; } - return ModCandidate.ID_VERSION_COMPARATOR.compare(a, b); + return ModCandidateImpl.ID_VERSION_COMPARATOR.compare(a, b); } }); - for (Map.Entry entry : entries) { - ModCandidate mod = entry.getKey(); + for (Map.Entry entry : entries) { + ModCandidateImpl mod = entry.getKey(); InactiveReason reason = entry.getValue(); String reasonKey = String.format("resolution.inactive.%s", reason.id); @@ -121,19 +120,16 @@ public int compare(Entry o1, Entry o2) { return sw.toString(); } - private static void formatFix(ModSolver.Fix fix, - ModSolver.Result result, Map selectedMods, Map> modsById, - Map> envDisabledMods, EnvType envType, - PrintWriter pw) { + private static void formatFix(ModSolver.Fix fix, ModSolver.Result result, ResolutionContext context, PrintWriter pw) { for (AddModVar mod : fix.modsToAdd) { - Set envDisabledAlternatives = envDisabledMods.get(mod.getId()); + Set envDisabledAlternatives = context.envDisabledMods.get(mod.getId()); if (envDisabledAlternatives == null) { pw.printf("\n\t - %s", Localization.format("resolution.solution.addMod", mod.getId(), formatVersionRequirements(mod.getVersionIntervals()))); } else { - String envKey = String.format("environment.%s", envType.name().toLowerCase(Locale.ENGLISH)); + String envKey = String.format("environment.%s", context.envType.name().toLowerCase(Locale.ENGLISH)); pw.printf("\n\t - %s", Localization.format("resolution.solution.replaceModEnvDisabled", formatOldMods(envDisabledAlternatives), @@ -143,23 +139,23 @@ private static void formatFix(ModSolver.Fix fix, } } - for (ModCandidate mod : fix.modsToRemove) { + for (ModCandidateImpl mod : fix.modsToRemove) { pw.printf("\n\t - %s", Localization.format("resolution.solution.removeMod", getName(mod), getVersion(mod), mod.getLocalPath())); } - for (Entry> entry : fix.modReplacements.entrySet()) { + for (Entry> entry : fix.modReplacements.entrySet()) { AddModVar newMod = entry.getKey(); - List oldMods = entry.getValue(); + List oldMods = entry.getValue(); String oldModsFormatted = formatOldMods(oldMods); if (oldMods.size() != 1 || !oldMods.get(0).getId().equals(newMod.getId())) { // replace mods with another mod (different mod id) String newModName = newMod.getId(); - ModCandidate alt = selectedMods.get(newMod.getId()); + ModCandidateImpl alt = context.selectedMods.get(newMod.getId()); if (alt != null) { newModName = getName(alt); } else { - List alts = modsById.get(newMod.getId()); + List alts = context.modsById.get(newMod.getId()); if (alts != null && !alts.isEmpty()) newModName = getName(alts.get(0)); } @@ -168,7 +164,7 @@ private static void formatFix(ModSolver.Fix fix, newModName, formatVersionRequirements(newMod.getVersionIntervals()))); } else { // replace mod version only - ModCandidate oldMod = oldMods.get(0); + ModCandidateImpl oldMod = oldMods.get(0); boolean hasOverlap = !VersionInterval.and(newMod.getVersionIntervals(), Collections.singletonList(new VersionIntervalImpl(oldMod.getVersion(), true, oldMod.getVersion(), true))).isEmpty(); @@ -187,10 +183,10 @@ private static void formatFix(ModSolver.Fix fix, for (ModDependency dep : oldMod.getDependencies()) { if (dep.getKind().isSoft()) continue; - ModCandidate mod = fix.activeMods.get(dep.getModId()); + ModCandidateImpl mod = fix.activeMods.get(dep.getModId()); if (mod != null) { - if (dep.matches(mod.getVersion()) != dep.getKind().isPositive()) { + if (ModResolver.depMatches(dep, mod) != dep.getKind().isPositive()) { pw.printf("\n\t\t - %s", Localization.format("resolution.solution.replaceModVersionDifferent.reqSupportedModVersion", mod.getId(), getVersion(mod))); @@ -219,28 +215,27 @@ private static void formatFix(ModSolver.Fix fix, } } - static String gatherWarnings(List uniqueSelectedMods, Map selectedMods, - Map> envDisabledMods, EnvType envType) { + static String gatherWarnings(ResolutionContext context) { StringWriter sw = new StringWriter(); try (PrintWriter pw = new PrintWriter(sw)) { - for (ModCandidate mod : uniqueSelectedMods) { + for (ModCandidateImpl mod : context.uniqueSelectedMods) { for (ModDependency dep : mod.getDependencies()) { - ModCandidate depMod; + ModCandidateImpl depMod; switch (dep.getKind()) { case RECOMMENDS: - depMod = selectedMods.get(dep.getModId()); + depMod = context.selectedMods.get(dep.getModId()); - if (depMod == null || !dep.matches(depMod.getVersion())) { - addErrorToList(mod, dep, toList(depMod), envDisabledMods.containsKey(dep.getModId()), true, "", pw); + if (depMod == null || !ModResolver.depMatches(dep, depMod)) { + addErrorToList(mod, dep, toList(depMod), context.envDisabledMods.containsKey(dep.getModId()), true, "", pw); } break; case CONFLICTS: - depMod = selectedMods.get(dep.getModId()); + depMod = context.selectedMods.get(dep.getModId()); - if (depMod != null && dep.matches(depMod.getVersion())) { + if (depMod != null && ModResolver.depMatches(dep, depMod)) { addErrorToList(mod, dep, toList(depMod), false, true, "", pw); } @@ -259,11 +254,11 @@ static String gatherWarnings(List uniqueSelectedMods, Map toList(ModCandidate mod) { + private static List toList(ModCandidateImpl mod) { return mod != null ? Collections.singletonList(mod) : Collections.emptyList(); } - private static void addErrorToList(ModCandidate mod, ModDependency dep, List matches, boolean presentForOtherEnv, boolean suggestFix, String prefix, PrintWriter pw) { + private static void addErrorToList(ModCandidateImpl mod, ModDependency dep, List matches, boolean presentForOtherEnv, boolean suggestFix, String prefix, PrintWriter pw) { Object[] args = new Object[] { getName(mod), getVersion(mod), @@ -281,8 +276,8 @@ private static void addErrorToList(ModCandidate mod, ModDependency dep, List paths = new ArrayList<>(); + List paths = new ArrayList<>(); paths.add(mod); - ModCandidate cur = mod; + ModCandidateImpl cur = mod; do { - ModCandidate best = null; + ModCandidateImpl best = null; int maxDiff = 0; - for (ModCandidate parent : cur.getParentMods()) { + for (ModCandidateImpl parent : cur.getContainingMods()) { int diff = cur.getMinNestLevel() - parent.getMinNestLevel(); if (diff > maxDiff) { @@ -353,7 +348,7 @@ private static void appendJijInfo(ModCandidate mod, String prefix, boolean menti StringBuilder pathSb = new StringBuilder(); for (int i = paths.size() - 1; i >= 0; i--) { - ModCandidate m = paths.get(i); + ModCandidateImpl m = paths.get(i); if (pathSb.length() > 0) pathSb.append(" -> "); pathSb.append(m.getLocalPath()); @@ -385,12 +380,12 @@ private static void appendJijInfo(ModCandidate mod, String prefix, boolean menti } @SuppressWarnings("unused") - private static String formatOldMods(Collection mods) { - List modsSorted = new ArrayList<>(mods); - modsSorted.sort(ModCandidate.ID_VERSION_COMPARATOR); + private static String formatOldMods(Collection mods) { + List modsSorted = new ArrayList<>(mods); + modsSorted.sort(ModCandidateImpl.ID_VERSION_COMPARATOR); List ret = new ArrayList<>(modsSorted.size()); - for (ModCandidate m : modsSorted) { + for (ModCandidateImpl m : modsSorted) { if (SHOW_PATH_INFO && m.hasPath() && !m.isBuiltin()) { ret.add(Localization.format("resolution.solution.replaceMod.oldMod", getName(m), getVersion(m), m.getLocalPath())); } else { @@ -401,7 +396,7 @@ private static String formatOldMods(Collection mods) { return formatEnumeration(ret, true); } - private static String getName(ModCandidate candidate) { + private static String getName(ModCandidateImpl candidate) { String typePrefix; switch (candidate.getMetadata().getType()) { @@ -416,11 +411,11 @@ private static String getName(ModCandidate candidate) { return String.format("%s'%s' (%s)", typePrefix, candidate.getMetadata().getName(), candidate.getId()); } - private static String getVersion(ModCandidate candidate) { + private static String getVersion(ModCandidateImpl candidate) { return candidate.getVersion().getFriendlyString(); } - private static String getVersions(Collection candidates) { + private static String getVersions(Collection candidates) { return candidates.stream().map(ResultAnalyzer::getVersion).collect(Collectors.joining("/")); } diff --git a/src/main/java/net/fabricmc/loader/impl/discovery/RuntimeModRemapper.java b/src/main/java/net/fabricmc/loader/impl/discovery/RuntimeModRemapper.java index 58744fdd5..0e06d099f 100644 --- a/src/main/java/net/fabricmc/loader/impl/discovery/RuntimeModRemapper.java +++ b/src/main/java/net/fabricmc/loader/impl/discovery/RuntimeModRemapper.java @@ -16,10 +16,12 @@ package net.fabricmc.loader.impl.discovery; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -28,18 +30,18 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; -import org.objectweb.asm.commons.Remapper; - import net.fabricmc.accesswidener.AccessWidenerReader; import net.fabricmc.accesswidener.AccessWidenerRemapper; import net.fabricmc.accesswidener.AccessWidenerWriter; import net.fabricmc.loader.impl.FormattedException; -import net.fabricmc.loader.impl.launch.FabricLauncher; import net.fabricmc.loader.impl.launch.FabricLauncherBase; +import net.fabricmc.loader.impl.launch.MappingConfiguration; import net.fabricmc.loader.impl.util.FileSystemUtil; import net.fabricmc.loader.impl.util.SystemProperties; import net.fabricmc.loader.impl.util.log.Log; @@ -48,13 +50,14 @@ import net.fabricmc.tinyremapper.InputTag; import net.fabricmc.tinyremapper.NonClassCopyMode; import net.fabricmc.tinyremapper.OutputConsumerPath; +import net.fabricmc.tinyremapper.OutputConsumerPath.ResourceRemapper; import net.fabricmc.tinyremapper.TinyRemapper; public final class RuntimeModRemapper { - public static void remap(Collection modCandidates, Path tmpDir, Path outputDir) { - List modsToRemap = new ArrayList<>(); + public static void remap(Collection modCandidates, Collection cpMods, Path tmpDir, Path outputDir) { + Set modsToRemap = new HashSet<>(); - for (ModCandidate mod : modCandidates) { + for (ModCandidateImpl mod : modCandidates) { if (mod.getRequiresRemap()) { modsToRemap.add(mod); } @@ -62,10 +65,13 @@ public static void remap(Collection modCandidates, Path tmpDir, Pa if (modsToRemap.isEmpty()) return; - FabricLauncher launcher = FabricLauncherBase.getLauncher(); + MappingConfiguration config = FabricLauncherBase.getLauncher().getMappingConfiguration(); + String modNs = MappingConfiguration.INTERMEDIARY_NAMESPACE; + String runtimeNs = config.getRuntimeNamespace(); + if (modNs.equals(runtimeNs)) return; TinyRemapper remapper = TinyRemapper.newRemapper() - .withMappings(TinyRemapperMappingsHelper.create(launcher.getMappingConfiguration().getMappings(), "intermediary", launcher.getTargetNamespace())) + .withMappings(TinyRemapperMappingsHelper.create(config.getMappings(), modNs, runtimeNs)) .renameInvalidLocals(false) .build(); @@ -75,84 +81,71 @@ public static void remap(Collection modCandidates, Path tmpDir, Pa throw new RuntimeException("Failed to populate remap classpath", e); } - Map infoMap = new HashMap<>(); + Map infoMap = new HashMap<>(); try { - for (ModCandidate mod : modsToRemap) { + // gather inputs and class path + + for (ModCandidateImpl mod : cpMods) { RemapInfo info = new RemapInfo(); infoMap.put(mod, info); - InputTag tag = remapper.createInputTag(); - info.tag = tag; - if (mod.hasPath()) { - List paths = mod.getPaths(); - if (paths.size() != 1) throw new UnsupportedOperationException("multiple path for "+mod); - - info.inputPath = paths.get(0); + info.inputPaths = mod.getPaths(); } else { - info.inputPath = mod.copyToDir(tmpDir, true); + info.inputPaths = Collections.singletonList(mod.copyToDir(tmpDir, true)); info.inputIsTemp = true; } - info.outputPath = outputDir.resolve(mod.getDefaultFileName()); - Files.deleteIfExists(info.outputPath); - - remapper.readInputsAsync(tag, info.inputPath); - } - - //Done in a 2nd loop as we need to make sure all the inputs are present before remapping - for (ModCandidate mod : modsToRemap) { - RemapInfo info = infoMap.get(mod); - OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(info.outputPath).build(); + if (modsToRemap.contains(mod)) { + InputTag tag = remapper.createInputTag(); + info.tag = tag; - FileSystemUtil.FileSystemDelegate delegate = FileSystemUtil.getJarFileSystem(info.inputPath, false); + info.outputPath = outputDir.resolve(mod.getDefaultFileName()); + Files.deleteIfExists(info.outputPath); - if (delegate.get() == null) { - throw new RuntimeException("Could not open JAR file " + info.inputPath.getFileName() + " for NIO reading!"); + remapper.readInputsAsync(tag, info.inputPaths.toArray(new Path[0])); + } else { + remapper.readClassPathAsync(info.inputPaths.toArray(new Path[0])); } + } - Path inputJar = delegate.get().getRootDirectories().iterator().next(); - outputConsumer.addNonClassFiles(inputJar, NonClassCopyMode.FIX_META_INF, remapper); + // copy non-classes, remap AWs, apply remapping - info.outputConsumerPath = outputConsumer; + for (ModCandidateImpl mod : modsToRemap) { + RemapInfo info = infoMap.get(mod); + List resourceRemappers = NonClassCopyMode.FIX_META_INF.remappers; - remapper.apply(outputConsumer, info.tag); - } + // aw remapping + ResourceRemapper awRemapper = createClassTweakerRemapper(mod, modNs, runtimeNs); - //Done in a 3rd loop as this can happen when the remapper is doing its thing. - for (ModCandidate mod : modsToRemap) { - RemapInfo info = infoMap.get(mod); + if (awRemapper != null) { + resourceRemappers = new ArrayList<>(resourceRemappers); + resourceRemappers.add(awRemapper); + } - String accessWidener = mod.getMetadata().getAccessWidener(); + try (OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(info.outputPath).build()) { + for (Path path : info.inputPaths) { + FileSystemUtil.FileSystemDelegate delegate = FileSystemUtil.getJarFileSystem(path, false); // TODO: close properly - if (accessWidener != null) { - info.accessWidenerPath = accessWidener; + if (delegate.get() == null) { + throw new RuntimeException("Could not open JAR file " + path + " for NIO reading!"); + } - try (FileSystemUtil.FileSystemDelegate jarFs = FileSystemUtil.getJarFileSystem(info.inputPath, false)) { - FileSystem fs = jarFs.get(); - info.accessWidener = remapAccessWidener(Files.readAllBytes(fs.getPath(accessWidener)), remapper.getRemapper()); - } catch (Throwable t) { - throw new RuntimeException("Error remapping access widener for mod '"+mod.getId()+"'!", t); + Path inputJar = delegate.get().getRootDirectories().iterator().next(); + outputConsumer.addNonClassFiles(inputJar, remapper, resourceRemappers); } + + remapper.apply(outputConsumer, info.tag); } } remapper.finish(); - for (ModCandidate mod : modsToRemap) { - RemapInfo info = infoMap.get(mod); - - info.outputConsumerPath.close(); + // update paths - if (info.accessWidenerPath != null) { - try (FileSystemUtil.FileSystemDelegate jarFs = FileSystemUtil.getJarFileSystem(info.outputPath, false)) { - FileSystem fs = jarFs.get(); - - Files.delete(fs.getPath(info.accessWidenerPath)); - Files.write(fs.getPath(info.accessWidenerPath), info.accessWidener); - } - } + for (ModCandidateImpl mod : modsToRemap) { + RemapInfo info = infoMap.get(mod); mod.setPaths(Collections.singletonList(info.outputPath)); } @@ -175,7 +168,11 @@ public static void remap(Collection modCandidates, Path tmpDir, Pa } finally { for (RemapInfo info : infoMap.values()) { try { - if (info.inputIsTemp) Files.deleteIfExists(info.inputPath); + if (info.inputIsTemp) { + for (Path path : info.inputPaths) { + Files.deleteIfExists(path); + } + } } catch (IOException e) { Log.warn(LogCategory.MOD_REMAP, "Error deleting temporary input jar %s", info.inputIsTemp, e); } @@ -183,12 +180,26 @@ public static void remap(Collection modCandidates, Path tmpDir, Pa } } - private static byte[] remapAccessWidener(byte[] input, Remapper remapper) { - AccessWidenerWriter writer = new AccessWidenerWriter(); - AccessWidenerRemapper remappingDecorator = new AccessWidenerRemapper(writer, remapper, "intermediary", "named"); - AccessWidenerReader accessWidenerReader = new AccessWidenerReader(remappingDecorator); - accessWidenerReader.read(input, "intermediary"); - return writer.write(); + private static ResourceRemapper createClassTweakerRemapper(ModCandidateImpl mod, String modNs, String runtimeNs) { + Collection classTweakers = mod.getMetadata().getClassTweakers(); + if (classTweakers.isEmpty()) return null; + + return new ResourceRemapper() { + @Override + public boolean canTransform(TinyRemapper remapper, Path relativePath) { + return classTweakers.contains(relativePath.toString()); + } + + @Override + public void transform(Path destinationDirectory, Path relativePath, InputStream input, TinyRemapper remapper) throws IOException { + AccessWidenerWriter writer = new AccessWidenerWriter(); + AccessWidenerRemapper remappingDecorator = new AccessWidenerRemapper(writer, remapper.getEnvironment().getRemapper(), modNs, runtimeNs); + AccessWidenerReader accessWidenerReader = new AccessWidenerReader(remappingDecorator); + accessWidenerReader.read(new BufferedReader(new InputStreamReader(input, AccessWidenerReader.ENCODING)), modNs); + + Files.write(destinationDirectory.resolve(relativePath.toString()), writer.write()); + } + }; } private static List getRemapClasspath() throws IOException { @@ -207,11 +218,8 @@ private static List getRemapClasspath() throws IOException { private static class RemapInfo { InputTag tag; - Path inputPath; + List inputPaths; Path outputPath; boolean inputIsTemp; - OutputConsumerPath outputConsumerPath; - String accessWidenerPath; - byte[] accessWidener; } } diff --git a/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java b/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java index 1495ad004..5a7ae3237 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java +++ b/src/main/java/net/fabricmc/loader/impl/game/GameProvider.java @@ -36,7 +36,6 @@ public interface GameProvider { // name directly referenced in net.fabricmc.load String getEntrypoint(); Path getLaunchDirectory(); - boolean isObfuscated(); boolean requiresUrlClassLoader(); boolean isEnabled(); @@ -53,6 +52,10 @@ default boolean displayCrash(Throwable exception, String context) { Arguments getArguments(); String[] getLaunchArguments(boolean sanitize); + default String getRuntimeNamespace(String defaultNs) { + return defaultNs; + } + default boolean canOpenErrorGui() { return true; } diff --git a/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java b/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java index 17dc37957..1bdb500a6 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java +++ b/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java @@ -155,15 +155,16 @@ public static final class FindResult { private static boolean emittedInfo = false; - public static Map deobfuscate(Map inputFileMap, String gameId, String gameVersion, Path gameDir, FabricLauncher launcher) { + public static Map deobfuscate(Map inputFileMap, String sourceNamespace, String gameId, String gameVersion, Path gameDir, FabricLauncher launcher) { Log.debug(LogCategory.GAME_REMAP, "Requesting deobfuscation of %s", inputFileMap); - if (launcher.isDevelopment()) { // in-dev is already deobfuscated + MappingConfiguration mappingConfig = launcher.getMappingConfiguration(); + String targetNamespace = mappingConfig.getRuntimeNamespace(); + + if (sourceNamespace.equals(targetNamespace)) { return inputFileMap; } - MappingConfiguration mappingConfig = launcher.getMappingConfiguration(); - if (!mappingConfig.matches(gameId, gameVersion)) { String mappingsGameId = mappingConfig.getGameId(); String mappingsGameVersion = mappingConfig.getGameVersion(); @@ -176,10 +177,10 @@ public static Map deobfuscate(Map inputFileMap, Stri gameVersion)); } - String targetNamespace = mappingConfig.getTargetNamespace(); TinyTree mappings = mappingConfig.getMappings(); if (mappings == null + || !mappings.getMetadata().getNamespaces().contains(sourceNamespace) || !mappings.getMetadata().getNamespaces().contains(targetNamespace)) { Log.debug(LogCategory.GAME_REMAP, "No mappings, using input files"); return inputFileMap; @@ -235,7 +236,7 @@ public static Map deobfuscate(Map inputFileMap, Stri try { Files.createDirectories(deobfJarDir); - deobfuscate0(inputFiles, outputFiles, tmpFiles, mappings, targetNamespace, launcher); + deobfuscate0(inputFiles, outputFiles, tmpFiles, mappings, sourceNamespace, targetNamespace, launcher); } catch (IOException e) { throw new RuntimeException("error remapping game jars "+inputFiles, e); } @@ -262,9 +263,10 @@ private static Path getDeobfJarDir(Path gameDir, String gameId, String gameVersi return ret.resolve(versionDirName.toString().replaceAll("[^\\w\\-\\. ]+", "_")); } - private static void deobfuscate0(List inputFiles, List outputFiles, List tmpFiles, TinyTree mappings, String targetNamespace, FabricLauncher launcher) throws IOException { + private static void deobfuscate0(List inputFiles, List outputFiles, List tmpFiles, + TinyTree mappings, String sourceNamespace, String targetNamespace, FabricLauncher launcher) throws IOException { TinyRemapper remapper = TinyRemapper.newRemapper() - .withMappings(TinyRemapperMappingsHelper.create(mappings, "official", targetNamespace)) + .withMappings(TinyRemapperMappingsHelper.create(mappings, sourceNamespace, targetNamespace)) .rebuildSourceFilenames(true) .build(); diff --git a/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java b/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java index ce051e500..aaec48267 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java +++ b/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java @@ -48,7 +48,7 @@ import net.fabricmc.loader.impl.util.log.LogCategory; public final class LibClassifier & LibraryType> { - private static final boolean DEBUG = System.getProperty(SystemProperties.DEBUG_LOG_LIB_CLASSIFICATION) != null; + private static final boolean DEBUG = SystemProperties.isSet(SystemProperties.DEBUG_LOG_LIB_CLASSIFICATION); private final List libs; private final Map origins; diff --git a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncher.java b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncher.java index 9566c4dda..ade90826a 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncher.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncher.java @@ -59,7 +59,7 @@ public interface FabricLauncher { String getEntrypoint(); - String getTargetNamespace(); + String getDefaultRuntimeNamespace(); List getClassPath(); } diff --git a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java index 856595331..96593e5ec 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java @@ -28,10 +28,13 @@ import net.fabricmc.loader.impl.FormattedException; import net.fabricmc.loader.impl.game.GameProvider; import net.fabricmc.loader.impl.gui.FabricGuiEntry; +import net.fabricmc.loader.impl.util.SystemProperties; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; public abstract class FabricLauncherBase implements FabricLauncher { + protected static final boolean isDevelopment = SystemProperties.isSet(SystemProperties.DEVELOPMENT); + private static boolean mixinReady; private static Map properties; private static FabricLauncher launcher; @@ -45,11 +48,24 @@ public static Class getClass(String className) throws ClassNotFoundException return Class.forName(className, true, getLauncher().getTargetClassLoader()); } + @Override + public final boolean isDevelopment() { + return isDevelopment; + } + @Override public MappingConfiguration getMappingConfiguration() { return mappingConfiguration; } + @Override + public final String getDefaultRuntimeNamespace() { + String ret = System.getProperty(SystemProperties.RUNTIME_MAPPING_NAMESPACE); + if (ret != null) return ret; + + return isDevelopment ? MappingConfiguration.NAMED_NAMESPACE : MappingConfiguration.INTERMEDIARY_NAMESPACE; + } + protected static void setProperties(Map propertiesA) { if (properties != null && properties != propertiesA) { throw new RuntimeException("Duplicate setProperties call!"); diff --git a/src/main/java/net/fabricmc/loader/impl/launch/FabricMixinBootstrap.java b/src/main/java/net/fabricmc/loader/impl/launch/FabricMixinBootstrap.java index a9cc9c4c9..09b0de495 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/FabricMixinBootstrap.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/FabricMixinBootstrap.java @@ -37,6 +37,8 @@ import net.fabricmc.loader.api.metadata.ModDependency.Kind; import net.fabricmc.loader.api.metadata.version.VersionInterval; import net.fabricmc.loader.impl.FabricLoaderImpl; +import net.fabricmc.loader.impl.LoaderExtensionApiImpl; +import net.fabricmc.loader.impl.LoaderExtensionApiImpl.MixinConfigEntry; import net.fabricmc.loader.impl.ModContainerImpl; import net.fabricmc.loader.impl.launch.knot.MixinServiceKnot; import net.fabricmc.loader.impl.launch.knot.MixinServiceKnotBootstrap; @@ -63,20 +65,21 @@ public static void init(EnvType side, FabricLoaderImpl loader) { if (FabricLauncherBase.getLauncher().isDevelopment()) { MappingConfiguration mappingConfiguration = FabricLauncherBase.getLauncher().getMappingConfiguration(); TinyTree mappings = mappingConfiguration.getMappings(); + final String modNs = MappingConfiguration.INTERMEDIARY_NAMESPACE; + String runtimeNs = mappingConfiguration.getRuntimeNamespace(); - if (mappings != null) { + if (mappings != null && !modNs.equals(runtimeNs)) { List namespaces = mappings.getMetadata().getNamespaces(); - if (namespaces.contains("intermediary") && namespaces.contains(mappingConfiguration.getTargetNamespace())) { + if (namespaces.contains(modNs) && namespaces.contains(runtimeNs)) { System.setProperty("mixin.env.remapRefMap", "true"); try { - MixinIntermediaryDevRemapper remapper = new MixinIntermediaryDevRemapper(mappings, "intermediary", mappingConfiguration.getTargetNamespace()); + MixinIntermediaryDevRemapper remapper = new MixinIntermediaryDevRemapper(mappings, modNs, runtimeNs); MixinEnvironment.getDefaultEnvironment().getRemappers().add(remapper); Log.info(LogCategory.MIXIN, "Loaded Fabric development mappings for mixin remapper!"); } catch (Exception e) { - Log.error(LogCategory.MIXIN, "Fabric development environment setup error - the game will probably crash soon!"); - e.printStackTrace(); + Log.error(LogCategory.MIXIN, "Fabric development environment setup error - the game will probably crash soon!", e); } } } @@ -87,7 +90,7 @@ public static void init(EnvType side, FabricLoaderImpl loader) { for (ModContainerImpl mod : loader.getModsInternal()) { for (String config : mod.getMetadata().getMixinConfigs(side)) { ModContainerImpl prev = configToModMap.putIfAbsent(config, mod); - if (prev != null) throw new RuntimeException(String.format("Non-unique Mixin config name %s used by the mods %s and %s", config, prev.getMetadata().getId(), mod.getMetadata().getId())); + if (prev != null) throw new RuntimeException(String.format("Non-unique Mixin config name %s used by the mods %s and %s", config, prev.getId(), mod.getId())); try { Mixins.addConfiguration(config); @@ -97,6 +100,20 @@ public static void init(EnvType side, FabricLoaderImpl loader) { } } + for (MixinConfigEntry entry : LoaderExtensionApiImpl.getMixinConfigs()) { + ModContainerImpl mod = loader.getModInternal(entry.modId); + if (mod == null) throw new RuntimeException(String.format("Unknown mod %s added through plugin API by %s", entry.modId, entry.extensionModId)); + + ModContainerImpl prev = configToModMap.putIfAbsent(entry.location, mod); + if (prev != null) throw new RuntimeException(String.format("Non-unique Mixin config name %s used by the mods %s and %s (through plugin %s)", entry.location, prev.getId(), entry.modId, entry.extensionModId)); + + try { + Mixins.addConfiguration(entry.location); + } catch (Throwable t) { + throw new RuntimeException(String.format("Error creating Mixin config %s for mod %s through plugin %s", entry.location, entry.modId, entry.extensionModId), t); + } + } + for (Config config : Mixins.getConfigs()) { ModContainerImpl mod = configToModMap.get(config.getName()); if (mod == null) continue; @@ -104,7 +121,7 @@ public static void init(EnvType side, FabricLoaderImpl loader) { try { IMixinConfig.class.getMethod("decorate", String.class, Object.class); - MixinConfigDecorator.apply(configToModMap); + MixinConfigDecorator.apply(configToModMap, side); } catch (NoSuchMethodException e) { Log.info(LogCategory.MIXIN, "Detected old Mixin version without config decoration support"); } @@ -123,18 +140,18 @@ private static final class MixinConfigDecorator { addVersion("0.12.0-", FabricUtil.COMPATIBILITY_0_10_0); } - static void apply(Map configToModMap) { + static void apply(Map configToModMap, EnvType env) { for (Config rawConfig : Mixins.getConfigs()) { ModContainerImpl mod = configToModMap.get(rawConfig.getName()); if (mod == null) continue; IMixinConfig config = rawConfig.getConfig(); config.decorate(FabricUtil.KEY_MOD_ID, mod.getMetadata().getId()); - config.decorate(FabricUtil.KEY_COMPATIBILITY, getMixinCompat(mod)); + config.decorate(FabricUtil.KEY_COMPATIBILITY, getMixinCompat(mod, env)); } } - private static int getMixinCompat(ModContainerImpl mod) { + private static int getMixinCompat(ModContainerImpl mod, EnvType env) { // infer from loader dependency by determining the least relevant loader version the mod accepts // AND any loader deps diff --git a/src/main/java/net/fabricmc/loader/impl/launch/MappingConfiguration.java b/src/main/java/net/fabricmc/loader/impl/launch/MappingConfiguration.java index d5edc62a0..3c6ca311d 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/MappingConfiguration.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/MappingConfiguration.java @@ -26,6 +26,7 @@ import java.util.jar.Manifest; import java.util.zip.ZipError; +import net.fabricmc.loader.impl.FabricLoaderImpl; import net.fabricmc.loader.impl.util.ManifestUtil; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; @@ -33,8 +34,13 @@ import net.fabricmc.mapping.tree.TinyTree; public final class MappingConfiguration { + public static final String OFFICIAL_NAMESPACE = "official"; + public static final String INTERMEDIARY_NAMESPACE = "intermediary"; + public static final String NAMED_NAMESPACE = "named"; + private boolean initialized; + private String namespace; private String gameId; private String gameVersion; private TinyTree mappings; @@ -64,18 +70,22 @@ public TinyTree getMappings() { return mappings; } - public String getTargetNamespace() { - return FabricLauncherBase.getLauncher().isDevelopment() ? "named" : "intermediary"; + public String getRuntimeNamespace() { + initialize(); + + return namespace; } public boolean requiresPackageAccessHack() { // TODO - return getTargetNamespace().equals("named"); + return getRuntimeNamespace().equals(NAMED_NAMESPACE); } private void initialize() { if (initialized) return; + namespace = FabricLoaderImpl.INSTANCE.getGameProvider().getRuntimeNamespace(FabricLauncherBase.getLauncher().getDefaultRuntimeNamespace()); + URL url = MappingConfiguration.class.getClassLoader().getResource("mappings/mappings.tiny"); if (url != null) { diff --git a/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java b/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java index c162461a1..dc44ecdab 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java @@ -54,7 +54,6 @@ public final class Knot extends FabricLauncherBase { protected Map properties = new HashMap<>(); private KnotClassLoaderInterface classLoader; - private boolean isDevelopment; private EnvType envType; private final List classPath = new ArrayList<>(); private GameProvider provider; @@ -131,24 +130,20 @@ protected ClassLoader init(String[] args) { Log.finishBuiltinConfig(); Log.info(LogCategory.GAME_PROVIDER, "Loading %s %s with Fabric Loader %s", provider.getGameName(), provider.getRawGameVersion(), FabricLoaderImpl.VERSION); - isDevelopment = Boolean.parseBoolean(System.getProperty(SystemProperties.DEVELOPMENT, "false")); - // Setup classloader // TODO: Provide KnotCompatibilityClassLoader in non-exclusive-Fabric pre-1.13 environments? - boolean useCompatibility = provider.requiresUrlClassLoader() || Boolean.parseBoolean(System.getProperty("fabric.loader.useCompatibilityClassLoader", "false")); + boolean useCompatibility = provider.requiresUrlClassLoader() || SystemProperties.isSet(SystemProperties.USE_COMPAT_CL); classLoader = KnotClassLoaderInterface.create(useCompatibility, isDevelopment(), envType, provider); ClassLoader cl = classLoader.getClassLoader(); - - provider.initialize(this); - Thread.currentThread().setContextClassLoader(cl); FabricLoaderImpl loader = FabricLoaderImpl.INSTANCE; loader.setGameProvider(provider); + provider.initialize(this); loader.load(); loader.freeze(); - FabricLoaderImpl.INSTANCE.loadAccessWideners(); + FabricLoaderImpl.INSTANCE.loadClassTweakers(); FabricMixinBootstrap.init(getEnvironmentType(), loader); FabricLauncherBase.finishMixinBootstrapping(); @@ -256,12 +251,6 @@ private static GameProvider findEmbedddedGameProvider() { } } - @Override - public String getTargetNamespace() { - // TODO: Won't work outside of Yarn - return isDevelopment ? "named" : "intermediary"; - } - @Override public List getClassPath() { return classPath; @@ -328,11 +317,6 @@ public Manifest getManifest(Path originPath) { return classLoader.getManifest(originPath); } - @Override - public boolean isDevelopment() { - return isDevelopment; - } - @Override public String getEntrypoint() { return provider.getEntrypoint(); diff --git a/src/main/java/net/fabricmc/loader/impl/launch/knot/KnotClassDelegate.java b/src/main/java/net/fabricmc/loader/impl/launch/knot/KnotClassDelegate.java index c793e6995..daa2b99d1 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/knot/KnotClassDelegate.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/knot/KnotClassDelegate.java @@ -55,10 +55,10 @@ import net.fabricmc.loader.impl.util.log.LogCategory; final class KnotClassDelegate implements KnotClassLoaderInterface { - private static final boolean LOG_CLASS_LOAD = System.getProperty(SystemProperties.DEBUG_LOG_CLASS_LOAD) != null; - private static final boolean LOG_CLASS_LOAD_ERRORS = LOG_CLASS_LOAD || System.getProperty(SystemProperties.DEBUG_LOG_CLASS_LOAD_ERRORS) != null; - private static final boolean LOG_TRANSFORM_ERRORS = System.getProperty(SystemProperties.DEBUG_LOG_TRANSFORM_ERRORS) != null; - private static final boolean DISABLE_ISOLATION = System.getProperty(SystemProperties.DEBUG_DISABLE_CLASS_PATH_ISOLATION) != null; + private static final boolean LOG_CLASS_LOAD = SystemProperties.isSet(SystemProperties.DEBUG_LOG_CLASS_LOAD); + private static final boolean LOG_CLASS_LOAD_ERRORS = LOG_CLASS_LOAD || SystemProperties.isSet(SystemProperties.DEBUG_LOG_CLASS_LOAD_ERRORS); + private static final boolean LOG_TRANSFORM_ERRORS = SystemProperties.isSet(SystemProperties.DEBUG_LOG_TRANSFORM_ERRORS); + private static final boolean DISABLE_ISOLATION = SystemProperties.isSet(SystemProperties.DEBUG_DISABLE_CLASS_PATH_ISOLATION); static final class Metadata { static final Metadata EMPTY = new Metadata(null, null); diff --git a/src/main/java/net/fabricmc/loader/impl/launch/server/FabricServerLauncher.java b/src/main/java/net/fabricmc/loader/impl/launch/server/FabricServerLauncher.java index 250383679..28a6a7b18 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/server/FabricServerLauncher.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/server/FabricServerLauncher.java @@ -54,7 +54,7 @@ public static void main(String[] args) { } } - boolean dev = Boolean.parseBoolean(System.getProperty(SystemProperties.DEVELOPMENT, "false")); + boolean dev = SystemProperties.isSet(SystemProperties.DEVELOPMENT); if (!dev) { try { diff --git a/src/main/java/net/fabricmc/loader/impl/lib/gson/JsonWriter.java b/src/main/java/net/fabricmc/loader/impl/lib/gson/JsonWriter.java new file mode 100644 index 000000000..f8061c6dd --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/lib/gson/JsonWriter.java @@ -0,0 +1,655 @@ +/* + * Copyright (C) 2010 Google Inc. + * Copyright (c) 2022 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file has been modified by the Fabric project (repackage, minor changes). + */ + +package net.fabricmc.loader.impl.lib.gson; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; + +/** + * Writes a JSON (RFC 7159) + * encoded value to a stream, one token at a time. The stream includes both + * literal values (strings, numbers, booleans and nulls) as well as the begin + * and end delimiters of objects and arrays. + * + *

Encoding JSON

+ * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON + * document must contain one top-level array or object. Call methods on the + * writer as you walk the structure's contents, nesting arrays and objects as + * necessary: + *
    + *
  • To write arrays, first call {@link #beginArray()}. + * Write each of the array's elements with the appropriate {@link #value} + * methods or by nesting other arrays and objects. Finally close the array + * using {@link #endArray()}. + *
  • To write objects, first call {@link #beginObject()}. + * Write each of the object's properties by alternating calls to + * {@link #name} with the property's value. Write property values with the + * appropriate {@link #value} method or by nesting other objects or arrays. + * Finally close the object using {@link #endObject()}. + *
+ * + *

Example

+ * Suppose we'd like to encode a stream of messages such as the following:
 {@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I stream JSON in Java?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "json_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@json_newb just use JsonWriter!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]}
+ * This code encodes the above structure:
   {@code
+ *   public void writeJsonStream(OutputStream out, List messages) throws IOException {
+ *     JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
+ *     writer.setIndent("    ");
+ *     writeMessagesArray(writer, messages);
+ *     writer.close();
+ *   }
+ *
+ *   public void writeMessagesArray(JsonWriter writer, List messages) throws IOException {
+ *     writer.beginArray();
+ *     for (Message message : messages) {
+ *       writeMessage(writer, message);
+ *     }
+ *     writer.endArray();
+ *   }
+ *
+ *   public void writeMessage(JsonWriter writer, Message message) throws IOException {
+ *     writer.beginObject();
+ *     writer.name("id").value(message.getId());
+ *     writer.name("text").value(message.getText());
+ *     if (message.getGeo() != null) {
+ *       writer.name("geo");
+ *       writeDoublesArray(writer, message.getGeo());
+ *     } else {
+ *       writer.name("geo").nullValue();
+ *     }
+ *     writer.name("user");
+ *     writeUser(writer, message.getUser());
+ *     writer.endObject();
+ *   }
+ *
+ *   public void writeUser(JsonWriter writer, User user) throws IOException {
+ *     writer.beginObject();
+ *     writer.name("name").value(user.getName());
+ *     writer.name("followers_count").value(user.getFollowersCount());
+ *     writer.endObject();
+ *   }
+ *
+ *   public void writeDoublesArray(JsonWriter writer, List doubles) throws IOException {
+ *     writer.beginArray();
+ *     for (Double value : doubles) {
+ *       writer.value(value);
+ *     }
+ *     writer.endArray();
+ *   }}
+ * + *

Each {@code JsonWriter} may be used to write a single JSON stream. + * Instances of this class are not thread safe. Calls that would result in a + * malformed JSON string will fail with an {@link IllegalStateException}. + * + * @author Jesse Wilson + * @since 1.6 + */ +public class JsonWriter implements Closeable, Flushable { + + /* + * From RFC 7159, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + * + * We also escape '\u2028' and '\u2029', which JavaScript interprets as + * newline characters. This prevents eval() from failing with a syntax + * error. http://code.google.com/p/google-gson/issues/detail?id=341 + */ + private static final String[] REPLACEMENT_CHARS; + private static final String[] HTML_SAFE_REPLACEMENT_CHARS; + static { + REPLACEMENT_CHARS = new String[128]; + for (int i = 0; i <= 0x1f; i++) { + REPLACEMENT_CHARS[i] = String.format("\\u%04x", i); + } + REPLACEMENT_CHARS['"'] = "\\\""; + REPLACEMENT_CHARS['\\'] = "\\\\"; + REPLACEMENT_CHARS['\t'] = "\\t"; + REPLACEMENT_CHARS['\b'] = "\\b"; + REPLACEMENT_CHARS['\n'] = "\\n"; + REPLACEMENT_CHARS['\r'] = "\\r"; + REPLACEMENT_CHARS['\f'] = "\\f"; + HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone(); + HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c"; + HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e"; + HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026"; + HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d"; + HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027"; + } + + /** The output data, containing at most one top-level array or object. */ + private final Writer out; + + private int[] stack = new int[32]; + private int stackSize = 0; + { + push(JsonScope.EMPTY_DOCUMENT); + } + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private String indent; + + /** + * The name/value separator; either ":" or ": ". + */ + private String separator = ":"; + + private boolean lenient; + + private boolean htmlSafe; + + private String deferredName; + + private boolean serializeNulls = true; + + /** + * Creates a new instance that writes a JSON-encoded stream to {@code out}. + * For best performance, ensure {@link Writer} is buffered; wrapping in + * {@link java.io.BufferedWriter BufferedWriter} if necessary. + */ + public JsonWriter(Writer out) { + if (out == null) { + throw new NullPointerException("out == null"); + } + this.out = out; + } + + /** + * Sets the indentation string to be repeated for each level of indentation + * in the encoded document. If {@code indent.isEmpty()} the encoded document + * will be compact. Otherwise the encoded document will be more + * human-readable. + * + * @param indent a string containing only whitespace. + */ + public final void setIndent(String indent) { + if (indent.length() == 0) { + this.indent = null; + this.separator = ":"; + } else { + this.indent = indent; + this.separator = ": "; + } + } + + /** + * Configure this writer to relax its syntax rules. By default, this writer + * only emits well-formed JSON as specified by RFC 7159. Setting the writer + * to lenient permits the following: + *

    + *
  • Top-level values of any type. With strict writing, the top-level + * value must be an object or an array. + *
  • Numbers may be {@link Double#isNaN() NaNs} or {@link + * Double#isInfinite() infinities}. + *
+ */ + public final void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Returns true if this writer has relaxed syntax rules. + */ + public boolean isLenient() { + return lenient; + } + + /** + * Configure this writer to emit JSON that's safe for direct inclusion in HTML + * and XML documents. This escapes the HTML characters {@code <}, {@code >}, + * {@code &} and {@code =} before writing them to the stream. Without this + * setting, your XML/HTML encoder should replace these characters with the + * corresponding escape sequences. + */ + public final void setHtmlSafe(boolean htmlSafe) { + this.htmlSafe = htmlSafe; + } + + /** + * Returns true if this writer writes JSON that's safe for inclusion in HTML + * and XML documents. + */ + public final boolean isHtmlSafe() { + return htmlSafe; + } + + /** + * Sets whether object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + /** + * Returns true if object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final boolean getSerializeNulls() { + return serializeNulls; + } + + /** + * Begins encoding a new array. Each call to this method must be paired with + * a call to {@link #endArray}. + * + * @return this writer. + */ + public JsonWriter beginArray() throws IOException { + writeDeferredName(); + return open(JsonScope.EMPTY_ARRAY, '['); + } + + /** + * Ends encoding the current array. + * + * @return this writer. + */ + public JsonWriter endArray() throws IOException { + return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, ']'); + } + + /** + * Begins encoding a new object. Each call to this method must be paired + * with a call to {@link #endObject}. + * + * @return this writer. + */ + public JsonWriter beginObject() throws IOException { + writeDeferredName(); + return open(JsonScope.EMPTY_OBJECT, '{'); + } + + /** + * Ends encoding the current object. + * + * @return this writer. + */ + public JsonWriter endObject() throws IOException { + return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, '}'); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(int empty, char openBracket) throws IOException { + beforeValue(); + push(empty); + out.write(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(int empty, int nonempty, char closeBracket) + throws IOException { + int context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem."); + } + if (deferredName != null) { + throw new IllegalStateException("Dangling name: " + deferredName); + } + + stackSize--; + if (context == nonempty) { + newline(); + } + out.write(closeBracket); + return this; + } + + private void push(int newTop) { + if (stackSize == stack.length) { + stack = Arrays.copyOf(stack, stackSize * 2); + } + stack[stackSize++] = newTop; + } + + /** + * Returns the value on the top of the stack. + */ + private int peek() { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + return stack[stackSize - 1]; + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(int topOfStack) { + stack[stackSize - 1] = topOfStack; + } + + /** + * Encodes the property name. + * + * @param name the name of the forthcoming value. May not be null. + * @return this writer. + */ + public JsonWriter name(String name) throws IOException { + if (name == null) { + throw new NullPointerException("name == null"); + } + if (deferredName != null) { + throw new IllegalStateException(); + } + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + deferredName = name; + return this; + } + + private void writeDeferredName() throws IOException { + if (deferredName != null) { + beforeName(); + string(deferredName); + deferredName = null; + } + } + + /** + * Encodes {@code value}. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter value(String value) throws IOException { + if (value == null) { + return nullValue(); + } + writeDeferredName(); + beforeValue(); + string(value); + return this; + } + + /** + * Writes {@code value} directly to the writer without quoting or + * escaping. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter jsonValue(String value) throws IOException { + if (value == null) { + return nullValue(); + } + writeDeferredName(); + beforeValue(); + out.append(value); + return this; + } + + /** + * Encodes {@code null}. + * + * @return this writer. + */ + public JsonWriter nullValue() throws IOException { + if (deferredName != null) { + if (serializeNulls) { + writeDeferredName(); + } else { + deferredName = null; + return this; // skip the name and the value + } + } + beforeValue(); + out.write("null"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(boolean value) throws IOException { + writeDeferredName(); + beforeValue(); + out.write(value ? "true" : "false"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(Boolean value) throws IOException { + if (value == null) { + return nullValue(); + } + writeDeferredName(); + beforeValue(); + out.write(value ? "true" : "false"); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this writer. + */ + public JsonWriter value(double value) throws IOException { + writeDeferredName(); + if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(); + out.append(Double.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(long value) throws IOException { + writeDeferredName(); + beforeValue(); + out.write(Long.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this writer. + */ + public JsonWriter value(Number value) throws IOException { + if (value == null) { + return nullValue(); + } + + writeDeferredName(); + String string = value.toString(); + if (!lenient + && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(); + out.append(string); + return this; + } + + /** + * Ensures all buffered data is written to the underlying {@link Writer} + * and flushes that writer. + */ + @Override + public void flush() throws IOException { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + out.flush(); + } + + /** + * Flushes and closes this writer and the underlying {@link Writer}. + * + * @throws IOException if the JSON document is incomplete. + */ + @Override + public void close() throws IOException { + out.close(); + + int size = stackSize; + if (size > 1 || size == 1 && stack[size - 1] != JsonScope.NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + stackSize = 0; + } + + private void string(String value) throws IOException { + String[] replacements = htmlSafe ? HTML_SAFE_REPLACEMENT_CHARS : REPLACEMENT_CHARS; + out.write('\"'); + int last = 0; + int length = value.length(); + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + String replacement; + if (c < 128) { + replacement = replacements[c]; + if (replacement == null) { + continue; + } + } else if (c == '\u2028') { + replacement = "\\u2028"; + } else if (c == '\u2029') { + replacement = "\\u2029"; + } else { + continue; + } + if (last < i) { + out.write(value, last, i - last); + } + out.write(replacement); + last = i + 1; + } + if (last < length) { + out.write(value, last, length - last); + } + out.write('\"'); + } + + private void newline() throws IOException { + if (indent == null) { + return; + } + + out.write('\n'); + for (int i = 1, size = stackSize; i < size; i++) { + out.write(indent); + } + } + + /** + * Inserts any necessary separators and whitespace before a name. Also + * adjusts the stack to expect the name's value. + */ + private void beforeName() throws IOException { + int context = peek(); + if (context == JsonScope.NONEMPTY_OBJECT) { // first in object + out.write(','); + } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object! + throw new IllegalStateException("Nesting problem."); + } + newline(); + replaceTop(JsonScope.DANGLING_NAME); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + */ + @SuppressWarnings("fallthrough") + private void beforeValue() throws IOException { + switch (peek()) { + case JsonScope.NONEMPTY_DOCUMENT: + if (!lenient) { + throw new IllegalStateException( + "JSON must have only one top-level value."); + } + // fall-through + case JsonScope.EMPTY_DOCUMENT: // first in document + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + break; + + case JsonScope.EMPTY_ARRAY: // first in array + replaceTop(JsonScope.NONEMPTY_ARRAY); + newline(); + break; + + case JsonScope.NONEMPTY_ARRAY: // another in array + out.append(','); + newline(); + break; + + case JsonScope.DANGLING_NAME: // value for name + out.append(separator); + replaceTop(JsonScope.NONEMPTY_OBJECT); + break; + + default: + throw new IllegalStateException("Nesting problem."); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/AbstractModMetadata.java b/src/main/java/net/fabricmc/loader/impl/metadata/AbstractModMetadata.java index fdf535752..6d95f496f 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/AbstractModMetadata.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/AbstractModMetadata.java @@ -23,11 +23,6 @@ public abstract class AbstractModMetadata implements ModMetadata { public static final String TYPE_BUILTIN = "builtin"; public static final String TYPE_FABRIC_MOD = "fabric"; - @Override - public boolean containsCustomElement(String key) { - return containsCustomValue(key); - } - @Override public boolean containsCustomValue(String key) { return getCustomValues().containsKey(key); diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/BuiltinModMetadata.java b/src/main/java/net/fabricmc/loader/impl/metadata/BuiltinModMetadata.java index b1e2e2c3d..7a3b34cc3 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/BuiltinModMetadata.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/BuiltinModMetadata.java @@ -31,8 +31,10 @@ import net.fabricmc.loader.api.metadata.CustomValue; import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.api.metadata.ModLoadCondition; import net.fabricmc.loader.api.metadata.ModMetadata; import net.fabricmc.loader.api.metadata.Person; +import net.fabricmc.loader.api.metadata.ProvidedMod; import net.fabricmc.loader.impl.util.version.VersionParser; public final class BuiltinModMetadata extends AbstractModMetadata { @@ -80,7 +82,7 @@ public String getId() { } @Override - public Collection getProvides() { + public Collection getAdditionallyProvidedMods() { return Collections.emptyList(); } @@ -94,6 +96,11 @@ public ModEnvironment getEnvironment() { return environment; } + @Override + public ModLoadCondition getLoadCondition() { + return ModLoadCondition.ALWAYS; + } + @Override public String getName() { return name; diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/CustomValueImpl.java b/src/main/java/net/fabricmc/loader/impl/metadata/CustomValueImpl.java index f13427763..2473d1224 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/CustomValueImpl.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/CustomValueImpl.java @@ -29,7 +29,7 @@ import net.fabricmc.loader.api.metadata.CustomValue; import net.fabricmc.loader.impl.lib.gson.JsonReader; -abstract class CustomValueImpl implements CustomValue { +public abstract class CustomValueImpl implements CustomValue { static final CustomValue BOOLEAN_TRUE = new BooleanImpl(true); static final CustomValue BOOLEAN_FALSE = new BooleanImpl(false); static final CustomValue NULL = new NullImpl(); @@ -48,7 +48,7 @@ public static CustomValue readCustomValue(JsonReader reader) throws IOException, reader.endObject(); - return new ObjectImpl(values); + return of(values); case BEGIN_ARRAY: reader.beginArray(); @@ -60,26 +60,50 @@ public static CustomValue readCustomValue(JsonReader reader) throws IOException, reader.endArray(); - return new ArrayImpl(entries); + return of(entries); case STRING: - return new StringImpl(reader.nextString()); + return of(reader.nextString()); case NUMBER: // TODO: Parse this somewhat more smartly? - return new NumberImpl(reader.nextDouble()); + return of(reader.nextDouble()); case BOOLEAN: - if (reader.nextBoolean()) { - return BOOLEAN_TRUE; - } - - return BOOLEAN_FALSE; + return of(reader.nextBoolean()); case NULL: reader.nextNull(); - return NULL; + return ofNull(); default: throw new ParseMetadataException(Objects.toString(reader.nextName()), reader); } } + public static CvObject of(Map map) { + Objects.requireNonNull(map, "null map"); + + return new ObjectImpl(map); + } + + public static CvArray of(List list) { + Objects.requireNonNull(list, "null list"); + + return new ArrayImpl(list); + } + + public static CustomValue of(String value) { + return value != null ? new StringImpl(value) : NULL; + } + + public static CustomValue of(Number value) { + return value != null ? new NumberImpl(value) : NULL; + } + + public static CustomValue of(boolean value) { + return value ? BOOLEAN_TRUE : BOOLEAN_FALSE; + } + + public static CustomValue ofNull() { + return NULL; + } + @Override public final CvObject getAsObject() { if (this instanceof ObjectImpl) { @@ -116,6 +140,18 @@ public final Number getAsNumber() { } } + @Override + public int getAsInteger() { + double value; + int ret; + + if (this instanceof NumberImpl && (value = ((NumberImpl) this).value.doubleValue()) == (ret = (int) value)) { + return ret; + } else { + throw new ClassCastException("can't convert "+getType().name()+" to int"); + } + } + @Override public final boolean getAsBoolean() { if (this instanceof BooleanImpl) { @@ -152,10 +188,20 @@ public CustomValue get(String key) { return entries.get(key); } + @Override + public CustomValue getOrDefault(String key, CustomValue defaultValue) { + return entries.getOrDefault(key, defaultValue); + } + @Override public Iterator> iterator() { return entries.entrySet().iterator(); } + + @Override + public String toString() { + return entries.toString(); + } } private static final class ArrayImpl extends CustomValueImpl implements CvArray { @@ -184,6 +230,11 @@ public CustomValue get(int index) { public Iterator iterator() { return entries.iterator(); } + + @Override + public String toString() { + return entries.toString(); + } } private static final class StringImpl extends CustomValueImpl { @@ -197,6 +248,11 @@ private static final class StringImpl extends CustomValueImpl { public CvType getType() { return CvType.STRING; } + + @Override + public String toString() { + return value.toString(); + } } private static final class NumberImpl extends CustomValueImpl { @@ -210,6 +266,11 @@ private static final class NumberImpl extends CustomValueImpl { public CvType getType() { return CvType.NUMBER; } + + @Override + public String toString() { + return value.toString(); + } } private static final class BooleanImpl extends CustomValueImpl { @@ -223,6 +284,11 @@ private static final class BooleanImpl extends CustomValueImpl { public CvType getType() { return CvType.BOOLEAN; } + + @Override + public String toString() { + return value ? "true" : "false"; + } } private static final class NullImpl extends CustomValueImpl { @@ -230,5 +296,10 @@ private static final class NullImpl extends CustomValueImpl { public CvType getType() { return CvType.NULL; } + + @Override + public String toString() { + return "null"; + } } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/DependencyOverrides.java b/src/main/java/net/fabricmc/loader/impl/metadata/DependencyOverrides.java index 4a1037eb0..7ccd8295f 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/DependencyOverrides.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/DependencyOverrides.java @@ -32,7 +32,7 @@ import java.util.Map; import java.util.stream.Collectors; -import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.extension.ModMetadataBuilder.ModDependencyBuilder; import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.impl.FormattedException; import net.fabricmc.loader.impl.lib.gson.JsonReader; @@ -101,7 +101,7 @@ private static List readKeys(JsonReader reader) throws IOException, Parse throw new ParseMetadataException("Dependency container must be an object!", reader); } - Map>> modOverrides = new EnumMap<>(ModDependency.Kind.class); + Map>> modOverrides = new EnumMap<>(ModDependency.Kind.class); reader.beginObject(); while (reader.hasNext()) { @@ -126,7 +126,7 @@ private static List readKeys(JsonReader reader) throws IOException, Parse reader); } - List deps = readDependencies(reader, kind); + List deps = readDependencies(reader, kind); if (!deps.isEmpty() || op == Operation.REPLACE) { modOverrides.computeIfAbsent(kind, ignore -> new EnumMap<>(Operation.class)).put(op, deps); @@ -137,11 +137,11 @@ private static List readKeys(JsonReader reader) throws IOException, Parse List ret = new ArrayList<>(); - for (Map.Entry>> entry : modOverrides.entrySet()) { + for (Map.Entry>> entry : modOverrides.entrySet()) { ModDependency.Kind kind = entry.getKey(); - Map> map = entry.getValue(); + Map> map = entry.getValue(); - List values = map.get(Operation.REPLACE); + List values = map.get(Operation.REPLACE); if (values != null) { ret.add(new Entry(Operation.REPLACE, kind, values)); // suppresses add+remove @@ -157,44 +157,19 @@ private static List readKeys(JsonReader reader) throws IOException, Parse return ret; } - private static List readDependencies(JsonReader reader, ModDependency.Kind kind) throws IOException, ParseMetadataException { + private static List readDependencies(JsonReader reader, ModDependency.Kind kind) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_OBJECT) { throw new ParseMetadataException("Dependency container must be an object!", reader); } - List ret = new ArrayList<>(); + List ret = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { - final String modId = reader.nextName(); - final List matcherStringList = new ArrayList<>(); + ModDependencyBuilder builder = ModDependencyBuilder.create(kind, reader.nextName()); + V0ModMetadataParser.readDependencyValue(reader, builder); - switch (reader.peek()) { - case STRING: - matcherStringList.add(reader.nextString()); - break; - case BEGIN_ARRAY: - reader.beginArray(); - - while (reader.hasNext()) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Dependency version range array must only contain string values", reader); - } - - matcherStringList.add(reader.nextString()); - } - - reader.endArray(); - break; - default: - throw new ParseMetadataException("Dependency version range must be a string or string array!", reader); - } - - try { - ret.add(new ModDependencyImpl(kind, modId, matcherStringList)); - } catch (VersionParsingException e) { - throw new ParseMetadataException(e); - } + ret.add((ModDependencyImpl) builder.build()); } reader.endObject(); @@ -208,12 +183,12 @@ public void apply(LoaderModMetadata metadata) { List modOverrides = dependencyOverrides.get(metadata.getId()); if (modOverrides == null) return; - List deps = new ArrayList<>(metadata.getDependencies()); + List deps = new ArrayList<>(metadata.getDependencies()); for (Entry entry : modOverrides) { switch (entry.operation) { case REPLACE: - for (Iterator it = deps.iterator(); it.hasNext(); ) { + for (Iterator it = deps.iterator(); it.hasNext(); ) { ModDependency dep = it.next(); if (dep.getKind() == entry.kind) { @@ -224,7 +199,7 @@ public void apply(LoaderModMetadata metadata) { deps.addAll(entry.values); break; case REMOVE: - for (Iterator it = deps.iterator(); it.hasNext(); ) { + for (Iterator it = deps.iterator(); it.hasNext(); ) { ModDependency dep = it.next(); if (dep.getKind() == entry.kind) { @@ -254,9 +229,9 @@ public Collection getAffectedModIds() { private static final class Entry { final Operation operation; final ModDependency.Kind kind; - final List values; + final List values; - Entry(Operation operation, ModDependency.Kind kind, List values) { + Entry(Operation operation, ModDependency.Kind kind, List values) { this.operation = operation; this.kind = kind; this.values = values; diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/LoaderModMetadata.java b/src/main/java/net/fabricmc/loader/impl/metadata/LoaderModMetadata.java index 0225b97f5..8fea32e83 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/LoaderModMetadata.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/LoaderModMetadata.java @@ -16,13 +16,13 @@ package net.fabricmc.loader.impl.metadata; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.Version; -import net.fabricmc.loader.api.metadata.ModDependency; /** * Internal variant of the ModMetadata interface. @@ -31,6 +31,8 @@ public interface LoaderModMetadata extends net.fabricmc.loader.metadata.LoaderModMetadata { int getSchemaVersion(); + String getLoadPhase(); + default String getOldStyleLanguageAdapter() { return "net.fabricmc.loader.language.JavaLanguageAdapter"; } @@ -38,10 +40,11 @@ default String getOldStyleLanguageAdapter() { Map getLanguageAdapterDefinitions(); Collection getJars(); Collection getMixinConfigs(EnvType type); - /* @Nullable */ - String getAccessWidener(); + Collection getClassTweakers(); @Override boolean loadsInEnvironment(EnvType type); + @Override + Collection getDependencies(); Collection getOldInitializers(); @Override @@ -49,8 +52,33 @@ default String getOldStyleLanguageAdapter() { @Override Collection getEntrypointKeys(); - void emitFormatWarnings(); - void setVersion(Version version); - void setDependencies(Collection dependencies); + void setDependencies(Collection dependencies); + + /** + * Adjust the metadata for the environment, stripping unsuitable deps. + */ + default void applyEnvironment(EnvType envType) { + Collection deps = getDependencies(); + boolean affected = false; + + for (ModDependencyImpl dep : deps) { + if (!dep.appliesInEnvironment(envType)) { + affected = true; + break; + } + } + + if (!affected) return; + + List newDeps = new ArrayList<>(deps.size() - 1); + + for (ModDependencyImpl dep : deps) { + if (dep.appliesInEnvironment(envType)) { + newDeps.add(dep); + } + } + + setDependencies(newDeps); + } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java b/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java index 6af58ab33..3441bca3c 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java @@ -29,15 +29,16 @@ import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.ProvidedMod; import net.fabricmc.loader.impl.FabricLoaderImpl; -import net.fabricmc.loader.impl.discovery.ModCandidate; +import net.fabricmc.loader.impl.discovery.ModCandidateImpl; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; public final class MetadataVerifier { private static final Pattern MOD_ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{1,63}"); - public static ModCandidate verifyIndev(ModCandidate mod) { + public static ModCandidateImpl verifyIndev(ModCandidateImpl mod) { if (FabricLoaderImpl.INSTANCE.isDevelopmentEnvironment()) { try { MetadataVerifier.verify(mod.getMetadata()); @@ -53,8 +54,8 @@ public static ModCandidate verifyIndev(ModCandidate mod) { static void verify(LoaderModMetadata metadata) throws ParseMetadataException { checkModId(metadata.getId(), "mod id"); - for (String providesDecl : metadata.getProvides()) { - checkModId(providesDecl, "provides declaration"); + for (ProvidedMod mod : metadata.getAdditionallyProvidedMods()) { + checkModId(mod.getId(), "provides declaration"); } // TODO: verify mod id and version decls in deps @@ -80,8 +81,6 @@ static void verify(LoaderModMetadata metadata) throws ParseMetadataException { Log.warn(LogCategory.METADATA, "Mod %s uses the version %s which isn't compatible with Loader's extended semantic version format (%s), SemVer is recommended for reliably evaluating dependencies and prioritizing newer version", metadata.getId(), version, exc.getMessage()); } - - metadata.emitFormatWarnings(); } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/ModDependencyImpl.java b/src/main/java/net/fabricmc/loader/impl/metadata/ModDependencyImpl.java index 0ce407630..8fecc7830 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/ModDependencyImpl.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ModDependencyImpl.java @@ -20,23 +20,34 @@ import java.util.Collections; import java.util.List; +import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.Version; -import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.ContactInformation; import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModEnvironment; import net.fabricmc.loader.api.metadata.version.VersionInterval; import net.fabricmc.loader.api.metadata.version.VersionPredicate; public final class ModDependencyImpl implements ModDependency { private Kind kind; private final String modId; - private final List matcherStringList; private final Collection ranges; - - public ModDependencyImpl(Kind kind, String modId, List matcherStringList) throws VersionParsingException { + private final ModEnvironment environment; + private final String reason; + private final ModDependency.Metadata metadata; + private final ModDependency.Metadata rootMetadata; + + ModDependencyImpl(Kind kind, + String modId, Collection versionOptions, + ModEnvironment environment, String reason, + ModDependency.Metadata metadata, ModDependency.Metadata rootMetadata) { this.kind = kind; this.modId = modId; - this.matcherStringList = matcherStringList; - this.ranges = VersionPredicate.parse(this.matcherStringList); + this.ranges = versionOptions; + this.environment = environment; + this.reason = reason; + this.metadata = metadata; + this.rootMetadata = rootMetadata; } @Override @@ -53,6 +64,26 @@ public String getModId() { return this.modId; } + ModEnvironment getEnvironment() { + return environment; + } + + boolean appliesInEnvironment(EnvType type) { + return environment.matches(type); + } + + String getReason() { + return reason; + } + + ModDependency.Metadata getMetadata() { + return metadata; + } + + ModDependency.Metadata getRootMetadata() { + return rootMetadata; + } + @Override public boolean matches(Version version) { for (VersionPredicate predicate : ranges) { @@ -86,12 +117,16 @@ public String toString() { builder.append(this.modId); builder.append(" @ ["); - for (int i = 0; i < matcherStringList.size(); i++) { - if (i > 0) { + boolean first = true; + + for (VersionPredicate range : ranges) { + if (first) { + first = false; + } else { builder.append(" || "); } - builder.append(matcherStringList.get(i)); + builder.append(range); } builder.append("]}"); @@ -113,4 +148,43 @@ public List getVersionIntervals() { return ret; } + + static final class Metadata implements ModDependency.Metadata { + private final String id; + private final String name; + private final String description; + private final ContactInformation contact; + + Metadata(String id, String name, String description, ContactInformation contact) { + this.id = id; + this.name = name; + this.description = description; + + if (contact != null) { + this.contact = contact; + } else { + this.contact = ContactInformation.EMPTY; + } + } + + @Override + public String getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public ContactInformation getContact() { + return contact; + } + } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataBuilderImpl.java b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataBuilderImpl.java new file mode 100644 index 000000000..25841937b --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataBuilderImpl.java @@ -0,0 +1,629 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.metadata; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; + +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.extension.ModMetadataBuilder; +import net.fabricmc.loader.api.metadata.ContactInformation; +import net.fabricmc.loader.api.metadata.CustomValue; +import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.api.metadata.ModLoadCondition; +import net.fabricmc.loader.api.metadata.Person; +import net.fabricmc.loader.api.metadata.ProvidedMod; +import net.fabricmc.loader.api.metadata.version.VersionPredicate; +import net.fabricmc.loader.impl.discovery.LoadPhases; +import net.fabricmc.loader.impl.metadata.ModMetadataImpl.EntrypointMetadataImpl; +import net.fabricmc.loader.impl.metadata.ModMetadataImpl.IconEntry; +import net.fabricmc.loader.impl.metadata.ModMetadataImpl.MapIconEntry; +import net.fabricmc.loader.impl.metadata.ModMetadataImpl.MixinEntry; +import net.fabricmc.loader.impl.metadata.ModMetadataImpl.ProvidedModImpl; +import net.fabricmc.loader.impl.metadata.ModMetadataImpl.SingleIconEntry; + +public final class ModMetadataBuilderImpl implements ModMetadataBuilder { + static final String DEFAULT_ENTRYPOINT_ADAPTER = "default"; + + int schemaVersion = ModMetadataParser.LATEST_VERSION; + + String id; + Version version; + + // Optional (id provides) + final List providedMods = new ArrayList<>(); + + // Optional (mod loading) + ModEnvironment environment = ModEnvironment.UNIVERSAL; // Default is always universal + ModLoadCondition loadCondition; + String loadPhase = LoadPhases.DEFAULT; + final Map> entrypoints = new HashMap<>(); + final List oldInitializers = new ArrayList<>(); + final List nestedMods = new ArrayList<>(); + final List mixins = new ArrayList<>(); + final List classTweakers = new ArrayList<>(); + + // Optional (dependency resolution) + List dependencies = new ArrayList<>(); + + // Optional (metadata) + String name = null; + String description = null; + final List authors = new ArrayList<>(); + final List contributors = new ArrayList<>(); + ContactInformation contact = null; + final List licenses = new ArrayList<>(); + final NavigableMap icons = new TreeMap<>(Comparator.naturalOrder());; + + // Optional (language adapter providers) + final Map languageAdapters = new HashMap<>(); + + // Optional (custom values) + final Map customValues = new HashMap<>(); + + public ModMetadataBuilderImpl() { } + + public ModMetadataBuilder setSchemaVersion(int version) { + this.schemaVersion = version; + + return this; + } + + @Override + public String getType() { + return ModMetadataImpl.TYPE_FABRIC_MOD; + } + + @Override + public String getId() { + return id; + } + + @Override + public ModMetadataBuilder setId(String modId) { + this.id = modId; + + return this; + } + + @Override + public Version getVersion() { + return version; + } + + @Override + public ModMetadataBuilder setVersion(String version) throws VersionParsingException { + return setVersion(Version.parse(version)); + } + + @Override + public ModMetadataBuilder setVersion(Version version) { + // replace default version in provided mods if it points to the old version + for (ProvidedModImpl mod : providedMods) { + if (!mod.hasOwnVersion) mod.setVersion(version); + } + + this.version = version; + + return this; + } + + @Override + public Collection getAdditionallyProvidedMods() { + return providedMods; + } + + @Override + public ModMetadataBuilder addProvidedMod(String modId, /* @Nullable */ Version version, boolean exclusive) { + Objects.requireNonNull(modId, "null modId"); + + boolean hasOwnVersion = version != null; + providedMods.add(new ProvidedModImpl(modId, hasOwnVersion ? version : this.version, hasOwnVersion, exclusive)); + + return this; + } + + @Override + public ModEnvironment getEnvironment() { + return environment; + } + + @Override + public ModMetadataBuilder setEnvironment(ModEnvironment environment) { + Objects.requireNonNull(environment, "null environment"); + + this.environment = environment; + + return this; + } + + @Override + public ModLoadCondition getLoadCondition() { + return loadCondition; + } + + @Override + public ModMetadataBuilder setLoadCondition(/* @Nullable */ ModLoadCondition loadCondition) { + this.loadCondition = loadCondition; + + return this; + } + + @Override + public ModMetadataBuilder setLoadPhase(/* @Nullable */ String loadPhase) { + if (loadPhase == null) loadPhase = LoadPhases.DEFAULT; + + this.loadPhase = loadPhase; + + return this; + } + + @Override + public ModMetadataBuilder addEntrypoint(String key, String value, /* @Nullable */ String adapter) { + Objects.requireNonNull(key, "null key"); + Objects.requireNonNull(value, "null value"); + + if (adapter == null) adapter = DEFAULT_ENTRYPOINT_ADAPTER; + entrypoints.computeIfAbsent(key, ignore -> new ArrayList<>()).add(new EntrypointMetadataImpl(adapter, value)); + + return this; + } + + public ModMetadataBuilder addOldInitializer(String initializer) { + Objects.requireNonNull(initializer, "null initializer"); + + oldInitializers.add(initializer); + + return this; + } + + @Override + public ModMetadataBuilder addNestedMod(String location) { + Objects.requireNonNull(location, "null location"); + + nestedMods.add(new ModMetadataImpl.NestedJarEntryImpl(location)); + + return this; + } + + @Override + public ModMetadataBuilder addMixinConfig(String location, /* @Nullable */ ModEnvironment environment) { + Objects.requireNonNull(location, "null location"); + + mixins.add(new MixinEntry(location, environment != null ? environment : ModEnvironment.UNIVERSAL)); + + return this; + } + + @Override + public ModMetadataBuilder addClassTweaker(String location) { + Objects.requireNonNull(location, "null location"); + + classTweakers.add(location); + + return this; + } + + @Override + public Collection getDependencies() { + return dependencies; + } + + @Override + public ModMetadataBuilder addDependency(ModDependency dependency) { + Objects.requireNonNull(dependency, "null dependency"); + if (dependency.getClass() != ModDependencyImpl.class) throw new IllegalArgumentException("invalid dependency class "+dependency.getClass().getName()); + + dependencies.add((ModDependencyImpl) dependency); + + return this; + } + + @Override + public String getName() { + return name; + } + + @Override + public ModMetadataBuilder setName(String name) { + this.name = name; + + return this; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public ModMetadataBuilder setDescription(String description) { + this.description = description; + + return this; + } + + @Override + public Collection getAuthors() { + return authors; + } + + @Override + public ModMetadataBuilder addAuthor(String name, /* @Nullable */ ContactInformation contact) { + return addAuthor(createPerson(name, contact)); + } + + @Override + public ModMetadataBuilder addAuthor(Person person) { + Objects.requireNonNull(person, "null person"); + + authors.add(person); + + return this; + } + + @Override + public Collection getContributors() { + return contributors; + } + + @Override + public ModMetadataBuilder addContributor(String name, /* @Nullable */ ContactInformation contact) { + return addContributor(createPerson(name, contact)); + } + + @Override + public ModMetadataBuilder addContributor(Person person) { + Objects.requireNonNull(person, "null person"); + + authors.add(person); + + return this; + } + + private static Person createPerson(String name, /* @Nullable */ ContactInformation contact) { + Objects.requireNonNull(name, "null name"); + + if (contact != null + && contact != ContactInformation.EMPTY + && !contact.asMap().isEmpty()) { + return new ContactInfoBackedPerson(name, contact); + } else { + return new SimplePerson(name); + } + } + + @Override + public ContactInformation getContact() { + return contact; + } + + @Override + public ModMetadataBuilder setContact(/* @Nullable */ ContactInformation contact) { + this.contact = contact; + + return this; + } + + @Override + public Collection getLicense() { + return licenses; + } + + @Override + public ModMetadataBuilder addLicense(String name) { + Objects.requireNonNull(name, "null name"); + + this.licenses.add(name); + + return this; + } + + @Override + public Optional getIconPath(int size) { + if (icons.isEmpty()) return Optional.empty(); + + Map.Entry entry = icons.ceilingEntry(size); + if (entry == null) entry = icons.lastEntry(); + + return Optional.of(entry.getValue()); + } + + @Override + public ModMetadataBuilder setIcon(String location) { + return addIcon(0, location); + } + + @Override + public ModMetadataBuilder addIcon(int size, String location) { + Objects.requireNonNull(location, "null location"); + + if (size <= 0) { + size = 0; + if (icons.size() > 1 || icons.size() == 1 && !icons.containsKey(0)) throw new IllegalArgumentException("mixing sized icons with single-size icon"); + } else { + if (icons.containsKey(0)) throw new IllegalArgumentException("mixing sized icons with single-size icon"); + } + + icons.put(size, location); + + return this; + } + + @Override + public ModMetadataBuilder addLanguageAdapter(String name, String cls) { + Objects.requireNonNull(name, "null name"); + Objects.requireNonNull(cls, "null cls"); + + languageAdapters.put(name, cls); + + return this; + } + + @Override + public boolean containsCustomValue(String key) { + return customValues.containsKey(key); + } + + @Override + public CustomValue getCustomValue(String key) { + return customValues.get(key); + } + + @Override + public Map getCustomValues() { + return customValues; + } + + @Override + public ModMetadataBuilder addCustomValue(String key, CustomValue value) { + Objects.requireNonNull(key, "null key"); + Objects.requireNonNull(value, "null value"); + + customValues.put(key, value); + + return this; + } + + @Override + public void fromJson(Reader reader) throws IOException { + try { + ModMetadataParser.readModMetadata(reader, new ArrayList<>(), this); + } catch (ParseMetadataException e) { + throw new IOException(e); + } + } + + @Override + public void toJson(Writer writer) throws IOException { + checkInitialized(); + + ModMetadataWriter.write(this, writer); + } + + @Override + public String toJson() { + StringWriter sw = new StringWriter(100); + + try { + toJson(sw); + } catch (IOException e) { + throw new UncheckedIOException(e); // shouldn't happen.. + } + + return sw.toString(); + } + + @Override + public ModMetadataImpl build() { + checkInitialized(); + + IconEntry icon; + + if (icons.isEmpty()) { + icon = null; + } else if (icons.size() == 1) { + icon = new SingleIconEntry(icons.values().iterator().next()); + } else { + icon = new MapIconEntry(icons); + } + + return new ModMetadataImpl(schemaVersion, + id, version, + providedMods, + environment, loadCondition, loadPhase, + entrypoints, nestedMods, + mixins, classTweakers, + dependencies, + name, description, + authors, contributors, contact, licenses, icon, + languageAdapters, + customValues, + oldInitializers); + } + + private void checkInitialized() { + if (id == null) throw new IllegalStateException("modId wasn't set"); + if (version == null) throw new IllegalStateException("version wasn't set"); + } + + public static final class ModDependencyBuilderImpl implements ModDependencyBuilder { + private ModDependency.Kind kind; + private String modId; + private final Collection versionOptions = new ArrayList<>(); + private ModEnvironment environment = ModEnvironment.UNIVERSAL; + private String reason; + private ModDependency.Metadata metadata; + private ModDependency.Metadata rootMetadata; + + @Override + public ModDependencyBuilder setKind(ModDependency.Kind kind) { + Objects.requireNonNull(kind, "null kind"); + + this.kind = kind; + + return this; + } + + @Override + public ModDependencyBuilder setModId(String modId) { + Objects.requireNonNull(modId, "null modId"); + + this.modId = modId; + + return this; + } + + @Override + public ModDependencyBuilder addVersion(String predicate) throws VersionParsingException { + return addVersion(VersionPredicate.parse(predicate)); + } + + @Override + public ModDependencyBuilder addVersion(VersionPredicate predicate) { + Objects.requireNonNull(predicate, "null predicate"); + + versionOptions.add(predicate); + + return this; + } + + @Override + public ModDependencyBuilder addVersions(Collection predicates) { + versionOptions.addAll(predicates); + + return this; + } + + @Override + public ModDependencyBuilder setEnvironment(ModEnvironment environment) { + this.environment = environment; + + return this; + } + + @Override + public ModDependencyBuilder setReason(/* @Nullable */ String reason) { + this.reason = reason; + + return this; + } + + @Override + public ModDependencyBuilder setMetadata(/* @Nullable */ ModDependency.Metadata metadata) { + if (metadata != null && metadata.getClass() != ModDependencyImpl.Metadata.class) throw new IllegalArgumentException("invalid metadata class "+metadata.getClass().getName()); + + this.metadata = metadata; + + return this; + } + + @Override + public ModDependencyBuilder setRootMetadata(/* @Nullable */ ModDependency.Metadata metadata) { + if (metadata != null && metadata.getClass() != ModDependencyImpl.Metadata.class) throw new IllegalArgumentException("invalid metadata class "+metadata.getClass().getName()); + + this.rootMetadata = metadata; + + return this; + } + + @Override + public ModDependency build() { + if (kind == null) throw new IllegalStateException("kind is not set"); + if (modId == null) throw new IllegalStateException("modId is not set"); + if (versionOptions.isEmpty()) versionOptions.add(VersionPredicate.any()); + + return new ModDependencyImpl(kind, modId, versionOptions, + environment, reason, + metadata, rootMetadata); + } + } + + public static final class ModDependencyMetadataBuilderImpl implements ModDependencyMetadataBuilder { + private String modId; + private String name; + private String description; + private ContactInformation contact; + + @Override + public ModDependencyMetadataBuilder setModId(/* @Nullable */ String modId) { + this.modId = modId; + + return this; + } + + @Override + public ModDependencyMetadataBuilder setName(/* @Nullable */ String name) { + this.name = name; + + return this; + } + + @Override + public ModDependencyMetadataBuilder setDescription(/* @Nullable */ String description) { + this.description = description; + + return this; + } + + @Override + public ModDependencyMetadataBuilder setContact(/* @Nullable */ ContactInformation contact) { + this.contact = contact; + + return this; + } + + @Override + public ModDependency.Metadata build() { + return new ModDependencyImpl.Metadata(modId, name, description, contact); + } + } + + public static final class ContactInformationBuilderImpl implements ContactInformationBuilder { + private final Map values = new HashMap<>(); + + @Override + public ContactInformationBuilder set(String key, String value) { + Objects.requireNonNull(key, "null key"); + Objects.requireNonNull(value, "null value"); + + values.put(key, value); + + return this; + } + + @Override + public ContactInformation build() { + return values.isEmpty() ? ContactInformation.EMPTY : new ContactInformationImpl(values); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadata.java b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataImpl.java similarity index 59% rename from src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadata.java rename to src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataImpl.java index d369064db..f09f759bb 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadata.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataImpl.java @@ -21,41 +21,41 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Optional; -import java.util.SortedMap; import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.metadata.ContactInformation; import net.fabricmc.loader.api.metadata.CustomValue; -import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.api.metadata.ModLoadCondition; import net.fabricmc.loader.api.metadata.Person; -import net.fabricmc.loader.impl.util.log.Log; -import net.fabricmc.loader.impl.util.log.LogCategory; +import net.fabricmc.loader.api.metadata.ProvidedMod; -final class V1ModMetadata extends AbstractModMetadata implements LoaderModMetadata { +final class ModMetadataImpl extends AbstractModMetadata implements LoaderModMetadata { static final IconEntry NO_ICON = size -> Optional.empty(); + private final int schemaVersion; + // Required values private final String id; private Version version; // Optional (id provides) - private final Collection provides; + private final Collection providedMods; // Optional (mod loading) private final ModEnvironment environment; + private final ModLoadCondition loadCondition; + private final String loadPhase; private final Map> entrypoints; private final Collection jars; private final Collection mixins; - /* @Nullable */ - private final String accessWidener; + private final Collection classTweakers; // Optional (dependency resolution) - private Collection dependencies; - // Happy little accidents - private final boolean hasRequires; + private Collection dependencies; // Optional (metadata) /* @Nullable */ @@ -73,24 +73,32 @@ final class V1ModMetadata extends AbstractModMetadata implements LoaderModMetada // Optional (custom values) private final Map customValues; - V1ModMetadata(String id, Version version, Collection provides, - ModEnvironment environment, Map> entrypoints, Collection jars, - Collection mixins, /* @Nullable */ String accessWidener, - Collection dependencies, boolean hasRequires, + // old (v0 metadata) + private final Collection oldInitializers; + + ModMetadataImpl(int schemaVersion, + String id, Version version, Collection providedMods, + ModEnvironment environment, ModLoadCondition loadCondition, String loadPhase, + Map> entrypoints, Collection jars, + Collection mixins, Collection classTweakers, + Collection dependencies, /* @Nullable */ String name, /* @Nullable */String description, Collection authors, Collection contributors, /* @Nullable */ContactInformation contact, Collection license, IconEntry icon, Map languageAdapters, - Map customValues) { + Map customValues, + Collection oldInitializers) { + this.schemaVersion = schemaVersion; this.id = id; this.version = version; - this.provides = Collections.unmodifiableCollection(provides); + this.providedMods = unmodifiable(providedMods); this.environment = environment; - this.entrypoints = Collections.unmodifiableMap(entrypoints); - this.jars = Collections.unmodifiableCollection(jars); - this.mixins = Collections.unmodifiableCollection(mixins); - this.accessWidener = accessWidener; - this.dependencies = Collections.unmodifiableCollection(dependencies); - this.hasRequires = hasRequires; + this.loadCondition = loadCondition; + this.loadPhase = loadPhase; + this.entrypoints = unmodifiable(entrypoints); + this.jars = unmodifiable(jars); + this.mixins = unmodifiable(mixins); + this.classTweakers = unmodifiable(classTweakers); + this.dependencies = unmodifiable(dependencies); this.name = name; // Empty description if not specified @@ -100,8 +108,8 @@ final class V1ModMetadata extends AbstractModMetadata implements LoaderModMetada this.description = ""; } - this.authors = Collections.unmodifiableCollection(authors); - this.contributors = Collections.unmodifiableCollection(contributors); + this.authors = unmodifiable(authors); + this.contributors = unmodifiable(contributors); if (contact != null) { this.contact = contact; @@ -109,21 +117,31 @@ final class V1ModMetadata extends AbstractModMetadata implements LoaderModMetada this.contact = ContactInformation.EMPTY; } - this.license = Collections.unmodifiableCollection(license); + this.license = unmodifiable(license); if (icon != null) { this.icon = icon; } else { - this.icon = V1ModMetadata.NO_ICON; + this.icon = NO_ICON; } - this.languageAdapters = Collections.unmodifiableMap(languageAdapters); - this.customValues = Collections.unmodifiableMap(customValues); + this.languageAdapters = unmodifiable(languageAdapters); + this.customValues = unmodifiable(customValues); + + this.oldInitializers = unmodifiable(oldInitializers); + } + + private static Collection unmodifiable(Collection c) { + return c.isEmpty() ? Collections.emptyList() : Collections.unmodifiableCollection(c); + } + + private static Map unmodifiable(Map m) { + return m.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(m); } @Override public int getSchemaVersion() { - return 1; + return schemaVersion; } @Override @@ -137,8 +155,8 @@ public String getId() { } @Override - public Collection getProvides() { - return this.provides; + public Collection getAdditionallyProvidedMods() { + return providedMods; } @Override @@ -149,6 +167,10 @@ public Version getVersion() { @Override public void setVersion(Version version) { this.version = version; + + for (ProvidedModImpl m : providedMods) { + if (!m.hasOwnVersion) m.setVersion(version); + } } @Override @@ -162,12 +184,22 @@ public boolean loadsInEnvironment(EnvType type) { } @Override - public Collection getDependencies() { + public ModLoadCondition getLoadCondition() { + return loadCondition; + } + + @Override + public String getLoadPhase() { + return loadPhase; + } + + @Override + public Collection getDependencies() { return dependencies; } @Override - public void setDependencies(Collection dependencies) { + public void setDependencies(Collection dependencies) { this.dependencies = Collections.unmodifiableCollection(dependencies); } @@ -244,13 +276,13 @@ public Collection getMixinConfigs(EnvType type) { } @Override - public String getAccessWidener() { - return this.accessWidener; + public Collection getClassTweakers() { + return classTweakers; } @Override public Collection getOldInitializers() { - return Collections.emptyList(); // Not applicable in V1 + return oldInitializers; } @Override @@ -274,9 +306,49 @@ public Collection getEntrypointKeys() { } @Override - public void emitFormatWarnings() { - if (hasRequires) { - Log.warn(LogCategory.METADATA, "Mod `%s` (%s) uses 'requires' key in fabric.mod.json, which is not supported - use 'depends'", this.id, this.version); + public String toString() { + return String.format("%s %s", id, version); + } + + static final class ProvidedModImpl implements ProvidedMod { + private final String id; + private Version version; + final boolean hasOwnVersion; + private final boolean exclusive; + + ProvidedModImpl(String id, Version version, boolean hasOwnVersion, boolean exclusive) { + this.id = id; + this.version = version; + this.hasOwnVersion = hasOwnVersion; + this.exclusive = exclusive; + } + + @Override + public String getId() { + return id; + } + + @Override + public Version getVersion() { + return version; + } + + void setVersion(Version version) { + this.version = version; + } + + @Override + public boolean isExclusive() { + return exclusive; + } + + @Override + public String toString() { + return String.format("%s %s (%s%s)", + id, + version, + (hasOwnVersion ? "" : "inherited, "), + (exclusive ? "exclusive" : "shared")); } } @@ -300,10 +372,10 @@ public String getValue() { } } - static final class JarEntry implements NestedJarEntry { + static final class NestedJarEntryImpl implements NestedJarEntry { private final String file; - JarEntry(String file) { + NestedJarEntryImpl(String file) { this.file = file; } @@ -314,8 +386,8 @@ public String getFile() { } static final class MixinEntry { - private final String config; - private final ModEnvironment environment; + final String config; + final ModEnvironment environment; MixinEntry(String config, ModEnvironment environment) { this.config = config; @@ -327,10 +399,10 @@ interface IconEntry { Optional getIconPath(int size); } - static final class Single implements IconEntry { + static final class SingleIconEntry implements IconEntry { private final String icon; - Single(String icon) { + SingleIconEntry(String icon) { this.icon = icon; } @@ -340,26 +412,21 @@ public Optional getIconPath(int size) { } } - static final class MapEntry implements IconEntry { - private final SortedMap icons; + static final class MapIconEntry implements IconEntry { + private final NavigableMap icons; - MapEntry(SortedMap icons) { + MapIconEntry(NavigableMap icons) { this.icons = icons; } @Override public Optional getIconPath(int size) { - int iconValue = -1; + if (icons.isEmpty()) return Optional.empty(); - for (int i : icons.keySet()) { - iconValue = i; - - if (iconValue >= size) { - break; - } - } + Map.Entry entry = icons.ceilingEntry(size); + if (entry == null) entry = icons.lastEntry(); - return Optional.of(icons.get(iconValue)); + return Optional.of(entry.getValue()); } } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataParser.java b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataParser.java index 9cb2ef49f..b0a9b17cf 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataParser.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataParser.java @@ -19,7 +19,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.Reader; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; @@ -31,7 +33,7 @@ import net.fabricmc.loader.impl.util.log.LogCategory; public final class ModMetadataParser { - public static final int LATEST_VERSION = 1; + public static final int LATEST_VERSION = 2; /** * Keys that will be ignored by any mod metadata parser. */ @@ -42,7 +44,18 @@ public final class ModMetadataParser { public static LoaderModMetadata parseMetadata(InputStream is, String modPath, List modParentPaths, VersionOverrides versionOverrides, DependencyOverrides depOverrides) throws ParseMetadataException { try { - LoaderModMetadata ret = readModMetadata(is); + ModMetadataBuilderImpl builder = new ModMetadataBuilderImpl(); + List warnings = new ArrayList<>(); + + readModMetadata(new InputStreamReader(is, StandardCharsets.UTF_8), warnings, builder); + + // Validate all required fields are present + if (builder.getId() == null) throw new ParseMetadataException.MissingField("id"); + if (builder.getVersion() == null) throw new ParseMetadataException.MissingField("version"); + + logWarningMessages(builder.getId(), warnings); + + LoaderModMetadata ret = builder.build(); versionOverrides.apply(ret); depOverrides.apply(ret); @@ -60,7 +73,7 @@ public static LoaderModMetadata parseMetadata(InputStream is, String modPath, Li } } - private static LoaderModMetadata readModMetadata(InputStream is) throws IOException, ParseMetadataException { + static void readModMetadata(Reader rawReader, List warnings, ModMetadataBuilderImpl builder) throws IOException, ParseMetadataException { // So some context: // Per the json specification, ordering of fields is not typically enforced. // Furthermore we cannot guarantee the `schemaVersion` is the first field in every `fabric.mod.json` @@ -77,7 +90,7 @@ private static LoaderModMetadata readModMetadata(InputStream is) throws IOExcept // Re-read the JSON file. int schemaVersion = 0; - try (JsonReader reader = new JsonReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + try (JsonReader reader = new JsonReader(rawReader)) { reader.setRewindEnabled(true); if (reader.peek() != JsonToken.BEGIN_OBJECT) { @@ -102,10 +115,10 @@ private static LoaderModMetadata readModMetadata(InputStream is) throws IOExcept if (firstField) { reader.setRewindEnabled(false); // Finish reading the metadata - LoaderModMetadata ret = readModMetadata(reader, schemaVersion); + readModMetadata(reader, schemaVersion, warnings, builder); reader.endObject(); - return ret; + return; } // schemaVersion found, but after some content -> start over to parse all data with the detected version @@ -125,23 +138,30 @@ private static LoaderModMetadata readModMetadata(InputStream is) throws IOExcept reader.setRewindEnabled(false); reader.beginObject(); - LoaderModMetadata ret = readModMetadata(reader, schemaVersion); + readModMetadata(reader, schemaVersion, warnings, builder); reader.endObject(); if (FabricLoader.getInstance().isDevelopmentEnvironment()) { - Log.warn(LogCategory.METADATA, "\"fabric.mod.json\" from mod %s did not have \"schemaVersion\" as first field.", ret.getId()); + Log.warn(LogCategory.METADATA, "\"fabric.mod.json\" from mod %s did not have \"schemaVersion\" as first field.", builder.getId()); } - - return ret; } } - private static LoaderModMetadata readModMetadata(JsonReader reader, int schemaVersion) throws IOException, ParseMetadataException { + private static void readModMetadata(JsonReader reader, int schemaVersion, List warnings, ModMetadataBuilderImpl builder) throws IOException, ParseMetadataException { + // don't forget to update LATEST_VERSION! + + builder.setSchemaVersion(schemaVersion); + switch (schemaVersion) { - case 1: - return V1ModMetadataParser.parse(reader); case 0: - return V0ModMetadataParser.parse(reader); + V0ModMetadataParser.parse(reader, warnings, builder); + break; + case 1: + V1ModMetadataParser.parse(reader, warnings, builder); + break; + case 2: + V2ModMetadataParser.parse(reader, warnings, builder); + break; default: if (schemaVersion > 0) { throw new ParseMetadataException(String.format("This version of fabric-loader doesn't support the newer schema version of \"%s\"" @@ -166,7 +186,4 @@ static void logWarningMessages(String id, List warnings) { Log.warn(LogCategory.METADATA, message.toString()); } - - private ModMetadataParser() { - } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataWriter.java b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataWriter.java new file mode 100644 index 000000000..971f47905 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataWriter.java @@ -0,0 +1,363 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.metadata; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import net.fabricmc.loader.api.metadata.CustomValue; +import net.fabricmc.loader.api.metadata.CustomValue.CvObject; +import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.api.metadata.Person; +import net.fabricmc.loader.api.metadata.ProvidedMod; +import net.fabricmc.loader.api.metadata.version.VersionPredicate; +import net.fabricmc.loader.impl.discovery.LoadPhases; +import net.fabricmc.loader.impl.lib.gson.JsonWriter; +import net.fabricmc.loader.impl.metadata.ModMetadataImpl.MixinEntry; + +final class ModMetadataWriter { + static void write(ModMetadataBuilderImpl meta, Writer writer) throws IOException { + @SuppressWarnings("resource") + JsonWriter jw = new JsonWriter(writer); + + jw.beginObject(); + + jw.name("schemaVersion").value(2); + jw.name("id").value(meta.id); + jw.name("version").value(meta.version.getFriendlyString()); + + if (!meta.providedMods.isEmpty()) { + jw.name("provides"); + jw.beginArray(); + + for (ProvidedMod provided : meta.providedMods) { + if (provided.getVersion().equals(meta.version) && provided.isExclusive()) { + jw.value(provided.getId()); + } else { + jw.beginObject(); + + jw.name("id").value(provided.getId()); + + if (!provided.getVersion().equals(meta.version)) { + jw.name("version").value(provided.getVersion().getFriendlyString()); + } + + if (!provided.isExclusive()) { + jw.name("exclusive").value(false); + } + + jw.endObject(); + } + } + + jw.endArray(); + } + + if (meta.environment != ModEnvironment.UNIVERSAL) { + jw.name("environment").value(serializeEnvironment(meta.environment)); + } + + if (meta.getLoadCondition() != null) { + jw.name("loadCondition").value(meta.getLoadCondition().name().toLowerCase(Locale.ENGLISH)); + } + + if (!meta.loadPhase.equals(LoadPhases.DEFAULT)) { + jw.name("loadPhase").value(meta.loadPhase); + } + + if (!meta.entrypoints.isEmpty()) { + jw.name("entrypoints"); + jw.beginObject(); + + for (Map.Entry> entry : meta.entrypoints.entrySet()) { + jw.name(entry.getKey()); + + boolean multiple = entry.getValue().size() != 1; + + if (multiple) jw.beginArray(); + + for (EntrypointMetadata entrypoint : entry.getValue()) { + if (entrypoint.getAdapter().equals(ModMetadataBuilderImpl.DEFAULT_ENTRYPOINT_ADAPTER)) { + jw.value(entrypoint.getValue()); + } else { + jw.beginObject(); + jw.name("value").value(entrypoint.getValue()); + jw.name("adapter").value(entrypoint.getAdapter()); + jw.endObject(); + } + } + + if (multiple) jw.endArray(); + } + + jw.endObject(); + } + + if (!meta.nestedMods.isEmpty()) { + jw.name("jars"); + jw.beginArray(); + + for (NestedJarEntry mod : meta.nestedMods) { + jw.beginObject(); + jw.name("file").value(mod.getFile()); + jw.endObject(); + } + + jw.endArray(); + } + + if (!meta.mixins.isEmpty()) { + jw.name("mixins"); + jw.beginArray(); + + for (MixinEntry mixin : meta.mixins) { + if (mixin.environment == ModEnvironment.UNIVERSAL) { + jw.value(mixin.config); + } else { + jw.beginObject(); + jw.name("config").value(mixin.config); + jw.name("environment").value(serializeEnvironment(mixin.environment)); + jw.endObject(); + } + } + + jw.endArray(); + } + + if (!meta.classTweakers.isEmpty()) { + jw.name("classTweakers"); + boolean multiple = meta.classTweakers.size() != 1; + + if (multiple) jw.beginArray(); + + for (String accessWidener : meta.classTweakers) { + jw.value(accessWidener); + } + + if (multiple) jw.endArray(); + } + + if (!meta.dependencies.isEmpty()) { + Map> groupedDeps = new EnumMap<>(ModDependency.Kind.class); + + for (ModDependency dep : meta.dependencies) { + groupedDeps.computeIfAbsent(dep.getKind(), ignore -> new ArrayList<>()).add(dep); + } + + for (Map.Entry> entry : groupedDeps.entrySet()) { + ModDependency.Kind kind = entry.getKey(); + List deps = entry.getValue(); + deps.sort(Comparator.comparing(ModDependency::getModId)); + + jw.name(kind.name().toLowerCase(Locale.ENGLISH)); + jw.beginObject(); + + for (ModDependency dep : deps) { + jw.name(dep.getModId()); + boolean multiple = dep.getVersionRequirements().size() != 1; + + if (multiple) jw.beginArray(); + + for (VersionPredicate predicate : dep.getVersionRequirements()) { + jw.value(predicate.toString()); + } + + if (multiple) jw.endArray(); + } + + jw.endObject(); + } + } + + if (meta.name != null) { + jw.name("name").value(meta.name); + } + + if (meta.description != null) { + jw.name("description").value(meta.description); + } + + if (!meta.authors.isEmpty()) { + jw.name("authors"); + jw.beginArray(); + + for (Person person : meta.authors) { + writePerson(person, jw); + } + + jw.endArray(); + } + + if (!meta.contributors.isEmpty()) { + jw.name("contributors"); + jw.beginArray(); + + for (Person person : meta.contributors) { + writePerson(person, jw); + } + + jw.endArray(); + } + + if (meta.contact != null && !meta.contact.asMap().isEmpty()) { + jw.name("contact"); + writeStringStringMap(meta.contact.asMap(), jw); + } + + if (!meta.licenses.isEmpty()) { + jw.name("licenses"); + + boolean multiple = meta.licenses.size() != 1; + + if (multiple) jw.beginArray(); + + for (String license : meta.licenses) { + jw.value(license); + } + + if (multiple) jw.endArray(); + } + + if (!meta.icons.isEmpty()) { + jw.name("icon"); + + if (meta.icons.size() == 1 && meta.icons.keySet().iterator().next() <= 0) { + jw.value(meta.icons.values().iterator().next()); + } else { + jw.beginObject(); + + for (Map.Entry entry : meta.icons.entrySet()) { + jw.name(entry.getKey().toString()).value(entry.getValue()); + } + + jw.endObject(); + } + } + + if (!meta.languageAdapters.isEmpty()) { + jw.name("languageAdapters"); + writeStringStringMap(meta.languageAdapters, jw); + } + + if (!meta.customValues.isEmpty()) { + List> entries = new ArrayList<>(meta.customValues.entrySet()); + entries.sort(Comparator.comparing(Map.Entry::getKey)); + + jw.name("custom"); + jw.beginObject(); + + for (Map.Entry entry : entries) { + jw.name(entry.getKey()); + writeCustomValue(entry.getValue(), jw); + } + + jw.endObject(); + } + + jw.endObject(); + + jw.flush(); + } + + private static String serializeEnvironment(ModEnvironment env) { + return env.name().toLowerCase(Locale.ENGLISH); + } + + private static void writePerson(Person person, JsonWriter jw) throws IOException { + Map contact = person.getContact().asMap(); + + if (contact.isEmpty()) { + jw.value(person.getName()); + } else { + jw.beginObject(); + + jw.name("name").value(person.getName()); + jw.name("contact"); + writeStringStringMap(contact, jw); + + jw.endObject(); + } + } + + private static void writeStringStringMap(Map map, JsonWriter jw) throws IOException { + List> entries = new ArrayList<>(map.entrySet()); + entries.sort(Comparator.comparing(Map.Entry::getKey)); + + jw.beginObject(); + + for (Map.Entry entry : entries) { + jw.name(entry.getKey()).value(entry.getValue()); + } + + jw.endObject(); + } + + private static void writeCustomValue(CustomValue value, JsonWriter jw) throws IOException { + switch (value.getType()) { + case OBJECT: { + jw.beginObject(); + + CvObject obj = value.getAsObject(); + List> entries = new ArrayList<>(obj.size()); + + for (Map.Entry entry : obj) { + entries.add(entry); + } + + entries.sort(Comparator.comparing(Map.Entry::getKey)); + + for (Map.Entry entry : entries) { + jw.name(entry.getKey()); + writeCustomValue(entry.getValue(), jw); + } + + jw.endObject(); + break; + } + case ARRAY: + jw.beginArray(); + + for (CustomValue v : value.getAsArray()) { + writeCustomValue(v, jw); + } + + jw.endArray(); + break; + case STRING: + jw.value(value.getAsString()); + break; + case NUMBER: + jw.value(value.getAsNumber()); + break; + case BOOLEAN: + jw.value(value.getAsBoolean()); + break; + case NULL: + jw.nullValue(); + break; + default: + throw new IllegalStateException("invalid cv type: "+value.getType()); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/ParseMetadataException.java b/src/main/java/net/fabricmc/loader/impl/metadata/ParseMetadataException.java index e4d114abb..9b4b21191 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/ParseMetadataException.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ParseMetadataException.java @@ -37,6 +37,10 @@ public ParseMetadataException(String message, Throwable throwable) { super(message, throwable); } + public ParseMetadataException(Throwable t, JsonReader reader) { + super((t.getMessage() != null ? t.getMessage() : "while reading").concat(reader.locationString()), t); + } + public ParseMetadataException(Throwable t) { super(t); } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/ParseWarning.java b/src/main/java/net/fabricmc/loader/impl/metadata/ParseWarning.java index 035659a39..6c2d51554 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/ParseWarning.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ParseWarning.java @@ -16,19 +16,21 @@ package net.fabricmc.loader.impl.metadata; +import net.fabricmc.loader.impl.lib.gson.JsonReader; + final class ParseWarning { private final int line; private final int column; private final String key; private final String reason; - ParseWarning(int line, int column, String key) { - this(line, column, key, null); + ParseWarning(JsonReader reader, String key) { + this(reader, key, null); } - ParseWarning(int line, int column, String key, /* @Nullable */ String reason) { - this.line = line; - this.column = column; + ParseWarning(JsonReader reader, String key, /* @Nullable */ String reason) { + this.line = reader.getLineNumber(); + this.column = reader.getColumn(); this.key = key; this.reason = reason; } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/ParserUtil.java b/src/main/java/net/fabricmc/loader/impl/metadata/ParserUtil.java new file mode 100644 index 000000000..a28f1572c --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/metadata/ParserUtil.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.metadata; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; +import java.util.stream.Collectors; + +import net.fabricmc.loader.impl.lib.gson.JsonReader; +import net.fabricmc.loader.impl.lib.gson.JsonToken; + +final class ParserUtil { + public static boolean readBoolean(JsonReader reader, String key) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.BOOLEAN) { + throw new ParseMetadataException(key+" must be a boolean", reader); + } + + return reader.nextBoolean(); + } + + public static String readString(JsonReader reader, String key) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException(key+" must be a string", reader); + } + + return reader.nextString(); + } + + public static > T readEnum(JsonReader reader, Class enumCls, String key) throws IOException, ParseMetadataException { + String value = readString(reader, key); + + try { + return Enum.valueOf(enumCls, value.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + String options = Arrays.stream(enumCls.getEnumConstants()).map(v -> v.name().toLowerCase(Locale.ENGLISH)).collect(Collectors.joining(", ")); + throw new ParseMetadataException(key+" "+value+" must be one of "+options, reader); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/V0ModMetadata.java b/src/main/java/net/fabricmc/loader/impl/metadata/V0ModMetadata.java deleted file mode 100644 index addba9aa5..000000000 --- a/src/main/java/net/fabricmc/loader/impl/metadata/V0ModMetadata.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright 2016 FabricMC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.fabricmc.loader.impl.metadata; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import net.fabricmc.api.EnvType; -import net.fabricmc.loader.api.Version; -import net.fabricmc.loader.api.metadata.ContactInformation; -import net.fabricmc.loader.api.metadata.CustomValue; -import net.fabricmc.loader.api.metadata.ModDependency; -import net.fabricmc.loader.api.metadata.ModEnvironment; -import net.fabricmc.loader.api.metadata.Person; - -final class V0ModMetadata extends AbstractModMetadata implements LoaderModMetadata { - private static final Mixins EMPTY_MIXINS = new Mixins(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - // Required - private final String id; - private Version version; - - // Optional (Environment) - private Collection dependencies; - private final String languageAdapter = "net.fabricmc.loader.language.JavaLanguageAdapter"; // TODO: Constants class? - private final Mixins mixins; - private final ModEnvironment environment; // REMOVEME: Replacing Side in old metadata with this - private final String initializer; - private final Collection initializers; - - // Optional (metadata) - private final String name; - private final String description; - private final Collection authors; - private final Collection contributors; - private final ContactInformation links; - private final String license; - - V0ModMetadata(String id, Version version, Collection dependencies, Mixins mixins, ModEnvironment environment, String initializer, Collection initializers, - String name, String description, Collection authors, Collection contributors, ContactInformation links, String license) { - this.id = id; - this.version = version; - this.dependencies = Collections.unmodifiableCollection(dependencies); - - if (mixins == null) { - this.mixins = V0ModMetadata.EMPTY_MIXINS; - } else { - this.mixins = mixins; - } - - this.environment = environment; - this.initializer = initializer; - this.initializers = Collections.unmodifiableCollection(initializers); - this.name = name; - - if (description == null) { - this.description = ""; - } else { - this.description = description; - } - - this.authors = Collections.unmodifiableCollection(authors); - this.contributors = Collections.unmodifiableCollection(contributors); - this.links = links; - this.license = license; - } - - @Override - public int getSchemaVersion() { - return 0; - } - - @Override - public String getType() { - return TYPE_FABRIC_MOD; - } - - @Override - public String getId() { - return this.id; - } - - @Override - public Collection getProvides() { - return Collections.emptyList(); - } - - @Override - public Version getVersion() { - return this.version; - } - - @Override - public void setVersion(Version version) { - this.version = version; - } - - @Override - public ModEnvironment getEnvironment() { - return this.environment; - } - - @Override - public boolean loadsInEnvironment(EnvType type) { - return this.environment.matches(type); - } - - @Override - public Collection getDependencies() { - return dependencies; - } - - @Override - public void setDependencies(Collection dependencies) { - this.dependencies = Collections.unmodifiableCollection(dependencies); - } - - // General metadata - - @Override - public String getName() { - if (this.name != null && this.name.isEmpty()) { - return this.id; - } - - return this.name; - } - - @Override - public String getDescription() { - return this.description; - } - - @Override - public Collection getAuthors() { - return this.authors; - } - - @Override - public Collection getContributors() { - return this.contributors; - } - - @Override - public ContactInformation getContact() { - return this.links; - } - - @Override - public Collection getLicense() { - return Collections.singleton(this.license); - } - - @Override - public Optional getIconPath(int size) { - // honor Mod Menu's de-facto standard - return Optional.of("assets/" + getId() + "/icon.png"); - } - - @Override - public String getOldStyleLanguageAdapter() { - return this.languageAdapter; - } - - @Override - public Map getCustomValues() { - return Collections.emptyMap(); - } - - @Override - public boolean containsCustomValue(String key) { - return false; - } - - @Override - public CustomValue getCustomValue(String key) { - return null; - } - - // Internals - - @Override - public Map getLanguageAdapterDefinitions() { - return Collections.emptyMap(); - } - - @Override - public Collection getJars() { - return Collections.emptyList(); - } - - @Override - public Collection getOldInitializers() { - if (this.initializer != null) { - return Collections.singletonList(this.initializer); - } else if (!this.initializers.isEmpty()) { - return this.initializers; - } else { - return Collections.emptyList(); - } - } - - @Override - public List getEntrypoints(String type) { - return Collections.emptyList(); - } - - @Override - public Collection getEntrypointKeys() { - return Collections.emptyList(); - } - - @Override - public void emitFormatWarnings() { } - - @Override - public Collection getMixinConfigs(EnvType type) { - List mixinConfigs = new ArrayList<>(this.mixins.common); - - switch (type) { - case CLIENT: - mixinConfigs.addAll(this.mixins.client); - break; - case SERVER: - mixinConfigs.addAll(this.mixins.server); - break; - } - - return mixinConfigs; - } - - @Override - public String getAccessWidener() { - return null; // intentional null - } - - static final class Mixins { - final Collection client; - final Collection common; - final Collection server; - - Mixins(Collection client, Collection common, Collection server) { - this.client = Collections.unmodifiableCollection(client); - this.common = Collections.unmodifiableCollection(common); - this.server = Collections.unmodifiableCollection(server); - } - } -} diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/V0ModMetadataParser.java b/src/main/java/net/fabricmc/loader/impl/metadata/V0ModMetadataParser.java index 0024e9c8c..c04256063 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/V0ModMetadataParser.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/V0ModMetadataParser.java @@ -17,99 +17,51 @@ package net.fabricmc.loader.impl.metadata; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.extension.ModMetadataBuilder; +import net.fabricmc.loader.api.extension.ModMetadataBuilder.ModDependencyBuilder; import net.fabricmc.loader.api.metadata.ContactInformation; import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.api.metadata.ModEnvironment; import net.fabricmc.loader.api.metadata.Person; import net.fabricmc.loader.impl.lib.gson.JsonReader; import net.fabricmc.loader.impl.lib.gson.JsonToken; -import net.fabricmc.loader.impl.util.version.VersionParser; final class V0ModMetadataParser { private static final Pattern WEBSITE_PATTERN = Pattern.compile("\\((.+)\\)"); private static final Pattern EMAIL_PATTERN = Pattern.compile("<(.+)>"); - public static LoaderModMetadata parse(JsonReader reader) throws IOException, ParseMetadataException { - List warnings = new ArrayList<>(); - - // All the values the `fabric.mod.json` may contain: - // Required - String id = null; - Version version = null; - - // Optional (mod loading) - List dependencies = new ArrayList<>(); - V0ModMetadata.Mixins mixins = null; - ModEnvironment environment = ModEnvironment.UNIVERSAL; // Default is always universal - String initializer = null; - List initializers = new ArrayList<>(); - - String name = null; - String description = null; - List authors = new ArrayList<>(); - List contributors = new ArrayList<>(); - ContactInformation links = null; - String license = null; - + public static void parse(JsonReader reader, List warnings, ModMetadataBuilderImpl builder) throws IOException, ParseMetadataException { while (reader.hasNext()) { final String key = reader.nextName(); switch (key) { case "schemaVersion": - // Duplicate field, make sure it matches our current schema version - if (reader.peek() != JsonToken.NUMBER) { - throw new ParseMetadataException("Duplicate \"schemaVersion\" field is not a number", reader); - } - - final int read = reader.nextInt(); - - if (read != 0) { - throw new ParseMetadataException(String.format("Duplicate \"schemaVersion\" field does not match the predicted schema version of 0. Duplicate field value is %s", read), reader); - } - + readSchemaVersion(reader, 0); break; case "id": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Mod id must be a non-empty string with a length of 3-64 characters.", reader); - } - - id = reader.nextString(); + readModId(reader, builder); break; case "version": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Version must be a non-empty string", reader); - } - - final String rawVersion = reader.nextString(); - - try { - version = VersionParser.parse(rawVersion, false); - } catch (VersionParsingException e) { - throw new ParseMetadataException(String.format("Failed to parse version: %s", rawVersion), e); - } - + readModVersion(reader, builder); break; case "requires": - readDependenciesContainer(reader, ModDependency.Kind.DEPENDS, dependencies, "requires"); + readDependency(reader, ModDependency.Kind.DEPENDS, key, builder); break; case "conflicts": - readDependenciesContainer(reader, ModDependency.Kind.BREAKS, dependencies, "conflicts"); + readDependency(reader, ModDependency.Kind.BREAKS, key, builder); break; case "mixins": - mixins = readMixins(warnings, reader); + readMixins(reader, warnings, builder); break; - case "side": + case "side": { if (reader.peek() != JsonToken.STRING) { throw new ParseMetadataException("Side must be a string", reader); } @@ -118,35 +70,36 @@ public static LoaderModMetadata parse(JsonReader reader) throws IOException, Par switch (rawEnvironment) { case "universal": - environment = ModEnvironment.UNIVERSAL; + builder.setEnvironment(ModEnvironment.UNIVERSAL); break; case "client": - environment = ModEnvironment.CLIENT; + builder.setEnvironment(ModEnvironment.CLIENT); break; case "server": - environment = ModEnvironment.SERVER; + builder.setEnvironment(ModEnvironment.SERVER); break; default: - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), rawEnvironment, "Invalid side type")); + warnings.add(new ParseWarning(reader, rawEnvironment, "Invalid side type")); } break; + } case "initializer": // `initializer` and `initializers` cannot be used at the same time - if (!initializers.isEmpty()) { - throw new ParseMetadataException("initializer and initializers should not be set at the same time! (mod ID '" + id + "')"); + if (!builder.oldInitializers.isEmpty()) { + throw new ParseMetadataException("initializer and initializers should not be set at the same time! (mod ID '" + builder.getId() + "')"); } if (reader.peek() != JsonToken.STRING) { throw new ParseMetadataException("Initializer must be a non-empty string", reader); } - initializer = reader.nextString(); + builder.addOldInitializer(reader.nextString()); break; case "initializers": // `initializer` and `initializers` cannot be used at the same time - if (initializer != null) { - throw new ParseMetadataException("initializer and initializers should not be set at the same time! (mod ID '" + id + "')"); + if (!builder.oldInitializers.isEmpty()) { + throw new ParseMetadataException("initializer and initializers should not be set at the same time! (mod ID '" + builder.getId() + "')"); } if (reader.peek() != JsonToken.BEGIN_ARRAY) { @@ -160,48 +113,40 @@ public static LoaderModMetadata parse(JsonReader reader) throws IOException, Par throw new ParseMetadataException("Initializer in initializers list must be a string", reader); } - initializers.add(reader.nextString()); + builder.addOldInitializer(reader.nextString()); } reader.endArray(); break; case "name": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Name must be a string", reader); - } - - name = reader.nextString(); + readModName(reader, builder); break; case "description": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Mod description must be a string", reader); - } - - description = reader.nextString(); + readModDescription(reader, builder); break; case "recommends": - readDependenciesContainer(reader, ModDependency.Kind.SUGGESTS, dependencies, "recommends"); + readDependency(reader, ModDependency.Kind.RECOMMENDS, "recommends", builder); break; case "authors": - readPeople(warnings, reader, authors); + readPeople(reader, true, warnings, builder); break; case "contributors": - readPeople(warnings, reader, contributors); + readPeople(reader, false, warnings, builder); break; case "links": - links = readLinks(warnings, reader); + builder.setContact(readLinks(reader, warnings)); break; case "license": if (reader.peek() != JsonToken.STRING) { throw new ParseMetadataException("License name must be a string", reader); } - license = reader.nextString(); + builder.addLicense(reader.nextString()); break; default: if (!ModMetadataParser.IGNORED_KEYS.contains(key)) { - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), key, "Unsupported root entry")); + warnings.add(new ParseWarning(reader, key, "Unsupported root entry")); } reader.skipValue(); @@ -209,26 +154,61 @@ public static LoaderModMetadata parse(JsonReader reader) throws IOException, Par } } - // Validate all required fields are resolved - if (id == null) { - throw new ParseMetadataException.MissingField("id"); + if (builder.getId() != null) { + builder.setIcon("assets/" + builder.getId() + "/icon.png"); + } + } + + static void readSchemaVersion(JsonReader reader, int expected) throws IOException, ParseMetadataException { + // Duplicate field, make sure it matches our current schema version + if (reader.peek() != JsonToken.NUMBER) { + throw new ParseMetadataException("Duplicate \"schemaVersion\" field is not a number", reader); + } + + final int read = reader.nextInt(); + + if (read != expected) { + throw new ParseMetadataException(String.format("Duplicate \"schemaVersion\" field does not match the predicted schema version of %d. Duplicate field value is %s", expected, read), reader); + } + } + + static void readModId(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException("Mod id must be a non-empty string with a length of 3-64 characters.", reader); + } + + builder.setId(reader.nextString()); + } + + static void readModVersion(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException("Version must be a non-empty string", reader); + } + + try { + builder.setVersion(reader.nextString()); + } catch (VersionParsingException e) { + throw new ParseMetadataException("Failed to parse version", e); } + } - if (version == null) { - throw new ParseMetadataException.MissingField("version"); + static void readModName(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException("Mod name must be a string", reader); } - ModMetadataParser.logWarningMessages(id, warnings); + builder.setName(reader.nextString()); + } - // Optional stuff - if (links == null) { - links = ContactInformation.EMPTY; + static void readModDescription(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException("Mod description must be a string", reader); } - return new V0ModMetadata(id, version, dependencies, mixins, environment, initializer, initializers, name, description, authors, contributors, links, license); + builder.setDescription(reader.nextString()); } - private static ContactInformation readLinks(List warnings, JsonReader reader) throws IOException, ParseMetadataException { + private static ContactInformation readLinks(JsonReader reader, List warnings) throws IOException, ParseMetadataException { final Map contactInfo = new HashMap<>(); switch (reader.peek()) { @@ -264,7 +244,7 @@ private static ContactInformation readLinks(List warnings, JsonRea contactInfo.put("sources", reader.nextString()); break; default: - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), key, "Unsupported links entry")); + warnings.add(new ParseWarning(reader, key, "Unsupported links entry")); reader.skipValue(); } } @@ -278,11 +258,7 @@ private static ContactInformation readLinks(List warnings, JsonRea return new ContactInformationImpl(contactInfo); } - private static V0ModMetadata.Mixins readMixins(List warnings, JsonReader reader) throws IOException, ParseMetadataException { - final List client = new ArrayList<>(); - final List common = new ArrayList<>(); - final List server = new ArrayList<>(); - + private static void readMixins(JsonReader reader, List warnings, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_OBJECT) { throw new ParseMetadataException("Expected mixins to be an object.", reader); } @@ -290,55 +266,54 @@ private static V0ModMetadata.Mixins readMixins(List warnings, Json reader.beginObject(); while (reader.hasNext()) { - final String environment = reader.nextName(); + String envName = reader.nextName(); + ModEnvironment env; - switch (environment) { + switch (envName) { case "client": - client.addAll(readStringArray(reader, "client")); + env = ModEnvironment.CLIENT; break; case "common": - common.addAll(readStringArray(reader, "common")); + env = ModEnvironment.UNIVERSAL; break; case "server": - server.addAll(readStringArray(reader, "server")); + env = ModEnvironment.SERVER; break; default: - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), environment, "Invalid environment type")); + warnings.add(new ParseWarning(reader, envName, "Invalid environment type")); reader.skipValue(); + continue; } - } - reader.endObject(); - return new V0ModMetadata.Mixins(client, common, server); - } + switch (reader.peek()) { + case NULL: + reader.nextNull(); + break; + case STRING: + builder.addMixinConfig(reader.nextString(), env); + break; + case BEGIN_ARRAY: + reader.beginArray(); - private static List readStringArray(JsonReader reader, String key) throws IOException, ParseMetadataException { - switch (reader.peek()) { - case NULL: - reader.nextNull(); - return Collections.emptyList(); - case STRING: - return Collections.singletonList(reader.nextString()); - case BEGIN_ARRAY: - reader.beginArray(); - final List list = new ArrayList<>(); + while (reader.hasNext()) { + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException(String.format("Expected entries in mixin %s to be an array of strings", envName), reader); + } - while (reader.hasNext()) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException(String.format("Expected entries in %s to be an array of strings", key), reader); + builder.addMixinConfig(reader.nextString(), env); } - list.add(reader.nextString()); + reader.endArray(); + break; + default: + throw new ParseMetadataException(String.format("Expected mixin %s to be a string or an array of strings", envName), reader); } - - reader.endArray(); - return list; - default: - throw new ParseMetadataException(String.format("Expected %s to be a string or an array of strings", key), reader); } + + reader.endObject(); } - private static void readDependenciesContainer(JsonReader reader, ModDependency.Kind kind, List dependencies, String name) throws IOException, ParseMetadataException { + static void readDependency(JsonReader reader, ModDependency.Kind kind, String name, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_OBJECT) { throw new ParseMetadataException(String.format("%s must be an object containing dependencies.", name), reader); } @@ -346,41 +321,39 @@ private static void readDependenciesContainer(JsonReader reader, ModDependency.K reader.beginObject(); while (reader.hasNext()) { - final String modId = reader.nextName(); - final List versionMatchers = new ArrayList<>(); + ModDependencyBuilder depBuilder = ModDependencyBuilder.create(kind, reader.nextName()); + readDependencyValue(reader, depBuilder); + + builder.addDependency(depBuilder.build()); + } + + reader.endObject(); + } + static void readDependencyValue(JsonReader reader, ModDependencyBuilder builder) throws IOException, ParseMetadataException { + try { switch (reader.peek()) { case STRING: - versionMatchers.add(reader.nextString()); + builder.addVersion(reader.nextString()); break; case BEGIN_ARRAY: reader.beginArray(); while (reader.hasNext()) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("List of version requirements must be strings", reader); - } - - versionMatchers.add(reader.nextString()); + builder.addVersion(ParserUtil.readString(reader, "dependency version")); } reader.endArray(); break; default: - throw new ParseMetadataException("Expected version to be a string or array", reader); - } - - try { - dependencies.add(new ModDependencyImpl(kind, modId, versionMatchers)); - } catch (VersionParsingException e) { - throw new ParseMetadataException(e); + throw new ParseMetadataException("Expected dependency version to be a string or array", reader); } + } catch (VersionParsingException e) { + throw new ParseMetadataException(e, reader); } - - reader.endObject(); } - private static void readPeople(List warnings, JsonReader reader, List people) throws IOException, ParseMetadataException { + private static void readPeople(JsonReader reader, boolean isAuthor, List warnings, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_ARRAY) { throw new ParseMetadataException("List of people must be an array", reader); } @@ -388,13 +361,19 @@ private static void readPeople(List warnings, JsonReader reader, L reader.beginArray(); while (reader.hasNext()) { - people.add(readPerson(warnings, reader)); + Person person = readPerson(reader, warnings); + + if (isAuthor) { + builder.addAuthor(person); + } else { + builder.addContributor(person); + } } reader.endArray(); } - private static Person readPerson(List warnings, JsonReader reader) throws IOException, ParseMetadataException { + private static Person readPerson(JsonReader reader, List warnings) throws IOException, ParseMetadataException { final HashMap contactMap = new HashMap<>(); String name = ""; @@ -449,7 +428,7 @@ private static Person readPerson(List warnings, JsonReader reader) contactMap.put("website", reader.nextString()); break; default: - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), key, "Unsupported contact information entry")); + warnings.add(new ParseWarning(reader, key, "Unsupported contact information entry")); reader.skipValue(); } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java b/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java index cf65a6ccc..3c776758b 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java @@ -17,218 +17,113 @@ package net.fabricmc.loader.impl.metadata; import java.io.IOException; -import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.SortedMap; +import java.util.NavigableMap; import java.util.TreeMap; -import net.fabricmc.loader.api.Version; -import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.extension.ModMetadataBuilder; import net.fabricmc.loader.api.metadata.ContactInformation; -import net.fabricmc.loader.api.metadata.CustomValue; import net.fabricmc.loader.api.metadata.ModDependency; import net.fabricmc.loader.api.metadata.ModEnvironment; import net.fabricmc.loader.api.metadata.Person; import net.fabricmc.loader.impl.lib.gson.JsonReader; import net.fabricmc.loader.impl.lib.gson.JsonToken; -import net.fabricmc.loader.impl.util.version.VersionParser; final class V1ModMetadataParser { /** * Reads a {@code fabric.mod.json} file of schema version {@code 1}. - * - * @param logger the logger to print warnings to - * @param reader the json reader to read the file with - * @return the metadata of this file, null if the file could not be parsed - * @throws IOException if there was any issue reading the file */ - static LoaderModMetadata parse(JsonReader reader) throws IOException, ParseMetadataException { - List warnings = new ArrayList<>(); - - // All the values the `fabric.mod.json` may contain: - // Required - String id = null; - Version version = null; - - // Optional (id provides) - List provides = new ArrayList<>(); - - // Optional (mod loading) - ModEnvironment environment = ModEnvironment.UNIVERSAL; // Default is always universal - Map> entrypoints = new HashMap<>(); - List jars = new ArrayList<>(); - List mixins = new ArrayList<>(); - String accessWidener = null; - - // Optional (dependency resolution) - List dependencies = new ArrayList<>(); - // Happy little accidents - boolean hasRequires = false; - - // Optional (metadata) - String name = null; - String description = null; - List authors = new ArrayList<>(); - List contributors = new ArrayList<>(); - ContactInformation contact = null; - List license = new ArrayList<>(); - V1ModMetadata.IconEntry icon = null; - - // Optional (language adapter providers) - Map languageAdapters = new HashMap<>(); - - // Optional (custom values) - Map customValues = new HashMap<>(); - + static void parse(JsonReader reader, List warnings, ModMetadataBuilderImpl builder) throws IOException, ParseMetadataException { while (reader.hasNext()) { final String key = reader.nextName(); // Work our way from required to entirely optional switch (key) { case "schemaVersion": - // Duplicate field, make sure it matches our current schema version - if (reader.peek() != JsonToken.NUMBER) { - throw new ParseMetadataException("Duplicate \"schemaVersion\" field is not a number", reader); - } - - final int read = reader.nextInt(); - - if (read != 1) { - throw new ParseMetadataException(String.format("Duplicate \"schemaVersion\" field does not match the predicted schema version of 1. Duplicate field value is %s", read), reader); - } - + V0ModMetadataParser.readSchemaVersion(reader, 1); break; case "id": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Mod id must be a non-empty string with a length of 3-64 characters.", reader); - } - - id = reader.nextString(); + V0ModMetadataParser.readModId(reader, builder); break; case "version": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Version must be a non-empty string", reader); - } - - try { - version = VersionParser.parse(reader.nextString(), false); - } catch (VersionParsingException e) { - throw new ParseMetadataException("Failed to parse version", e); - } - + V0ModMetadataParser.readModVersion(reader, builder); break; case "provides": - readProvides(reader, provides); + readProvides(reader, builder); break; case "environment": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Environment must be a string", reader); - } - - environment = readEnvironment(reader); + builder.setEnvironment(readEnvironment(reader)); break; case "entrypoints": - readEntrypoints(warnings, reader, entrypoints); + readEntrypoints(reader, warnings, builder); break; case "jars": - readNestedJarEntries(warnings, reader, jars); + readNestedJarEntries(reader, warnings, builder); break; case "mixins": - readMixinConfigs(warnings, reader, mixins); + readMixinConfigs(reader, warnings, builder); break; case "accessWidener": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Access Widener file must be a string", reader); - } - - accessWidener = reader.nextString(); + builder.addClassTweaker(ParserUtil.readString(reader, key)); break; case "depends": - readDependenciesContainer(reader, ModDependency.Kind.DEPENDS, dependencies); + V0ModMetadataParser.readDependency(reader, ModDependency.Kind.DEPENDS, key, builder); break; case "recommends": - readDependenciesContainer(reader, ModDependency.Kind.RECOMMENDS, dependencies); + V0ModMetadataParser.readDependency(reader, ModDependency.Kind.RECOMMENDS, key, builder); break; case "suggests": - readDependenciesContainer(reader, ModDependency.Kind.SUGGESTS, dependencies); + V0ModMetadataParser.readDependency(reader, ModDependency.Kind.SUGGESTS, key, builder); break; case "conflicts": - readDependenciesContainer(reader, ModDependency.Kind.CONFLICTS, dependencies); + V0ModMetadataParser.readDependency(reader, ModDependency.Kind.CONFLICTS, key, builder); break; case "breaks": - readDependenciesContainer(reader, ModDependency.Kind.BREAKS, dependencies); - break; - case "requires": - hasRequires = true; - reader.skipValue(); + V0ModMetadataParser.readDependency(reader, ModDependency.Kind.BREAKS, key, builder); break; case "name": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Mod name must be a string", reader); - } - - name = reader.nextString(); + V0ModMetadataParser.readModName(reader, builder); break; case "description": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Mod description must be a string", reader); - } - - description = reader.nextString(); + V0ModMetadataParser.readModDescription(reader, builder); break; case "authors": - readPeople(warnings, reader, authors); + readPeople(reader, true, warnings, builder); break; case "contributors": - readPeople(warnings, reader, contributors); + readPeople(reader, false, warnings, builder); break; case "contact": - contact = readContactInfo(reader); + builder.setContact(readContactInfo(reader)); break; case "license": - readLicense(reader, license); + readLicense(reader, builder); break; case "icon": - icon = readIcon(reader); + readIcon(reader, builder); break; case "languageAdapters": - readLanguageAdapters(reader, languageAdapters); + readLanguageAdapters(reader, builder); break; case "custom": - readCustomValues(reader, customValues); + readCustomValues(reader, builder); break; default: if (!ModMetadataParser.IGNORED_KEYS.contains(key)) { - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), key, "Unsupported root entry")); + warnings.add(new ParseWarning(reader, key, "Unsupported root entry")); } reader.skipValue(); break; } } - - // Validate all required fields are resolved - if (id == null) { - throw new ParseMetadataException.MissingField("id"); - } - - if (version == null) { - throw new ParseMetadataException.MissingField("version"); - } - - ModMetadataParser.logWarningMessages(id, warnings); - - return new V1ModMetadata(id, version, provides, - environment, entrypoints, jars, mixins, accessWidener, - dependencies, hasRequires, - name, description, authors, contributors, contact, license, icon, languageAdapters, customValues); } - private static void readProvides(JsonReader reader, List provides) throws IOException, ParseMetadataException { + private static void readProvides(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_ARRAY) { throw new ParseMetadataException("Provides must be an array"); } @@ -236,17 +131,17 @@ private static void readProvides(JsonReader reader, List provides) throw reader.beginArray(); while (reader.hasNext()) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Provided id must be a string", reader); - } - - provides.add(reader.nextString()); + builder.addProvidedMod(ParserUtil.readString(reader, "provided id"), null, true); } reader.endArray(); } - private static ModEnvironment readEnvironment(JsonReader reader) throws ParseMetadataException, IOException { + static ModEnvironment readEnvironment(JsonReader reader) throws ParseMetadataException, IOException { + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException("Environment must be a string", reader); + } + final String environment = reader.nextString().toLowerCase(Locale.ROOT); if (environment.isEmpty() || environment.equals("*")) { @@ -260,7 +155,7 @@ private static ModEnvironment readEnvironment(JsonReader reader) throws ParseMet } } - private static void readEntrypoints(List warnings, JsonReader reader, Map> entrypoints) throws IOException, ParseMetadataException { + private static void readEntrypoints(JsonReader reader, List warnings, ModMetadataBuilder builder) throws IOException, ParseMetadataException { // Entrypoints must be an object if (reader.peek() != JsonToken.BEGIN_OBJECT) { throw new ParseMetadataException("Entrypoints must be an object", reader); @@ -271,8 +166,6 @@ private static void readEntrypoints(List warnings, JsonReader read while (reader.hasNext()) { final String key = reader.nextName(); - List metadata = new ArrayList<>(); - if (reader.peek() != JsonToken.BEGIN_ARRAY) { throw new ParseMetadataException("Entrypoint list must be an array!", reader); } @@ -280,56 +173,62 @@ private static void readEntrypoints(List warnings, JsonReader read reader.beginArray(); while (reader.hasNext()) { - String adapter = "default"; - String value = null; + readEntrypoint(reader, key, warnings, builder); + } + + reader.endArray(); + } + + reader.endObject(); + } + + static void readEntrypoint(JsonReader reader, String key, List warnings, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + String adapter = null; + String value = null; + + // Entrypoints may be specified directly as a string or as an object to allow specification of the language adapter to use. + switch (reader.peek()) { + case STRING: + value = reader.nextString(); + break; + case BEGIN_OBJECT: + reader.beginObject(); - // Entrypoints may be specified directly as a string or as an object to allow specification of the language adapter to use. - switch (reader.peek()) { - case STRING: + while (reader.hasNext()) { + final String entryKey = reader.nextName(); + switch (entryKey) { + case "adapter": + adapter = reader.nextString(); + break; + case "value": value = reader.nextString(); break; - case BEGIN_OBJECT: - reader.beginObject(); - - while (reader.hasNext()) { - final String entryKey = reader.nextName(); - switch (entryKey) { - case "adapter": - adapter = reader.nextString(); - break; - case "value": - value = reader.nextString(); - break; - default: - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), entryKey, "Invalid entry in entrypoint metadata")); - reader.skipValue(); - break; - } + default: + if (warnings != null) { + warnings.add(new ParseWarning(reader, entryKey, "Invalid entry in entrypoint metadata")); + reader.skipValue(); + } else { + throw new ParseMetadataException("Invalid key "+key+" in entrypoint entry", reader); } - reader.endObject(); break; - default: - throw new ParseMetadataException("Entrypoint must be a string or object with \"value\" field", reader); } - - if (value == null) { - throw new ParseMetadataException.MissingField("Entrypoint value must be present"); - } - - metadata.add(new V1ModMetadata.EntrypointMetadataImpl(adapter, value)); } - reader.endArray(); + reader.endObject(); + break; + default: + throw new ParseMetadataException("Entrypoint must be a string or object with \"value\" field", reader); + } - // Empty arrays are acceptable, do not check if the List of metadata is empty - entrypoints.put(key, metadata); + if (value == null) { + throw new ParseMetadataException.MissingField("Entrypoint value must be present"); } - reader.endObject(); + builder.addEntrypoint(key, value, adapter); } - private static void readNestedJarEntries(List warnings, JsonReader reader, List jars) throws IOException, ParseMetadataException { + static void readNestedJarEntries(JsonReader reader, List warnings, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_ARRAY) { throw new ParseMetadataException("Jar entries must be in an array", reader); } @@ -348,14 +247,12 @@ private static void readNestedJarEntries(List warnings, JsonReader final String key = reader.nextName(); if (key.equals("file")) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("\"file\" entry in jar object must be a string", reader); - } - - file = reader.nextString(); - } else { - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), key, "Invalid entry in jar entry")); + file = ParserUtil.readString(reader, "nested jar file"); + } else if (warnings != null) { + warnings.add(new ParseWarning(reader, key, "Invalid entry in jar entry")); reader.skipValue(); + } else { + throw new ParseMetadataException("Invalid key "+key+" in nested jar entry", reader); } } @@ -365,13 +262,13 @@ private static void readNestedJarEntries(List warnings, JsonReader throw new ParseMetadataException("Missing mandatory key 'file' in JAR entry!", reader); } - jars.add(new V1ModMetadata.JarEntry(file)); + builder.addNestedMod(file); } reader.endArray(); } - private static void readMixinConfigs(List warnings, JsonReader reader, List mixins) throws IOException, ParseMetadataException { + private static void readMixinConfigs(JsonReader reader, List warnings, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_ARRAY) { throw new ParseMetadataException("Mixin configs must be in an array", reader); } @@ -382,7 +279,7 @@ private static void readMixinConfigs(List warnings, JsonReader rea switch (reader.peek()) { case STRING: // All mixin configs specified via string are assumed to be universal - mixins.add(new V1ModMetadata.MixinEntry(reader.nextString(), ModEnvironment.UNIVERSAL)); + builder.addMixinConfig(reader.nextString(), null); break; case BEGIN_OBJECT: reader.beginObject(); @@ -399,32 +296,24 @@ private static void readMixinConfigs(List warnings, JsonReader rea environment = V1ModMetadataParser.readEnvironment(reader); break; case "config": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Value of \"config\" must be a string", reader); - } - - config = reader.nextString(); + config = ParserUtil.readString(reader, key); break; default: - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), key, "Invalid entry in mixin config entry")); + warnings.add(new ParseWarning(reader, key, "Invalid entry in mixin config entry")); reader.skipValue(); } } reader.endObject(); - if (environment == null) { - environment = ModEnvironment.UNIVERSAL; // Default to universal - } - if (config == null) { throw new ParseMetadataException.MissingField("Missing mandatory key 'config' in mixin entry!"); } - mixins.add(new V1ModMetadata.MixinEntry(config, environment)); + builder.addMixinConfig(config, environment); break; default: - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), "Invalid mixin entry type")); + warnings.add(new ParseWarning(reader, "Mixin list must be a string or object")); reader.skipValue(); break; } @@ -433,49 +322,7 @@ private static void readMixinConfigs(List warnings, JsonReader rea reader.endArray(); } - private static void readDependenciesContainer(JsonReader reader, ModDependency.Kind kind, List out) throws IOException, ParseMetadataException { - if (reader.peek() != JsonToken.BEGIN_OBJECT) { - throw new ParseMetadataException("Dependency container must be an object!", reader); - } - - reader.beginObject(); - - while (reader.hasNext()) { - final String modId = reader.nextName(); - final List matcherStringList = new ArrayList<>(); - - switch (reader.peek()) { - case STRING: - matcherStringList.add(reader.nextString()); - break; - case BEGIN_ARRAY: - reader.beginArray(); - - while (reader.hasNext()) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Dependency version range array must only contain string values", reader); - } - - matcherStringList.add(reader.nextString()); - } - - reader.endArray(); - break; - default: - throw new ParseMetadataException("Dependency version range must be a string or string array!", reader); - } - - try { - out.add(new ModDependencyImpl(kind, modId, matcherStringList)); - } catch (VersionParsingException e) { - throw new ParseMetadataException(e); - } - } - - reader.endObject(); - } - - private static void readPeople(List warnings, JsonReader reader, List people) throws IOException, ParseMetadataException { + private static void readPeople(JsonReader reader, boolean isAuthor, List warnings, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_ARRAY) { throw new ParseMetadataException("List of people must be an array", reader); } @@ -483,61 +330,61 @@ private static void readPeople(List warnings, JsonReader reader, L reader.beginArray(); while (reader.hasNext()) { - switch (reader.peek()) { - case STRING: - // Just a name - people.add(new SimplePerson(reader.nextString())); - break; - case BEGIN_OBJECT: - // Map-backed impl - reader.beginObject(); - // Name is required - String personName = null; - ContactInformation contactInformation = null; + Person person = readPerson(reader, warnings); - while (reader.hasNext()) { - final String key = reader.nextName(); + if (isAuthor) { + builder.addAuthor(person); + } else { + builder.addContributor(person); + } + } - switch (key) { - case "name": - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Name of person in dependency container must be a string", reader); - } + reader.endArray(); + } - personName = reader.nextString(); - break; - // Effectively optional - case "contact": - contactInformation = V1ModMetadataParser.readContactInfo(reader); - break; - default: - // Ignore unsupported keys - warnings.add(new ParseWarning(reader.getLineNumber(), reader.getColumn(), key, "Invalid entry in person")); - reader.skipValue(); - } - } + private static Person readPerson(JsonReader reader, List warnings) throws IOException, ParseMetadataException { + switch (reader.peek()) { + case STRING: + // Just a name + return new SimplePerson(reader.nextString()); + case BEGIN_OBJECT: + // Map-backed impl + reader.beginObject(); + // Name is required + String personName = null; + ContactInformation contactInformation = ContactInformation.EMPTY; // Empty if not specified - reader.endObject(); + while (reader.hasNext()) { + final String key = reader.nextName(); - if (personName == null) { - throw new ParseMetadataException.MissingField("Person object must have a 'name' field!"); + switch (key) { + case "name": + personName = ParserUtil.readString(reader, "person name"); + break; + // Effectively optional + case "contact": + contactInformation = V1ModMetadataParser.readContactInfo(reader); + break; + default: + // Ignore unsupported keys + warnings.add(new ParseWarning(reader, key, "Invalid entry in person")); + reader.skipValue(); } + } - if (contactInformation == null) { - contactInformation = ContactInformation.EMPTY; // Empty if not specified - } + reader.endObject(); - people.add(new ContactInfoBackedPerson(personName, contactInformation)); - break; - default: - throw new ParseMetadataException("Person type must be an object or string!", reader); + if (personName == null) { + throw new ParseMetadataException.MissingField("Person object must have a 'name' field!"); } - } - reader.endArray(); + return new ContactInfoBackedPerson(personName, contactInformation); + default: + throw new ParseMetadataException("Person type must be an object or string!", reader); + } } - private static ContactInformation readContactInfo(JsonReader reader) throws IOException, ParseMetadataException { + static ContactInformation readContactInfo(JsonReader reader) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_OBJECT) { throw new ParseMetadataException("Contact info must in an object", reader); } @@ -553,7 +400,7 @@ private static ContactInformation readContactInfo(JsonReader reader) throws IOEx throw new ParseMetadataException("Contact information entries must be a string", reader); } - map.put(key, reader.nextString()); + map.put(key, ParserUtil.readString(reader, "contact information value")); } reader.endObject(); @@ -562,20 +409,16 @@ private static ContactInformation readContactInfo(JsonReader reader) throws IOEx return new ContactInformationImpl(map); } - private static void readLicense(JsonReader reader, List license) throws IOException, ParseMetadataException { + static void readLicense(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { switch (reader.peek()) { case STRING: - license.add(reader.nextString()); + builder.addLicense(reader.nextString()); break; case BEGIN_ARRAY: reader.beginArray(); while (reader.hasNext()) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("List of licenses must only contain strings", reader); - } - - license.add(reader.nextString()); + builder.addLicense(ParserUtil.readString(reader, "license")); } reader.endArray(); @@ -585,22 +428,18 @@ private static void readLicense(JsonReader reader, List license) throws } } - private static V1ModMetadata.IconEntry readIcon(JsonReader reader) throws IOException, ParseMetadataException { + static void readIcon(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { switch (reader.peek()) { case STRING: - return new V1ModMetadata.Single(reader.nextString()); + builder.setIcon(reader.nextString()); + break; case BEGIN_OBJECT: reader.beginObject(); - final SortedMap iconMap = new TreeMap<>(Comparator.naturalOrder()); + final NavigableMap iconMap = new TreeMap<>(Comparator.naturalOrder()); while (reader.hasNext()) { - if (reader.peek() != JsonToken.STRING) { - throw new ParseMetadataException("Icon path must be a string", reader); - } - String key = reader.nextName(); - int size; try { @@ -612,6 +451,12 @@ private static V1ModMetadata.IconEntry readIcon(JsonReader reader) throws IOExce if (size < 1) { throw new ParseMetadataException("Size must be positive!", reader); } + + if (reader.peek() != JsonToken.STRING) { + throw new ParseMetadataException("Icon path must be a string", reader); + } + + builder.addIcon(size, reader.nextString()); } reader.endObject(); @@ -620,13 +465,13 @@ private static V1ModMetadata.IconEntry readIcon(JsonReader reader) throws IOExce throw new ParseMetadataException("Icon object must not be empty!", reader); } - return new V1ModMetadata.MapEntry(iconMap); + break; default: throw new ParseMetadataException("Icon entry must be an object or string!", reader); } } - private static void readLanguageAdapters(JsonReader reader, Map languageAdapters) throws IOException, ParseMetadataException { + static void readLanguageAdapters(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_OBJECT) { throw new ParseMetadataException("Language adapters must be in an object", reader); } @@ -640,13 +485,13 @@ private static void readLanguageAdapters(JsonReader reader, Map throw new ParseMetadataException("Value of language adapter entry must be a string", reader); } - languageAdapters.put(adapter, reader.nextString()); + builder.addLanguageAdapter(adapter, reader.nextString()); } reader.endObject(); } - private static void readCustomValues(JsonReader reader, Map customValues) throws IOException, ParseMetadataException { + static void readCustomValues(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { if (reader.peek() != JsonToken.BEGIN_OBJECT) { throw new ParseMetadataException("Custom values must be in an object!", reader); } @@ -654,12 +499,9 @@ private static void readCustomValues(JsonReader reader, Map reader.beginObject(); while (reader.hasNext()) { - customValues.put(reader.nextName(), CustomValueImpl.readCustomValue(reader)); + builder.addCustomValue(reader.nextName(), CustomValueImpl.readCustomValue(reader)); } reader.endObject(); } - - private V1ModMetadataParser() { - } } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/V2ModMetadataParser.java b/src/main/java/net/fabricmc/loader/impl/metadata/V2ModMetadataParser.java new file mode 100644 index 000000000..1a4c0519b --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/metadata/V2ModMetadataParser.java @@ -0,0 +1,458 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.metadata; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.extension.ModMetadataBuilder; +import net.fabricmc.loader.api.extension.ModMetadataBuilder.ModDependencyBuilder; +import net.fabricmc.loader.api.extension.ModMetadataBuilder.ModDependencyMetadataBuilder; +import net.fabricmc.loader.api.metadata.ContactInformation; +import net.fabricmc.loader.api.metadata.ModDependency; +import net.fabricmc.loader.api.metadata.ModEnvironment; +import net.fabricmc.loader.api.metadata.ModLoadCondition; +import net.fabricmc.loader.api.metadata.Person; +import net.fabricmc.loader.impl.lib.gson.JsonReader; +import net.fabricmc.loader.impl.lib.gson.JsonToken; + +final class V2ModMetadataParser { + /** + * Reads a {@code fabric.mod.json} file of schema version {@code 2}. + * + *

Changes over v1: + *

    + *
  • provides also accepts objects with id/version/exclusive + *
  • entrypoints object can use string values without an array + *
  • added loadCondition + *
  • added loadPhase + *
  • accessWidener -> classTweakers, can take string or array of strings + *
  • license -> licenses + *
  • dependencies accept object value form with additional environment, reason, metadata (id, name, description, contact), root metadata (same as dep metadata) + *
+ */ + static void parse(JsonReader reader, List warnings, ModMetadataBuilderImpl builder) throws IOException, ParseMetadataException { + while (reader.hasNext()) { + final String key = reader.nextName(); + + // Work our way from required to entirely optional + switch (key) { + case "schemaVersion": + V0ModMetadataParser.readSchemaVersion(reader, 2); + break; + case "id": + V0ModMetadataParser.readModId(reader, builder); + break; + case "version": + V0ModMetadataParser.readModVersion(reader, builder); + break; + case "provides": + readProvides(reader, builder); + break; + case "environment": + builder.setEnvironment(V1ModMetadataParser.readEnvironment(reader)); + break; + case "loadCondition": + builder.setLoadCondition(ParserUtil.readEnum(reader, ModLoadCondition.class, key)); + break; + case "loadPhase": + builder.setLoadPhase(ParserUtil.readString(reader, key)); + break; + case "entrypoints": + readEntrypoints(reader, builder); + break; + case "jars": + V1ModMetadataParser.readNestedJarEntries(reader, null, builder); + break; + case "mixins": + readMixinConfigs(reader, builder); + break; + case "classTweakers": + readClassTweakers(reader, builder); + break; + case "depends": + readDependency(reader, ModDependency.Kind.DEPENDS, builder); + break; + case "recommends": + readDependency(reader, ModDependency.Kind.RECOMMENDS, builder); + break; + case "suggests": + readDependency(reader, ModDependency.Kind.SUGGESTS, builder); + break; + case "conflicts": + readDependency(reader, ModDependency.Kind.CONFLICTS, builder); + break; + case "breaks": + readDependency(reader, ModDependency.Kind.BREAKS, builder); + break; + case "name": + V0ModMetadataParser.readModName(reader, builder); + break; + case "description": + V0ModMetadataParser.readModDescription(reader, builder); + break; + case "authors": + readPeople(reader, true, builder); + break; + case "contributors": + readPeople(reader, false, builder); + break; + case "contact": + builder.setContact(V1ModMetadataParser.readContactInfo(reader)); + break; + case "licenses": + V1ModMetadataParser.readLicense(reader, builder); + break; + case "icon": + V1ModMetadataParser.readIcon(reader, builder); + break; + case "languageAdapters": + V1ModMetadataParser.readLanguageAdapters(reader, builder); + break; + case "custom": + V1ModMetadataParser.readCustomValues(reader, builder); + break; + default: + if (!ModMetadataParser.IGNORED_KEYS.contains(key)) { + warnings.add(new ParseWarning(reader, key, "Unsupported root entry")); + } + + reader.skipValue(); + break; + } + } + } + + private static void readProvides(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.BEGIN_ARRAY) { + throw new ParseMetadataException("Provides must be an array"); + } + + reader.beginArray(); + + while (reader.hasNext()) { + switch (reader.peek()) { + case STRING: + builder.addProvidedMod(reader.nextString(), null, true); + break; + case BEGIN_OBJECT: { + reader.beginObject(); + + String id = null; + Version version = null; + boolean exclusive = true; + + while (reader.hasNext()) { + final String key = reader.nextName(); + + switch (key) { + case "id": + id = ParserUtil.readString(reader, key); + break; + case "version": + try { + version = Version.parse(ParserUtil.readString(reader, key)); + } catch (VersionParsingException e) { + throw new ParseMetadataException(e, reader); + } + + break; + case "exclusive": + exclusive = ParserUtil.readBoolean(reader, key); + break; + default: + throw new ParseMetadataException("Invalid key "+key+" in mixin config entry", reader); + } + } + + reader.endObject(); + + if (id == null) { + throw new ParseMetadataException.MissingField("Missing mandatory key 'id' in provides entry!"); + } + + builder.addProvidedMod(id, version, exclusive); + break; + } + default: + throw new ParseMetadataException("Provides entry must be a string or object!", reader); + } + } + + reader.endArray(); + } + + private static void readEntrypoints(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + // Entrypoints must be an object + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new ParseMetadataException("Entrypoints must be an object", reader); + } + + reader.beginObject(); + + while (reader.hasNext()) { + final String key = reader.nextName(); + + switch (reader.peek()) { + case STRING: + V1ModMetadataParser.readEntrypoint(reader, key, null, builder); + break; + case BEGIN_ARRAY: + reader.beginArray(); + + while (reader.hasNext()) { + V1ModMetadataParser.readEntrypoint(reader, key, null, builder); + } + + reader.endArray(); + break; + default: + throw new ParseMetadataException("Entrypoint list must be a string or array!", reader); + } + } + + reader.endObject(); + } + + private static void readMixinConfigs(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.BEGIN_ARRAY) { + throw new ParseMetadataException("Mixin configs must be in an array", reader); + } + + reader.beginArray(); + + while (reader.hasNext()) { + switch (reader.peek()) { + case STRING: + // All mixin configs specified via string are assumed to be universal + builder.addMixinConfig(reader.nextString(), null); + break; + case BEGIN_OBJECT: + reader.beginObject(); + + String config = null; + ModEnvironment environment = null; + + while (reader.hasNext()) { + final String key = reader.nextName(); + + switch (key) { + // Environment is optional + case "environment": + environment = V1ModMetadataParser.readEnvironment(reader); + break; + case "config": + config = ParserUtil.readString(reader, key); + break; + default: + throw new ParseMetadataException("Invalid key "+key+" in mixin config entry", reader); + } + } + + reader.endObject(); + + if (config == null) { + throw new ParseMetadataException.MissingField("Missing mandatory key 'config' in mixin entry!"); + } + + builder.addMixinConfig(config, environment); + break; + default: + throw new ParseMetadataException("Mixin list must be a string or object!", reader); + } + } + + reader.endArray(); + } + + private static void readClassTweakers(JsonReader reader, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + switch (reader.peek()) { + case STRING: + builder.addClassTweaker(reader.nextString()); + break; + case BEGIN_ARRAY: + reader.beginArray(); + + while (reader.hasNext()) { + builder.addClassTweaker(ParserUtil.readString(reader, "class tweaker")); + } + + reader.endArray(); + break; + default: + throw new ParseMetadataException("classTweakers must be a string or array of strings!", reader); + } + } + + private static void readDependency(JsonReader reader, ModDependency.Kind kind, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new ParseMetadataException(String.format("%s must be an object containing dependencies.", kind.name().toLowerCase(Locale.ENGLISH)), reader); + } + + reader.beginObject(); + + while (reader.hasNext()) { + ModDependencyBuilder depBuilder = ModDependencyBuilder.create(kind, reader.nextName()); + + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + V0ModMetadataParser.readDependencyValue(reader, depBuilder); + } else { + ModDependencyMetadataBuilder metaBuilder = ModDependencyMetadataBuilder.create(); + boolean metaUsed = false; + reader.beginObject(); + + while (reader.hasNext()) { + String key = reader.nextName(); + + switch (key) { + case "version": // if this is absent depBuilder will default to any + V0ModMetadataParser.readDependencyValue(reader, depBuilder); + break; + case "environment": + depBuilder.setEnvironment(V1ModMetadataParser.readEnvironment(reader)); + break; + case "reason": + depBuilder.setReason(ParserUtil.readString(reader, key)); + break; + case "root": + switch (reader.peek()) { + case STRING: + depBuilder.setRootMetadata(ModDependencyMetadataBuilder.create().setModId(ParserUtil.readString(reader, key)).build()); + break; + case BEGIN_OBJECT: { + ModDependencyMetadataBuilder rootMetaBuilder = ModDependencyMetadataBuilder.create(); + reader.beginObject(); + + while (reader.hasNext()) { + String metaKey = reader.nextName(); + + if (!readDependencyMetadata(reader, metaKey, rootMetaBuilder)) { + throw new ParseMetadataException("Invalid key "+metaKey+" in dependency root value", reader); + } + } + + reader.endObject(); + depBuilder.setRootMetadata(rootMetaBuilder.build()); + break; + } + default: + throw new ParseMetadataException("Dependency root metadata must be a string or object", reader); + } + default: + if (readDependencyMetadata(reader, key, metaBuilder)) { + metaUsed = true; + } else { + throw new ParseMetadataException("Invalid key "+key+" in dependency value", reader); + } + } + } + + reader.endObject(); + + if (metaUsed) depBuilder.setMetadata(metaBuilder.build()); + } + + builder.addDependency(depBuilder.build()); + } + + reader.endObject(); + } + + private static boolean readDependencyMetadata(JsonReader reader, String key, ModDependencyMetadataBuilder builder) throws IOException, ParseMetadataException { + switch (key) { + case "id": + builder.setModId(ParserUtil.readString(reader, key)); + break; + case "name": + builder.setName(ParserUtil.readString(reader, key)); + break; + case "description": + builder.setDescription(ParserUtil.readString(reader, key)); + break; + case "contact": + builder.setContact(V1ModMetadataParser.readContactInfo(reader)); + break; + default: + return false; + } + + return true; + } + + private static void readPeople(JsonReader reader, boolean isAuthor, ModMetadataBuilder builder) throws IOException, ParseMetadataException { + if (reader.peek() != JsonToken.BEGIN_ARRAY) { + throw new ParseMetadataException("List of people must be an array", reader); + } + + reader.beginArray(); + + while (reader.hasNext()) { + Person person = readPerson(reader); + + if (isAuthor) { + builder.addAuthor(person); + } else { + builder.addContributor(person); + } + } + + reader.endArray(); + } + + private static Person readPerson(JsonReader reader) throws IOException, ParseMetadataException { + switch (reader.peek()) { + case STRING: + // Just a name + return new SimplePerson(reader.nextString()); + case BEGIN_OBJECT: + // Map-backed impl + reader.beginObject(); + // Name is required + String personName = null; + ContactInformation contactInformation = ContactInformation.EMPTY; // Empty if not specified + + while (reader.hasNext()) { + final String key = reader.nextName(); + + switch (key) { + case "name": + personName = ParserUtil.readString(reader, "person name"); + break; + // Effectively optional + case "contact": + contactInformation = V1ModMetadataParser.readContactInfo(reader); + break; + default: + throw new ParseMetadataException("Invalid key "+key+" in person entry", reader); + } + } + + reader.endObject(); + + if (personName == null) { + throw new ParseMetadataException.MissingField("Person object must have a 'name' field!"); + } + + return new ContactInfoBackedPerson(personName, contactInformation); + default: + throw new ParseMetadataException("Person type must be an object or string!", reader); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/util/ExceptionUtil.java b/src/main/java/net/fabricmc/loader/impl/util/ExceptionUtil.java index 7e4e4a9b4..b96f32d36 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/ExceptionUtil.java +++ b/src/main/java/net/fabricmc/loader/impl/util/ExceptionUtil.java @@ -22,7 +22,7 @@ import java.util.function.Function; public final class ExceptionUtil { - private static final boolean THROW_DIRECTLY = System.getProperty(SystemProperties.DEBUG_THROW_DIRECTLY) != null; + private static final boolean THROW_DIRECTLY = SystemProperties.isSet(SystemProperties.DEBUG_THROW_DIRECTLY); public static T gatherExceptions(Throwable exc, T prev, Function mainExcFactory) throws T { exc = unwrap(exc); diff --git a/src/main/java/net/fabricmc/loader/impl/util/PhaseSorting.java b/src/main/java/net/fabricmc/loader/impl/util/PhaseSorting.java new file mode 100644 index 000000000..350a43046 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/util/PhaseSorting.java @@ -0,0 +1,274 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.PriorityQueue; + +import net.fabricmc.loader.impl.util.log.Log; +import net.fabricmc.loader.impl.util.log.LogCategory; + +/** + * Contains phase-sorting logic for {@link ArrayBackedEvent}. + */ +public final class PhaseSorting

, E> { + private static boolean ENABLE_CYCLE_WARNING = true; + + /** + * Registered phases. + */ + private final Map> phases = new HashMap<>(); + /** + * Phases sorted in the correct dependency order. + */ + private final List> sortedPhases = new ArrayList<>(); + + public void add(P phaseIdentifier, E element) { + Objects.requireNonNull(phaseIdentifier, "Tried to register an element for a null phase!"); + Objects.requireNonNull(element, "Tried to register a null element!"); + + getOrCreatePhase(phaseIdentifier, true).addElement(element); + } + + public List get(P phase) { + PhaseData data = phases.get(phase); + + return data != null ? data.elements : Collections.emptyList(); + } + + public List getAll() { + if (sortedPhases.size() == 1) { + // Special case with a single phase: use the array of the phase directly. + return sortedPhases.get(0).elements; + } else { + List ret = new ArrayList<>(); + + for (PhaseData existingPhase : sortedPhases) { + ret.addAll(existingPhase.elements); + } + + return ret; + } + } + + private PhaseData getOrCreatePhase(P id, boolean sortIfCreate) { + PhaseData phase = phases.get(id); + + if (phase == null) { + phase = new PhaseData<>(id); + phases.put(id, phase); + sortedPhases.add(phase); + + if (sortIfCreate) { + sortPhases(); + } + } + + return phase; + } + + public void addPhaseOrdering(P firstPhase, P secondPhase) { + Objects.requireNonNull(firstPhase, "Tried to add an ordering for a null phase."); + Objects.requireNonNull(secondPhase, "Tried to add an ordering for a null phase."); + if (firstPhase.equals(secondPhase)) throw new IllegalArgumentException("Tried to add a phase that depends on itself."); + + PhaseData first = getOrCreatePhase(firstPhase, false); + PhaseData second = getOrCreatePhase(secondPhase, false); + first.subsequentPhases.add(second); + second.previousPhases.add(first); + sortPhases(); + } + + public List

getUsedPhases() { + List

ret = new ArrayList<>(sortedPhases.size()); + + for (PhaseData phase : sortedPhases) { + if (!phase.elements.isEmpty()) { + ret.add(phase.id); + } + } + + return ret; + } + + public int getPhaseIndex(P phase) { + for (int i = 0; i < sortedPhases.size(); i++) { + if (sortedPhases.get(i).id.equals(phase)) return i; + } + + return -1; + } + + /** + * Deterministically sort a list of phases. + * 1) Compute phase SCCs (i.e. cycles). + * 2) Sort phases by id within SCCs. + * 3) Sort SCCs with respect to each other by respecting constraints, and by id in case of a tie. + */ + void sortPhases() { + // FIRST KOSARAJU SCC VISIT + List> toposort = new ArrayList<>(sortedPhases.size()); + + for (PhaseData phase : sortedPhases) { + forwardVisit(phase, null, toposort); + } + + clearStatus(toposort); + Collections.reverse(toposort); + + // SECOND KOSARAJU SCC VISIT + Map, PhaseScc> phaseToScc = new IdentityHashMap<>(); + + for (PhaseData phase : toposort) { + if (phase.visitStatus == 0) { + List> sccPhases = new ArrayList<>(); + // Collect phases in SCC. + backwardVisit(phase, sccPhases); + // Sort phases by id. + sccPhases.sort(Comparator.comparing(p -> p.id)); + // Mark phases as belonging to this SCC. + PhaseScc scc = new PhaseScc<>(sccPhases); + + for (PhaseData phaseInScc : sccPhases) { + phaseToScc.put(phaseInScc, scc); + } + } + } + + clearStatus(toposort); + + // Build SCC graph + for (PhaseScc scc : phaseToScc.values()) { + for (PhaseData phase : scc.phases) { + for (PhaseData subsequentPhase : phase.subsequentPhases) { + PhaseScc subsequentScc = phaseToScc.get(subsequentPhase); + + if (subsequentScc != scc) { + scc.subsequentSccs.add(subsequentScc); + subsequentScc.inDegree++; + } + } + } + } + + // Order SCCs according to priorities. When there is a choice, use the SCC with the lowest id. + // The priority queue contains all SCCs that currently have 0 in-degree. + PriorityQueue> pq = new PriorityQueue<>(Comparator.comparing(scc -> scc.phases.get(0).id)); + sortedPhases.clear(); + + for (PhaseScc scc : phaseToScc.values()) { + if (scc.inDegree == 0) { + pq.add(scc); + // Prevent adding the same SCC multiple times, as phaseToScc may contain the same value multiple times. + scc.inDegree = -1; + } + } + + while (!pq.isEmpty()) { + PhaseScc scc = pq.poll(); + sortedPhases.addAll(scc.phases); + + for (PhaseScc subsequentScc : scc.subsequentSccs) { + subsequentScc.inDegree--; + + if (subsequentScc.inDegree == 0) { + pq.add(subsequentScc); + } + } + } + } + + private void forwardVisit(PhaseData phase, PhaseData parent, List> toposort) { + if (phase.visitStatus == 0) { + // Not yet visited. + phase.visitStatus = 1; + + for (PhaseData data : phase.subsequentPhases) { + forwardVisit(data, phase, toposort); + } + + toposort.add(phase); + phase.visitStatus = 2; + } else if (phase.visitStatus == 1 && ENABLE_CYCLE_WARNING) { + // Already visiting, so we have found a cycle. + Log.warn(LogCategory.GENERAL, + "Phase ordering conflict detected.%nPhase %s is ordered both before and after phase %s.", + phase.id, + parent.id); + } + } + + private void clearStatus(List> phases) { + for (PhaseData phase : phases) { + phase.visitStatus = 0; + } + } + + private void backwardVisit(PhaseData phase, List> sccPhases) { + if (phase.visitStatus == 0) { + phase.visitStatus = 1; + sccPhases.add(phase); + + for (PhaseData data : phase.previousPhases) { + backwardVisit(data, sccPhases); + } + } + } + + @Override + public String toString() { + return sortedPhases.toString(); + } + + private static final class PhaseScc

, E> { + final List> phases; + final List> subsequentSccs = new ArrayList<>(); + int inDegree = 0; + + private PhaseScc(List> phases) { + this.phases = phases; + } + } + + private static final class PhaseData

, E> { + final P id; + List elements = new ArrayList<>(); + final List> subsequentPhases = new ArrayList<>(); + final List> previousPhases = new ArrayList<>(); + int visitStatus = 0; // 0: not visited, 1: visiting, 2: visited + + PhaseData(P id) { + this.id = id; + } + + void addElement(E element) { + elements.add(element); + } + + @Override + public String toString() { + return String.format("%s:%s", id, elements); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java b/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java index 331a255fd..ae93613fc 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java +++ b/src/main/java/net/fabricmc/loader/impl/util/SystemProperties.java @@ -19,7 +19,13 @@ public final class SystemProperties { // whether fabric loader is running in a development environment / mode, affects class path mod discovery, remapping, logging, ... public static final String DEVELOPMENT = "fabric.development"; + // whether to use a class loader that is an instance of URLClassLoader + public static final String USE_COMPAT_CL = "fabric.loader.useCompatibilityClassLoader"; public static final String SIDE = "fabric.side"; + // mapping namespace used by the game, defaults to named if DEVELOPMENT is set or official otherwise + public static final String GAME_MAPPING_NAMESPACE = "fabric.gameMappingNamespace"; + // mapping namespace to use at runtime, defaults to named if DEVELOPMENT is set or intermediary otherwise + public static final String RUNTIME_MAPPING_NAMESPACE = "fabric.runtimeMappingNamespace"; // skips the embedded MC game provider, letting ServiceLoader-provided ones take over public static final String SKIP_MC_PROVIDER = "fabric.skipMcProvider"; // game jar paths for common/client/server, replaces lookup from class path if present, env specific takes precedence @@ -63,6 +69,9 @@ public final class SystemProperties { // replace mod versions (modA:versionA,modB:versionB,...) public static final String DEBUG_REPLACE_VERSION = "fabric.debug.replaceVersion"; - private SystemProperties() { + public static boolean isSet(String property) { + String val = System.getProperty(property); + + return val != null && !val.equalsIgnoreCase("false"); } } diff --git a/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java b/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java index cd315897d..3e3c7851d 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java +++ b/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java @@ -35,7 +35,13 @@ public final class VersionPredicateParser { private static final VersionComparisonOperator[] OPERATORS = VersionComparisonOperator.values(); + public static VersionPredicate any() { + return AnyVersionPredicate.INSTANCE; + } + public static VersionPredicate parse(String predicate) throws VersionParsingException { + if (predicate.isEmpty() || predicate.equals("*")) return AnyVersionPredicate.INSTANCE; + List predicateList = new ArrayList<>(); for (String s : predicate.split(" ")) { diff --git a/src/test/java/net/fabricmc/test/V1ModJsonParsingTests.java b/src/test/java/net/fabricmc/test/V1ModJsonParsingTests.java index fd520dab8..3a7caabff 100644 --- a/src/test/java/net/fabricmc/test/V1ModJsonParsingTests.java +++ b/src/test/java/net/fabricmc/test/V1ModJsonParsingTests.java @@ -151,7 +151,7 @@ private void validateRequiredValues(LoaderModMetadata metadata) { public void testLongFile() throws IOException, ParseMetadataException { final LoaderModMetadata modMetadata = parseMetadata(specPath.resolve("long.json")); - if (!modMetadata.getAccessWidener().equals("examplemod.accessWidener")) { + if (!modMetadata.getClassTweakers().equals(Collections.singletonList("examplemod.accessWidener"))) { throw new RuntimeException("Incorrect access widener entry"); }