mirror of
https://github.com/AutismDevelopment/Autism-Client.git
synced 2026-10-28 17:29:46 +00:00
704 lines
31 KiB
Kotlin
704 lines
31 KiB
Kotlin
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 = "<mod>-<mc>" (e.g. 3.1-26.2). Dev/source builds add a "-dev" suffix so they're distinguishable
|
|
// from a tagged release; a release is built with: gradlew build -Prelease
|
|
version = libs.versions.mod.version.get() + "-" + libs.versions.minecraft.get() +
|
|
(if (project.hasProperty("release")) "" else "-dev")
|
|
group = properties["maven_group"] as String
|
|
}
|
|
|
|
repositories {
|
|
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")
|
|
|
|
// Eclipse Paho MQTT client (standalone, no transitive deps) — powers the redundant MQTT relay mesh
|
|
// (EMQX/HiveMQ public brokers over secure WebSocket) so the encrypted matchmaking transport survives any
|
|
// single public service timing out. Bundled jar-in-jar like the other runtime libs.
|
|
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
|
|
include("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
|
|
|
|
// 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")
|
|
|
|
testImplementation(platform("org.junit:junit-bom:5.11.4"))
|
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
|
|
|
}
|
|
|
|
tasks.test {
|
|
useJUnitPlatform()
|
|
}
|
|
|
|
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<FieldSpec>
|
|
)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Raster icons committed directly as PNG (e.g. the Matchmaking logo) are copied as-is; the engine
|
|
// recolors/whitens them at draw time just like the SVG-derived icons.
|
|
iconSourceDir.listFiles { file -> file.isFile && file.extension.equals("png", ignoreCase = true) }
|
|
?.sortedBy { it.name.lowercase() }
|
|
?.forEach { png ->
|
|
png.copyTo(iconTargetDir.resolve(png.nameWithoutExtension.lowercase() + ".png"), overwrite = true)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 ${libs.versions.minecraft.get()}; 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<String>, baseOffset: Int, namespaceIndex: Int): String {
|
|
val index = baseOffset + namespaceIndex
|
|
return if (index in parts.indices) parts[index].trim() else ""
|
|
}
|
|
|
|
val namedToIntermediary = linkedMapOf<String, String>()
|
|
val classAliases = linkedMapOf<String, String>()
|
|
|
|
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<String, String>,
|
|
ambiguous: MutableSet<String>,
|
|
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<String, String>()
|
|
val methodAliases = linkedMapOf<String, String>()
|
|
val ambiguousFieldKeys = linkedSetOf<String>()
|
|
val ambiguousMethodKeys = linkedSetOf<String>()
|
|
|
|
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 == "<init>" || namedName == "<clinit>") 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<String> {
|
|
val out = mutableListOf<String>()
|
|
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<String, List<FieldSpec>> {
|
|
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<SourceFile> {
|
|
val sourcesJar = file("${System.getProperty("user.home")}/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/$minecraftVersion/minecraft-merged-deobf-$minecraftVersion-sources.jar")
|
|
val loomCacheSourceJar = file(".gradle/loom-cache/minecraftMaven/net/minecraft")
|
|
.takeIf { it.isDirectory }
|
|
?.walkTopDown()
|
|
?.firstOrNull { file ->
|
|
file.isFile &&
|
|
file.name.endsWith("-$minecraftVersion-sources.jar") &&
|
|
file.invariantSeparatorsPath.contains("/minecraft-merged-")
|
|
}
|
|
val sourceArchive = when {
|
|
sourcesJar.isFile -> sourcesJar
|
|
loomCacheSourceJar != null -> loomCacheSourceJar
|
|
else -> null
|
|
}
|
|
if (sourceArchive != null) {
|
|
ZipFile(sourceArchive).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()
|
|
}
|
|
|
|
logger.warn("Minecraft packet sources are unavailable for $minecraftVersion; generating an empty packet schema resource and using runtime fallback decoding.")
|
|
return emptyList()
|
|
}
|
|
|
|
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(),
|
|
"fabric_api_version" to libs.versions.fabric.api.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<JavaCompile> {
|
|
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:<version>")`.
|
|
// Run: ./gradlew publishToMavenLocal
|
|
publishing {
|
|
publications {
|
|
create<MavenPublication>("mavenJava") {
|
|
groupId = "com.autismclient"
|
|
artifactId = "autism"
|
|
version = project.version.toString()
|
|
from(components["java"])
|
|
}
|
|
}
|
|
repositories {
|
|
mavenLocal()
|
|
}
|
|
}
|
|
|
|
// Pin the templates' versions to this client's. Runs on publishToMavenLocal.
|
|
val syncAddonTemplateVersions by tasks.registering {
|
|
group = "addon templates"
|
|
description = "Pin addon-templates/*/gradle/libs.versions.toml to this client's versions."
|
|
|
|
val clientCatalog = rootDir.resolve("gradle/libs.versions.toml")
|
|
val templateCatalogs = listOf("minimal", "advanced").map {
|
|
rootDir.resolve("addon-templates/$it/gradle/libs.versions.toml")
|
|
}
|
|
inputs.file(clientCatalog)
|
|
outputs.files(templateCatalogs)
|
|
|
|
doLast {
|
|
val client = clientCatalog.readText()
|
|
fun readVersion(key: String): String {
|
|
val m = Regex("(?m)^\\s*" + Regex.escape(key) + "\\s*=\\s*\"([^\"]*)\"").find(client)
|
|
?: throw GradleException("Key '$key' not found in $clientCatalog")
|
|
return m.groupValues[1]
|
|
}
|
|
// the template's own mod-version is left alone
|
|
val values = linkedMapOf(
|
|
"minecraft" to readVersion("minecraft"),
|
|
"fabric-loader" to readVersion("fabric-loader"),
|
|
"fabric-api" to readVersion("fabric-api"),
|
|
"loom" to readVersion("loom"),
|
|
"autism" to readVersion("mod-version")
|
|
)
|
|
templateCatalogs.forEach { file ->
|
|
if (!file.exists()) throw GradleException("Missing template catalog: $file")
|
|
var text = file.readText()
|
|
values.forEach { (key, value) ->
|
|
val re = Regex("(?m)^(\\s*" + Regex.escape(key) + "\\s*=\\s*\")[^\"]*(\")")
|
|
if (re.containsMatchIn(text)) {
|
|
text = re.replace(text) { mr -> mr.groupValues[1] + value + mr.groupValues[2] }
|
|
} else {
|
|
logger.warn("syncAddonTemplateVersions: '$key' not found in ${file.name}; skipped.")
|
|
}
|
|
}
|
|
file.writeText(text)
|
|
}
|
|
logger.lifecycle(
|
|
"Synced addon templates: minecraft=${values["minecraft"]}, fabric-loader=${values["fabric-loader"]}, " +
|
|
"fabric-api=${values["fabric-api"]}, loom=${values["loom"]}, autism=${values["autism"]}"
|
|
)
|
|
}
|
|
}
|
|
|
|
tasks.named("publishToMavenLocal") {
|
|
dependsOn(syncAddonTemplateVersions)
|
|
}
|
|
|
|
// Scaffold/scan tasks call addon-toolkit.py (these need Python 3). Building needs no Python.
|
|
fun resolvePythonCommand(): List<String> {
|
|
val isWindows = System.getProperty("os.name").lowercase().contains("win")
|
|
val candidates = if (isWindows) listOf(listOf("py", "-3"), listOf("python"), listOf("python3"))
|
|
else listOf(listOf("python3"), listOf("python"))
|
|
for (candidate in candidates) {
|
|
try {
|
|
if (ProcessBuilder(candidate + "--version").redirectErrorStream(true).start().waitFor() == 0) return candidate
|
|
} catch (_: Exception) {
|
|
}
|
|
}
|
|
throw GradleException("Python 3 was not found on PATH (needed for the addon toolkit tasks).")
|
|
}
|
|
|
|
fun toolkitArgs(project: Project, action: String): List<String> {
|
|
val args = mutableListOf(
|
|
project.rootDir.resolve("addon-templates/addon-toolkit.py").absolutePath,
|
|
action,
|
|
"--non-interactive",
|
|
)
|
|
// gradleProperty, not findProperty: names like "project"/"name" clash with Project getters.
|
|
fun prop(name: String) = project.providers.gradleProperty(name).orNull?.takeIf { it.isNotBlank() }
|
|
args += listOf("--template", prop("template") ?: "minimal")
|
|
mapOf(
|
|
"name" to "--name", "out" to "--output", "project" to "--project", "addonId" to "--addon-id",
|
|
"package" to "--package", "author" to "--author", "description" to "--description",
|
|
"mavenGroup" to "--maven-group", "archiveName" to "--archive-name", "addonVersion" to "--addon-version",
|
|
).forEach { (propName, flag) -> prop(propName)?.let { args += listOf(flag, it) } }
|
|
if (project.providers.gradleProperty("advanced").isPresent) args += "--advanced"
|
|
if (project.providers.gradleProperty("yes").isPresent) args += "--yes"
|
|
if (project.providers.gradleProperty("noPublish").isPresent) args += "--no-publish"
|
|
return args
|
|
}
|
|
|
|
listOf(
|
|
Triple("newAddon", "setup", "Scaffold a new addon. Props: -Pname= -Ptemplate=minimal|advanced -Pout= (optional -PaddonId/-Ppackage/-Pauthor/-Padvanced)."),
|
|
Triple("scanAddon", "scan", "Validate one addon project. Prop: -Pproject=<path>."),
|
|
Triple("validateAddons", "validate", "Validate the in-repo addon system and shipped templates."),
|
|
Triple("cleanAddon", "clean", "Delete an addon project's build/.gradle. Props: -Pproject=<path> -Pyes."),
|
|
).forEach { (taskName, action, taskDescription) ->
|
|
tasks.register(taskName) {
|
|
group = "addon toolkit"
|
|
description = taskDescription
|
|
doLast {
|
|
val command = resolvePythonCommand() + toolkitArgs(project, action)
|
|
val process = ProcessBuilder(command).directory(project.rootDir).redirectErrorStream(true).start()
|
|
process.inputStream.bufferedReader().forEachLine { logger.lifecycle(it) }
|
|
val exit = process.waitFor()
|
|
if (exit != 0) throw GradleException("addon toolkit '$action' failed (exit code $exit).")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pure Gradle, no Python: publish the API, then build each shipped template via its own wrapper.
|
|
tasks.register("buildAllTemplates") {
|
|
group = "addon toolkit"
|
|
description = "Publish the API locally and build both shipped templates (no Python needed)."
|
|
dependsOn("publishToMavenLocal")
|
|
doLast {
|
|
val isWindows = System.getProperty("os.name").lowercase().contains("win")
|
|
for (name in listOf("minimal", "advanced")) {
|
|
val dir = project.rootDir.resolve("addon-templates/$name")
|
|
val wrapper = dir.resolve(if (isWindows) "gradlew.bat" else "gradlew")
|
|
if (!wrapper.isFile) throw GradleException("Missing Gradle wrapper: $wrapper")
|
|
val cmd = if (!isWindows && !wrapper.canExecute()) listOf("sh", wrapper.absolutePath, "build")
|
|
else listOf(wrapper.absolutePath, "build")
|
|
logger.lifecycle("Building $name template")
|
|
val process = ProcessBuilder(cmd).directory(dir).redirectErrorStream(true).start()
|
|
process.inputStream.bufferedReader().forEachLine { logger.lifecycle(it) }
|
|
if (process.waitFor() != 0) throw GradleException("$name template build failed.")
|
|
}
|
|
}
|
|
}
|