Skip to content

Commit 7973ce7

Browse files
committed
Test: Fabric plugin bootstrap
1 parent c2faa09 commit 7973ce7

File tree

6 files changed

+135
-37
lines changed

6 files changed

+135
-37
lines changed

common/src/main/kotlin/com/lambda/core/Loader.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import com.lambda.interaction.RotationManager
1010
import com.lambda.module.ModuleRegistry
1111
import com.lambda.plugin.PluginRegistry
1212
import com.lambda.util.Communication.ascii
13+
import com.lambda.util.FolderRegister.plugins
1314
import kotlin.system.measureTimeMillis
1415

1516
object Loader {
1617
private val loadables = listOf(
17-
PluginRegistry,
1818
ModuleRegistry,
1919
CommandManager,
2020
RotationManager,
@@ -28,6 +28,8 @@ object Loader {
2828
ascii.split("\n").forEach { LOG.info(it) }
2929
LOG.info("Initializing ${Lambda.MOD_NAME} ${Lambda.VERSION}")
3030

31+
PluginRegistry.load(plugins) // TODO: Find something else
32+
3133
val initTime = measureTimeMillis {
3234
loadables.forEach { loadable ->
3335
var info: String
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
package com.lambda.plugin
22

3+
import java.io.*
34
import java.util.jar.JarFile
45

6+
57
class PluginClassLoader(
68
private val jarFile: JarFile,
79
parent: ClassLoader,
8-
) : ClassLoader(parent) {
10+
) : ClassLoader(parent), Closeable {
911
private val classes = mutableMapOf<String, ByteArray>()
1012
private val resources = mutableMapOf<String, ByteArray>()
1113

14+
val mixinFileName: String?
15+
get() = resources.keys.firstOrNull { it.endsWith(".mixins.json") }
16+
17+
val accessWidenerFileName: String?
18+
get() = resources.keys.firstOrNull { it.endsWith(".accesswidener") }
19+
20+
public override fun findClass(name: String): Class<*> {
21+
val clazz = classes[name] ?: return super.findClass(name)
22+
return defineClass(name, clazz, 0, clazz.size)
23+
}
24+
25+
override fun close() {
26+
jarFile.close()
27+
}
28+
1229
init {
1330
jarFile.entries().asSequence().forEach { entry ->
1431
if (entry.isDirectory) return@forEach
@@ -24,13 +41,4 @@ class PluginClassLoader(
2441
}
2542
}
2643
}
27-
28-
public override fun findClass(name: String): Class<*> {
29-
val clazz = classes[name] ?: return parent.loadClass(name)
30-
return defineClass(name, clazz, 0, clazz.size)
31-
}
32-
33-
fun close() {
34-
jarFile.close()
35-
}
3644
}
Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,91 @@
11
package com.lambda.plugin
22

33
import com.lambda.Lambda.LOG
4-
import com.lambda.core.Loadable
5-
import com.lambda.util.FolderRegister.createIfNotExists
6-
import com.lambda.util.FolderRegister.listRecursive
7-
import com.lambda.util.FolderRegister.plugins
84
import java.io.File
5+
import java.lang.reflect.InvocationTargetException
6+
import java.lang.reflect.Method
7+
import java.net.URL
98
import java.util.jar.JarFile
109

11-
object PluginRegistry : Loadable {
12-
private fun loadPlugin(file: File) {
13-
if (!file.name.endsWith(".jar")) return
14-
if (file.length() == 0L) return LOG.error("The plugin $file is empty")
10+
object PluginRegistry {
11+
private val knotClassLoader: ClassLoader = Thread.currentThread().contextClassLoader
12+
private val addUrlMethod: Method
13+
14+
private var loadClass: Class<*>? = null
15+
private var loadMethod: Method? = null
16+
private var loadInstance: Any? = null // TODO: This won't work with constructors
17+
18+
init {
19+
try {
20+
addUrlMethod = knotClassLoader.javaClass.getMethod("addUrlFwd", URL::class.java)
21+
addUrlMethod.isAccessible = true
22+
} catch (e: NoSuchMethodException) {
23+
throw RuntimeException("Failed to get the addURL method from the KnotClassLoader.")
24+
}
25+
}
26+
27+
@Throws(
28+
IllegalAccessException::class,
29+
IllegalArgumentException::class,
30+
InvocationTargetException::class)
31+
fun feedJarToKnot(jar: File) {
32+
addUrlMethod.invoke(knotClassLoader, jar.toURI().toURL())
33+
}
34+
35+
private fun preLoadPlugin(file: File): PluginClassLoader? {
36+
runCatching { feedJarToKnot(file) }
37+
.onFailure {
38+
LOG.error("Failed to add the URL of the plugin $file", it)
39+
return null
40+
}
41+
42+
LOG.debug("Added the URL of the plugin {} to the Knot class loader", file)
1543

1644
val jar = JarFile(file)
17-
val loader = PluginClassLoader(jar, this::class.java.classLoader)
45+
val loader = PluginClassLoader(jar, knotClassLoader)
46+
1847
val mainClass = jar.manifest.mainAttributes.getValue("Main-Class")
19-
?: return LOG.error("The plugin $jar does not have a main class")
48+
?: return null.also { LOG.error("The plugin $jar does not have a main class") }
2049

21-
val clazz = loader.loadClass(mainClass)
22-
val instance =
23-
clazz.declaredFields.firstOrNull { it.name == "INSTANCE" }?.get(null) ?: clazz.constructors.firstOrNull()
24-
?.newInstance()
25-
?: return LOG.error("The plugin $jar does not have an object instance or a public constructor")
50+
loadClass = loader.loadClass(mainClass)
2651

27-
val loadMethod =
28-
clazz.methods.find { it.name == "load" } ?: return LOG.warn("The plugin $jar does not have a load method")
52+
loadInstance =
53+
loadClass?.declaredFields?.firstOrNull { it.name == "INSTANCE" }?.get(null)
54+
?: loadClass?.constructors?.firstOrNull()?.newInstance()
55+
?: return null.also { LOG.error("The plugin $jar does not have an object instance or a public constructor") }
2956

30-
loadMethod.invoke(instance)
31-
loader.close()
32-
}
57+
loadMethod =
58+
loadClass?.methods?.find { it.name == "load" } ?: return null
59+
.also { LOG.warn("The plugin $jar does not have a load method") }
3360

34-
override fun load(): String {
35-
plugins.createIfNotExists()
61+
return loader
62+
}
3663

37-
val plugins = plugins.listRecursive()
38-
plugins.forEach { file ->
39-
loadPlugin(file)
64+
private fun loadPlugin(file: File) {
65+
runCatching {
66+
loadMethod?.invoke(loadInstance)
4067
}
68+
.onFailure {
69+
return LOG.error("""
70+
A serious error occurred while loading a plugin.
71+
This is likely a bug in the plugin itself but it could also be a bug in Lambda.
72+
If you are a developer, please check the plugin's main class and load method.
73+
If you are a regular user, please report this issue to Lambda team and the plugin developer.
74+
75+
Plugin: $file
76+
Stacktrace: ${Thread.currentThread().stackTrace.joinToString("\n")}
77+
""".trimIndent())
78+
}
79+
}
80+
81+
fun preLoad(path: File): List<PluginClassLoader> {
82+
val plugins = path.walk().filter { it.isFile && it.extension == "jar" }.toList()
83+
return plugins.mapNotNull { preLoadPlugin(it) }
84+
}
4185

42-
return "Loaded ${plugins.count()} plugins"
86+
fun load(path: File): String {
87+
val plugins = path.walk().filter { it.isFile && it.extension == "jar" }.toList()
88+
plugins.forEach { loadPlugin(it) }
89+
return "Loaded ${plugins.size} plugins"
4390
}
4491
}

common/src/main/kotlin/com/lambda/plugin/api/Plugin.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ abstract class Plugin(
1212
val loadBefore: List<String>? = null,
1313
val loadAfter: List<String>? = null,
1414
) : Nameable {
15+
open fun preLoad() {} // This is invoked before the game launches
1516
abstract fun load()
1617
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.lambda.fabric
2+
3+
import com.lambda.Lambda.LOG
4+
import com.lambda.plugin.PluginRegistry
5+
import net.fabricmc.loader.api.FabricLoader
6+
import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint
7+
import org.spongepowered.asm.launch.MixinBootstrap
8+
import org.spongepowered.asm.mixin.Mixins
9+
import kotlin.system.measureTimeMillis
10+
11+
object LambdaBootstrap : PreLaunchEntrypoint {
12+
// This function ensure that plugins have their
13+
// mixins and access widener configurations loaded
14+
// before the game starts
15+
override fun onPreLaunch() {
16+
MixinBootstrap.init() // Important to initialize Mixins
17+
18+
val runDirectory = FabricLoader.getInstance().gameDir.resolve("lambda/plugins").toFile()
19+
if (!runDirectory.exists()) runDirectory.mkdirs()
20+
21+
var validPlugins = 0
22+
23+
val time = measureTimeMillis {
24+
PluginRegistry.preLoad(runDirectory)
25+
.forEach { loader ->
26+
Mixins.addConfiguration(loader.mixinFileName)
27+
// Mixins.addConfiguration(accessWidener.absolutePath) // TODO: Add access widener support
28+
validPlugins++
29+
}
30+
}
31+
32+
LOG.info("Pre-loaded $validPlugins plugins in $time ms.")
33+
}
34+
}

fabric/src/main/resources/fabric.mod.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
"adapter": "kotlin",
2121
"value": "com.lambda.fabric.LambdaFabric"
2222
}
23+
],
24+
"preLaunch": [
25+
{
26+
"adapter": "kotlin",
27+
"value": "com.lambda.fabric.LambdaBootstrap"
28+
}
2329
]
2430
},
2531
"mixins": [

0 commit comments

Comments
 (0)