first commit

This commit is contained in:
2026-04-03 01:08:40 +02:00
commit a54a37e068
24 changed files with 706 additions and 0 deletions
@@ -0,0 +1,25 @@
package dev.zerozipp.agent
import dev.zerozipp.loader.event.*
import java.io.File
@Suppress("unused") class Client {
private val base = Client::class
private var mods = modules()
private fun modules(): List<Module> = runCatching {
val source = base.java.protectionDomain.codeSource
val location = File(source.location.toURI())
val mods = File(location.parentFile, "mods")
val loader = base.java.classLoader
if(!mods.isDirectory) return emptyList()
val files = mods.listFiles { it.extension == "jar" }
files?.map { Module(it, loader) }?: emptyList()
}.getOrDefault(emptyList())
fun reload() = run { mods = modules() }
fun event(event: Event) = mods.forEach {
module -> module.event(this, event)
}
}
@@ -0,0 +1,24 @@
package dev.zerozipp.agent
import dev.zerozipp.loader.event.*
import java.util.jar.JarFile
import java.io.File
import java.net.*
class Module(jar: File, parent: ClassLoader) {
private val instance: Any? = runCatching {
val urls = arrayOf(jar.toURI().toURL())
val manifest = JarFile(jar).use { it.manifest }
val attributes = manifest.mainAttributes
val name = attributes.getValue("Mod-Class")
val loader = URLClassLoader(urls, parent)
val base = Class.forName(name, true, loader)
base.getConstructor().newInstance()
}.getOrNull()
fun event(client: Client, event: Event) = runCatching {
val (c, e) = Client::class.java to Event::class.java
instance?.javaClass?.getDeclaredMethod("event", c, e)
}.getOrNull()?.invoke(instance, client, event)
}
@@ -0,0 +1,25 @@
@file:JvmName("Main")
package dev.zerozipp.injector
import com.sun.tools.attach.*
import dev.zerozipp.loader.Agent
import java.io.File
val targets = setOf(
"net.minecraft.client.main.Main",
"org.prismlauncher.EntryPoint",
"org.multimc.EntryPoint"
)
fun main() {
val domain = Agent::class.java.protectionDomain
val jar = File(domain.codeSource.location.toURI())
val machines = VirtualMachine.list()
for(desc in machines) if(targets.any {
desc.displayName().startsWith(it)
}) VirtualMachine.attach(desc).also {
it.loadAgent(jar.absolutePath)
it.detach()
}
}
@@ -0,0 +1,63 @@
@file:JvmName("Agent")
package dev.zerozipp.loader
import dev.zerozipp.agent.Client
import dev.zerozipp.loader.inject.*
import net.bytebuddy.agent.builder.AgentBuilder
import net.bytebuddy.agent.builder.AgentBuilder.*
import net.bytebuddy.matcher.ElementMatchers.*
import net.bytebuddy.description.method.*
import net.bytebuddy.asm.Advice
import java.lang.instrument.*
import java.util.jar.JarFile
import java.nio.file.Path
import java.io.File
@Suppress("unused") object Agent {
private const val KNOT = "net.fabricmc.loader.impl.launch.knot"
@JvmStatic fun premain(args: String?, inst: Instrumentation?) = attach(inst!!)
@JvmStatic fun agentmain(args: String?, inst: Instrumentation?) = attach(inst!!)
val injections = listOf(Tick::class, Render::class, Gui::class)
val client: Client by lazy { Client() }
fun AgentBuilder.inject(base: Class<*>): AgentBuilder {
val inject = base.getAnnotation(Inject::class.java)
val name = named<MethodDescription>(inject.method)
val descriptor = if(inject.desc.isEmpty()) { name }
else { name.and(hasDescriptor(inject.desc)) }
val advice = Advice.to(base).on(descriptor)
return type(named(inject.target)).transform {
builder, _, _, _, _ -> builder.visit(advice)
}
}
private fun getKnotDelegate(): Any? {
val base = Class.forName("$KNOT.KnotClassLoader")
val delegate = base.getDeclaredField("delegate")
val threads = Thread.getAllStackTraces().keys
val loaders = threads.mapNotNull { it.contextClassLoader }
val knot = loaders.firstOrNull { it.javaClass == base }
return delegate.apply { isAccessible = true }.get(knot)
}
private fun exposeToKnot(jar: Path) {
val delegate = getKnotDelegate()
val base = Class.forName("$KNOT.KnotClassDelegate")
val source = base.getMethod("addCodeSource", Path::class.java)
source.apply { isAccessible = true }.invoke(delegate, jar)
}
private fun attach(inst: Instrumentation) {
val domain = Client::class.java.protectionDomain
val location = File(domain.codeSource.location.toURI())
inst.appendToSystemClassLoaderSearch(JarFile(location))
runCatching { exposeToKnot(location.toPath()) }
var builder: AgentBuilder = Default()
builder = builder.disableClassFormatChanges()
builder = builder.with(RedefinitionStrategy.RETRANSFORMATION)
for(base in injections) builder = builder.inject(base.java)
builder.installOn(inst)
}
}
@@ -0,0 +1,10 @@
package dev.zerozipp.loader
@Suppress("unused")
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Inject(
val target: String,
val method: String,
val desc: String
)
@@ -0,0 +1,6 @@
package dev.zerozipp.loader.event
data class Event(
val target: String,
val state: State
)
@@ -0,0 +1,5 @@
package dev.zerozipp.loader.event
enum class State {
ENTER, EXIT
}
@@ -0,0 +1,16 @@
package dev.zerozipp.loader.inject
import dev.zerozipp.loader.event.Event
import dev.zerozipp.loader.event.State.*
import dev.zerozipp.loader.Agent.client
import dev.zerozipp.loader.Inject
import net.bytebuddy.asm.Advice.*
@Suppress("unused") @Inject(
"net.minecraft.client.gui.Gui",
method = "extractRenderState", desc = "") object Gui {
@OnMethodEnter @JvmStatic fun enter() = client.event(enter)
@OnMethodExit @JvmStatic fun exit() = client.event(exit)
@JvmField val enter = Event("gui", ENTER)
@JvmField val exit = Event("gui", EXIT)
}
@@ -0,0 +1,16 @@
package dev.zerozipp.loader.inject
import dev.zerozipp.loader.event.Event
import dev.zerozipp.loader.event.State.*
import dev.zerozipp.loader.Agent.client
import dev.zerozipp.loader.Inject
import net.bytebuddy.asm.Advice.*
@Suppress("unused") @Inject(
"net.minecraft.client.renderer.LevelRenderer",
method = "extractLevel", desc = "") object Render {
@OnMethodEnter @JvmStatic fun enter() = client.event(enter)
@OnMethodExit @JvmStatic fun exit() = client.event(exit)
@JvmField val enter = Event("render", ENTER)
@JvmField val exit = Event("render", EXIT)
}
@@ -0,0 +1,16 @@
package dev.zerozipp.loader.inject
import dev.zerozipp.loader.event.Event
import dev.zerozipp.loader.event.State.*
import dev.zerozipp.loader.Agent.client
import dev.zerozipp.loader.Inject
import net.bytebuddy.asm.Advice.*
@Suppress("unused") @Inject(
"net.minecraft.client.Minecraft",
method = "tick", desc = "") object Tick {
@OnMethodEnter @JvmStatic fun enter() = client.event(enter)
@OnMethodExit @JvmStatic fun exit() = client.event(exit)
@JvmField val enter = Event("tick", ENTER)
@JvmField val exit = Event("tick", EXIT)
}