Interface Injection (#496)
* Added interface injection via fabric.mod.json. * Added interface injection * Added amending of class signature with injected interface.
This commit is contained in:
		
							parent
							
								
									03d3950d11
								
							
						
					
					
						commit
						ccfe12eb17
					
				
					 9 changed files with 422 additions and 0 deletions
				
			
		|  | @ -132,4 +132,13 @@ public interface LoomGradleExtensionAPI { | |||
| 	 * @return the intermediary url template | ||||
| 	 */ | ||||
| 	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(); | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,299 @@ | |||
| /* | ||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||
|  * | ||||
|  * Copyright (c) 2021 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.configuration.ifaceinject; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.nio.file.Path; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.LinkedHashSet; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| import com.google.common.hash.Hasher; | ||||
| import com.google.common.hash.Hashing; | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.JsonArray; | ||||
| import com.google.gson.JsonElement; | ||||
| import com.google.gson.JsonObject; | ||||
| import org.gradle.api.Project; | ||||
| import org.objectweb.asm.ClassReader; | ||||
| import org.objectweb.asm.ClassVisitor; | ||||
| import org.objectweb.asm.ClassWriter; | ||||
| import org.objectweb.asm.commons.Remapper; | ||||
| 
 | ||||
| import net.fabricmc.loom.LoomGradleExtension; | ||||
| import net.fabricmc.loom.configuration.RemappedConfigurationEntry; | ||||
| import net.fabricmc.loom.configuration.processors.JarProcessor; | ||||
| import net.fabricmc.loom.util.Constants; | ||||
| import net.fabricmc.loom.util.Pair; | ||||
| import net.fabricmc.loom.util.TinyRemapperHelper; | ||||
| import net.fabricmc.loom.util.ZipUtils; | ||||
| import net.fabricmc.tinyremapper.TinyRemapper; | ||||
| 
 | ||||
| public class InterfaceInjectionProcessor implements JarProcessor { | ||||
| 	// Filename used to store hash of injected interfaces in processed jar file | ||||
| 	private static final String HASH_FILENAME = "injected_interfaces.sha256"; | ||||
| 
 | ||||
| 	private final Map<String, List<InjectedInterface>> injectedInterfaces; | ||||
| 	private final Project project; | ||||
| 	private final LoomGradleExtension extension; | ||||
| 	private final byte[] inputHash; | ||||
| 	private Map<String, List<InjectedInterface>> remappedInjectedInterfaces; | ||||
| 
 | ||||
| 	public InterfaceInjectionProcessor(Project project) { | ||||
| 		this.project = project; | ||||
| 		this.extension = LoomGradleExtension.get(project); | ||||
| 		this.injectedInterfaces = getInjectedInterfaces().stream() | ||||
| 				.collect(Collectors.groupingBy(InjectedInterface::className)); | ||||
| 
 | ||||
| 		this.inputHash = hashInjectedInterfaces(); | ||||
| 	} | ||||
| 
 | ||||
| 	public boolean isEmpty() { | ||||
| 		return injectedInterfaces.isEmpty(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void setup() { | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void process(File jarFile) { | ||||
| 		// Lazily remap from intermediary->named | ||||
| 		if (remappedInjectedInterfaces == null) { | ||||
| 			TinyRemapper tinyRemapper = createTinyRemapper(); | ||||
| 			Remapper remapper = tinyRemapper.getEnvironment().getRemapper(); | ||||
| 
 | ||||
| 			try { | ||||
| 				remappedInjectedInterfaces = new HashMap<>(injectedInterfaces.size()); | ||||
| 
 | ||||
| 				for (Map.Entry<String, List<InjectedInterface>> entry : injectedInterfaces.entrySet()) { | ||||
| 					String namedClassName = remapper.map(entry.getKey()); | ||||
| 					remappedInjectedInterfaces.put( | ||||
| 							namedClassName, | ||||
| 							entry.getValue().stream() | ||||
| 									.map(injectedInterface -> | ||||
| 											new InjectedInterface( | ||||
| 													injectedInterface.modId(), | ||||
| 													namedClassName, | ||||
| 													remapper.map(injectedInterface.ifaceName()) | ||||
| 											)) | ||||
| 									.toList() | ||||
| 					); | ||||
| 				} | ||||
| 			} finally { | ||||
| 				tinyRemapper.finish(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		project.getLogger().lifecycle("Processing file: " + jarFile.getName()); | ||||
| 
 | ||||
| 		try { | ||||
| 			ZipUtils.transform(jarFile.toPath(), getTransformers()); | ||||
| 		} catch (IOException e) { | ||||
| 			throw new RuntimeException("Failed to apply interface injections to " + jarFile, e); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private List<Pair<String, ZipUtils.UnsafeUnaryOperator<byte[]>>> getTransformers() { | ||||
| 		return remappedInjectedInterfaces.keySet().stream() | ||||
| 				.map(string -> new Pair<>(string.replaceAll("\\.", "/") + ".class", getTransformer(string))) | ||||
| 				.collect(Collectors.toList()); | ||||
| 	} | ||||
| 
 | ||||
| 	private ZipUtils.UnsafeUnaryOperator<byte[]> getTransformer(String className) { | ||||
| 		return input -> { | ||||
| 			ClassReader reader = new ClassReader(input); | ||||
| 			ClassWriter writer = new ClassWriter(0); | ||||
| 			List<InjectedInterface> ifaces = remappedInjectedInterfaces.get(className); | ||||
| 			ClassVisitor classVisitor = new InjectingClassVisitor(Constants.ASM_VERSION, writer, ifaces); | ||||
| 
 | ||||
| 			// Log which mods add which interface to the class | ||||
| 			project.getLogger().info("Injecting interfaces into " + className + ": " | ||||
| 					+ ifaces.stream().map(i -> i.ifaceName() + " [" + i.modId() + "]" | ||||
| 			).collect(Collectors.joining(", "))); | ||||
| 
 | ||||
| 			reader.accept(classVisitor, 0); | ||||
| 			return writer.toByteArray(); | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public boolean isInvalid(File file) { | ||||
| 		byte[] hash; | ||||
| 
 | ||||
| 		try { | ||||
| 			hash = ZipUtils.unpackNullable(file.toPath(), HASH_FILENAME); | ||||
| 		} catch (IOException e) { | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		if (hash == null) { | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		return !Arrays.equals(inputHash, hash); | ||||
| 	} | ||||
| 
 | ||||
| 	private List<InjectedInterface> getInjectedInterfaces() { | ||||
| 		List<InjectedInterface> result = new ArrayList<>(); | ||||
| 
 | ||||
| 		for (RemappedConfigurationEntry entry : Constants.MOD_COMPILE_ENTRIES) { | ||||
| 			// Only apply injected interfaces from mods that are part of the compile classpath | ||||
| 			if (!entry.compileClasspath()) { | ||||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			Set<File> artifacts = extension.getLazyConfigurationProvider(entry.sourceConfiguration()) | ||||
| 					.get() | ||||
| 					.resolve(); | ||||
| 
 | ||||
| 			for (File artifact : artifacts) { | ||||
| 				result.addAll(InjectedInterface.fromModJar(artifact.toPath())); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return result; | ||||
| 	} | ||||
| 
 | ||||
| 	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. | ||||
| 		 */ | ||||
| 		public static List<InjectedInterface> fromModJar(Path modJarPath) { | ||||
| 			byte[] modJsonBytes; | ||||
| 
 | ||||
| 			try { | ||||
| 				modJsonBytes = ZipUtils.unpackNullable(modJarPath, "fabric.mod.json"); | ||||
| 			} catch (IOException e) { | ||||
| 				throw new RuntimeException("Failed to extract fabric.mod.json from " + modJarPath); | ||||
| 			} | ||||
| 
 | ||||
| 			if (modJsonBytes == null) { | ||||
| 				return Collections.emptyList(); | ||||
| 			} | ||||
| 
 | ||||
| 			JsonObject jsonObject = new Gson().fromJson(new String(modJsonBytes, StandardCharsets.UTF_8), JsonObject.class); | ||||
| 
 | ||||
| 			String modId = jsonObject.get("id").getAsString(); | ||||
| 
 | ||||
| 			if (!jsonObject.has("custom")) { | ||||
| 				return Collections.emptyList(); | ||||
| 			} | ||||
| 
 | ||||
| 			JsonObject custom = jsonObject.getAsJsonObject("custom"); | ||||
| 
 | ||||
| 			if (!custom.has("loom:injected_interfaces")) { | ||||
| 				return Collections.emptyList(); | ||||
| 			} | ||||
| 
 | ||||
| 			JsonObject addedIfaces = custom.getAsJsonObject("loom:injected_interfaces"); | ||||
| 
 | ||||
| 			List<InjectedInterface> result = new ArrayList<>(); | ||||
| 
 | ||||
| 			for (String className : addedIfaces.keySet()) { | ||||
| 				JsonArray ifaceNames = addedIfaces.getAsJsonArray(className); | ||||
| 
 | ||||
| 				for (JsonElement ifaceName : ifaceNames) { | ||||
| 					result.add(new InjectedInterface(modId, className, ifaceName.getAsString())); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return result; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private static class InjectingClassVisitor extends ClassVisitor { | ||||
| 		private final List<InjectedInterface> injectedInterfaces; | ||||
| 
 | ||||
| 		InjectingClassVisitor(int asmVersion, ClassWriter writer, List<InjectedInterface> injectedInterfaces) { | ||||
| 			super(asmVersion, writer); | ||||
| 			this.injectedInterfaces = injectedInterfaces; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { | ||||
| 			Set<String> modifiedInterfaces = new LinkedHashSet<>(interfaces.length + injectedInterfaces.size()); | ||||
| 			Collections.addAll(modifiedInterfaces, interfaces); | ||||
| 
 | ||||
| 			for (InjectedInterface injectedInterface : injectedInterfaces) { | ||||
| 				modifiedInterfaces.add(injectedInterface.ifaceName()); | ||||
| 			} | ||||
| 
 | ||||
| 			// See JVMS: https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-ClassSignature | ||||
| 			if (signature != null) { | ||||
| 				var resultingSignature = new StringBuilder(signature); | ||||
| 
 | ||||
| 				for (InjectedInterface injectedInterface : injectedInterfaces) { | ||||
| 					String superinterfaceSignature = "L" + injectedInterface.ifaceName() + ";"; | ||||
| 
 | ||||
| 					if (resultingSignature.indexOf(superinterfaceSignature) == -1) { | ||||
| 						resultingSignature.append(superinterfaceSignature); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				signature = resultingSignature.toString(); | ||||
| 			} | ||||
| 
 | ||||
| 			super.visit(version, access, name, signature, superName, modifiedInterfaces.toArray(new String[0])); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private TinyRemapper createTinyRemapper() { | ||||
| 		try { | ||||
| 			TinyRemapper tinyRemapper = TinyRemapperHelper.getTinyRemapper(project, "intermediary", "named"); | ||||
| 			tinyRemapper.readClassPath(TinyRemapperHelper.getMinecraftDependencies(project)); | ||||
| 			tinyRemapper.readClassPath(extension.getMinecraftMappedProvider().getIntermediaryJar().toPath()); | ||||
| 
 | ||||
| 			return tinyRemapper; | ||||
| 		} catch (IOException e) { | ||||
| 			throw new RuntimeException("Failed to create tiny remapper for intermediary->named", e); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private byte[] hashInjectedInterfaces() { | ||||
| 		// Hash the interfaces we're about to inject to not have to repeat this everytime | ||||
| 		Hasher hasher = Hashing.sha256().newHasher(); | ||||
| 
 | ||||
| 		for (Map.Entry<String, List<InjectedInterface>> entry : injectedInterfaces.entrySet()) { | ||||
| 			hasher.putString("class:", StandardCharsets.UTF_8); | ||||
| 			hasher.putString(entry.getKey(), StandardCharsets.UTF_8); | ||||
| 
 | ||||
| 			for (InjectedInterface ifaceName : entry.getValue()) { | ||||
| 				hasher.putString("iface:", StandardCharsets.UTF_8); | ||||
| 				hasher.putString(ifaceName.ifaceName(), StandardCharsets.UTF_8); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return hasher.hash().asBytes(); | ||||
| 	} | ||||
| } | ||||
|  | @ -56,6 +56,7 @@ import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; | |||
| import net.fabricmc.loom.configuration.DependencyProvider; | ||||
| import net.fabricmc.loom.configuration.accesswidener.AccessWidenerJarProcessor; | ||||
| import net.fabricmc.loom.configuration.accesswidener.TransitiveAccessWidenerJarProcessor; | ||||
| import net.fabricmc.loom.configuration.ifaceinject.InterfaceInjectionProcessor; | ||||
| import net.fabricmc.loom.configuration.processors.JarProcessorManager; | ||||
| import net.fabricmc.loom.configuration.processors.MinecraftProcessedProvider; | ||||
| import net.fabricmc.loom.configuration.providers.MinecraftProviderImpl; | ||||
|  | @ -159,6 +160,14 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (extension.getEnableInterfaceInjection().get()) { | ||||
| 			InterfaceInjectionProcessor jarProcessor = new InterfaceInjectionProcessor(getProject()); | ||||
| 
 | ||||
| 			if (!jarProcessor.isEmpty()) { | ||||
| 				extension.getGameJarProcessors().add(jarProcessor); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		extension.getAccessWidenerPath().finalizeValue(); | ||||
| 		extension.getGameJarProcessors().finalizeValue(); | ||||
| 		JarProcessorManager processorManager = new JarProcessorManager(extension.getGameJarProcessors().get()); | ||||
|  |  | |||
|  | @ -63,6 +63,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | |||
| 	protected final Property<Boolean> setupRemappedVariants; | ||||
| 	protected final Property<Boolean> transitiveAccessWideners; | ||||
| 	protected final Property<String> intermediary; | ||||
| 	protected final Property<Boolean> enableInterfaceInjection; | ||||
| 
 | ||||
| 	private final ModVersionParser versionParser; | ||||
| 
 | ||||
|  | @ -88,6 +89,9 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | |||
| 		this.transitiveAccessWideners.finalizeValueOnRead(); | ||||
| 		this.intermediary = project.getObjects().property(String.class) | ||||
| 				.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); | ||||
| 
 | ||||
|  | @ -181,6 +185,11 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA | |||
| 		return intermediary; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public Property<Boolean> getEnableInterfaceInjection() { | ||||
| 		return enableInterfaceInjection; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void disableDeprecatedPomGeneration(MavenPublication publication) { | ||||
| 		net.fabricmc.loom.configuration.MavenPublication.excludePublication(publication); | ||||
|  |  | |||
|  | @ -0,0 +1,51 @@ | |||
| /* | ||||
|  * This file is part of fabric-loom, licensed under the MIT License (MIT). | ||||
|  * | ||||
|  * Copyright (c) 2016-2021 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.test.integration | ||||
| 
 | ||||
| import net.fabricmc.loom.test.util.GradleProjectTestTrait | ||||
| import net.fabricmc.loom.util.ZipUtils | ||||
| import spock.lang.Specification | ||||
| import spock.lang.Unroll | ||||
| 
 | ||||
| import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS | ||||
| import static org.gradle.testkit.runner.TaskOutcome.SUCCESS | ||||
| 
 | ||||
| class InterfaceInjectionTest extends Specification implements GradleProjectTestTrait { | ||||
| 	@Unroll | ||||
| 	def "interface injection (gradle #version)"() { | ||||
| 		setup: | ||||
| 			def gradle = gradleProject(project: "interfaceInjection", version: version) | ||||
| 			ZipUtils.pack(new File(gradle.projectDir, "dummyDependency").toPath(), new File(gradle.projectDir, "dummy.jar").toPath()) | ||||
| 
 | ||||
| 		when: | ||||
| 			def result = gradle.run(task: "build") | ||||
| 
 | ||||
| 		then: | ||||
| 			result.task(":build").outcome == SUCCESS | ||||
| 
 | ||||
| 		where: | ||||
| 			version << STANDARD_TEST_VERSIONS | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/test/resources/projects/interfaceInjection/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/test/resources/projects/interfaceInjection/build.gradle
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| // This is used by a range of tests that append to this file before running the gradle tasks. | ||||
| // Can be used for tests that require minimal custom setup | ||||
| plugins { | ||||
| 	id 'fabric-loom' | ||||
| 	id 'maven-publish' | ||||
| } | ||||
| 
 | ||||
| archivesBaseName = "fabric-example-mod" | ||||
| version = "1.0.0" | ||||
| group = "com.example" | ||||
| 
 | ||||
| dependencies { | ||||
| 	minecraft "com.mojang:minecraft:1.17.1" | ||||
| 	mappings "net.fabricmc:yarn:1.17.1+build.59:v2" | ||||
| 	modImplementation "net.fabricmc:fabric-loader:0.11.6" | ||||
| 
 | ||||
| 	modImplementation files("dummy.jar") | ||||
| } | ||||
|  | @ -0,0 +1,12 @@ | |||
| { | ||||
|   "schemaVersion": 1, | ||||
|   "id": "dummy", | ||||
|   "version": "1", | ||||
|   "name": "Dummy Mod", | ||||
|   "custom": { | ||||
|     "fabric-api:module-lifecycle": "stable", | ||||
|     "loom:injected_interfaces": { | ||||
|       "net/minecraft/class_2248": ["InjectedInterface"] | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,10 @@ | |||
| import net.minecraft.block.Blocks; | ||||
| 
 | ||||
| import net.fabricmc.api.ModInitializer; | ||||
| 
 | ||||
| public class ExampleMod implements ModInitializer { | ||||
| 	@Override | ||||
| 	public void onInitialize() { | ||||
| 		Blocks.AIR.newMethodThatDidNotExist(); | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| 
 | ||||
| public interface InjectedInterface { | ||||
| 	default void newMethodThatDidNotExist() { | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in a new issue