rework dsl, add mod configurations (remapped), add support for other mappings #3

Merged
Ecorous merged 11 commits from owlsys/other-mappings into mistress 2024-07-04 14:39:40 -04:00
17 changed files with 845 additions and 251 deletions

View file

@ -7,7 +7,7 @@ plugins {
}
group = "dev.frogmc"
version = "0.0.1-alpha.12"
version = "0.0.1-alpha.13"
repositories {
maven {
@ -18,17 +18,20 @@ repositories {
name = "FrogMC Maven Snapshots"
url = uri("https://maven.frogmc.dev/snapshots")
}
mavenLocal()
mavenCentral()
}
dependencies {
implementation("dev.frogmc:thyroxine:0.0.1-alpha.5")
implementation("dev.frogmc:thyroxine:0.0.1-alpha.6")
implementation("org.ow2.asm:asm:9.7")
implementation("org.ow2.asm:asm-commons:9.7")
implementation("org.ow2.asm:asm-tree:9.7")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.vineflower:vineflower:1.10.1")
testImplementation(kotlin("test"))
implementation("com.electronwill.night-config:toml:3.7.3")
implementation("com.electronwill.night-config:toml:3.8.0")
implementation("com.google.jimfs:jimfs:1.3.0")
}

View file

@ -12,4 +12,6 @@ object Constants {
const val GEN_RUN_CONFIGS_TASK = "genRunConfigs"
const val RUN_CLIENT_TASK = "runClient"
const val RUNT_SERVER_TASK = "runServer"
const val CLEAR_LOCAL_CACHE_TASK = "clearLocalCache"
const val CLEAR_GLOBAL_CACHE_TASK = "clearGlobalCache"
}

View file

@ -0,0 +1,70 @@
package dev.frogmc.phytotelma
import org.gradle.api.plugins.JavaPlugin
object ModConfigurations {
val configurations = listOf(
ModConfiguration(
JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME, listOf(
JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_COMPILE_CLASSPATH_CONFIGURATION_NAME
), DependencyType.RUNTIME
),
ModConfiguration(
JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME, listOf(
JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_COMPILE_CLASSPATH_CONFIGURATION_NAME
), DependencyType.RUNTIME
),
ModConfiguration(
JavaPlugin.API_CONFIGURATION_NAME, listOf(
JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_COMPILE_CLASSPATH_CONFIGURATION_NAME
), DependencyType.COMPILE
),
ModConfiguration(
JavaPlugin.COMPILE_ONLY_API_CONFIGURATION_NAME, listOf(
JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_COMPILE_CLASSPATH_CONFIGURATION_NAME
), DependencyType.COMPILE
),
ModConfiguration(
JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, listOf(
JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_COMPILE_CLASSPATH_CONFIGURATION_NAME
)
),
ModConfiguration(
JavaPlugin.TEST_COMPILE_ONLY_CONFIGURATION_NAME, listOf(
JavaPlugin.TEST_COMPILE_CLASSPATH_CONFIGURATION_NAME
)
),
ModConfiguration(
JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME, listOf(
JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME,
JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME,
)
),
ModConfiguration(
JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME, listOf(
JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME,
)
)
)
}
data class ModConfiguration(
val name: String,
val classpathNames: List<String>,
val dependencyType: DependencyType = DependencyType.NONE,
)
enum class DependencyType {
NONE,
RUNTIME,
COMPILE
}

View file

@ -1,29 +1,14 @@
package dev.frogmc.phytotelma
import dev.frogmc.phytotelma.accesswidener.AccessWidener
import dev.frogmc.phytotelma.build.PhytotelmaBuildTask
import dev.frogmc.phytotelma.common.Env
import dev.frogmc.phytotelma.ext.PhytotelmaGradleExtension
import dev.frogmc.phytotelma.ext.PhytotelmaGradleExtensionImpl
import dev.frogmc.phytotelma.run.AssetDownloader
import dev.frogmc.phytotelma.run.RunConfigGenerator
import dev.frogmc.phytotelma.run.task.RunGameTask
import dev.frogmc.phytotelma.vineflower.ParchmentJavadocProvider
import dev.frogmc.thyroxine.provider.ParchmentProvider
import net.fabricmc.fernflower.api.IFabricJavadocProvider
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPlugin
import org.jetbrains.java.decompiler.main.Fernflower
import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger
import org.jetbrains.java.decompiler.main.decompiler.SingleFileSaver
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences
import java.io.PrintStream
import org.gradle.configurationcache.extensions.capitalized
import java.net.URI
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.exists
class PhytotelmaPlugin : Plugin<Project> {
@ -43,6 +28,10 @@ class PhytotelmaPlugin : Plugin<Project> {
}
project.repositories.apply {
maven {
it.name = "Remapped Mod Dependencies"
it.url = project.layout.buildDirectory.asFile.get().resolve("remappedMods").toURI()
}
maven {
it.name = "Minecraft/Local"
it.url = localCacheDir.toUri()
@ -68,70 +57,20 @@ class PhytotelmaPlugin : Plugin<Project> {
PhytotelmaGradleExtensionImpl::class.java
)
project.extensions.findByType(PhytotelmaGradleExtension::class.java) ?: return
ModConfigurations.configurations.forEach { conf ->
project.configurations.create("mod" + conf.name.capitalized()) { c ->
c.isCanBeResolved = true
c.isCanBeConsumed = false
project.tasks.register("genSources") {task ->
task.group = Constants.TASK_GROUP
task.actions.add {
val projectData = ProjectStorage.get(it.project)
val fileName = projectData.remappedGameJarPath!!.fileName.toString()
val output =
projectData.remappedGameJarPath!!.resolveSibling(
fileName.substring(
0,
fileName.lastIndexOf("-")
) + "-sources.jar"
)
if (output.exists()) {
println("Output $output already exists, deleting!")
output.deleteExisting()
when (conf.dependencyType) {
DependencyType.RUNTIME -> project.configurations.getByName(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME)
.extendsFrom(c)
DependencyType.COMPILE -> project.configurations.getByName(JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME)
.extendsFrom(c)
DependencyType.NONE -> {}
}
val options = mutableMapOf<String, Any>()
println("Preparing Parchment...")
val parchment = ParchmentProvider.getParchment(
projectData.minecraftVersion!!,
projectData.parchmentVersion!!,
globalCacheDir.resolve("org/parchmentmc/parchment/${projectData.minecraftVersion}/${projectData.parchmentVersion}/")
)
options[IFabricJavadocProvider.PROPERTY_NAME] = ParchmentJavadocProvider(parchment)
println("Decompiling...")
val logger = PrintStreamLogger(PrintStream(System.out))
options.putAll(
mapOf(
IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES to "1",
IFernflowerPreferences.BYTECODE_SOURCE_MAPPING to "1",
IFernflowerPreferences.REMOVE_SYNTHETIC to "1",
IFernflowerPreferences.LOG_LEVEL to "error",
IFernflowerPreferences.THREADS to Runtime.getRuntime().availableProcessors().toString(),
IFernflowerPreferences.INDENT_STRING to "\t"
)
)
val decomp = Fernflower(
SingleFileSaver(output.toFile()),
options, logger
)
decomp.addSource(projectData.remappedGameJarPath!!.toFile())
decomp.decompileContext()
}
}
project.tasks.register(Constants.GEN_RUN_CONFIGS_TASK) {task ->
task.group = Constants.TASK_GROUP
task.doFirst {
RunConfigGenerator.generate(project)
}
}
project.tasks.register(Constants.RUN_CLIENT_TASK, RunGameTask::class.java, Env.CLIENT)
project.tasks.register(Constants.RUNT_SERVER_TASK, RunGameTask::class.java, Env.SERVER)
project.tasks.register(Constants.DOWNLOAD_ASSETS_TASK) { task ->
task.group = Constants.TASK_GROUP
task.doFirst {
AssetDownloader.download(it.project)
}
}
@ -149,16 +88,6 @@ class PhytotelmaPlugin : Plugin<Project> {
it.isCanBeConsumed = false
it.isTransitive = false
}
val buildTask = project.tasks.register(Constants.BUILD_TASK, PhytotelmaBuildTask::class.java)
project.tasks.getByName("build").dependsOn(buildTask)
project.tasks.register(Constants.UPDATE_AW_TASK) { task ->
task.group = Constants.TASK_GROUP
task.actions.addFirst {
AccessWidener.apply(project, ProjectStorage.get(it.project).remappedGameJarPath!!)
}
}
}
companion object {

View file

@ -1,5 +1,6 @@
package dev.frogmc.phytotelma
import dev.frogmc.thyroxine.api.data.MappingBundle
import org.gradle.api.Project
import java.nio.file.Path
@ -9,14 +10,18 @@ object ProjectStorage {
fun get(project: Project): ProjectData {
if (!data.containsKey(project)) {
data[project] = ProjectData(null, null, null, null)
data[project] = ProjectData(null, null, null, null, null, null)
}
return data[project]!!
}
}
class ProjectData(var localCacheDir: Path?,
class ProjectData(
var localCacheDir: Path?,
var minecraftVersion: String?,
var remappedGameJarPath: Path?,
var parchmentVersion: String?)
var mappings: MappingBundle?,
var mappingsName: String?,
var targetNamespace: String?
)

View file

@ -1,3 +1,5 @@
@file:Suppress("unused")
package dev.frogmc.phytotelma
import com.google.gson.Gson
@ -5,9 +7,11 @@ import com.google.gson.GsonBuilder
import dev.frogmc.phytotelma.common.CachingHttpClient
import org.gradle.api.Project
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.*
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.writeBytes
import kotlin.io.path.writeText
object VersionChecker {
private var validVersions = mutableListOf<VersionUrl>()
@ -77,17 +81,6 @@ object VersionChecker {
return fetchVersionData(version).downloads.client
}
fun downloadAssetIndex(version: String, path: Path): String {
val index = fetchVersionData(version).assetIndex
val dest = path.resolve(index.id + ".json")
if (dest.notExists()) {
CachingHttpClient.getUncached(URI.create(index.url)).use {
Files.copy(it, dest)
}
}
return index.id
}
private fun rawDownload(url: String): ByteArray {
return CachingHttpClient.getUncached(URI.create(url)).readAllBytes()
}

View file

@ -9,6 +9,7 @@ import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.objectweb.asm.*
import org.objectweb.asm.tree.ClassNode
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystems
import java.nio.file.Path
@ -45,18 +46,20 @@ object AccessWidener {
private fun findDependencyAWs(project: Project): Stream<String> {
val conf = project.configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)
val projectDeps = conf.dependencies.filterIsInstance<ProjectDependency>()
.mapNotNull { getAWFile(it.dependencyProject) }.stream().flatMap { readTransitiveAW(it) }
val dependencies = conf.resolvedConfiguration.files.map { it.toPath() }.filter { it.exists() && it.isRegularFile() }.stream().flatMap {
.mapNotNull { getAWFile(it.dependencyProject) }.flatMap { readTransitiveAW(it) }
val dependencies =
conf.resolvedConfiguration.files.map { it.toPath() }.filter { it.exists() && it.isRegularFile() }.stream()
.flatMap {
FileSystems.newFileSystem(it).use { fs ->
val metadata = fs.getPath(Constants.MOD_METADATA_FILE)
PARSER.parse(metadata, FileNotFoundAction.READ_NOTHING)
.get<String>("frog.extensions.accesswidener")?.let { name ->
return@flatMap readTransitiveAW(metadata.resolveSibling(name))
return@flatMap readTransitiveAW(metadata.resolveSibling(name)).stream()
}
}
return@flatMap null
}.filter { it != null }
return Stream.concat(projectDeps, dependencies)
return Stream.concat(projectDeps.stream(), dependencies)
}
fun needsUpdate(project: Project): Boolean {
@ -70,24 +73,75 @@ object AccessWidener {
return false
}
private fun readAW(project: Project): Stream<String> {
fun checkAW(path: Path, gamePath: Path) {
val reader = path.bufferedReader(StandardCharsets.UTF_8)
if (!HEADER.test(reader.readLine() ?: "")) {
error("AccessWidener validation failed (invalid header)")
}
FileSystems.newFileSystem(gamePath).use { fs ->
reader.lines().toList().forEachIndexed { index, line ->
val checkString = line.substring(0, line.indexOf("#"))
if (checkString.isNotEmpty()) {
val parts = checkString.split(SEPARATOR)
if (parts.size < 3) {
error("AccessWidener validation failed in line ${index + 2}: $line")
}
if (parts[1] == "class") {
val target = parts[2]
if (fs.getPath(target).notExists()) {
error("AccessWidener validation failed in line ${index + 2}: $line (Invalid target)")
}
}
if (parts.size < 5) {
error("AccessWidener validation failed in line ${index + 2}: $line (Declaration missing)")
}
val owner = parts[2]
val name = parts[3]
val desc = parts[4]
when (parts[1]) {
"method" -> {
val classReader = ClassReader(fs.getPath(owner).readBytes())
val node = ClassNode()
classReader.accept(node, 0)
if (node.methods.none { it.name.equals(name) && it.desc.equals(desc) }) {
error("AccessWidener validation failed in line ${index + 2}: $line (Could not find target method)")
}
}
"field" -> {
val classReader = ClassReader(fs.getPath(owner).readBytes())
val node = ClassNode()
classReader.accept(node, 0)
if (node.fields.none { it.name.equals(name) && it.desc.equals(desc) }) {
error("AccessWidener validation failed in line ${index + 2}: $line (Could not find target field)")
}
}
}
}
}
}
}
private fun readAW(project: Project): List<String> {
val awFile = getAWFile(project)
println("Found accesswidener in project: $awFile")
return awFile?.bufferedReader(StandardCharsets.UTF_8)
.takeIf { HEADER.test(it?.readLine() ?: "") }?.lines()
?.map { it.replace("transitive-", "") }
?: Stream.empty()
?.map { it.replace("transitive-", "") }?.toList()
?: emptyList()
}
private fun readTransitiveAW(path: Path): Stream<String> {
return path.bufferedReader(StandardCharsets.UTF_8).takeIf { HEADER.test(it.readLine() ?: "") }?.lines()?.filter { it.startsWith("transitive-") }?: Stream.empty()
private fun readTransitiveAW(path: Path): List<String> {
return path.bufferedReader(StandardCharsets.UTF_8).takeIf { HEADER.test(it.readLine() ?: "") }?.lines()
?.filter { it.startsWith("transitive-") }?.toList() ?: emptyList()
}
private fun readAllAWs(project: Project): Stream<Entry> {
return Stream.concat(
findDependencyAWs(project),
readAW(project)
readAW(project).stream()
)
.filter { it.isNotBlank() }
.map { if (it.contains("#")) it.split("#")[0] else it }.filter { !HEADER.test(it) }

View file

@ -0,0 +1,260 @@
package dev.frogmc.phytotelma.ext
import dev.frogmc.phytotelma.PhytotelmaPlugin
import dev.frogmc.phytotelma.mappings.filterClasses
import dev.frogmc.phytotelma.mappings.renameDstNamespace
import dev.frogmc.phytotelma.mappings.renameNamespaces
import dev.frogmc.thyroxine.api.data.MappingBundle
import dev.frogmc.thyroxine.parser.tiny.TinyV2Parser
import dev.frogmc.thyroxine.provider.MojmapProvider
import dev.frogmc.thyroxine.provider.ParchmentProvider
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Provider
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystems
import java.nio.file.Path
import javax.inject.Inject
import kotlin.io.path.readText
@Suppress("MemberVisibilityCanBePrivate", "unused")
abstract class MinecraftConfiguration @Inject constructor(
private val project: Project,
private val objects: ObjectFactory
) {
var version: String? = null
var mappings: Provider<MappingBundle> = mojmapParchment()
internal lateinit var mappingsName: String
internal lateinit var targetNamespace: String
fun mojmapParchment(): Provider<MappingBundle> {
return mojmapParchment {}
}
fun mojmapParchment(action: Action<ParchmentConfiguration>): Provider<MappingBundle> {
return project.provider {
val conf = objects.newInstance(ParchmentConfiguration::class.java)
conf.gameVersion = version
action.execute(conf)
val cacheDir = PhytotelmaPlugin.globalCacheDir
if (conf.version == null) {
conf.version = ParchmentProvider.findForMinecraftVersion(conf.gameVersion)
}
mappingsName = "mojmap($version)+parchment(${conf.gameVersion}, ${conf.version})"
targetNamespace = "mojmap"
return@provider MappingBundle.merge(
MojmapProvider.get(
version,
cacheDir.resolve("net/minecraft/client/$version/client-$version.txt")
).orElseThrow().reverse(),
ParchmentProvider.getParchment(
version,
cacheDir.resolve("org/parchmentmc/parchment/${conf.gameVersion}/${conf.version}")
)
).renameDstNamespace(targetNamespace)
}
}
fun parchment(action: Action<ParchmentConfiguration>): Provider<MappingBundle> {
return project.provider {
val conf = objects.newInstance(ParchmentConfiguration::class.java)
conf.gameVersion = version
action.execute(conf)
val cacheDir = PhytotelmaPlugin.globalCacheDir
if (conf.version == null) {
conf.version = ParchmentProvider.findForMinecraftVersion(conf.gameVersion)
}
mappingsName = "parchment(${conf.gameVersion}, ${conf.version})"
targetNamespace = "parchment"
return@provider ParchmentProvider.getParchment(
version,
cacheDir.resolve("org/parchmentmc/parchment/${conf.gameVersion}/${conf.version}")
).renameNamespaces("mojmap", "parchment")
}
}
fun mojmap(): Provider<MappingBundle> {
return project.provider {
val cacheDir = PhytotelmaPlugin.globalCacheDir
mappingsName = "mojmap($version)"
targetNamespace = "mojmap"
return@provider MojmapProvider.get(
version,
cacheDir.resolve("net/minecraft/client/$version/client-$version.txt")
).orElseThrow().reverse().renameDstNamespace(targetNamespace)
}
}
fun quiltMappings(action: Action<VersionConfiguration>): Provider<MappingBundle> {
return project.provider {
val conf = objects.newInstance(VersionConfiguration::class.java)
action.execute(conf)
if (conf.version == null) {
error("No version provided for quilt mappings!")
}
mappingsName = "quilt-mappings(${conf.version})"
targetNamespace = "quilt-mappings"
// Use qm via intermediary because hashed publications are broken
return@provider twoStepMappings(
"net.fabricmc:intermediary:$version:v2",
"org.quiltmc:quilt-mappings:${conf.version}:intermediary-v2"
).filterClasses { !it.startsWith("net/minecraft/unmapped") }
.flatten(true).renameDstNamespace(targetNamespace)
}
}
fun yarn(action: Action<VersionConfiguration>): Provider<MappingBundle> {
return project.provider {
val conf = objects.newInstance(VersionConfiguration::class.java)
action.execute(conf)
if (conf.version == null) {
error("No version provided for yarn!")
}
mappingsName = "yarn(${conf.version})"
targetNamespace = "yarn"
// Use qm via intermediary because hashed publications are broken
return@provider twoStepMappings(
"net.fabricmc:intermediary:$version:v2",
"net.fabricmc:yarn:${conf.version}:v2"
).filterClasses { !it.startsWith("net/minecraft/class_") }
.flatten(true).renameDstNamespace(targetNamespace)
}
}
fun twoStepMappings(action: Action<TwoStepMappingsConfiguration>): Provider<MappingBundle> {
return project.provider {
val conf = objects.newInstance(TwoStepMappingsConfiguration::class.java)
action.execute(conf)
if (conf.first == null) {
error("No version provided for first mapping step!")
}
if (conf.second == null) {
error("No version provided for second mapping step!")
}
mappingsName = "custom/two-step(${conf.first!!}, ${conf.second!!})"
val bundle = twoStepMappings(conf.first!!, conf.second!!)
targetNamespace = bundle.flattenData().dstNamespace
return@provider bundle
}
}
fun tinyMappings(action: Action<NameConfiguration>): Provider<MappingBundle> {
return project.provider {
val conf = objects.newInstance(NameConfiguration::class.java)
action.execute(conf)
if (conf.name == null) {
error("No maven coordinate provided for tiny mappings!")
}
mappingsName = "custom/tiny(${conf.name})"
val bundle = tinyMappings(conf.name!!)
targetNamespace = bundle.flattenData().dstNamespace
return@provider bundle
}
}
fun mappings(action: Action<MappingBundle>): Provider<MappingBundle> {
return project.provider {
val bundle = MappingBundle()
action.execute(bundle)
mappingsName = "custom(${bundle.srcNamespaces()}, ${bundle.dstNamespaces()})"
targetNamespace = bundle.flattenData().dstNamespace
return@provider bundle
}
}
fun layer(action: Action<LayerConfiguration>): Provider<MappingBundle> {
return project.provider {
val conf = objects.newInstance(LayerConfiguration::class.java)
action.execute(conf)
var back = MappingBundle()
var name = "layer["
val layers = conf.layers.mapIndexed { index, provider ->
val bundle = provider.get().flatten(true)
if (index == 0) {
name += mappingsName
back.insert(bundle)
return@mapIndexed bundle
}
name += ", $mappingsName"
back = back.flatten(false)
val prevDst = back.data[0].dstNamespace
val dst = bundle.data[0].dstNamespace
val remapped = MappingBundle.merge(bundle.reverse(), back)
.flatten(prevDst, dst)
.insert(back.docsForNamespace(dst))
back.insert(remapped)
return@mapIndexed remapped
}
mappingsName = "$name]"
val result = MappingBundle.merge(*layers.toTypedArray())
.flatten("official", layers.last().dstNamespaces()[0])
targetNamespace = result.data[0].dstNamespace
return@provider result
}
}
private fun twoStepMappings(intermediary: String, mappings: String): MappingBundle {
return MappingBundle.merge(tinyMappings(intermediary), tinyMappings(mappings))
}
private fun tinyMappings(name: String): MappingBundle {
mavenMappings(name).let { path ->
FileSystems.newFileSystem(path).use {
return TinyV2Parser.parse(it.getPath("mappings/mappings.tiny").readText(StandardCharsets.UTF_8))
}
}
}
@Suppress("UnstableApiUsage")
private fun mavenMappings(name: String): Path {
val configuration = project.configurations.create("mappings") {
it.isCanBeResolved = true
it.isCanBeDeclared = true
}
configuration.dependencies.add(project.dependencies.create(name))
val path = configuration.resolve().first().toPath()
project.configurations.remove(configuration)
return path
}
abstract class TwoStepMappingsConfiguration {
var first: String? = null
var second: String? = null
}
abstract class ParchmentConfiguration {
var gameVersion: String? = null
var version: String? = null
}
abstract class LayerConfiguration {
internal val layers = mutableListOf<Provider<MappingBundle>>()
fun add(mappings: Provider<MappingBundle>): LayerConfiguration {
layers.add(mappings)
return this
}
}
}
abstract class NameConfiguration {
var name: String? = null
}
abstract class VersionConfiguration {
var version: String? = null
}

View file

@ -1,25 +1,13 @@
package dev.frogmc.phytotelma.ext
import org.gradle.api.provider.Provider
import org.gradle.api.Action
@Suppress("unused")
interface PhytotelmaGradleExtension {
fun minecraft(version: String, parchmentGameVersion: String? = null, parchmentVersion: String? = null)
fun minecraft(action: Action<MinecraftConfiguration>)
fun minecraft(version: Provider<String>, parchmentGameVersion: Provider<String>? = null, parchmentVersion: Provider<String>? = null) {
minecraft(version.get(), parchmentGameVersion?.get(), parchmentVersion?.get())
}
fun loader(version: String)
fun loader(version: Provider<String>) {
loader(version.get())
}
fun froglib(version: String)
fun froglib(version: Provider<String>) {
froglib(version.get())
}
fun loader(action: Action<VersionConfiguration>)
fun froglib(action: Action<VersionConfiguration>)
}

View file

@ -1,105 +1,328 @@
package dev.frogmc.phytotelma.ext
import dev.frogmc.phytotelma.Constants
import dev.frogmc.phytotelma.PhytotelmaPlugin
import dev.frogmc.phytotelma.ProjectStorage
import dev.frogmc.phytotelma.VersionChecker
import com.electronwill.nightconfig.core.file.FileNotFoundAction
import com.electronwill.nightconfig.toml.TomlParser
import dev.frogmc.phytotelma.*
import dev.frogmc.phytotelma.accesswidener.AccessWidener
import dev.frogmc.phytotelma.build.PhytotelmaBuildTask
import dev.frogmc.phytotelma.common.Env
import dev.frogmc.phytotelma.mappings.renameDstNamespace
import dev.frogmc.phytotelma.run.AssetDownloader
import dev.frogmc.phytotelma.run.RunConfigGenerator
import dev.frogmc.phytotelma.run.task.RunGameTask
import dev.frogmc.phytotelma.vineflower.FrogJavadocProvider
import dev.frogmc.thyroxine.Thyroxine
import dev.frogmc.thyroxine.parser.ProguardParser
import dev.frogmc.thyroxine.api.Mapper
import dev.frogmc.thyroxine.api.data.MappingBundle
import dev.frogmc.thyroxine.provider.MojmapProvider
import dev.frogmc.thyroxine.provider.ParchmentProvider
import net.fabricmc.fernflower.api.IFabricJavadocProvider
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.tasks.Delete
import org.gradle.configurationcache.extensions.capitalized
import org.jetbrains.java.decompiler.main.Fernflower
import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger
import org.jetbrains.java.decompiler.main.decompiler.SingleFileSaver
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences
import java.io.OutputStream
import java.io.PrintStream
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import javax.inject.Inject
import kotlin.io.path.createParentDirectories
import kotlin.io.path.notExists
import kotlin.io.path.*
abstract class PhytotelmaGradleExtensionImpl : PhytotelmaGradleExtension {
abstract class PhytotelmaGradleExtensionImpl @Inject constructor(
private val project: Project,
private val objects: ObjectFactory
) : PhytotelmaGradleExtension {
@Inject
abstract fun getProject(): Project
private fun setupTasks() {
project.tasks.register("genSources") { task ->
task.group = Constants.TASK_GROUP
task.actions.add {
val projectData = ProjectStorage.get(it.project)
val fileName = projectData.remappedGameJarPath!!.fileName.toString()
val output =
projectData.remappedGameJarPath!!.resolveSibling(
fileName.substring(
0,
fileName.lastIndexOf("-")
) + "-sources.jar"
)
if (output.exists()) {
println("Output $output already exists, deleting!")
output.deleteExisting()
}
val options = mutableMapOf<String, Any>()
println("Preparing Parchment...")
val javadocs = ProjectStorage.get(project).mappings!!
options[IFabricJavadocProvider.PROPERTY_NAME] =
FrogJavadocProvider(javadocs.get(ProjectStorage.get(project).targetNamespace))
override fun minecraft(version: String, parchmentGameVersion: String?, parchmentVersion: String?) {
if (VersionChecker.validateVersion(version, offlineMode = getProject().gradle.startParameter.isOffline)) {
println("Setting up Minecraft...")
val parchmentGameVer = parchmentGameVersion?: version
val parchment =
parchmentVersion ?: kotlin.runCatching { ParchmentProvider.findForMinecraftVersion(parchmentGameVer) }
.getOrDefault("")
val projectData = ProjectStorage.get(getProject())
println("Decompiling...")
val logger = PrintStreamLogger(PrintStream(System.out))
options.putAll(
mapOf(
IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES to "1",
IFernflowerPreferences.BYTECODE_SOURCE_MAPPING to "1",
IFernflowerPreferences.REMOVE_SYNTHETIC to "1",
IFernflowerPreferences.LOG_LEVEL to "error",
IFernflowerPreferences.THREADS to Runtime.getRuntime().availableProcessors().toString(),
IFernflowerPreferences.INDENT_STRING to "\t"
)
)
val decomp = Fernflower(
SingleFileSaver(output.toFile()),
options, logger
)
decomp.addSource(projectData.remappedGameJarPath!!.toFile())
decomp.decompileContext()
}
}
project.tasks.register(Constants.GEN_RUN_CONFIGS_TASK) { task ->
task.group = Constants.TASK_GROUP
task.doFirst {
RunConfigGenerator.generate(project)
}
}
project.tasks.register(Constants.RUN_CLIENT_TASK, RunGameTask::class.java, Env.CLIENT)
project.tasks.register(Constants.RUNT_SERVER_TASK, RunGameTask::class.java, Env.SERVER)
project.tasks.register(Constants.DOWNLOAD_ASSETS_TASK) { task ->
task.group = Constants.TASK_GROUP
task.doFirst {
AssetDownloader.download(it.project, true)
}
}
val buildTask = project.tasks.register(Constants.BUILD_TASK, PhytotelmaBuildTask::class.java)
project.tasks.getByName(JavaBasePlugin.BUILD_TASK_NAME).dependsOn(buildTask)
project.tasks.getByName(JavaPlugin.JAR_TASK_NAME).actions.addLast { task ->
val storage = ProjectStorage.get(project)
if (storage.targetNamespace != "mojmap") {
val mappings = MappingBundle.merge(
storage.mappings!!.reverse(), MojmapProvider.get(
storage.minecraftVersion!!,
PhytotelmaPlugin.globalCacheDir.resolve("net/minecraft/client/${storage.minecraftVersion}/client-${storage.minecraftVersion}.txt")
).orElseThrow().reverse().renameDstNamespace("mojmap")
).forNamespaces(storage.targetNamespace, "mojmap")
val parser = TomlParser()
task.outputs.files.forEach { file ->
val temp = Files.createTempFile("", file.name)
Files.copy(file.toPath(), temp, StandardCopyOption.REPLACE_EXISTING)
FileSystems.newFileSystem(temp).use { fs ->
val metadata = fs.getPath(Constants.MOD_METADATA_FILE)
parser.parse(metadata, FileNotFoundAction.READ_NOTHING)
.get<String>("frog.extensions.accesswidener")?.let { name ->
val aw = metadata.resolveSibling(name)
AccessWidener.checkAW(aw, ProjectStorage.get(project).remappedGameJarPath!!)
val mapper = Mapper(mappings) { listOf() }
val buffer = buildString {
aw.forEachLine {
if (it.contains("\\t") && !it.startsWith("#")) {
val parts = it.split("[\\t #]+".toRegex()).toMutableList()
if (parts.size > 2) {
val type = parts[1]
when (type) {
"class" -> {
parts[2] = mapper.map(parts[2])
}
"fields" -> {
parts[3] = mapper.mapFieldName(parts[2], parts[3], parts[4])
parts[4] = mapper.mapDesc(parts[4])
parts[3] = mapper.map(parts[2])
}
"methods" -> {
parts[3] = mapper.mapMethodName(parts[2], parts[3], parts[4])
parts[4] = mapper.mapMethodDesc(parts[4])
parts[2] = mapper.map(parts[2])
}
}
appendLine(parts.joinToString("\\t"))
return@forEachLine
}
}
appendLine(it)
}
}
aw.writeText(buffer)
}
}
Thyroxine.remap(mappings, temp, file.toPath(), false, false)
Files.deleteIfExists(temp)
}
}
}
project.tasks.register(Constants.UPDATE_AW_TASK) { task ->
task.group = Constants.TASK_GROUP
task.actions.addFirst {
AccessWidener.apply(project, ProjectStorage.get(it.project).remappedGameJarPath!!)
}
}
project.tasks.register(Constants.CLEAR_LOCAL_CACHE_TASK, Delete::class.java) { task ->
task.group = Constants.TASK_GROUP
task.delete = setOf(ProjectStorage.get(project).localCacheDir)
}
project.tasks.register(Constants.CLEAR_GLOBAL_CACHE_TASK, Delete::class.java) { task ->
task.group = Constants.TASK_GROUP
task.delete = setOf(PhytotelmaPlugin.globalCacheDir)
}
project.afterEvaluate {
remapModDependencies()
}
}
private fun remapModDependencies() {
val out = System.out
// Mute the output from thyroxine as there may be a lot of remapping operations here
System.setOut(PrintStream(OutputStream.nullOutputStream()))
ModConfigurations.configurations.forEach { conf ->
val artifacts = project.configurations.getByName("mod" + conf.name.capitalized())
.resolvedConfiguration.resolvedArtifacts
if (artifacts.isEmpty()) {
return
}
val target = project.configurations.create("mod" + conf.name.capitalized() + "Mapped") { c ->
c.isTransitive = false
conf.classpathNames.forEach {
project.configurations.getByName(it).extendsFrom(c)
}
}
val storage = ProjectStorage.get(project)
val mappings = MappingBundle.merge(
storage.mappings!!.reverse(), MojmapProvider.get(
storage.minecraftVersion!!,
PhytotelmaPlugin.globalCacheDir.resolve("net/minecraft/client/${storage.minecraftVersion}/client-${storage.minecraftVersion}.txt")
).orElseThrow().reverse().renameDstNamespace("mojmap")
).forNamespaces("mojmap", storage.targetNamespace)
val targetPath = project.layout.buildDirectory.asFile.get().toPath().resolve("remappedMods")
.resolve("dev/frogmc/phytotelma/remapped_mods")
val remappedPaths = mutableListOf<Path>()
artifacts.forEach { artifact ->
val group = artifact.moduleVersion.id.group
val name = artifact.moduleVersion.id.name
val groupname = (group + "_" + name).replace(".", "_")
val version = artifact.moduleVersion.id.version
val classifier = artifact.classifier
val remappedPath = targetPath.resolve(groupname).resolve(version)
.resolve(groupname + "-" + version + (classifier?.let { "-$it" } ?: "") + ".jar")
remappedPath.createParentDirectories()
remappedPaths.add(remappedPath)
val pom = remappedPath.resolveSibling(remappedPath.fileName.toString().removeSuffix(".jar") + ".pom")
pom.writeText(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" +
"\t\t xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
"\t<modelVersion>4.0.0</modelVersion>\n" +
"\t<groupId>dev.frogmc.phytotelma.remapped_mods</groupId>\n" +
"\t<artifactId>$groupname</artifactId>\n" +
"\t<version>$version</version>\n" +
"</project>"
)
remappedPaths.add(pom)
Thyroxine.remap(mappings, artifact.file.toPath(), remappedPath, false, false)
project.dependencies.add(
target.name,
"dev.frogmc.phytotelma.remapped_mods:$groupname:$version" + (classifier?.let { ":$it" } ?: "")
)
}
}
System.setOut(out)
}
override fun minecraft(action: Action<MinecraftConfiguration>) {
setupTasks()
val mcConf = objects.newInstance(MinecraftConfiguration::class.java)
action.execute(mcConf)
if (mcConf.version == null) {
error("No Minecraft version provided!")
}
val version = mcConf.version!!
if (VersionChecker.validateVersion(version, offlineMode = project.gradle.startParameter.isOffline)) {
val projectData = ProjectStorage.get(project)
projectData.minecraftVersion = version
projectData.parchmentVersion = parchment
println("Valid version! $version")
val clientJar = VersionChecker.downloadClient(getProject(), version)
val mappings = mcConf.mappings.get()
println("Using mappings: " + mcConf.mappingsName)
projectData.mappings = mappings
val clientJar = VersionChecker.downloadClient(project, version)
val remappedJar =
projectData.localCacheDir!!.resolve("net/minecraft/client/$version/client-$version-remapped.jar")
remappedJar.createParentDirectories()
projectData.remappedGameJarPath = remappedJar
println("Time to setup Minecraft!")
val applyAW = AccessWidener.needsUpdate(getProject())
if (remappedJar.notExists() || applyAW) {
val applyAW = AccessWidener.needsUpdate(project)
if (remappedJar.notExists() || applyAW || mcConf.mappingsName != projectData.mappingsName) {
projectData.mappingsName = mcConf.mappingsName
println("Remapping the game...")
val data = kotlin.runCatching {
ProguardParser.read(
MojmapProvider.get(
version,
clientJar.resolveSibling("client-$version.txt")
).orElseThrow()
).reverse()
}.getOrNull()
val paramMappings = if (parchment.isNotEmpty()) kotlin.runCatching {
ParchmentProvider.getParchment(
parchmentGameVer, parchment,
PhytotelmaPlugin.globalCacheDir.resolve("org/parchmentmc/parchment/$parchmentGameVer/$parchment"),
getProject().gradle.startParameter.isRefreshDependencies
projectData.targetNamespace = mcConf.targetNamespace
Thyroxine.remap(
mappings.forNamespaces("official", mcConf.targetNamespace),
clientJar, remappedJar, true, true
)
}.getOrNull() else {
println("Parameter mappings will not be present as no parchment version was found. It may be possible to declare it manually.")
null
}
if (paramMappings == null && parchment.isNotEmpty()) {
println("Parameter mappings will not be present as the version $parchmentVersion for minecraft version $parchmentGameVer could not be found")
}
if (data != null) {
Thyroxine.remap(data, clientJar, remappedJar, true, paramMappings)
} else {
error("Failed to remap the game as no mojmap version was found for game version $version. Other mapping formats may be implemented in the future.")
}
}
println("Adding dependencies...")
getProject().dependencies.add(
project.dependencies.add(
Constants.MINECRAFT_CONFIGURATION,
"net.minecrell:terminalconsoleappender:1.2.0"
)
VersionChecker.getDependencies(version) {
getProject().dependencies.add(Constants.MINECRAFT_CONFIGURATION, it)
project.dependencies.add(Constants.MINECRAFT_CONFIGURATION, it)
}
VersionChecker.savePomFile(version, remappedJar.parent)
getProject().dependencies.add(Constants.MINECRAFT_CONFIGURATION, "net.minecraft:client:$version:remapped")
project.dependencies.add(Constants.MINECRAFT_CONFIGURATION, "net.minecraft:client:$version:remapped")
println("Generating run configurations...")
RunConfigGenerator.generate(getProject())
RunConfigGenerator.generate(project)
AssetDownloader.download(project)
if (applyAW) {
getProject().afterEvaluate {
project.afterEvaluate {
println("Applying AccessWideners...")
AccessWidener.apply(getProject(), remappedJar)
AccessWidener.apply(project, remappedJar)
}
}
}
}
println("Done!")
} else {
println("Invalid version! $version")
error("Invalid minecraft version provided: $version")
override fun loader(action: Action<VersionConfiguration>) {
val conf = objects.newInstance(VersionConfiguration::class.java)
action.execute(conf)
if (conf.version == null) {
error("No loader version provided!")
}
project.dependencies.add("implementation", "dev.frogmc:frogloader:${conf.version!!}")
}
override fun loader(version: String) {
getProject().dependencies.add("implementation", "dev.frogmc:frogloader:$version")
override fun froglib(action: Action<VersionConfiguration>) {
val conf = objects.newInstance(VersionConfiguration::class.java)
action.execute(conf)
if (conf.version == null) {
error("No froglib version provided!")
}
override fun froglib(version: String) {
getProject().dependencies.add("implementation", "dev.frogmc:froglib:$version")
project.dependencies.add("modImplementation", "dev.frogmc:froglib:${conf.version!!}")
}
}

View file

@ -0,0 +1,48 @@
package dev.frogmc.phytotelma.mappings
import dev.frogmc.thyroxine.api.data.DocumentationData
import dev.frogmc.thyroxine.api.data.MappingBundle
import dev.frogmc.thyroxine.api.data.MappingData
fun MappingBundle.renameNamespaces(newSrc: String?, newDst: String): MappingBundle {
val oldData = if (srcNamespaces().size > 1 || dstNamespaces().size > 1) flattenData() else data[0]
val newData = MappingData(newSrc ?: oldData.srcNamespace, newDst)
val newDocs = DocumentationData(newDst)
oldData.apply {
newData.classes.putAll(classes)
newData.fields.putAll(fields)
newData.methods.putAll(methods)
newData.parameters.putAll(parameters)
}
return MappingBundle(newData, get(oldData.dstNamespace)?.let { newDocs.insert(it) })
}
fun MappingBundle.renameDstNamespace(newDst: String): MappingBundle {
return renameNamespaces(null, newDst)
}
fun MappingBundle.filterClasses(filter: (String) -> Boolean): MappingBundle {
val oldData = data.last()
val newData = MappingData(oldData.srcNamespace, oldData.dstNamespace)
val oldDocs = if (documentation.isNotEmpty()) documentation.last() else null
oldData.apply {
newData.classes.putAll(classes.filter { filter.invoke(it.value) })
newData.fields.putAll(fields.filter { newData.classes.containsKey(it.key.owner) })
newData.methods.putAll(methods.filter { newData.classes.containsKey(it.key.owner) })
newData.parameters.putAll(parameters.filter { newData.classes.containsKey(it.key.owner) })
}
val bundle = MappingBundle(data.subList(0, data.size - 1).plus(newData), mutableListOf())
if (oldDocs != null) {
val newDocs = DocumentationData(newData.dstNamespace)
newDocs.classes.addAll(oldDocs.classes.filter { filter.invoke(it.name) })
bundle.insert(newDocs)
}
return bundle
}

View file

@ -9,43 +9,59 @@ import dev.frogmc.phytotelma.common.CachingHttpClient
import org.gradle.api.Project
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.notExists
import kotlin.io.path.reader
object AssetDownloader {
private const val ASSETS_URL = "https://resources.download.minecraft.net"
fun download(project: Project) {
fun download(project: Project, manualInvocation: Boolean = false) {
val version = ProjectStorage.get(project).minecraftVersion!!
val path = PhytotelmaPlugin.globalCacheDir.resolve("assets")
val id = VersionChecker.downloadAssetIndex(version, path.resolve("indexes"))
val assetIndex = VersionChecker.fetchVersionData(version).assetIndex
val dest = path.resolve("indexes").resolve(assetIndex.id + ".json")
var overwrite = manualInvocation
if (dest.notExists()) {
CachingHttpClient.getUncached(URI.create(assetIndex.url)).use {
Files.copy(it, dest)
}
overwrite = true
}
val index = Gson().fromJson(
path.resolve("indexes").resolve("$id.json").reader(StandardCharsets.UTF_8),
path.resolve("indexes").resolve("${assetIndex.id}.json").reader(StandardCharsets.UTF_8),
JsonObject::class.java
)
val objectsPath = path.resolve("objects")
objectsPath.createDirectories()
val entries = index.getAsJsonObject("objects").entrySet()
val totalSize = entries.size
if (overwrite) {
print("Downloading $totalSize assets... ()")
System.out.flush()
}
entries.forEachIndexed { i, it ->
val hash = it.value.asJsonObject.get("hash").asString
/*val size = it.value.asJsonObject.get("size").asInt
val name = it.key*/
// TODO asset downloading for versions <=1.7 (legacy)
if (overwrite) {
print("\rDownloading $totalSize assets... ($i/$totalSize): $hash" + " ".repeat(30))
System.out.flush()
get(objectsPath, hash)
}
get(objectsPath, hash, overwrite)
}
if (overwrite) {
println("\rDownloading $totalSize assets... Done!" + " ".repeat(50))
}
}
private fun get(localDir: Path, hash: String) {
private fun get(localDir: Path, hash: String, overwrite: Boolean) {
val shortHash = hash.substring(0, 2)
val path = localDir.resolve("$shortHash/$hash")
CachingHttpClient.downloadTo(URI.create("$ASSETS_URL/$shortHash/$hash"), path, true)
CachingHttpClient.downloadTo(URI.create("$ASSETS_URL/$shortHash/$hash"), path, overwrite)
}
}

View file

@ -34,7 +34,7 @@ object RunConfigGenerator {
assetIndexPath.createDirectories()
}
val projectData = ProjectStorage.get(project)
val indexId = VersionChecker.downloadAssetIndex(projectData.minecraftVersion!!, assetIndexPath)
val indexId = VersionChecker.fetchVersionData(projectData.minecraftVersion!!).assetIndex.id
val projectName = if (project.rootDir == project.projectDir) "" else " (${project.name})"

View file

@ -29,7 +29,7 @@ abstract class RunGameTask @Inject constructor(env: Env) : JavaExec() {
if (assetIndexPath.notExists()) {
assetIndexPath.createDirectories()
}
val indexId = VersionChecker.downloadAssetIndex(ProjectStorage.get(project).minecraftVersion!!, assetIndexPath)
val indexId = VersionChecker.fetchVersionData(ProjectStorage.get(project).minecraftVersion!!).assetIndex.id
val log4jPath = project.rootDir.resolve(".gradle/phytotelma/log4j.xml").toPath().absolute()
if (log4jPath.notExists()) {
log4jPath.createParentDirectories()

View file

@ -0,0 +1,28 @@
package dev.frogmc.phytotelma.vineflower
import dev.frogmc.thyroxine.api.data.DocumentationData
import net.fabricmc.fernflower.api.IFabricJavadocProvider
import org.jetbrains.java.decompiler.struct.StructClass
import org.jetbrains.java.decompiler.struct.StructField
import org.jetbrains.java.decompiler.struct.StructMethod
import java.util.stream.Collectors
class FrogJavadocProvider(data: DocumentationData) : IFabricJavadocProvider {
private val classMap = data.classes.stream().collect(Collectors.toMap({ it.name() }, { it }))
override fun getClassDoc(structClass: StructClass): String? {
return classMap[structClass.qualifiedName]?.javadoc?.joinToString("\n")
}
override fun getFieldDoc(structClass: StructClass, structField: StructField): String? {
return classMap[structClass.qualifiedName]
?.getField(structField.name, structField.descriptor)
?.orElse(null)?.javadoc?.joinToString("\n")
}
override fun getMethodDoc(structClass: StructClass, structMethod: StructMethod): String? {
return classMap[structClass.qualifiedName]?.getMethod(structMethod.name, structMethod.descriptor)
?.orElse(null)?.javadoc?.joinToString("\n")
}
}

View file

@ -1,25 +0,0 @@
package dev.frogmc.phytotelma.vineflower
import dev.frogmc.thyroxine.api.data.Parchment
import net.fabricmc.fernflower.api.IFabricJavadocProvider
import org.jetbrains.java.decompiler.struct.StructClass
import org.jetbrains.java.decompiler.struct.StructField
import org.jetbrains.java.decompiler.struct.StructMethod
class ParchmentJavadocProvider(private val parchment: Parchment) : IFabricJavadocProvider {
override fun getClassDoc(structClass: StructClass): String? {
return parchment.getClass(structClass.qualifiedName).orElse(null)?.javadoc?.joinToString("\n")
}
override fun getFieldDoc(structClass: StructClass, structField: StructField): String? {
return parchment.getClass(structClass.qualifiedName)
.flatMap { it.getField(structField.name, structField.descriptor) }
.orElse(null)?.javadoc?.joinToString("\n")
}
override fun getMethodDoc(structClass: StructClass, structMethod: StructMethod): String? {
return parchment.getClass(structClass.qualifiedName)
.flatMap { it.getMethod(structMethod.name, structMethod.descriptor) }
.orElse(null)?.javadoc?.joinToString("\n")
}
}