Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Loom's Interface Injection support for Fabric providers + Fix Integration tests #47

Merged
merged 10 commits into from
Dec 1, 2023
4 changes: 2 additions & 2 deletions .github/workflows/test-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:

steps:
- uses: actions/checkout@v3
- run: gradle test --tests ${{ matrix.test }} --stacktrace --warning-mode fail
- run: gradle test --tests ${{ matrix.test }} --stacktrace --info --warning-mode fail
env:
TEST_WARNING_MODE: fail
id: test
Expand Down Expand Up @@ -90,7 +90,7 @@ jobs:
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
- run: ./gradlew test --tests ${{ matrix.test }} --stacktrace --warning-mode fail
- run: ./gradlew test --tests ${{ matrix.test }} --stacktrace --info --warning-mode fail
env:
TEST_WARNING_MODE: fail
id: test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package xyz.wagyourtail.unimined.internal.mapping.ii

import org.gradle.api.logging.Logger
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import xyz.wagyourtail.unimined.util.openZipFileSystem
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import kotlin.io.path.inputStream

object InterfaceInjectionMinecraftTransformer {
fun transform(
injections: Map<String, List<String>>,
baseMinecraft: Path,
output: Path,
logger: Logger
): Boolean {
if (injections.isNotEmpty()) {
Files.copy(baseMinecraft, output, StandardCopyOption.REPLACE_EXISTING)
output.openZipFileSystem(mapOf("mutable" to true)).use { fs ->
logger.debug("Transforming $output with ${injections.values.sumOf { it.size }} interface injections")

for (target in injections.keys) {
try {
val targetClass = "/" + target.replace(".", "/") + ".class"
val targetPath = fs.getPath(targetClass)
logger.debug("Transforming $targetPath")
if (Files.exists(targetPath)) {
val reader = ClassReader(targetPath.inputStream())
val writer = ClassWriter(0)

val node = ClassNode(Opcodes.ASM9)
reader.accept(node, 0)

if (node.interfaces == null) {
node.interfaces = arrayListOf();
}

for (injected in injections[target]!!) {
if (!node.interfaces.contains(injected)) node.interfaces.add(injected)
}

if (node.signature != null) {
val resultingSignature = StringBuilder(node.signature)

for (injected in injections[target]!!) {
val computedSignature = "L" + injected.replace(".", "/") + ";"

if (!resultingSignature.contains(computedSignature)) resultingSignature.append(computedSignature)
}

node.signature = resultingSignature.toString()
}

node.accept(writer);
Files.write(
targetPath,
writer.toByteArray(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
} else {
logger.warn("Could not find class $targetClass in $output")
}
} catch (e: Exception) {
logger.warn(
"An error occurred while transforming $target with interface injection in $output",
e
)
}
}
}


return true
}

return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,8 @@ abstract class FabricLikeMinecraftTransformer(
// fabric provides its own asm, exclude asm-all from vanilla minecraftLibraries
return !library.name.startsWith("org.ow2.asm:asm-all")
}

fun getModJsonPath(): File? {
return provider.sourceSet.resources.first { it.name.equals(modJsonName) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ package xyz.wagyourtail.unimined.internal.minecraft.patch.fabric

import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import xyz.wagyourtail.unimined.api.minecraft.EnvType
import xyz.wagyourtail.unimined.api.runs.RunConfig
import xyz.wagyourtail.unimined.api.unimined
import xyz.wagyourtail.unimined.internal.mapping.ii.InterfaceInjectionMinecraftTransformer
import xyz.wagyourtail.unimined.internal.minecraft.MinecraftProvider
import xyz.wagyourtail.unimined.internal.minecraft.patch.MinecraftJar
import xyz.wagyourtail.unimined.util.getShortSha1
import java.io.InputStreamReader
import java.nio.file.Files
import kotlin.io.path.createDirectories
import kotlin.io.path.exists

abstract class FabricMinecraftTransformer(
project: Project,
Expand Down Expand Up @@ -60,4 +68,84 @@ abstract class FabricMinecraftTransformer(
"-Dfabric.remapClasspathFile=${intermediaryClasspath}"
)
}

override fun afterRemap(baseMinecraft: MinecraftJar): MinecraftJar {
val baseMinecraft = super.afterRemap(baseMinecraft)

val injections = hashMapOf<String, List<String>>()

this.collectInterfaceInjections(baseMinecraft, injections)

return if (injections.isNotEmpty()) {
val oldSuffix = if (baseMinecraft.awOrAt != null) baseMinecraft.awOrAt + "+" else ""

val output = MinecraftJar(
baseMinecraft,
parentPath = provider.localCache.resolve("fabric").createDirectories(),
awOrAt = "${oldSuffix}ii+${injections.hashCode()}"
thecatcore marked this conversation as resolved.
Show resolved Hide resolved
);

if (!output.path.exists() || project.unimined.forceReload) {
if (InterfaceInjectionMinecraftTransformer.transform(
injections,
baseMinecraft.path,
output.path,
project.logger
)
) {
output
} else baseMinecraft
} else output
} else baseMinecraft
}

private fun collectInterfaceInjections(baseMinecraft: MinecraftJar, injections: HashMap<String, List<String>>) {
val modJsonPath = this.getModJsonPath()

if (modJsonPath != null && modJsonPath.exists()) {
val json = JsonParser.parseReader(InputStreamReader(Files.newInputStream(modJsonPath.toPath()))).asJsonObject

val custom = json.getAsJsonObject("custom")
thecatcore marked this conversation as resolved.
Show resolved Hide resolved

if (custom != null) {
val interfaces = custom.getAsJsonObject("loom:injected_interfaces")

if (interfaces != null) {
injections.putAll(interfaces.entrySet()
.filterNotNull()
.filter { it.key != null && it.value != null && it.value.isJsonArray }
.map {
val element = it.value!!

Pair(it.key!!, if (element.isJsonArray) {
element.asJsonArray.mapNotNull { name -> name.asString }
} else arrayListOf())
}
.map {
var target = it.first

val clazz = provider.mappings.mappingTree.getClass(
target,
provider.mappings.mappingTree.getNamespaceId(prodNamespace.name)
)

if (clazz != null) {
var newTarget = clazz.getName(provider.mappings.mappingTree.getNamespaceId(baseMinecraft.mappingNamespace.name))

if (newTarget == null) {
newTarget = clazz.getName(provider.mappings.mappingTree.getNamespaceId(baseMinecraft.fallbackNamespace.name))
}

if (newTarget != null) {
target = newTarget
}
}

Pair(target, it.second)
}
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class MinecraftForgeMinecraftTransformer(project: Project, provider: MinecraftPr

override fun addMavens() {
project.unimined.minecraftForgeMaven()
project.unimined.neoForgedMaven()
}

override fun loader(dep: Any, action: Dependency.() -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package xyz.wagyourtail.unimined.test.integration

import org.gradle.testkit.runner.TaskOutcome
import org.gradle.testkit.runner.UnexpectedBuildFailure
import org.junit.jupiter.api.Test
import xyz.wagyourtail.unimined.util.runTestProject

class BabricModloaderB1_7_3Test {
@Test
fun test_babric_modloader_b1_7_3() {
val result = runTestProject("b1.7.3-Babric-Modloader")
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
try {
val result = runTestProject("b1.7.3-Babric-Modloader")

try {
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
} catch (e: Exception) {
println(result.output)
throw Exception(e)
}
} catch (e: UnexpectedBuildFailure) {
println(e)
throw Exception("build failed", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package xyz.wagyourtail.unimined.test.integration

import org.gradle.testkit.runner.TaskOutcome
import org.gradle.testkit.runner.UnexpectedBuildFailure
import org.junit.jupiter.api.Test
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import xyz.wagyourtail.unimined.util.openZipFileSystem
import xyz.wagyourtail.unimined.util.runTestProject
import java.nio.file.Files
import kotlin.io.path.inputStream
import kotlin.test.*

class FabricInterfaceInjectionTest {
@Test
fun test_fabric_interface_injection() {
val projectName = "Fabric-Interface-Injection"
try {
val result = runTestProject(projectName)

try {
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
} catch (e: Exception) {
println(result.output)
throw Exception(e)
}
} catch (e: UnexpectedBuildFailure) {
println(e)
throw Exception("build failed", e)
}

val fs = openZipFileSystem(projectName, ".gradle/unimined/local/fabric/fabric/minecraft-1.14.4-fabric-merged+fixed-mojmap+intermediary-ii+-1221611203.jar")

assertNotNull(fs, "Couldn't find the interface injected jar!")

fs.use {
val target = it.getPath("/net/minecraft/advancements/Advancement.class")

assertNotNull(target, "Couldn't find the injected class in mc jar!")
assertTrue(Files.exists(target), "Couldn't find the injected class in mc jar!")

val reader = ClassReader(target.inputStream())
val node = ClassNode(Opcodes.ASM9)
reader.accept(node, 0)

assertNotNull(node.interfaces, "Injected class doesn't have any interface!")
assertEquals(1, node.interfaces.size)
assertContains(node.interfaces, "com/example/fabric/ExampleInterface")
}

fs.close()
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package xyz.wagyourtail.unimined.test.integration

import org.gradle.testkit.runner.TaskOutcome
import org.gradle.testkit.runner.UnexpectedBuildFailure
import org.junit.jupiter.api.Test
import xyz.wagyourtail.unimined.util.runTestProject

class Forge1_3_2Test {
@Test
fun test_forge_1_3_2() {
val result = runTestProject("1.3.2-Forge")
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
try {
val result = runTestProject("1.3.2-Forge")

try {
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
} catch (e: Exception) {
println(result.output)
throw Exception(e)
}
} catch (e: UnexpectedBuildFailure) {
println(e)
throw Exception("build failed", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package xyz.wagyourtail.unimined.test.integration

import org.gradle.testkit.runner.TaskOutcome
import org.gradle.testkit.runner.UnexpectedBuildFailure
import org.junit.jupiter.api.Test
import xyz.wagyourtail.unimined.util.runTestProject

class Forge1_6_4Test {
@Test
fun test_forge_1_6_4() {
val result = runTestProject("1.6.4-Forge")
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
try {
val result = runTestProject("1.6.4-Forge")

try {
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
} catch (e: Exception) {
println(result.output)
throw Exception(e)
}
} catch (e: UnexpectedBuildFailure) {
println(e)
throw Exception("build failed", e)
}
}
}
Loading