Interface Injection (#496)
* Added interface injection via fabric.mod.json. * Added interface injection * Added amending of class signature with injected interface.dev/0.11
parent
03d3950d11
commit
ccfe12eb17
|
@ -132,4 +132,13 @@ public interface LoomGradleExtensionAPI {
|
||||||
* @return the intermediary url template
|
* @return the intermediary url template
|
||||||
*/
|
*/
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.DependencyProvider;
|
||||||
import net.fabricmc.loom.configuration.accesswidener.AccessWidenerJarProcessor;
|
import net.fabricmc.loom.configuration.accesswidener.AccessWidenerJarProcessor;
|
||||||
import net.fabricmc.loom.configuration.accesswidener.TransitiveAccessWidenerJarProcessor;
|
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.JarProcessorManager;
|
||||||
import net.fabricmc.loom.configuration.processors.MinecraftProcessedProvider;
|
import net.fabricmc.loom.configuration.processors.MinecraftProcessedProvider;
|
||||||
import net.fabricmc.loom.configuration.providers.MinecraftProviderImpl;
|
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.getAccessWidenerPath().finalizeValue();
|
||||||
extension.getGameJarProcessors().finalizeValue();
|
extension.getGameJarProcessors().finalizeValue();
|
||||||
JarProcessorManager processorManager = new JarProcessorManager(extension.getGameJarProcessors().get());
|
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> 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 ModVersionParser versionParser;
|
private final ModVersionParser versionParser;
|
||||||
|
|
||||||
|
@ -88,6 +89,9 @@ 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);
|
||||||
|
|
||||||
|
@ -181,6 +185,11 @@ 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);
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 New Issue