Support injecting interfaces from the mod source. Add a comment to target classes saying what mod is providing an injected interface. (#581)
This commit is contained in:
		
							parent
							
								
									e4330a11dc
								
							
						
					
					
						commit
						9662a8b3de
					
				
					 11 changed files with 285 additions and 84 deletions
				
			
		|  | @ -0,0 +1,52 @@ | ||||||
|  | /* | ||||||
|  |  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||||
|  |  * | ||||||
|  |  * Copyright (c) 2022 FabricMC | ||||||
|  |  * | ||||||
|  |  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  |  * of this software and associated documentation files (the "Software"), to deal | ||||||
|  |  * in the Software without restriction, including without limitation the rights | ||||||
|  |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  |  * copies of the Software, and to permit persons to whom the Software is | ||||||
|  |  * furnished to do so, subject to the following conditions: | ||||||
|  |  * | ||||||
|  |  * The above copyright notice and this permission notice shall be included in all | ||||||
|  |  * copies or substantial portions of the Software. | ||||||
|  |  * | ||||||
|  |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  |  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  |  * SOFTWARE. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package net.fabricmc.loom.api; | ||||||
|  | 
 | ||||||
|  | import org.gradle.api.provider.ListProperty; | ||||||
|  | import org.gradle.api.provider.Property; | ||||||
|  | import org.gradle.api.tasks.SourceSet; | ||||||
|  | 
 | ||||||
|  | public interface InterfaceInjectionExtensionAPI { | ||||||
|  | 	/** | ||||||
|  | 	 * When true loom will inject interfaces declared in dependency mod manifests into the minecraft jar file. | ||||||
|  | 	 * This is used to expose interfaces that are implemented on Minecraft classes by mixins at runtime | ||||||
|  | 	 * in the dev environment. | ||||||
|  | 	 * | ||||||
|  | 	 * @return the property controlling interface injection. | ||||||
|  | 	 */ | ||||||
|  | 	Property<Boolean> getEnableDependencyInterfaceInjection(); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Contains a list of {@link SourceSet} that may contain a fabric.mod.json file with interfaces to inject. | ||||||
|  | 	 * By default, this list contains only the main {@link SourceSet}. | ||||||
|  | 	 * | ||||||
|  | 	 * @return the list property containing the {@link SourceSet} | ||||||
|  | 	 */ | ||||||
|  | 	ListProperty<SourceSet> getInterfaceInjectionSourceSets(); | ||||||
|  | 
 | ||||||
|  | 	default boolean isEnabled() { | ||||||
|  | 		return getEnableDependencyInterfaceInjection().get() || !getInterfaceInjectionSourceSets().get().isEmpty(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /* | /* | ||||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). |  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||||
|  * |  * | ||||||
|  * Copyright (c) 2021 FabricMC |  * Copyright (c) 2021-2022 FabricMC | ||||||
|  * |  * | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  * of this software and associated documentation files (the "Software"), to deal | ||||||
|  | @ -86,6 +86,12 @@ public interface LoomGradleExtensionAPI { | ||||||
| 	// TODO: move this from LoomGradleExtensionAPI to LoomGradleExtension once getRefmapName & setRefmapName is removed. | 	// TODO: move this from LoomGradleExtensionAPI to LoomGradleExtension once getRefmapName & setRefmapName is removed. | ||||||
| 	MixinExtensionAPI getMixin(); | 	MixinExtensionAPI getMixin(); | ||||||
| 
 | 
 | ||||||
|  | 	default void interfaceInjection(Action<InterfaceInjectionExtensionAPI> action) { | ||||||
|  | 		action.execute(getInterfaceInjection()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	InterfaceInjectionExtensionAPI getInterfaceInjection(); | ||||||
|  | 
 | ||||||
| 	Property<String> getCustomMinecraftManifest(); | 	Property<String> getCustomMinecraftManifest(); | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
|  | @ -131,15 +137,6 @@ public interface LoomGradleExtensionAPI { | ||||||
| 	 */ | 	 */ | ||||||
| 	Property<String> getIntermediaryUrl(); | 	Property<String> getIntermediaryUrl(); | ||||||
| 
 | 
 | ||||||
| 	/** |  | ||||||
| 	 * When true loom will inject interfaces declared in mod manifests into the minecraft jar file. |  | ||||||
| 	 * This is used to expose interfaces that are implemented on Minecraft classes by mixins at runtime |  | ||||||
| 	 * in the dev environment. |  | ||||||
| 	 * |  | ||||||
| 	 * @return the property controlling interface injection. |  | ||||||
| 	 */ |  | ||||||
| 	Property<Boolean> getEnableInterfaceInjection(); |  | ||||||
| 
 |  | ||||||
| 	@ApiStatus.Experimental | 	@ApiStatus.Experimental | ||||||
| 	Property<MinecraftJarConfiguration> getMinecraftJarConfiguration(); | 	Property<MinecraftJarConfiguration> getMinecraftJarConfiguration(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /* | /* | ||||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). |  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||||
|  * |  * | ||||||
|  * Copyright (c) 2016-2021 FabricMC |  * Copyright (c) 2016-2022 FabricMC | ||||||
|  * |  * | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  * of this software and associated documentation files (the "Software"), to deal | ||||||
|  | @ -233,7 +233,7 @@ public final class CompileConfiguration { | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (extension.getEnableInterfaceInjection().get()) { | 		if (extension.getInterfaceInjection().isEnabled()) { | ||||||
| 			InterfaceInjectionProcessor jarProcessor = new InterfaceInjectionProcessor(project); | 			InterfaceInjectionProcessor jarProcessor = new InterfaceInjectionProcessor(project); | ||||||
| 
 | 
 | ||||||
| 			if (!jarProcessor.isEmpty()) { | 			if (!jarProcessor.isEmpty()) { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /* | /* | ||||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). |  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||||
|  * |  * | ||||||
|  * Copyright (c) 2021 FabricMC |  * Copyright (c) 2021-2022 FabricMC | ||||||
|  * |  * | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  * of this software and associated documentation files (the "Software"), to deal | ||||||
|  | @ -24,58 +24,44 @@ | ||||||
| 
 | 
 | ||||||
| package net.fabricmc.loom.configuration.accesswidener; | package net.fabricmc.loom.configuration.accesswidener; | ||||||
| 
 | 
 | ||||||
| import java.io.IOException; |  | ||||||
| import java.io.Reader; |  | ||||||
| import java.io.Writer; |  | ||||||
| import java.nio.charset.StandardCharsets; |  | ||||||
| import java.nio.file.Files; |  | ||||||
| import java.nio.file.Path; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 | 
 | ||||||
|  | import org.gradle.api.Project; | ||||||
| import org.gradle.api.logging.Logger; | import org.gradle.api.logging.Logger; | ||||||
| 
 | 
 | ||||||
| import net.fabricmc.accesswidener.AccessWidenerReader; | import net.fabricmc.accesswidener.AccessWidenerReader; | ||||||
| import net.fabricmc.accesswidener.AccessWidenerVisitor; | import net.fabricmc.accesswidener.AccessWidenerVisitor; | ||||||
| import net.fabricmc.accesswidener.TransitiveOnlyFilter; | import net.fabricmc.accesswidener.TransitiveOnlyFilter; | ||||||
|  | import net.fabricmc.loom.LoomGradleExtension; | ||||||
| import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; | import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; | ||||||
| import net.fabricmc.mappingio.MappingReader; | import net.fabricmc.loom.task.GenerateSourcesTask; | ||||||
| import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; |  | ||||||
| import net.fabricmc.mappingio.format.Tiny2Writer; |  | ||||||
| import net.fabricmc.mappingio.tree.MappingTree; | import net.fabricmc.mappingio.tree.MappingTree; | ||||||
| import net.fabricmc.mappingio.tree.MemoryMappingTree; | import net.fabricmc.mappingio.tree.MemoryMappingTree; | ||||||
| 
 | 
 | ||||||
| public final class TransitiveAccessWidenerMappingsProcessor { | public record TransitiveAccessWidenerMappingsProcessor(Project project) implements GenerateSourcesTask.MappingsProcessor { | ||||||
| 	private TransitiveAccessWidenerMappingsProcessor() { | 	@Override | ||||||
|  | 	public boolean transform(MemoryMappingTree mappings) { | ||||||
|  | 		final LoomGradleExtension extension = LoomGradleExtension.get(project); | ||||||
|  | 		List<AccessWidenerFile> accessWideners = extension.getTransitiveAccessWideners(); | ||||||
|  | 
 | ||||||
|  | 		if (accessWideners.isEmpty()) { | ||||||
|  | 			return false; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 	public static void process(Path inputMappings, Path outputMappings, List<AccessWidenerFile> accessWideners, Logger logger) { | 		if (!MappingsNamespace.INTERMEDIARY.toString().equals(mappings.getSrcNamespace())) { | ||||||
| 		MemoryMappingTree mappingTree = new MemoryMappingTree(); | 			throw new IllegalStateException("Mapping tree must have intermediary src mappings not " + mappings.getSrcNamespace()); | ||||||
| 
 |  | ||||||
| 		try (Reader reader = Files.newBufferedReader(inputMappings, StandardCharsets.UTF_8)) { |  | ||||||
| 			MappingReader.read(reader, new MappingSourceNsSwitch(mappingTree, MappingsNamespace.INTERMEDIARY.toString())); |  | ||||||
| 		} catch (IOException e) { |  | ||||||
| 			throw new RuntimeException("Failed to read mappings", e); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if (!MappingsNamespace.INTERMEDIARY.toString().equals(mappingTree.getSrcNamespace())) { |  | ||||||
| 			throw new IllegalStateException("Mapping tree must have intermediary src mappings not " + mappingTree.getSrcNamespace()); |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		for (AccessWidenerFile accessWidener : accessWideners) { | 		for (AccessWidenerFile accessWidener : accessWideners) { | ||||||
| 			MappingCommentVisitor mappingCommentVisitor = new MappingCommentVisitor(accessWidener.modId(), mappingTree, logger); | 			MappingCommentVisitor mappingCommentVisitor = new MappingCommentVisitor(accessWidener.modId(), mappings, project.getLogger()); | ||||||
| 			AccessWidenerReader accessWidenerReader = new AccessWidenerReader(new TransitiveOnlyFilter(mappingCommentVisitor)); | 			AccessWidenerReader accessWidenerReader = new AccessWidenerReader(new TransitiveOnlyFilter(mappingCommentVisitor)); | ||||||
| 			accessWidenerReader.read(accessWidener.content()); | 			accessWidenerReader.read(accessWidener.content()); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		try (Writer writer = Files.newBufferedWriter(outputMappings, StandardCharsets.UTF_8)) { | 		return true; | ||||||
| 			Tiny2Writer tiny2Writer = new Tiny2Writer(writer, false); |  | ||||||
| 			mappingTree.accept(new MappingSourceNsSwitch(tiny2Writer, MappingsNamespace.NAMED.toString())); |  | ||||||
| 		} catch (IOException e) { |  | ||||||
| 			throw new RuntimeException("Failed to write mappings", e); |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private static record MappingCommentVisitor(String modId, MemoryMappingTree mappingTree, Logger logger) implements AccessWidenerVisitor { | 	private record MappingCommentVisitor(String modId, MemoryMappingTree mappingTree, Logger logger) implements AccessWidenerVisitor { | ||||||
| 		@Override | 		@Override | ||||||
| 		public void visitClass(String name, AccessWidenerReader.AccessType access, boolean transitive) { | 		public void visitClass(String name, AccessWidenerReader.AccessType access, boolean transitive) { | ||||||
| 			MappingTree.ClassMapping classMapping = mappingTree.getClass(name); | 			MappingTree.ClassMapping classMapping = mappingTree.getClass(name); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /* | /* | ||||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). |  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||||
|  * |  * | ||||||
|  * Copyright (c) 2021 FabricMC |  * Copyright (c) 2021-2022 FabricMC | ||||||
|  * |  * | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  * of this software and associated documentation files (the "Software"), to deal | ||||||
|  | @ -26,7 +26,9 @@ package net.fabricmc.loom.configuration.ifaceinject; | ||||||
| 
 | 
 | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  | import java.io.UncheckedIOException; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
|  | @ -40,40 +42,47 @@ import java.util.stream.Collectors; | ||||||
| import com.google.common.base.Preconditions; | import com.google.common.base.Preconditions; | ||||||
| import com.google.common.hash.Hasher; | import com.google.common.hash.Hasher; | ||||||
| import com.google.common.hash.Hashing; | import com.google.common.hash.Hashing; | ||||||
| import com.google.gson.Gson; |  | ||||||
| import com.google.gson.JsonArray; | import com.google.gson.JsonArray; | ||||||
| import com.google.gson.JsonElement; | import com.google.gson.JsonElement; | ||||||
| import com.google.gson.JsonObject; | import com.google.gson.JsonObject; | ||||||
| import org.gradle.api.Project; | import org.gradle.api.Project; | ||||||
|  | import org.gradle.api.tasks.SourceSet; | ||||||
| import org.objectweb.asm.ClassReader; | import org.objectweb.asm.ClassReader; | ||||||
| import org.objectweb.asm.ClassVisitor; | import org.objectweb.asm.ClassVisitor; | ||||||
| import org.objectweb.asm.ClassWriter; | import org.objectweb.asm.ClassWriter; | ||||||
| import org.objectweb.asm.commons.Remapper; | import org.objectweb.asm.commons.Remapper; | ||||||
| 
 | 
 | ||||||
| import net.fabricmc.loom.LoomGradleExtension; | import net.fabricmc.loom.LoomGradleExtension; | ||||||
|  | import net.fabricmc.loom.LoomGradlePlugin; | ||||||
|  | import net.fabricmc.loom.api.InterfaceInjectionExtensionAPI; | ||||||
| import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; | import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; | ||||||
| import net.fabricmc.loom.configuration.RemappedConfigurationEntry; | import net.fabricmc.loom.configuration.RemappedConfigurationEntry; | ||||||
| import net.fabricmc.loom.configuration.processors.JarProcessor; | import net.fabricmc.loom.configuration.processors.JarProcessor; | ||||||
|  | import net.fabricmc.loom.task.GenerateSourcesTask; | ||||||
| import net.fabricmc.loom.util.Checksum; | import net.fabricmc.loom.util.Checksum; | ||||||
| import net.fabricmc.loom.util.Constants; | import net.fabricmc.loom.util.Constants; | ||||||
| import net.fabricmc.loom.util.Pair; | import net.fabricmc.loom.util.Pair; | ||||||
| import net.fabricmc.loom.util.TinyRemapperHelper; | import net.fabricmc.loom.util.TinyRemapperHelper; | ||||||
| import net.fabricmc.loom.util.ZipUtils; | import net.fabricmc.loom.util.ZipUtils; | ||||||
|  | import net.fabricmc.mappingio.tree.MappingTree; | ||||||
|  | import net.fabricmc.mappingio.tree.MemoryMappingTree; | ||||||
| import net.fabricmc.tinyremapper.TinyRemapper; | import net.fabricmc.tinyremapper.TinyRemapper; | ||||||
| 
 | 
 | ||||||
| public class InterfaceInjectionProcessor implements JarProcessor { | public class InterfaceInjectionProcessor implements JarProcessor, GenerateSourcesTask.MappingsProcessor { | ||||||
| 	// Filename used to store hash of injected interfaces in processed jar file | 	// Filename used to store hash of injected interfaces in processed jar file | ||||||
| 	private static final String HASH_FILENAME = "injected_interfaces.sha256"; | 	private static final String HASH_FILENAME = "injected_interfaces.sha256"; | ||||||
| 
 | 
 | ||||||
| 	private final Map<String, List<InjectedInterface>> injectedInterfaces; | 	private final Map<String, List<InjectedInterface>> injectedInterfaces; | ||||||
| 	private final Project project; | 	private final Project project; | ||||||
| 	private final LoomGradleExtension extension; | 	private final LoomGradleExtension extension; | ||||||
|  | 	private final InterfaceInjectionExtensionAPI interfaceInjectionExtension; | ||||||
| 	private final byte[] inputHash; | 	private final byte[] inputHash; | ||||||
| 	private Map<String, List<InjectedInterface>> remappedInjectedInterfaces; | 	private Map<String, List<InjectedInterface>> remappedInjectedInterfaces; | ||||||
| 
 | 
 | ||||||
| 	public InterfaceInjectionProcessor(Project project) { | 	public InterfaceInjectionProcessor(Project project) { | ||||||
| 		this.project = project; | 		this.project = project; | ||||||
| 		this.extension = LoomGradleExtension.get(project); | 		this.extension = LoomGradleExtension.get(project); | ||||||
|  | 		this.interfaceInjectionExtension = this.extension.getInterfaceInjection(); | ||||||
| 		this.injectedInterfaces = getInjectedInterfaces().stream() | 		this.injectedInterfaces = getInjectedInterfaces().stream() | ||||||
| 				.collect(Collectors.groupingBy(InjectedInterface::className)); | 				.collect(Collectors.groupingBy(InjectedInterface::className)); | ||||||
| 
 | 
 | ||||||
|  | @ -158,6 +167,20 @@ public class InterfaceInjectionProcessor implements JarProcessor { | ||||||
| 	private List<InjectedInterface> getInjectedInterfaces() { | 	private List<InjectedInterface> getInjectedInterfaces() { | ||||||
| 		List<InjectedInterface> result = new ArrayList<>(); | 		List<InjectedInterface> result = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|  | 		if (interfaceInjectionExtension.getEnableDependencyInterfaceInjection().get()) { | ||||||
|  | 			result.addAll(getDependencyInjectedInterfaces()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for (SourceSet sourceSet : interfaceInjectionExtension.getInterfaceInjectionSourceSets().get()) { | ||||||
|  | 			result.addAll(getSourceInjectedInterface(sourceSet)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return result; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private List<InjectedInterface> getDependencyInjectedInterfaces() { | ||||||
|  | 		List<InjectedInterface> result = new ArrayList<>(); | ||||||
|  | 
 | ||||||
| 		for (RemappedConfigurationEntry entry : Constants.MOD_COMPILE_ENTRIES) { | 		for (RemappedConfigurationEntry entry : Constants.MOD_COMPILE_ENTRIES) { | ||||||
| 			// Only apply injected interfaces from mods that are part of the compile classpath | 			// Only apply injected interfaces from mods that are part of the compile classpath | ||||||
| 			if (!entry.compileClasspath()) { | 			if (!entry.compileClasspath()) { | ||||||
|  | @ -176,12 +199,81 @@ public class InterfaceInjectionProcessor implements JarProcessor { | ||||||
| 		return result; | 		return result; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	private List<InjectedInterface> getSourceInjectedInterface(SourceSet sourceSet) { | ||||||
|  | 		final File fabricModJson; | ||||||
|  | 
 | ||||||
|  | 		try { | ||||||
|  | 			fabricModJson = sourceSet.getResources() | ||||||
|  | 					.matching(patternFilterable -> patternFilterable.include("fabric.mod.json")) | ||||||
|  | 					.getSingleFile(); | ||||||
|  | 		} catch (IllegalStateException e) { | ||||||
|  | 			// File not found | ||||||
|  | 			return Collections.emptyList(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		final String jsonString; | ||||||
|  | 
 | ||||||
|  | 		try { | ||||||
|  | 			jsonString = Files.readString(fabricModJson.toPath(), StandardCharsets.UTF_8); | ||||||
|  | 		} catch (IOException e) { | ||||||
|  | 			throw new UncheckedIOException("Failed to read fabric.mod.json", e); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		final JsonObject jsonObject = LoomGradlePlugin.GSON.fromJson(jsonString, JsonObject.class); | ||||||
|  | 
 | ||||||
|  | 		return InjectedInterface.fromJson(jsonObject); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean transform(MemoryMappingTree mappings) { | ||||||
|  | 		if (injectedInterfaces.isEmpty()) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (!MappingsNamespace.INTERMEDIARY.toString().equals(mappings.getSrcNamespace())) { | ||||||
|  | 			throw new IllegalStateException("Mapping tree must have intermediary src mappings not " + mappings.getSrcNamespace()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for (Map.Entry<String, List<InjectedInterface>> entry : injectedInterfaces.entrySet()) { | ||||||
|  | 			final String className = entry.getKey(); | ||||||
|  | 			final List<InjectedInterface> injectedInterfaces = entry.getValue(); | ||||||
|  | 
 | ||||||
|  | 			MappingTree.ClassMapping classMapping = mappings.getClass(className); | ||||||
|  | 
 | ||||||
|  | 			if (classMapping == null) { | ||||||
|  | 				final String modIds = injectedInterfaces.stream().map(InjectedInterface::modId).distinct().collect(Collectors.joining(",")); | ||||||
|  | 				project.getLogger().warn("Failed to find class ({}) to add injected interfaces from mod(s) ({})", className, modIds); | ||||||
|  | 				continue; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			classMapping.setComment(appendComment(classMapping.getComment(), injectedInterfaces)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static String appendComment(String comment, List<InjectedInterface> injectedInterfaces) { | ||||||
|  | 		for (InjectedInterface injectedInterface : injectedInterfaces) { | ||||||
|  | 			String iiComment = "Interface {@link %s} injected by mod %s".formatted(injectedInterface.ifaceName.substring(injectedInterface.ifaceName.lastIndexOf("/") + 1), injectedInterface.modId); | ||||||
|  | 
 | ||||||
|  | 			if (comment == null || !comment.contains(iiComment)) { | ||||||
|  | 				if (comment == null) { | ||||||
|  | 					comment = iiComment; | ||||||
|  | 				} else { | ||||||
|  | 					comment += "\n" + iiComment; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return comment; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	private record InjectedInterface(String modId, String className, String ifaceName) { | 	private record InjectedInterface(String modId, String className, String ifaceName) { | ||||||
| 		/** | 		/** | ||||||
| 		 * Reads the injected interfaces contained in a mod jar, or returns null if there is none. | 		 * Reads the injected interfaces contained in a mod jar, or returns null if there is none. | ||||||
| 		 */ | 		 */ | ||||||
| 		public static List<InjectedInterface> fromModJar(Path modJarPath) { | 		public static List<InjectedInterface> fromModJar(Path modJarPath) { | ||||||
| 			byte[] modJsonBytes; | 			final byte[] modJsonBytes; | ||||||
| 
 | 
 | ||||||
| 			try { | 			try { | ||||||
| 				modJsonBytes = ZipUtils.unpackNullable(modJarPath, "fabric.mod.json"); | 				modJsonBytes = ZipUtils.unpackNullable(modJarPath, "fabric.mod.json"); | ||||||
|  | @ -193,26 +285,30 @@ public class InterfaceInjectionProcessor implements JarProcessor { | ||||||
| 				return Collections.emptyList(); | 				return Collections.emptyList(); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			JsonObject jsonObject = new Gson().fromJson(new String(modJsonBytes, StandardCharsets.UTF_8), JsonObject.class); | 			final JsonObject jsonObject = LoomGradlePlugin.GSON.fromJson(new String(modJsonBytes, StandardCharsets.UTF_8), JsonObject.class); | ||||||
| 
 | 
 | ||||||
| 			String modId = jsonObject.get("id").getAsString(); | 			return fromJson(jsonObject); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public static List<InjectedInterface> fromJson(JsonObject jsonObject) { | ||||||
|  | 			final String modId = jsonObject.get("id").getAsString(); | ||||||
| 
 | 
 | ||||||
| 			if (!jsonObject.has("custom")) { | 			if (!jsonObject.has("custom")) { | ||||||
| 				return Collections.emptyList(); | 				return Collections.emptyList(); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			JsonObject custom = jsonObject.getAsJsonObject("custom"); | 			final JsonObject custom = jsonObject.getAsJsonObject("custom"); | ||||||
| 
 | 
 | ||||||
| 			if (!custom.has("loom:injected_interfaces")) { | 			if (!custom.has("loom:injected_interfaces")) { | ||||||
| 				return Collections.emptyList(); | 				return Collections.emptyList(); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			JsonObject addedIfaces = custom.getAsJsonObject("loom:injected_interfaces"); | 			final JsonObject addedIfaces = custom.getAsJsonObject("loom:injected_interfaces"); | ||||||
| 
 | 
 | ||||||
| 			List<InjectedInterface> result = new ArrayList<>(); | 			final List<InjectedInterface> result = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
| 			for (String className : addedIfaces.keySet()) { | 			for (String className : addedIfaces.keySet()) { | ||||||
| 				JsonArray ifaceNames = addedIfaces.getAsJsonArray(className); | 				final JsonArray ifaceNames = addedIfaces.getAsJsonArray(className); | ||||||
| 
 | 
 | ||||||
| 				for (JsonElement ifaceName : ifaceNames) { | 				for (JsonElement ifaceName : ifaceNames) { | ||||||
| 					result.add(new InjectedInterface(modId, className, ifaceName.getAsString())); | 					result.add(new InjectedInterface(modId, className, ifaceName.getAsString())); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /* | /* | ||||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). |  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||||
|  * |  * | ||||||
|  * Copyright (c) 2021 FabricMC |  * Copyright (c) 2021-2022 FabricMC | ||||||
|  * |  * | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  * of this software and associated documentation files (the "Software"), to deal | ||||||
|  | @ -30,10 +30,13 @@ import org.gradle.api.Project; | ||||||
| import org.gradle.api.artifacts.Dependency; | import org.gradle.api.artifacts.Dependency; | ||||||
| import org.gradle.api.file.ConfigurableFileCollection; | import org.gradle.api.file.ConfigurableFileCollection; | ||||||
| import org.gradle.api.file.RegularFileProperty; | import org.gradle.api.file.RegularFileProperty; | ||||||
|  | import org.gradle.api.plugins.JavaPluginExtension; | ||||||
| import org.gradle.api.provider.ListProperty; | import org.gradle.api.provider.ListProperty; | ||||||
| import org.gradle.api.provider.Property; | import org.gradle.api.provider.Property; | ||||||
| import org.gradle.api.publish.maven.MavenPublication; | import org.gradle.api.publish.maven.MavenPublication; | ||||||
|  | import org.gradle.api.tasks.SourceSet; | ||||||
| 
 | 
 | ||||||
|  | import net.fabricmc.loom.api.InterfaceInjectionExtensionAPI; | ||||||
| import net.fabricmc.loom.api.LoomGradleExtensionAPI; | import net.fabricmc.loom.api.LoomGradleExtensionAPI; | ||||||
| import net.fabricmc.loom.api.MixinExtensionAPI; | import net.fabricmc.loom.api.MixinExtensionAPI; | ||||||
| import net.fabricmc.loom.api.decompilers.DecompilerOptions; | import net.fabricmc.loom.api.decompilers.DecompilerOptions; | ||||||
|  | @ -62,9 +65,9 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | ||||||
| 	protected final Property<Boolean> setupRemappedVariants; | 	protected final Property<Boolean> setupRemappedVariants; | ||||||
| 	protected final Property<Boolean> transitiveAccessWideners; | 	protected final Property<Boolean> transitiveAccessWideners; | ||||||
| 	protected final Property<String> intermediary; | 	protected final Property<String> intermediary; | ||||||
| 	protected final Property<Boolean> enableInterfaceInjection; |  | ||||||
| 	private final Property<Boolean> runtimeOnlyLog4j; | 	private final Property<Boolean> runtimeOnlyLog4j; | ||||||
| 	private final Property<MinecraftJarConfiguration> minecraftJarConfiguration; | 	private final Property<MinecraftJarConfiguration> minecraftJarConfiguration; | ||||||
|  | 	private final InterfaceInjectionExtensionAPI interfaceInjectionExtension; | ||||||
| 
 | 
 | ||||||
| 	private final ModVersionParser versionParser; | 	private final ModVersionParser versionParser; | ||||||
| 
 | 
 | ||||||
|  | @ -88,9 +91,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | ||||||
| 		this.transitiveAccessWideners.finalizeValueOnRead(); | 		this.transitiveAccessWideners.finalizeValueOnRead(); | ||||||
| 		this.intermediary = project.getObjects().property(String.class) | 		this.intermediary = project.getObjects().property(String.class) | ||||||
| 				.convention("https://maven.fabricmc.net/net/fabricmc/intermediary/%1$s/intermediary-%1$s-v2.jar"); | 				.convention("https://maven.fabricmc.net/net/fabricmc/intermediary/%1$s/intermediary-%1$s-v2.jar"); | ||||||
| 		this.enableInterfaceInjection = project.getObjects().property(Boolean.class) |  | ||||||
| 				.convention(true); |  | ||||||
| 		this.enableInterfaceInjection.finalizeValueOnRead(); |  | ||||||
| 
 | 
 | ||||||
| 		this.versionParser = new ModVersionParser(project); | 		this.versionParser = new ModVersionParser(project); | ||||||
| 
 | 
 | ||||||
|  | @ -108,6 +108,18 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | ||||||
| 
 | 
 | ||||||
| 		this.runtimeOnlyLog4j = project.getObjects().property(Boolean.class).convention(false); | 		this.runtimeOnlyLog4j = project.getObjects().property(Boolean.class).convention(false); | ||||||
| 		this.runtimeOnlyLog4j.finalizeValueOnRead(); | 		this.runtimeOnlyLog4j.finalizeValueOnRead(); | ||||||
|  | 
 | ||||||
|  | 		this.interfaceInjectionExtension = project.getObjects().newInstance(InterfaceInjectionExtensionAPI.class); | ||||||
|  | 
 | ||||||
|  | 		// Add main source set by default | ||||||
|  | 		interfaceInjection(interfaceInjection -> { | ||||||
|  | 			final JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); | ||||||
|  | 			final SourceSet main = javaPluginExtension.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); | ||||||
|  | 			interfaceInjection.getInterfaceInjectionSourceSets().add(main); | ||||||
|  | 
 | ||||||
|  | 			interfaceInjection.getInterfaceInjectionSourceSets().finalizeValueOnRead(); | ||||||
|  | 			interfaceInjection.getEnableDependencyInterfaceInjection().convention(true).finalizeValueOnRead(); | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
|  | @ -202,11 +214,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | ||||||
| 		return intermediary; | 		return intermediary; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override |  | ||||||
| 	public Property<Boolean> getEnableInterfaceInjection() { |  | ||||||
| 		return enableInterfaceInjection; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	@Override | 	@Override | ||||||
| 	public void disableDeprecatedPomGeneration(MavenPublication publication) { | 	public void disableDeprecatedPomGeneration(MavenPublication publication) { | ||||||
| 		net.fabricmc.loom.configuration.MavenPublication.excludePublication(publication); | 		net.fabricmc.loom.configuration.MavenPublication.excludePublication(publication); | ||||||
|  | @ -222,6 +229,11 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | ||||||
| 		return runtimeOnlyLog4j; | 		return runtimeOnlyLog4j; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public InterfaceInjectionExtensionAPI getInterfaceInjection() { | ||||||
|  | 		return interfaceInjectionExtension; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// This is here to ensure that LoomGradleExtensionApiImpl compiles without any unimplemented methods | 	// This is here to ensure that LoomGradleExtensionApiImpl compiles without any unimplemented methods | ||||||
| 	private final class EnsureCompile extends LoomGradleExtensionApiImpl { | 	private final class EnsureCompile extends LoomGradleExtensionApiImpl { | ||||||
| 		private EnsureCompile() { | 		private EnsureCompile() { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| /* | /* | ||||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). |  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||||
|  * |  * | ||||||
|  * Copyright (c) 2016-2021 FabricMC |  * Copyright (c) 2016-2022 FabricMC | ||||||
|  * |  * | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  * of this software and associated documentation files (the "Software"), to deal | ||||||
|  | @ -26,12 +26,16 @@ package net.fabricmc.loom.task; | ||||||
| 
 | 
 | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  | import java.io.Reader; | ||||||
| import java.io.UncheckedIOException; | import java.io.UncheckedIOException; | ||||||
|  | import java.io.Writer; | ||||||
| import java.lang.reflect.Constructor; | import java.lang.reflect.Constructor; | ||||||
| import java.lang.reflect.InvocationTargetException; | import java.lang.reflect.InvocationTargetException; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
| import java.nio.file.Files; | import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
| import java.nio.file.StandardCopyOption; | import java.nio.file.StandardCopyOption; | ||||||
|  | import java.util.ArrayList; | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
|  | @ -57,8 +61,9 @@ import org.jetbrains.annotations.Nullable; | ||||||
| import net.fabricmc.loom.api.decompilers.DecompilationMetadata; | import net.fabricmc.loom.api.decompilers.DecompilationMetadata; | ||||||
| import net.fabricmc.loom.api.decompilers.DecompilerOptions; | import net.fabricmc.loom.api.decompilers.DecompilerOptions; | ||||||
| import net.fabricmc.loom.api.decompilers.LoomDecompiler; | import net.fabricmc.loom.api.decompilers.LoomDecompiler; | ||||||
| import net.fabricmc.loom.configuration.accesswidener.AccessWidenerFile; | import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; | ||||||
| import net.fabricmc.loom.configuration.accesswidener.TransitiveAccessWidenerMappingsProcessor; | import net.fabricmc.loom.configuration.accesswidener.TransitiveAccessWidenerMappingsProcessor; | ||||||
|  | import net.fabricmc.loom.configuration.ifaceinject.InterfaceInjectionProcessor; | ||||||
| import net.fabricmc.loom.decompilers.LineNumberRemapper; | import net.fabricmc.loom.decompilers.LineNumberRemapper; | ||||||
| import net.fabricmc.loom.util.Constants; | import net.fabricmc.loom.util.Constants; | ||||||
| import net.fabricmc.loom.util.FileSystemUtil; | import net.fabricmc.loom.util.FileSystemUtil; | ||||||
|  | @ -69,6 +74,10 @@ import net.fabricmc.loom.util.gradle.ThreadedSimpleProgressLogger; | ||||||
| import net.fabricmc.loom.util.gradle.WorkerDaemonClientsManagerHelper; | import net.fabricmc.loom.util.gradle.WorkerDaemonClientsManagerHelper; | ||||||
| import net.fabricmc.loom.util.ipc.IPCClient; | import net.fabricmc.loom.util.ipc.IPCClient; | ||||||
| import net.fabricmc.loom.util.ipc.IPCServer; | import net.fabricmc.loom.util.ipc.IPCServer; | ||||||
|  | import net.fabricmc.mappingio.MappingReader; | ||||||
|  | import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; | ||||||
|  | import net.fabricmc.mappingio.format.Tiny2Writer; | ||||||
|  | import net.fabricmc.mappingio.tree.MemoryMappingTree; | ||||||
| 
 | 
 | ||||||
| public abstract class GenerateSourcesTask extends AbstractLoomTask { | public abstract class GenerateSourcesTask extends AbstractLoomTask { | ||||||
| 	private final DecompilerOptions decompilerOptions; | 	private final DecompilerOptions decompilerOptions; | ||||||
|  | @ -300,16 +309,43 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private Path getMappings() { | 	private Path getMappings() { | ||||||
| 		Path baseMappings = getExtension().getMappingsProvider().tinyMappings; | 		Path inputMappings = getExtension().getMappingsProvider().tinyMappings; | ||||||
| 
 | 
 | ||||||
| 		if (getExtension().getEnableTransitiveAccessWideners().get()) { | 		MemoryMappingTree mappingTree = new MemoryMappingTree(); | ||||||
| 			List<AccessWidenerFile> accessWideners = getExtension().getTransitiveAccessWideners(); |  | ||||||
| 
 | 
 | ||||||
| 			if (accessWideners.isEmpty()) { | 		try (Reader reader = Files.newBufferedReader(inputMappings, StandardCharsets.UTF_8)) { | ||||||
| 				return baseMappings; | 			MappingReader.read(reader, new MappingSourceNsSwitch(mappingTree, MappingsNamespace.INTERMEDIARY.toString())); | ||||||
|  | 		} catch (IOException e) { | ||||||
|  | 			throw new RuntimeException("Failed to read mappings", e); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 			Path outputMappings; | 		final List<MappingsProcessor> mappingsProcessors = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 		if (getExtension().getEnableTransitiveAccessWideners().get()) { | ||||||
|  | 			mappingsProcessors.add(new TransitiveAccessWidenerMappingsProcessor(getProject())); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (getExtension().getInterfaceInjection().isEnabled()) { | ||||||
|  | 			mappingsProcessors.add(new InterfaceInjectionProcessor(getProject())); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (mappingsProcessors.isEmpty()) { | ||||||
|  | 			return inputMappings; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		boolean transformed = false; | ||||||
|  | 
 | ||||||
|  | 		for (MappingsProcessor mappingsProcessor : mappingsProcessors) { | ||||||
|  | 			if (mappingsProcessor.transform(mappingTree)) { | ||||||
|  | 				transformed = true; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (!transformed) { | ||||||
|  | 			return inputMappings; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		final Path outputMappings; | ||||||
| 
 | 
 | ||||||
| 		try { | 		try { | ||||||
| 			outputMappings = Files.createTempFile("loom-transitive-mappings", ".tiny"); | 			outputMappings = Files.createTempFile("loom-transitive-mappings", ".tiny"); | ||||||
|  | @ -317,12 +353,18 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { | ||||||
| 			throw new RuntimeException("Failed to create temp file", e); | 			throw new RuntimeException("Failed to create temp file", e); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 			TransitiveAccessWidenerMappingsProcessor.process(baseMappings, outputMappings, accessWideners, getProject().getLogger()); | 		try (Writer writer = Files.newBufferedWriter(outputMappings, StandardCharsets.UTF_8)) { | ||||||
|  | 			Tiny2Writer tiny2Writer = new Tiny2Writer(writer, false); | ||||||
|  | 			mappingTree.accept(new MappingSourceNsSwitch(tiny2Writer, MappingsNamespace.NAMED.toString())); | ||||||
|  | 		} catch (IOException e) { | ||||||
|  | 			throw new RuntimeException("Failed to write mappings", e); | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		return outputMappings; | 		return outputMappings; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		return baseMappings; | 	public interface MappingsProcessor { | ||||||
|  | 		boolean transform(MemoryMappingTree mappings); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private static Constructor<LoomDecompiler> getDecompilerConstructor(String clazz) { | 	private static Constructor<LoomDecompiler> getDecompilerConstructor(String clazz) { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ | ||||||
|   "version": "1", |   "version": "1", | ||||||
|   "name": "Dummy Mod", |   "name": "Dummy Mod", | ||||||
|   "custom": { |   "custom": { | ||||||
|     "fabric-api:module-lifecycle": "stable", |  | ||||||
|     "loom:injected_interfaces": { |     "loom:injected_interfaces": { | ||||||
|       "net/minecraft/class_2248": ["InjectedInterface"] |       "net/minecraft/class_2248": ["InjectedInterface"] | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -6,5 +6,6 @@ public class ExampleMod implements ModInitializer { | ||||||
| 	@Override | 	@Override | ||||||
| 	public void onInitialize() { | 	public void onInitialize() { | ||||||
| 		Blocks.AIR.newMethodThatDidNotExist(); | 		Blocks.AIR.newMethodThatDidNotExist(); | ||||||
|  | 		Blocks.AIR.anotherNewMethodThatDidNotExist(); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | 
 | ||||||
|  | public interface OwnInjectedInterface { | ||||||
|  | 	default void anotherNewMethodThatDidNotExist() { | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | { | ||||||
|  |   "schemaVersion": 1, | ||||||
|  |   "id": "owndummy", | ||||||
|  |   "version": "1", | ||||||
|  |   "name": "Own Dummy Mod", | ||||||
|  |   "custom": { | ||||||
|  |     "loom:injected_interfaces": { | ||||||
|  |       "net/minecraft/class_2248": ["OwnInjectedInterface"] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in a new issue