summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2023-12-14 18:11:17 +0100
committerLinnea Gräf <nea@nea.moe>2023-12-14 18:11:17 +0100
commita453cc833f52cf1ccd52288edb73c6f0fbc0c7cc (patch)
tree27704f77968903b7fb38debf42cefda7ed85b705
parent9058126b0d77c315436226a589ade0748d8a2905 (diff)
downloadarchenemy-a453cc833f52cf1ccd52288edb73c6f0fbc0c7cc.tar.gz
archenemy-a453cc833f52cf1ccd52288edb73c6f0fbc0c7cc.tar.bz2
archenemy-a453cc833f52cf1ccd52288edb73c6f0fbc0c7cc.zip
Add support for jar merging
-rw-r--r--archenemyexample/build.gradle.kts18
-rw-r--r--archenemyexample/src/fabricMain/kotlin/doStuff.kt2
-rw-r--r--plugin/build.gradle.kts6
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt69
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt23
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt7
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt508
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt98
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt8
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt9
-rw-r--r--plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt33
11 files changed, 776 insertions, 5 deletions
diff --git a/archenemyexample/build.gradle.kts b/archenemyexample/build.gradle.kts
index fae9a49..7ae9646 100644
--- a/archenemyexample/build.gradle.kts
+++ b/archenemyexample/build.gradle.kts
@@ -9,7 +9,8 @@ repositories {
mavenCentral()
maven("https://maven.fabricmc.net")
}
-val minecraft = mojang.minecraft("1.20.2", MCSide.CLIENT) as ModuleDependency
+val minecraftClient = mojang.minecraft("1.20.2", MCSide.CLIENT) as ModuleDependency
+val minecraftServer = mojang.minecraft("1.20.2", MCSide.CLIENT) as ModuleDependency
val officialMappings = mojang.officialMappings(
"1.20.2", MCSide.CLIENT
)
@@ -30,7 +31,7 @@ kotlin {
this.dependencies {
implementation(
mojang.mapJar(
- minecraft,
+ minecraftClient,
officialMappings,
"official",
"named"
@@ -44,12 +45,21 @@ kotlin {
compilations.named("main").get().run {
defaultSourceSet.dependsOn(allJvm)
this.dependencies {
- val thingy = mojang.mapJar(
- minecraft,
+ val intermediaryClient = mojang.mapJar(
+ minecraftClient,
intermediaryMappings,
"official",
"intermediary"
)
+ val intermediaryServer = mojang.mapJar(
+ minecraftServer,
+ intermediaryMappings,
+ "official",
+ "intermediary"
+ )
+ val thingy = mojang.mergeJar(
+ intermediaryClient, intermediaryServer
+ )
implementation(
mojang.mapJar(
thingy as ModuleDependency,
diff --git a/archenemyexample/src/fabricMain/kotlin/doStuff.kt b/archenemyexample/src/fabricMain/kotlin/doStuff.kt
index d88a17e..fc163c2 100644
--- a/archenemyexample/src/fabricMain/kotlin/doStuff.kt
+++ b/archenemyexample/src/fabricMain/kotlin/doStuff.kt
@@ -1,5 +1,7 @@
import net.minecraft.client.MinecraftClient
+import net.minecraft.server.dedicated.DedicatedServer
actual fun doStuff(args: Int) {
val client = MinecraftClient.getInstance()
+ val dedicated: DedicatedServer = TODO()
} \ No newline at end of file
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts
index f3d7c28..35f1832 100644
--- a/plugin/build.gradle.kts
+++ b/plugin/build.gradle.kts
@@ -18,6 +18,12 @@ dependencies {
implementation("net.fabricmc:mapping-io:0.1.8")
implementation("net.fabricmc:tiny-remapper:0.8.6")
+ implementation("org.ow2.asm:asm:9.5")
+ implementation("org.ow2.asm:asm-commons:9.5")
+ implementation("org.ow2.asm:asm-tree:9.5")
+ implementation("org.ow2.asm:asm-analysis:9.5")
+ implementation("org.ow2.asm:asm-util:9.5")
+
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt
new file mode 100644
index 0000000..601cb19
--- /dev/null
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt
@@ -0,0 +1,69 @@
+package moe.nea.archenemy.mojang
+
+import moe.nea.archenemy.util.DownloadUtils
+import moe.nea.archenemy.util.getNullsafeIdentifier
+import net.minecraftforge.artifactural.api.artifact.Artifact
+import net.minecraftforge.artifactural.api.artifact.ArtifactIdentifier
+import net.minecraftforge.artifactural.api.artifact.Streamable
+import net.minecraftforge.artifactural.api.repository.Repository
+import org.gradle.api.artifacts.Dependency
+import java.io.File
+import java.security.MessageDigest
+
+abstract class AbstractTransformerRepository<T : CHashable>(
+ val repoIdentifier: String,
+ val extension: ArchenemyMojangExtension,
+) : Repository {
+ init {
+ require(!repoIdentifier.contains("."))
+ }
+
+ data class HashContainer<T : CHashable>(val t: T) {
+ val transformerHash = kotlin.run {
+ val digest = MessageDigest.getInstance("SHA-256")
+ t.updateHash(digest)
+ DownloadUtils.bytesToHex(digest.digest())
+ }
+ }
+
+ fun resolveDirect(vararg dependency: Dependency): Set<File> {
+ return extension.project.configurations.detachedConfiguration(*dependency).also { it.isTransitive = false }
+ .resolve()
+ }
+
+ fun lazyTransform(file: File, function: (file: File) -> Unit): Streamable {
+ return Streamable {
+ if (!file.exists()) function(file)
+ file.inputStream()
+ }
+ }
+
+ private val providers: MutableMap<String, HashContainer<T>> = mutableMapOf()
+ val baseDirectory = extension.getLocalCacheDirectory().resolve("precache-$repoIdentifier")
+ fun getCoordinate(t: T): String {
+ val container = HashContainer(t)
+ providers[container.transformerHash] = container
+ val c = getClassifier(t)
+ return "archenemy.$repoIdentifier.${container.transformerHash}:${getName(t)}:${getVersion(t)}" + if (c != null) ":$c" else ""
+ }
+
+ protected abstract fun getName(t: T): String
+ protected abstract fun getVersion(t: T): String
+ protected open fun getClassifier(t: T): String? = null
+ override fun getArtifact(identifier: ArtifactIdentifier?): Artifact {
+ if (identifier == null) return Artifact.none()
+ if (!identifier.group.startsWith("archenemy.${this.repoIdentifier}.")) return Artifact.none()
+ val hash = identifier.group.split(".")[2]
+ val provider = providers[hash] ?: error("Unregistered archenemy ${this.repoIdentifier} dependeny")
+ val coordinate = getCoordinate(provider.t)
+ require(coordinate.startsWith(identifier.group + ":" + identifier.name + ":" + identifier.version))
+ if (identifier.extension == "pom") return Artifact.none()
+ return getArtifact(
+ getNullsafeIdentifier(identifier),
+ provider.t,
+ baseDirectory.resolve("$hash-${identifier.classifier}.${identifier.extension}")
+ ) ?: Artifact.none()
+ }
+
+ protected abstract fun getArtifact(identifier: ArtifactIdentifier, value: T, file: File): Artifact?
+} \ No newline at end of file
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt
index 8125900..e1a8bfe 100644
--- a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt
@@ -21,9 +21,15 @@ abstract class ArchenemyMojangExtension(val project: Project) {
GradleRepositoryAdapter.add(
project.repositories,
"Minecraft Mapped Provider",
- getLocalCacheDirectory().resolve("minecraft-transformation-provider"),
+ getLocalCacheDirectory().resolve("minecraft-mapped-provider"),
mappedRepositoryProvider
)
+ GradleRepositoryAdapter.add(
+ project.repositories,
+ "Minecraft Merged Provider",
+ getLocalCacheDirectory().resolve("minecraft-merged-provider"),
+ mergedRepositoryProvider
+ )
project.repositories.maven {
it.name = "Minecraft Libraries"
it.url = URI("https://libraries.minecraft.net/")
@@ -31,6 +37,7 @@ abstract class ArchenemyMojangExtension(val project: Project) {
}
private val mappedRepositoryProvider = MappedRepositoryProvider(this)
+ private val mergedRepositoryProvider = MergedRepositoryProvider(this)
fun yarnMappings(dependency: Dependency): MappingDependency {
dependency as ModuleDependency
@@ -56,6 +63,20 @@ abstract class ArchenemyMojangExtension(val project: Project) {
return yarnMappings(project.dependencies.create("net.fabricmc:intermediary:$version:v2"))
}
+ fun mergeJar(
+ base: Dependency,
+ overlay: Dependency,
+ ): Dependency {
+ base as ModuleDependency
+ overlay as ModuleDependency
+ _registerMinecraftProvider
+ return project.dependencies.create(
+ mergedRepositoryProvider.getCoordinate(
+ MergedRepositoryProvider.Coordinate(base, overlay)
+ )
+ )
+ }
+
fun mapJar(
dependency: Dependency,
mappings: MappingDependency,
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt
new file mode 100644
index 0000000..bfac9c7
--- /dev/null
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt
@@ -0,0 +1,7 @@
+package moe.nea.archenemy.mojang
+
+import java.security.MessageDigest
+
+interface CHashable {
+ fun updateHash(digest: MessageDigest)
+}
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt
new file mode 100644
index 0000000..655cf85
--- /dev/null
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt
@@ -0,0 +1,508 @@
+package moe.nea.archenemy.mojang
+
+import moe.nea.archenemy.util.EnvType
+import org.objectweb.asm.Attribute
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.tree.*
+
+// One to one copied from unimined. Definitely needs improvement to be better suited to my needs, but for now this will do and is much better than writing a barebones thing myself.
+class ClassMerger(
+ val toMergedClass: (ClassNode, EnvType) -> Unit = { _, _ -> },
+ val toMergedFields: (FieldNode, EnvType) -> Unit = { _, _ -> },
+ val toMergedMethods: (MethodNode, EnvType) -> Unit = { _, _ -> }
+) {
+ fun accept(client: ClassNode?, server: ClassNode?): ClassNode {
+ if (client == null) {
+ if (server!!.access and Opcodes.ACC_INTERFACE == 0) {
+ toMergedClass(server, EnvType.SERVER)
+ return server
+ } else {
+ // make interface methods & fields sided
+ server.methods?.forEach { toMergedMethods(it, EnvType.SERVER) }
+ server.fields?.forEach { toMergedFields(it, EnvType.SERVER) }
+ return server
+ }
+ }
+ if (server == null) {
+ if (client.access and Opcodes.ACC_INTERFACE == 0) {
+ toMergedClass(client, EnvType.CLIENT)
+ return client
+ } else {
+ // make interface methods & fields sided
+ client.methods?.forEach { toMergedMethods(it, EnvType.CLIENT) }
+ client.fields?.forEach { toMergedFields(it, EnvType.CLIENT) }
+ return client
+ }
+ }
+ if (!areClassMetadataEqual(client, server)) {
+ throw IllegalArgumentException("Class metadata is not equal!")
+ }
+ // weaker access
+ val merged = ClassNode()
+ merged.version = client.version.coerceAtLeast(server.version)
+ merged.access = selectWeakerAccess(client.access, server.access)
+ merged.name = client.name
+ merged.signature = client.signature
+ merged.superName = client.superName
+ merged.interfaces = client.interfaces?.toMutableSet()?.apply { addAll(server.interfaces ?: setOf()) }?.toList()
+ ?: server.interfaces
+ merged.sourceFile = client.sourceFile
+ merged.sourceDebug = client.sourceDebug
+ merged.module = client.module
+ merged.outerClass = client.outerClass
+ merged.outerMethod = client.outerMethod
+ merged.outerMethodDesc = client.outerMethodDesc
+ // merge client and server annotations
+ merged.visibleAnnotations = client.visibleAnnotations?.toMutableList()?.apply {
+ for (a in server.visibleAnnotations ?: listOf()) {
+ if (!any { areAnnotationNodesEqual(it, a) }) {
+ add(a)
+ }
+ }
+ } ?: server.visibleAnnotations
+ merged.invisibleAnnotations = client.invisibleAnnotations?.toMutableList()?.apply {
+ for (a in server.invisibleAnnotations ?: listOf()) {
+ if (!any { areAnnotationNodesEqual(it, a) }) {
+ add(a)
+ }
+ }
+ } ?: server.invisibleAnnotations
+ merged.visibleTypeAnnotations = client.visibleTypeAnnotations?.toMutableList()?.apply {
+ for (a in server.visibleTypeAnnotations ?: listOf()) {
+ if (!any { areAnnotationNodesEqual(it, a) }) {
+ add(a)
+ }
+ }
+ } ?: server.visibleTypeAnnotations
+ merged.invisibleTypeAnnotations = client.invisibleTypeAnnotations?.toMutableList()?.apply {
+ for (a in server.invisibleTypeAnnotations ?: listOf()) {
+ if (!any { areAnnotationNodesEqual(it, a) }) {
+ add(a)
+ }
+ }
+ } ?: server.invisibleTypeAnnotations
+ merged.attrs = client.attrs?.toMutableList()?.apply {
+ for (a in server.attrs ?: listOf()) {
+ if (!any { areAttributesEqual(it, a) }) {
+ add(a)
+ }
+ }
+ } ?: server.attrs
+ // merge inner classes
+ merged.innerClasses = client.innerClasses?.toMutableList()?.apply {
+ for (a in server.innerClasses ?: listOf()) {
+ if (!any { areInnerClassNodesEqual(it, a) }) {
+ add(a)
+ }
+ }
+ } ?: server.innerClasses
+ merged.nestHostClass = client.nestHostClass
+ merged.nestMembers = client.nestMembers?.toMutableSet()
+ ?.apply { addAll(server.nestMembers ?: setOf()) }
+ ?.toList() ?: server.nestMembers
+ merged.permittedSubclasses = client.permittedSubclasses?.toMutableSet()
+ ?.apply { addAll(server.permittedSubclasses ?: setOf()) }
+ ?.toList() ?: server.permittedSubclasses
+ // merge record components
+ merged.recordComponents = client.recordComponents?.toMutableList()?.apply {
+ for (a in server.recordComponents ?: listOf()) {
+ if (!any { areRecordComponentNodesEqual(it, a) }) {
+ add(a)
+ }
+ }
+ } ?: server.recordComponents
+
+ // merge fields
+ val fields = client.fields.map { it to EnvType.CLIENT }.toMutableList()
+ outer@ for (field in server.fields) {
+ for (f in fields) {
+ if (areFieldNodesEqual(f.first, field)) {
+ fields.remove(f)
+ field.access = selectWeakerAccess(f.first.access, field.access)
+ fields.add(field to EnvType.COMBINED)
+ continue@outer
+ }
+ }
+ fields.add(field to EnvType.SERVER)
+ }
+ fields.forEach { toMergedFields(it.first, it.second) }
+ merged.fields = mutableListOf()
+ for (f in fields) {
+ merged.fields.add(f.first)
+ }
+
+ // merge methods
+ val methods = client.methods.map { it to EnvType.CLIENT }.toMutableList()
+ outer@ for (method in server.methods) {
+ for (m in methods) {
+ if (areMethodNodesEqual(m.first, method)) {
+ methods.remove(m)
+ method.access = selectWeakerAccess(m.first.access, method.access)
+ methods.add(method to EnvType.COMBINED)
+ continue@outer
+ }
+ }
+ methods.add(method to EnvType.SERVER)
+ }
+ methods.forEach { toMergedMethods(it.first, it.second) }
+ merged.methods = mutableListOf()
+ for (m in methods) {
+ merged.methods.add(m.first)
+ }
+ toMergedClass(merged, EnvType.COMBINED)
+ return merged
+ }
+
+ companion object {
+ fun areFieldNodesEqual(a: FieldNode, b: FieldNode): Boolean {
+ if (a.name != b.name) return false
+ if (a.desc != b.desc) return false
+ if (a.value != b.value) return false
+ // check static part of access
+ if (a.access and Opcodes.ACC_STATIC != b.access and Opcodes.ACC_STATIC) return false
+ val aVisibleAnnotations = a.visibleAnnotations?.toMutableList() ?: mutableListOf()
+ val bVisibleAnnotations = b.visibleAnnotations?.toMutableList() ?: mutableListOf()
+ if (aVisibleAnnotations.size != bVisibleAnnotations.size) return false
+ aVisibleAnnotations.removeIf {
+ bVisibleAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bVisibleAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aVisibleAnnotations.isNotEmpty()) return false
+ if (bVisibleAnnotations.isNotEmpty()) return false
+ val aInvisibleAnnotations = a.invisibleAnnotations?.toMutableList() ?: mutableListOf()
+ val bInvisibleAnnotations = b.invisibleAnnotations?.toMutableList() ?: mutableListOf()
+ if (aInvisibleAnnotations.size != bInvisibleAnnotations.size) return false
+ aInvisibleAnnotations.removeIf {
+ bInvisibleAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bInvisibleAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aInvisibleAnnotations.isNotEmpty()) return false
+ if (bInvisibleAnnotations.isNotEmpty()) return false
+ val aVisibleTypeAnnotations = a.visibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ val bVisibleTypeAnnotations = b.visibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ if (aVisibleTypeAnnotations.size != bVisibleTypeAnnotations.size) return false
+ aVisibleTypeAnnotations.removeIf {
+ bVisibleTypeAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bVisibleTypeAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aVisibleTypeAnnotations.isNotEmpty()) return false
+ if (bVisibleTypeAnnotations.isNotEmpty()) return false
+ val aInvisibleTypeAnnotations = a.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ val bInvisibleTypeAnnotations = b.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ if (aInvisibleTypeAnnotations.size != bInvisibleTypeAnnotations.size) return false
+ aInvisibleTypeAnnotations.removeIf {
+ bInvisibleTypeAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bInvisibleTypeAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aInvisibleTypeAnnotations.isNotEmpty()) return false
+ if (bInvisibleTypeAnnotations.isNotEmpty()) return false
+ val aAttributes = a.attrs?.toMutableList() ?: mutableListOf()
+ val bAttributes = b.attrs?.toMutableList() ?: mutableListOf()
+ if (aAttributes.size != bAttributes.size) return false
+ aAttributes.removeIf {
+ bAttributes.any { it2 ->
+ if (areAttributesEqual(it, it2)) {
+ bAttributes.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aAttributes.isNotEmpty()) return false
+ if (bAttributes.isNotEmpty()) return false
+ return true
+ }
+
+ fun areMethodNodesEqual(a: MethodNode, b: MethodNode): Boolean {
+ if (a.name != b.name) return false
+ if (a.desc != b.desc) return false
+ // check static part of access
+ if (a.access and Opcodes.ACC_STATIC != b.access and Opcodes.ACC_STATIC) return false
+// val aParameters = a.parameters?.toMutableList() ?: mutableListOf()
+// val bParameters = b.parameters?.toMutableList() ?: mutableListOf()
+// if (aParameters.size != bParameters.size) return false
+// aParameters.removeIf {
+// bParameters.any { it2 ->
+// areParamsEqual(it, it2)
+// }
+// }
+// if (aParameters.isNotEmpty()) return false
+ val aVisibleAnnotations = a.visibleAnnotations?.toMutableList() ?: mutableListOf()
+ val bVisibleAnnotations = b.visibleAnnotations?.toMutableList() ?: mutableListOf()
+ if (aVisibleAnnotations.size != bVisibleAnnotations.size) return false
+ aVisibleAnnotations.removeIf {
+ bVisibleAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bVisibleAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aVisibleAnnotations.isNotEmpty()) return false
+ if (bVisibleAnnotations.isNotEmpty()) return false
+ val aInvisibleAnnotations = a.invisibleAnnotations?.toMutableList() ?: mutableListOf()
+ val bInvisibleAnnotations = b.invisibleAnnotations?.toMutableList() ?: mutableListOf()
+ if (aInvisibleAnnotations.size != bInvisibleAnnotations.size) return false
+ aInvisibleAnnotations.removeIf {
+ bInvisibleAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bInvisibleAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aInvisibleAnnotations.isNotEmpty()) return false
+ if (bInvisibleAnnotations.isNotEmpty()) return false
+ val aVisibleTypeAnnotations = a.visibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ val bVisibleTypeAnnotations = b.visibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ if (aVisibleTypeAnnotations.size != bVisibleTypeAnnotations.size) return false
+ aVisibleTypeAnnotations.removeIf {
+ bVisibleTypeAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bVisibleTypeAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aVisibleTypeAnnotations.isNotEmpty()) return false
+ if (bVisibleTypeAnnotations.isNotEmpty()) return false
+ val aInvisibleTypeAnnotations = a.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ val bInvisibleTypeAnnotations = b.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ if (aInvisibleTypeAnnotations.size != bInvisibleTypeAnnotations.size) return false
+ aInvisibleTypeAnnotations.removeIf {
+ bInvisibleTypeAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bInvisibleTypeAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aInvisibleTypeAnnotations.isNotEmpty()) return false
+ if (bInvisibleTypeAnnotations.isNotEmpty()) return false
+ val aAttributes = a.attrs?.toMutableList() ?: mutableListOf()
+ val bAttributes = b.attrs?.toMutableList() ?: mutableListOf()
+ if (aAttributes.size != bAttributes.size) return false
+ aAttributes.removeIf {
+ bAttributes.any { it2 ->
+ if (areAttributesEqual(it, it2)) {
+ bAttributes.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aAttributes.isNotEmpty()) return false
+ if (bAttributes.isNotEmpty()) return false
+
+ // check content
+ if (a.instructions.size() != b.instructions.size()) return false
+ val aInstructions = a.instructions
+ val bInstructions = b.instructions
+ for (i in 0 until aInstructions.size()) {
+ if (!areInstructionsEqual(
+ aInstructions[i],
+ bInstructions[i]
+ )
+ ) throw IllegalStateException("Instructions are not equal: ${aInstructions[i]} != ${bInstructions[i]} at index $i in ${a.name} ${a.desc} (${a.instructions.size()} instructions) and ${b.name} ${b.desc} (${b.instructions.size()} instructions)")
+ }
+
+ return true
+ }
+
+ fun areAnnotationNodesEqual(a: AnnotationNode, b: AnnotationNode): Boolean {
+ return a.desc == b.desc // TODO: check values, maybe?
+ }
+
+ fun areAttributesEqual(a: Attribute, b: Attribute): Boolean {
+ return a.type == b.type // TODO: check values, maybe?
+ }
+
+ fun areInstructionsEqual(a: AbstractInsnNode, b: AbstractInsnNode): Boolean {
+ if (a.opcode != b.opcode) return false
+ if (a.type != b.type) return false
+ return true // TODO: contents of instructions
+ }
+
+ fun selectWeakerAccess(a: Int, b: Int): Int {
+ if (a == b) return a
+ // should be final
+ var access = a and Opcodes.ACC_FINAL.inv()
+ if (a and Opcodes.ACC_FINAL != 0 && b and Opcodes.ACC_FINAL != 0) {
+ access = access or Opcodes.ACC_FINAL
+ }
+
+ val aAccess = Access.from(a)
+ val bAccess = Access.from(b)
+ access = access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED or Opcodes.ACC_PRIVATE).inv()
+ access = access or Access.max(aAccess, bAccess).value
+
+ // all other flags must be the same
+ val other = (Opcodes.ACC_FINAL or Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED or Opcodes.ACC_PRIVATE).inv()
+ if ((a and other) != (b and other)) {
+ throw IllegalStateException("Other access is not the same: ${a.toString(16)} != ${b.toString(16)}")
+ }
+ return access
+ }
+
+ fun areClassMetadataEqual(a: ClassNode, b: ClassNode): Boolean {
+ // require equal static
+ if (a.access and Opcodes.ACC_STATIC != b.access and Opcodes.ACC_STATIC) return false
+ if (a.name != b.name) return false
+ if (a.superName != b.superName) return false
+ if (a.outerClass != b.outerClass) return false
+ if (a.outerMethod != b.outerMethod) return false
+ if (a.outerMethodDesc != b.outerMethodDesc) return false
+ if (a.nestHostClass != b.nestHostClass) return false
+ return true
+ }
+
+ fun areInnerClassNodesEqual(a: InnerClassNode, b: InnerClassNode): Boolean {
+ if (a.name != b.name) return false
+ if (a.outerName != b.outerName) return false
+ if (a.innerName != b.innerName) return false
+ if (a.access != b.access) return false
+ return true
+ }
+
+ fun areRecordComponentNodesEqual(a: RecordComponentNode, b: RecordComponentNode): Boolean {
+ if (a.name != b.name) return false
+ if (a.descriptor != b.descriptor) return false
+ val aVisibleAnnotations = a.visibleAnnotations?.toMutableList() ?: mutableListOf()
+ val bVisibleAnnotations = b.visibleAnnotations?.toMutableList() ?: mutableListOf()
+ if (aVisibleAnnotations.size != bVisibleAnnotations.size) return false
+ aVisibleAnnotations.removeIf {
+ bVisibleAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bVisibleAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aVisibleAnnotations.isNotEmpty()) return false
+ if (bVisibleAnnotations.isNotEmpty()) return false
+ val aInvisibleAnnotations = a.invisibleAnnotations?.toMutableList() ?: mutableListOf()
+ val bInvisibleAnnotations = b.invisibleAnnotations?.toMutableList() ?: mutableListOf()
+ if (aInvisibleAnnotations.size != bInvisibleAnnotations.size) return false
+ aInvisibleAnnotations.removeIf {
+ bInvisibleAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bInvisibleAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aInvisibleAnnotations.isNotEmpty()) return false
+ if (bInvisibleAnnotations.isNotEmpty()) return false
+ val aVisibleTypeAnnotations = a.visibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ val bVisibleTypeAnnotations = b.visibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ if (aVisibleTypeAnnotations.size != bVisibleTypeAnnotations.size) return false
+ aVisibleTypeAnnotations.removeIf {
+ bVisibleTypeAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bVisibleTypeAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aVisibleTypeAnnotations.isNotEmpty()) return false
+ if (bVisibleTypeAnnotations.isNotEmpty()) return false
+ val aInvisibleTypeAnnotations = a.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ val bInvisibleTypeAnnotations = b.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf()
+ if (aInvisibleTypeAnnotations.size != bInvisibleTypeAnnotations.size) return false
+ aInvisibleTypeAnnotations.removeIf {
+ bInvisibleTypeAnnotations.any { it2 ->
+ if (areAnnotationNodesEqual(it, it2)) {
+ bInvisibleTypeAnnotations.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aInvisibleTypeAnnotations.isNotEmpty()) return false
+ if (bInvisibleTypeAnnotations.isNotEmpty()) return false
+ val aAttributes = a.attrs?.toMutableList() ?: mutableListOf()
+ val bAttributes = b.attrs?.toMutableList() ?: mutableListOf()
+ if (aAttributes.size != bAttributes.size) return false
+ aAttributes.removeIf {
+ bAttributes.any { it2 ->
+ if (areAttributesEqual(it, it2)) {
+ bAttributes.remove(it2)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ if (aAttributes.isNotEmpty()) return false
+ if (bAttributes.isNotEmpty()) return false
+ return true
+ }
+ }
+
+
+ private enum class Access(val value: Int) {
+ PRIVATE(Opcodes.ACC_PRIVATE),
+ PACKAGE_PRIVATE(0),
+ PROTECTED(Opcodes.ACC_PROTECTED),
+ PUBLIC(Opcodes.ACC_PUBLIC),
+ ;
+
+ companion object {
+ fun from(access: Int): Access {
+ return when (access and (Opcodes.ACC_PRIVATE or Opcodes.ACC_PROTECTED or Opcodes.ACC_PUBLIC)) {
+ Opcodes.ACC_PRIVATE -> PRIVATE
+ Opcodes.ACC_PROTECTED -> PROTECTED
+ Opcodes.ACC_PUBLIC -> PUBLIC
+ else -> PACKAGE_PRIVATE
+ }
+ }
+
+ fun max(a: Access, b: Access): Access {
+ return values().last { it == a || it == b }
+ }
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt
new file mode 100644
index 0000000..e1c1b65
--- /dev/null
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt
@@ -0,0 +1,98 @@
+package moe.nea.archenemy.mojang
+
+import moe.nea.archenemy.util.readZipFs
+import moe.nea.archenemy.util.update
+import moe.nea.archenemy.util.updateGMV
+import moe.nea.archenemy.util.zipFs
+import net.minecraftforge.artifactural.api.artifact.Artifact
+import net.minecraftforge.artifactural.api.artifact.ArtifactIdentifier
+import net.minecraftforge.artifactural.api.artifact.ArtifactType
+import net.minecraftforge.artifactural.base.artifact.StreamableArtifact
+import org.gradle.api.artifacts.ModuleDependency
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.tree.ClassNode
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.InputStream
+import java.nio.file.Path
+import java.security.MessageDigest
+import kotlin.io.path.createDirectories
+import kotlin.io.path.outputStream
+
+class MergedRepositoryProvider(archenemyMojangExtension: ArchenemyMojangExtension) :
+ AbstractTransformerRepository<MergedRepositoryProvider.Coordinate>("merged", archenemyMojangExtension) {
+ data class Coordinate(val base: ModuleDependency, val overlay: ModuleDependency) : CHashable {
+ override fun updateHash(digest: MessageDigest) {
+ digest.update("merged")
+ digest.updateGMV("base", base)
+ digest.updateGMV("overlay", overlay)
+ }
+ }
+
+ override fun getName(t: Coordinate): String {
+ return t.base.name
+ }
+
+ override fun getVersion(t: Coordinate): String {
+ return t.base.version!!
+ }
+
+ override fun getArtifact(identifier: ArtifactIdentifier, value: Coordinate, file: File): Artifact? {
+ if (!identifier.classifier.isNullOrBlank())
+ return null
+ if (identifier.extension != "jar") return null
+ val baseJar = resolveDirect(value.base).singleOrNull() ?: return null
+ val overlayJar = resolveDirect(value.overlay).singleOrNull() ?: return null
+ return StreamableArtifact.ofStreamable(
+ identifier,
+ ArtifactType.BINARY,
+ lazyTransform(file) { mergeJar(baseJar, overlayJar, it) })
+ }
+
+ private fun readClasses(
+ path: Path,
+ readClassFile: (String, ClassNode) -> Unit,
+ readResource: (String, InputStream) -> Unit
+ ) {
+ path.readZipFs { path, inputStream ->
+ if (path.startsWith("META-INF/")) return@readZipFs
+ if (path.endsWith(".class")) {
+ val classReader = ClassReader(inputStream)
+ val classNode = ClassNode()
+ classReader.accept(classNode, 0)
+ readClassFile(path, classNode)
+ } else {
+ readResource(path, inputStream)
+ }
+ }
+ }
+
+ fun mergeClass(base: ClassNode, overlay: ClassNode): ClassNode {
+ return ClassMerger().accept(base, overlay)
+ }
+
+ private fun mergeJar(baseJar: File, overlayJar: File, target: File) {
+ target.toPath().zipFs().use { fs ->
+ val classNodes = mutableMapOf<String, ClassNode>()
+ fun readResource(path: String, inputStream: InputStream) {
+ val p = fs.getPath(path)
+ p.parent?.createDirectories()
+ p.outputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+
+ fun readClass(path: String, classNode: ClassNode) {
+ classNodes.merge(path, classNode, ::mergeClass)
+ }
+ readClasses(baseJar.toPath(), ::readClass, ::readResource)
+ readClasses(overlayJar.toPath(), ::readClass, ::readResource)
+ classNodes.forEach { (path, node) ->
+ val writer = ClassWriter(0)
+ node.accept(writer)
+ readResource(path, ByteArrayInputStream(writer.toByteArray()))
+ }
+ }
+ }
+}
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt b/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt
index 4f50000..d2de936 100644
--- a/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt
@@ -1,5 +1,6 @@
package moe.nea.archenemy.util
+import org.gradle.api.artifacts.ModuleDependency
import java.security.MessageDigest
fun MessageDigest.updateField(text: String, value: String) {
@@ -11,4 +12,11 @@ fun MessageDigest.updateField(text: String, value: String) {
fun MessageDigest.update(text: String) {
this.update(text.encodeToByteArray())
+}
+
+fun MessageDigest.updateGMV(name: String, moduleDependency: ModuleDependency) {
+ this.updateField(
+ name,
+ (moduleDependency.group ?: "") + ":" + moduleDependency.name + ":" + (moduleDependency.version ?: "")
+ )
} \ No newline at end of file
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt b/plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt
new file mode 100644
index 0000000..72d18b6
--- /dev/null
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt
@@ -0,0 +1,9 @@
+package moe.nea.archenemy.util
+
+enum class EnvType(val classifier: String?, val mcp: Int) {
+ CLIENT("client", 0),
+ SERVER("server", 1),
+ COMBINED(null, 2),
+ DATAGEN("server", 1)
+ ;
+} \ No newline at end of file
diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt b/plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt
new file mode 100644
index 0000000..0b2f70e
--- /dev/null
+++ b/plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt
@@ -0,0 +1,33 @@
+package moe.nea.archenemy.util
+
+import java.io.InputStream
+import java.net.URI
+import java.nio.file.FileSystem
+import java.nio.file.FileSystems
+import java.nio.file.Path
+import java.util.zip.ZipInputStream
+import java.util.zip.ZipOutputStream
+import kotlin.io.path.createDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.inputStream
+import kotlin.io.path.outputStream
+
+// TODO: Figure out license shit with unimined cause i copied some of these utils?
+fun Path.zipFs(): FileSystem {
+ if (!exists()) {
+ parent.createDirectories()
+ ZipOutputStream(this.outputStream()).use { }
+ }
+ return FileSystems.newFileSystem(URI.create("jar:${toUri()}"), mapOf("create" to true), null)
+}
+
+fun Path.readZipFs(reader: (String, InputStream) -> Unit) {
+ ZipInputStream(this.inputStream()).use { stream ->
+ for (entry in generateSequence { stream.nextEntry }) {
+ if (entry.isDirectory) {
+ continue
+ }
+ reader(entry.name, stream)
+ }
+ }
+}