version = getSubprojectVersion(project) loom { accessWidenerPath = file('src/main/resources/fabric-transitive-access-wideners-v1.accesswidener') } testDependencies(project, [ ':fabric-rendering-v1', ':fabric-object-builder-api-v1' ]) import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.ClassNode import java.nio.file.FileSystem import java.nio.file.FileSystems import java.nio.file.Files import java.nio.file.Path tasks.register('generateAccessWidener') { doLast { List lines = new ArrayList<>(); lines.add("accessWidener v2 named") lines.add("") lines.add("# DO NOT EDIT BY HAND! This file is generated automatically.") lines.add("# Edit \"template.accesswidener\" instead then run \"gradlew generateAccessWidener\".") lines.add("") lines.addAll(file("template.accesswidener").text.lines().toList()) Path commonJar = loom.namedMinecraftProvider.parentMinecraftProvider.commonJar.path FileSystems.newFileSystem(URI.create("jar:${commonJar.toUri()}"), [create: false]).withCloseable { fs -> generateBlockConstructors(lines, fs) lines.add("") generateItemConstructors(lines, fs) lines.add("") generateCreatorEntityTrackedDataMethods(lines, fs) lines.add("") } Path clientJar = loom.namedMinecraftProvider.parentMinecraftProvider.clientOnlyJar.path FileSystems.newFileSystem(URI.create("jar:${clientJar.toUri()}"), [create: false]).withCloseable { fs -> generateRenderPhaseFields(lines, fs) lines.add("") generateRenderPhaseInnerClasses(lines, fs) } file('src/main/resources/fabric-transitive-access-wideners-v1.accesswidener').text = String.join('\n', lines) + '\n' validateAccessWidener(lines) } } def generateBlockConstructors(List lines, FileSystem fs) { lines.add("# Constructors of non-abstract block classes") Files.list(fs.getPath("net/minecraft/block")) .filter { Files.isRegularFile(it) && it.toString().endsWith(".class") } .map { loadClass(it) } .sorted(Comparator.comparing { it.name }) .filter { (it.access & Opcodes.ACC_ABSTRACT) == 0 } .forEach { node -> for (def method : node.methods) { // Checklist for finding block constructors as of 1.19.3: // - class directly in net.minecraft.block (excluding subpackages) // - method name == (by definition) // - contains an AbstractBlock$Settings parameter // - only taking into account non-abstract classes and non-public constructors // Block constructor... if (method.name == "" && Type.getArgumentTypes(method.desc).any { it.internalName == 'net/minecraft/block/AbstractBlock$Settings' }) { // ...and non-public if ((method.access & Opcodes.ACC_PUBLIC) == 0) { lines.add("transitive-accessible method $node.name $method.desc") } } } } } def generateItemConstructors(List lines, FileSystem fs) { lines.add("# Constructors of non-abstract item classes") Files.list(fs.getPath("net/minecraft/item")) .filter { Files.isRegularFile(it) && it.toString().endsWith(".class") } .map { loadClass(it) } .sorted(Comparator.comparing { it.name }) .filter { (it.access & Opcodes.ACC_ABSTRACT) == 0 } .forEach { node -> for (def method : node.methods) { // Checklist for finding item constructors as of 1.19.3: // - class directly in net.minecraft.item (excluding subpackages) // - method name == (by definition) // - contains an Item$Settings parameter // - only taking into account non-abstract classes and non-public constructors // Item constructor... if (method.name == "" && Type.getArgumentTypes(method.desc).any { it.internalName == 'net/minecraft/item/Item$Settings' }) { // ...and non-public if ((method.access & Opcodes.ACC_PUBLIC) == 0) { lines.add("transitive-accessible method $node.name $method.desc") } } } } } def generateRenderPhaseFields(List lines, FileSystem fs) { lines.add("# Protected static fields of RenderPhase") for (def field : loadClass(fs.getPath("net/minecraft/client/render/RenderPhase.class")).fields) { // All protected static fields of RenderPhase if ((field.access & Opcodes.ACC_PROTECTED) != 0 && (field.access & Opcodes.ACC_STATIC) != 0) { lines.add("transitive-accessible field net/minecraft/client/render/RenderPhase ${field.name} ${field.desc}") } } } def generateRenderPhaseInnerClasses(List lines, FileSystem fs) { lines.add("# Protected static inner classes of RenderPhase") for (def innerClass : loadClass(fs.getPath("net/minecraft/client/render/RenderPhase.class")).innerClasses) { // All protected static inner classes of RenderPhase if ((innerClass.access & Opcodes.ACC_PROTECTED) != 0 && (innerClass.access & Opcodes.ACC_STATIC) != 0) { lines.add("transitive-accessible class net/minecraft/client/render/RenderPhase\$${innerClass.innerName}") } } } def generateTrackedDataFields(String className, List lines, FileSystem fs, String... extraMethods) { // using a set to prevent duplicates from multiple dataTracker references in a single method // linked to preserve order and improve generated access widener readability Set collectedWideners = new LinkedHashSet<>() loadClass(fs.getPath("${className}.class")).accept( new ClassVisitor(Opcodes.ASM9) { @Override MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { boolean isExtra = extraMethods.contains(name) if (!isExtra) { // check for desired methods if (!name.startsWith("get") && !name.startsWith("set")) { return null } // check methods as genuine basic getter/setters int parameterCount = Type.getArgumentCount(descriptor) if (name.startsWith("get") && parameterCount != 0) { return null } if (name.startsWith("set") && parameterCount != 1) { return null } } else { println name } return new MethodVisitor(Opcodes.ASM9) { @Override void visitFieldInsn(int opcode, String owner, String fieldName, String fieldDescriptor) { // check references its dataTracker field if (isExtra || (fieldName == "dataTracker" && opcode == Opcodes.GETFIELD)) { collectedWideners.add("transitive-accessible method $className $name $descriptor") } } } } } ) lines.addAll(collectedWideners) } def generateCreatorEntityTrackedDataMethods(List lines, FileSystem fs) { lines.add("# Private tracked data related methods of DisplayEntity (plus its subclasses) and InteractionEntity") generateTrackedDataFields("net/minecraft/entity/decoration/DisplayEntity", lines, fs, "getTransformation") generateTrackedDataFields("net/minecraft/entity/decoration/DisplayEntity\$ItemDisplayEntity", lines, fs) generateTrackedDataFields("net/minecraft/entity/decoration/DisplayEntity\$BlockDisplayEntity", lines, fs) generateTrackedDataFields("net/minecraft/entity/decoration/DisplayEntity\$TextDisplayEntity", lines, fs) generateTrackedDataFields("net/minecraft/entity/decoration/InteractionEntity", lines, fs, "shouldRespond") } ClassNode loadClass(Path path) { def node = new ClassNode() Files.newInputStream(path).withCloseable { is -> new ClassReader(is).accept(node, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES) } return node } def validateAccessWidener(List lines) { List exceptions = new ArrayList<>() for (int i = 0; i < lines.size(); i++) { String line = lines.get(i) if (line.isBlank() || line.startsWith("#") || line.startsWith("transitive-") || line.startsWith("accessWidener")) continue exceptions.add(String.valueOf(i + 1)) } if (exceptions.size() > 0) { throw new InvalidUserDataException("\"fabric-transitive-access-wideners-v1.accesswidener\" contains non-transitive access modifiers at lines: [" + String.join(", ", exceptions) + "]") } } generateResources.dependsOn generateAccessWidener