import java.io.OutputStream import java.util.zip.ZipFile buildscript { repositories { mavenCentral() } dependencies { classpath("org.apache.xmlgraphics:batik-transcoder:1.17") classpath("org.apache.xmlgraphics:batik-codec:1.17") } } plugins { alias(libs.plugins.fabric.loom) `maven-publish` } base { archivesName = properties["archives_base_name"] as String version = libs.versions.mod.version.get() group = properties["maven_group"] as String } repositories { maven { name = "meteor-maven" url = uri("https://maven.meteordev.org/releases") } mavenCentral() } dependencies { minecraft(libs.minecraft) implementation(libs.fabric.loader) implementation(libs.fabric.api) implementation("net.java.dev.jna:jna:5.13.0") implementation("net.java.dev.jna:jna-platform:5.13.0") implementation("io.netty:netty-handler-proxy:4.1.118.Final") implementation("io.netty:netty-codec-socks:4.1.118.Final") include("io.netty:netty-handler-proxy:4.1.118.Final") include("io.netty:netty-codec-socks:4.1.118.Final") implementation("de.florianreuth:waybackauthlib:1.1.0") include("de.florianreuth:waybackauthlib:1.1.0") implementation("com.github.weisj:jsvg:2.1.0") include("com.github.weisj:jsvg:2.1.0") // Bundle a newer MixinExtras (jar-in-jar) so Fabric Loader loads it over the old 0.5.0 it ships // with. Required because ViaFabricPlus (and our own WrapOperation/ModifyExpressionValue mixins) // need MixinExtras >= 0.5.3; without this, joining a server crashes during mixin application. implementation("io.github.llamalad7:mixinextras-fabric:0.5.4") include("io.github.llamalad7:mixinextras-fabric:0.5.4") } val generatedAutismResourcesDir = layout.buildDirectory.dir("generated/resources/autism/main") data class SourceFile(val path: String, val text: String) data class FieldSpec(val name: String, val type: String, val kind: String, val editable: Boolean) data class PacketSpec( val className: String, val protocol: String, val direction: String, val codecStyle: String, val packetType: String, val source: String, val complete: Boolean, val fields: List ) sourceSets { main { resources.srcDir(generatedAutismResourcesDir) } } val generateVanillaUiAssets by tasks.registering { // Semantic feature icons used by the vanilla-friendly UI. Structural // actions such as close and reorder are rendered as text symbols. val iconSourceDir = file("assets/icons") val outputDir = generatedAutismResourcesDir.map { it.dir("assets/autismclient") } inputs.dir(iconSourceDir) outputs.dir(outputDir) doLast { val targetRoot = outputDir.get().asFile targetRoot.parentFile.resolve("yu" + "ng" + "light").deleteRecursively() targetRoot.deleteRecursively() val iconTargetDir = targetRoot.resolve("textures/gui/vanillaui/icons") iconTargetDir.mkdirs() val pngTranscoderClass = Class.forName("org.apache.batik.transcoder.image.PNGTranscoder") val transcoderInputClass = Class.forName("org.apache.batik.transcoder.TranscoderInput") val transcoderOutputClass = Class.forName("org.apache.batik.transcoder.TranscoderOutput") val transcodingHintsKeyClass = Class.forName("org.apache.batik.transcoder.TranscodingHints\$Key") val transcoder = pngTranscoderClass.getDeclaredConstructor().newInstance() val widthField = pngTranscoderClass.getField("KEY_WIDTH") val heightField = pngTranscoderClass.getField("KEY_HEIGHT") val addHint = pngTranscoderClass.getMethod("addTranscodingHint", transcodingHintsKeyClass, Any::class.java) addHint.invoke(transcoder, widthField.get(null), 256f) addHint.invoke(transcoder, heightField.get(null), 256f) val inputCtor = transcoderInputClass.getConstructor(String::class.java) val outputCtor = transcoderOutputClass.getConstructor(OutputStream::class.java) val transcode = pngTranscoderClass.getMethod("transcode", transcoderInputClass, transcoderOutputClass) iconSourceDir.listFiles { file -> file.isFile && file.extension.equals("svg", ignoreCase = true) } ?.sortedBy { it.name.lowercase() } ?.forEach { svg -> val outputFile = iconTargetDir.resolve(svg.nameWithoutExtension.lowercase() + ".png") outputFile.outputStream().use { out -> val input = inputCtor.newInstance(svg.toURI().toString()) val output = outputCtor.newInstance(out) transcode.invoke(transcoder, input, output) } } } } val generateAutismInspectorMappings by tasks.registering { val mappingFiles = fileTree(".gradle/loom-cache/source_mappings") { include("**/*.tiny") } val outputFile = generatedAutismResourcesDir.map { it.file("autism-inspector-mappings.tsv") } inputs.files(mappingFiles) outputs.file(outputFile) doLast { run { val target = outputFile.get().asFile target.parentFile.mkdirs() target.writeText( "# Official Mojang mappings are used for Minecraft 26.1.2; no Yarn aliases are generated.\n", Charsets.UTF_8 ) return@doLast } val tinyFile = mappingFiles.files .filter { it.isFile } .maxByOrNull { it.lastModified() } ?: error("Missing Loom source mappings under ${project.file(".gradle/loom-cache/source_mappings").absolutePath}") val lines = tinyFile.readLines(Charsets.UTF_8) val header = lines.firstOrNull { it.startsWith("tiny\t") } ?: error("Invalid tiny mapping file: ${tinyFile.absolutePath}") val namespaces = header.split('\t').drop(3) val namedIndex = namespaces.indexOf("named") val intermediaryIndex = namespaces.indexOf("intermediary") require(namedIndex >= 0 && intermediaryIndex >= 0) { "Tiny mapping header must expose named and intermediary namespaces: $header" } fun readNamespace(parts: List, baseOffset: Int, namespaceIndex: Int): String { val index = baseOffset + namespaceIndex return if (index in parts.indices) parts[index].trim() else "" } val namedToIntermediary = linkedMapOf() val classAliases = linkedMapOf() for (raw in lines) { val trimmed = raw.trimStart('\t') if (!trimmed.startsWith("c\t")) continue val parts = trimmed.split('\t') val namedName = readNamespace(parts, 1, namedIndex) val intermediaryName = readNamespace(parts, 1, intermediaryIndex) if (namedName.isBlank() || intermediaryName.isBlank()) continue namedToIntermediary[namedName] = intermediaryName classAliases[intermediaryName.replace('/', '.')] = namedName.substringAfterLast('/').replace('$', '.') } fun remapDescriptorToIntermediary(descriptor: String): String { if (descriptor.isBlank()) return descriptor val classRef = Regex("L([^;]+);") return classRef.replace(descriptor) { match -> val namedInternalName = match.groupValues[1] val intermediaryInternalName = namedToIntermediary[namedInternalName] ?: namedInternalName "L$intermediaryInternalName;" } } fun storeAlias( target: MutableMap, ambiguous: MutableSet, key: String, alias: String ) { if (key.isBlank() || alias.isBlank() || ambiguous.contains(key)) return val existing = target[key] if (existing == null) { target[key] = alias return } if (existing != alias) { target.remove(key) ambiguous.add(key) } } val fieldAliases = linkedMapOf() val methodAliases = linkedMapOf() val ambiguousFieldKeys = linkedSetOf() val ambiguousMethodKeys = linkedSetOf() var currentOwner = "" for (raw in lines) { val trimmed = raw.trimStart('\t') if (trimmed.isBlank()) continue val parts = trimmed.split('\t') when (parts.firstOrNull()) { "c" -> { currentOwner = readNamespace(parts, 1, intermediaryIndex).replace('/', '.') } "f" -> { if (currentOwner.isBlank()) continue val descriptor = if (parts.size > 1) remapDescriptorToIntermediary(parts[1]) else "" val namedName = readNamespace(parts, 2, namedIndex) val intermediaryName = readNamespace(parts, 2, intermediaryIndex) if (namedName.isBlank() || intermediaryName.isBlank()) continue storeAlias(fieldAliases, ambiguousFieldKeys, "$currentOwner#$intermediaryName#$descriptor", namedName) } "m" -> { if (currentOwner.isBlank()) continue val descriptor = if (parts.size > 1) remapDescriptorToIntermediary(parts[1]) else "" val namedName = readNamespace(parts, 2, namedIndex) val intermediaryName = readNamespace(parts, 2, intermediaryIndex) if (namedName.isBlank() || intermediaryName.isBlank()) continue if (namedName == "" || namedName == "") continue storeAlias(methodAliases, ambiguousMethodKeys, "$currentOwner#$intermediaryName#$descriptor", namedName) } } } val authAliasFragments = listOf( "session", "accesstoken", "refreshtoken", "authtoken", "clientsession", "playersession", "publicsession" ) fun isAuthAlias(alias: String): Boolean { val normalized = alias.lowercase().replace(Regex("[^a-z0-9]"), "") return authAliasFragments.any { normalized.contains(it) } } val filteredClassAliases = classAliases.filter { (_, alias) -> !isAuthAlias(alias) } val blockedOwners = classAliases.keys.filter { owner -> val alias = classAliases[owner] ?: return@filter false isAuthAlias(alias) }.toSet() val target = outputFile.get().asFile target.parentFile.mkdirs() target.printWriter(Charsets.UTF_8).use { out -> out.println("# Yarn deobfuscation mappings") filteredClassAliases.toSortedMap().forEach { (owner, alias) -> out.println("C\t$owner\t$alias") } fieldAliases.toSortedMap().forEach { (key, alias) -> val parts = key.split('#', limit = 3) if (parts.size == 3 && !blockedOwners.contains(parts[0]) && !isAuthAlias(alias)) { out.println("F\t${parts[0]}\t${parts[1]}\t${parts[2]}\t$alias") } } methodAliases.toSortedMap().forEach { (key, alias) -> val parts = key.split('#', limit = 3) if (parts.size == 3 && !blockedOwners.contains(parts[0]) && !isAuthAlias(alias)) { out.println("M\t${parts[0]}\t${parts[1]}\t${parts[2]}\t$alias") } } } } } val generateAutismPacketSchemas by tasks.registering { val minecraftVersion = libs.versions.minecraft.get() val outputFile = generatedAutismResourcesDir.map { it.file("autism-packet-schemas.tsv") } outputs.file(outputFile) doLast { fun normalizeWhitespace(value: String): String = value.replace(Regex("\\s+"), " ").trim() fun splitTopLevelComma(value: String): List { val out = mutableListOf() var depth = 0 var start = 0 for (i in value.indices) { when (value[i]) { '<', '(', '[', '{' -> depth++ '>', ')', ']', '}' -> if (depth > 0) depth-- ',' -> if (depth == 0) { out += value.substring(start, i).trim() start = i + 1 } } } val tail = value.substring(start).trim() if (tail.isNotBlank()) out += tail return out } fun protocolFromPath(path: String): String { val normalized = path.replace('\\', '/') val marker = "/network/protocol/" val idx = normalized.indexOf(marker) val tail = if (idx >= 0) normalized.substring(idx + marker.length) else normalized.substringAfter("net/minecraft/network/protocol/", "") return tail.substringBefore('/').ifBlank { "unknown" } } fun directionFromName(simpleName: String): String = when { simpleName.startsWith("Clientbound") -> "S2C" simpleName.startsWith("Serverbound") -> "C2S" else -> "ANY" } fun kindForType(type: String): String { val lower = type.lowercase() return when { lower == "byte" || lower == "short" || lower == "int" || lower == "long" || lower == "float" || lower == "double" -> "number" lower == "boolean" -> "boolean" lower == "string" -> "string" lower.contains("itemstack") || lower.contains("hashedstack") -> "item" lower.contains("component") -> "component" lower.contains("identifier") || lower.contains("resourcekey") -> "identifier" lower.contains("holder<") || lower == "holder" -> "holder" lower.contains("blockpos") || lower.contains("chunkpos") -> "position" lower.contains("vec3") || lower.contains("positionmoverotation") -> "vector" lower.contains("uuid") -> "uuid" lower.startsWith("optional<") || lower.contains(".optional<") -> "optional" lower.startsWith("list<") || lower.startsWith("set<") || lower.contains("list<") || lower.contains("set<") -> "list" lower.startsWith("map<") || lower.contains("map<") || lower.contains("int2objectmap") -> "map" lower.contains("bitset") -> "bitset" lower.contains("enumset") || lower.contains("relative") || lower.contains("containerinput") -> "enum" else -> "object" } } fun editableFor(kind: String): Boolean = kind in setOf("number", "boolean", "string", "identifier", "enum", "uuid") fun parseComponent(raw: String): FieldSpec? { val cleaned = normalizeWhitespace(raw) .replace(Regex("^@[\\w.]+(?:\\([^)]*\\))?\\s+"), "") val idx = cleaned.lastIndexOf(' ') if (idx <= 0 || idx >= cleaned.length - 1) return null val type = cleaned.substring(0, idx).trim() val name = cleaned.substring(idx + 1).trim().removeSuffix("...") if (!name.matches(Regex("[A-Za-z_$][A-Za-z0-9_$]*"))) return null val kind = kindForType(type) return FieldSpec(name, type, kind, editableFor(kind)) } fun parseFields(text: String): Pair> { val record = Regex("""public\s+record\s+\w+\s*\((.*?)\)\s*implements""", setOf(RegexOption.DOT_MATCHES_ALL)) .find(text) if (record != null) { val fields = splitTopLevelComma(record.groupValues[1]).mapNotNull(::parseComponent) return "record" to fields } val fields = Regex("""(?m)^\s*(?:private|protected)\s+final\s+([^;=]+?)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*;""") .findAll(text) .mapNotNull { match -> val type = normalizeWhitespace(match.groupValues[1]) val name = match.groupValues[2] val kind = kindForType(type) FieldSpec(name, type, kind, editableFor(kind)) } .toList() return if (fields.isNotEmpty()) "fields" to fields else "fallback" to emptyList() } fun loadSources(): List { val sourcesJar = file("${System.getProperty("user.home")}/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/$minecraftVersion/minecraft-merged-deobf-$minecraftVersion-sources.jar") if (sourcesJar.isFile) { ZipFile(sourcesJar).use { zip -> return zip.entries().asSequence() .filter { !it.isDirectory } .filter { it.name.startsWith("net/minecraft/network/protocol/") && it.name.endsWith("Packet.java") } .map { entry -> SourceFile(entry.name, zip.getInputStream(entry).bufferedReader(Charsets.UTF_8).use { it.readText() }) } .toList() } } val localRoot = file("mc-src$minecraftVersion/libs/net.minecraft.minecraft-merged-deobf/net/minecraft/network/protocol") if (localRoot.isDirectory) { return localRoot.walkTopDown() .filter { it.isFile && it.name.endsWith("Packet.java") } .map { file -> SourceFile(file.relativeTo(localRoot.parentFile.parentFile.parentFile.parentFile).invariantSeparatorsPath, file.readText(Charsets.UTF_8)) } .toList() } error("Missing Minecraft deobf sources jar and local mc-src fallback for $minecraftVersion") } fun packetTypeOf(text: String): String { return Regex("""return\s+([A-Za-z0-9_]+PacketTypes\.[A-Z0-9_]+)\s*;""") .find(text) ?.groupValues ?.get(1) ?: "" } fun codecStyleOf(text: String): String = when { text.contains("StreamCodec.unit") -> "UNIT" text.contains("StreamCodec.composite") -> "COMPOSITE" text.contains("Packet.codec") -> "PACKET_CODEC" else -> "CUSTOM" } fun escape(value: String): String = value .replace('\t', ' ') .replace('\n', ' ') .replace('|', '/') .replace('~', '-') val specs = loadSources().mapNotNull { source -> val pkg = Regex("""package\s+([A-Za-z0-9_.]+)\s*;""").find(source.text)?.groupValues?.get(1) ?: return@mapNotNull null val simpleName = Regex("""public\s+(?:abstract\s+)?(?:record|class)\s+([A-Za-z0-9_]+Packet)\b""") .find(source.text) ?.groupValues ?.get(1) ?: return@mapNotNull null val codecStyle = codecStyleOf(source.text) val parsed = parseFields(source.text) val sourceKind = if (codecStyle == "UNIT") "unit" else parsed.first val complete = codecStyle == "UNIT" || parsed.second.isNotEmpty() PacketSpec( "$pkg.$simpleName", protocolFromPath(source.path), directionFromName(simpleName), codecStyle, packetTypeOf(source.text), sourceKind, complete, if (codecStyle == "UNIT") emptyList() else parsed.second ) }.sortedBy { it.className } val target = outputFile.get().asFile target.parentFile.mkdirs() target.printWriter(Charsets.UTF_8).use { out -> out.println("# Generated from Minecraft $minecraftVersion packet sources. Fields preserve source/record order where available.") out.println("# class\tprotocol\tdirection\tcodec\tpacketType\tsource\tcomplete\tfields(name~type~kind~editable|...)") for (spec in specs) { val fields = spec.fields.joinToString("|") { field -> listOf(field.name, field.type, field.kind, field.editable.toString()).joinToString("~", transform = ::escape) } val columns = listOf( spec.className, spec.protocol, spec.direction, spec.codecStyle, spec.packetType, spec.source, spec.complete.toString() ).joinToString("\t", transform = ::escape) out.println("$columns\t$fields") } } } } tasks { processResources { dependsOn(generateAutismInspectorMappings) dependsOn(generateAutismPacketSchemas) dependsOn(generateVanillaUiAssets) val propertyMap = mapOf( "version" to project.version, "mc_version" to libs.versions.minecraft.get() ) inputs.properties(propertyMap) filteringCharset = "UTF-8" exclude("addon-template.mixins.json") exclude("assets/template/**") filesMatching("fabric.mod.json") { expand(propertyMap) } } jar { inputs.property("archivesName", project.base.archivesName.get()) from("LICENSE") { rename { "${it}_${inputs.properties["archivesName"]}" } } // ModMenu API is a compile-only soft dependency: we ship local stubs of its two API // interfaces so we can compile the integration without a cross-version ModMenu artifact, but // we must NOT bundle them — at runtime the real ModMenu provides them (and if ModMenu is // absent, our integration entrypoint is simply never loaded). exclude("com/terraformersmc/**") } java { sourceCompatibility = JavaVersion.VERSION_25 targetCompatibility = JavaVersion.VERSION_25 toolchain { languageVersion.set(JavaLanguageVersion.of(25)) } } withType { options.encoding = "UTF-8" options.release.set(25) options.compilerArgs.add("-Xlint:deprecation") options.compilerArgs.add("-Xlint:unchecked") options.compilerArgs.add("-Xlint:-restricted") } } // Publish the Loom-remapped jar to the local Maven repo so the standalone addon-template (and any // third-party addon) can depend on it via `modImplementation("com.autismclient:autism:")`. // Run: ./gradlew publishToMavenLocal publishing { publications { create("mavenJava") { groupId = "com.autismclient" artifactId = "autism" version = project.version.toString() from(components["java"]) } } repositories { mavenLocal() } }