add extensions, add event system including asm crimes

This commit is contained in:
moehreag 2024-06-05 02:26:00 +02:00
parent 577386681d
commit f2cf3b501c
20 changed files with 554 additions and 18 deletions

View file

@ -2,30 +2,46 @@ import dev.frogmc.phytotelma.ext.minecraft
import dev.frogmc.phytotelma.ext.loader
plugins {
id("java")
id("dev.frogmc.phytotelma").version("0.0.1-SNAPSHOT")
java
id("dev.frogmc.phytotelma") version "0.0.1-SNAPSHOT"
id("io.freefair.lombok") version "8.+"
}
group = "dev.frogmc"
version = "0.0.1-SNAPSHOT"
repositories {
maven {
name = "FrogMC Maven Releases"
url = uri("https://maven.frogmc.dev/releases")
}
maven {
name = "FrogMC Maven Snapshots"
url = uri("https://maven.frogmc.dev/snapshots")
}
mavenLocal()
mavenCentral()
}
minecraft(libs.versions.minecraft).loader(libs.versions.frogloader)
ext.set("mcVer", libs.versions.minecraft)
ext.set("loaderVer", libs.versions.frogloader)
java {
minecraft(project.libs.versions.minecraft).loader(project.libs.versions.frogloader)
subprojects {
}
@Suppress("UNCHECKED_CAST")
fun setUpLibrary(project: Project) {
with(project) {
println("Configuring: ${project.name}")
apply(plugin = "dev.frogmc.phytotelma")
group = "dev.frogmc"
version = "0.0.1-SNAPSHOT"
repositories {
mavenLocal()
}
minecraft(rootProject.ext.get("mcVer") as Provider<String>).loader(rootProject.ext.get("loaderVer") as Provider<String>)
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
}

View file

@ -2,5 +2,3 @@
minecraft = "1.20.6"
frogloader = "0.0.1-SNAPSHOT"
[libraries]

View file

@ -14,4 +14,3 @@ pluginManagement {
}
rootProject.name = "froglib"

View file

@ -0,0 +1,8 @@
package dev.frogmc.froglib.entrypoints;
import dev.frogmc.frogloader.api.mod.ModProperties;
public interface ClientExtension {
void onClientInit(ModProperties mod);
}

View file

@ -0,0 +1,22 @@
package dev.frogmc.froglib.entrypoints;
import dev.frogmc.frogloader.api.FrogLoader;
public class ExtensionLauncher {
public static void invokeClient() {
System.out.println("Starting client extension");
FrogLoader.getInstance().getMods().forEach(mod -> {
mod.extensions().runIfPresent("client", ClientExtension.class, ext -> ext.onClientInit(mod));
mod.extensions().runIfPresent("init", MainExtension.class, ext -> ext.onInit(mod));
});
}
public static void invokeServer() {
System.out.println("Starting server extension");
FrogLoader.getInstance().getMods().forEach(mod -> {
mod.extensions().runIfPresent("server", ServerExtension.class, ext -> ext.onServerInit(mod));
mod.extensions().runIfPresent("init", MainExtension.class, ext -> ext.onInit(mod));
});
}
}

View file

@ -0,0 +1,8 @@
package dev.frogmc.froglib.entrypoints;
import dev.frogmc.frogloader.api.mod.ModProperties;
public interface MainExtension {
void onInit(ModProperties mod);
}

View file

@ -0,0 +1,8 @@
package dev.frogmc.froglib.entrypoints;
import dev.frogmc.frogloader.api.mod.ModProperties;
public interface ServerExtension {
void onServerInit(ModProperties mod);
}

View file

@ -0,0 +1,17 @@
package dev.frogmc.froglib.entrypoints.mixin.client;
import dev.frogmc.froglib.entrypoints.ExtensionLauncher;
import net.minecraft.client.main.Main;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Main.class)
public class MainMixin {
@Inject(method = "main", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;setName(Ljava/lang/String;)V"))
private static void initializeClient(String[] args, CallbackInfo ci) {
ExtensionLauncher.invokeClient();
}
}

View file

@ -0,0 +1,17 @@
package dev.frogmc.froglib.entrypoints.mixin.server;
import dev.frogmc.froglib.entrypoints.ExtensionLauncher;
import net.minecraft.server.Main;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Main.class)
public class MinecraftServerMixin {
@Inject(method = "main", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;spin(Ljava/util/function/Function;)Lnet/minecraft/server/MinecraftServer;"))
private static void initializeServer(String[] args, CallbackInfo ci) {
ExtensionLauncher.invokeServer();
}
}

View file

@ -0,0 +1,25 @@
package dev.frogmc.froglib.events.api;
import java.util.List;
import java.util.function.Function;
import dev.frogmc.froglib.events.impl.EventImpl;
public interface Event<T> {
static <T> Event<T> of(T listener){
return EventImpl.of(listener);
}
static <T> Event<T> of(Class<T> clazz){
return EventImpl.of(clazz);
}
static <T> Event<T> of(Function<List<T>, T> invokerFactory){
return EventImpl.of(invokerFactory);
}
void register(T listener);
T invoker();
}

View file

@ -0,0 +1,9 @@
package dev.frogmc.froglib.events.api;
import dev.frogmc.froglib.events.api.types.ClientStartupEvent;
import dev.frogmc.froglib.events.api.types.ServerStartupEvent;
public class Events {
public static final Event<ClientStartupEvent> CLIENT_STARTUP = Event.of(ClientStartupEvent.class);
public static final Event<ServerStartupEvent> SERVER_STARTUP = Event.of(ServerStartupEvent.class);
}

View file

@ -0,0 +1,9 @@
package dev.frogmc.froglib.events.api.types;
import net.minecraft.client.Minecraft;
import net.minecraft.client.main.GameConfig;
public interface ClientStartupEvent {
void onClientInit(Minecraft client, GameConfig config);
}

View file

@ -0,0 +1,7 @@
package dev.frogmc.froglib.events.api.types;
import net.minecraft.server.dedicated.DedicatedServer;
public interface ServerStartupEvent {
void onServerInit(DedicatedServer server);
}

View file

@ -0,0 +1,230 @@
package dev.frogmc.froglib.events.impl;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
import lombok.extern.slf4j.Slf4j;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.ClassNode;
@Slf4j
public class EventGenerator {
private static final MethodHandle defineClass;
private static final Map<String, Integer> classNameMap = new HashMap<>();
private static final boolean DEBUG_EXPORT;
static {
try {
defineClass = MethodHandles.privateLookupIn(MixinClassLoader.class, MethodHandles.lookup()).findVirtual(MixinClassLoader.class, "defineClass", MethodType.methodType(Class.class, String.class, byte[].class, int.class, int.class));
DEBUG_EXPORT = Boolean.getBoolean("froglib.events.export_invokers");
if (DEBUG_EXPORT){
Files.deleteIfExists(Paths.get(".frogmc/generated"));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static MethodHandle generateInvoker(Class<?> handlerClass) throws Throwable {
Method functionalMethod = null;
for (Method m : handlerClass.getMethods()) {
int modifier = m.getModifiers();
if (Modifier.isPublic(modifier) && Modifier.isAbstract(modifier) && m.getReturnType() == void.class) {
if (functionalMethod != null) {
throw new IllegalArgumentException("Not a functional interface");
}
functionalMethod = m;
}
}
if (functionalMethod == null) {
throw new IllegalArgumentException("Not a functional interface");
}
String pkg = "dev/frogmc/froglib/events/generated/";
String name = pkg + "FrogEventInvoker$" + Math.abs(functionalMethod.hashCode())+"$"+classNameMap.compute(handlerClass.getName(), (s, i) -> i == null ? 1 : i+1);
ClassNode node = new ClassNode();
node.visit(Opcodes.V21, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, name, null, "java/lang/Object", null);
MethodType functional = MethodHandles.lookup().unreflect(functionalMethod).type();
String funcDesc = functional.descriptorString().substring(functional.descriptorString().indexOf(';') + 1, functional.descriptorString().length() - 2);
MethodVisitor methodVisitor;
String type = "L" + handlerClass.getName().replace(".", "/") + ";";
Class<?>[] parameterArray = functional.parameterArray();
{
methodVisitor = node.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "invoker",
"(Ljava/util/List;)" + type,
"(Ljava/util/List<" + type + ">;)" + type,
null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(2, label0);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitInvokeDynamicInsn(functionalMethod.getName(), "(Ljava/util/List;)" + type,
new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;" +
"Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
false), Type.getType("(" + funcDesc + ")V"),
new Handle(Opcodes.H_INVOKESTATIC, name, "lambda$invoker$1",
"(Ljava/util/List;" + funcDesc + ")V", false),
Type.getType("(" + funcDesc + ")V"));
methodVisitor.visitInsn(Opcodes.ARETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("list", "Ljava/util/List;",
"Ljava/util/List<" + type + ">;", label0, label1, 0);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = node.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC,
"lambda$invoker$1", "(Ljava/util/List;" + funcDesc + ")V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(2, label0);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
for (int i = 0; i < parameterArray.length - 1; i++) {
methodVisitor.visitVarInsn(Opcodes.ALOAD, i + 1);
}
methodVisitor.visitInvokeDynamicInsn("accept", "(" + funcDesc + ")Ljava/util/function/Consumer;",
new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;" +
"Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
false), Type.getType("(Ljava/lang/Object;)V"),
new Handle(Opcodes.H_INVOKESTATIC, name, "lambda$invoker$0",
"(" + funcDesc + type + ")V", false),
Type.getType("(" + type + ")V"));
methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "forEach", "(Ljava/util/function/Consumer;)V", true);
methodVisitor.visitInsn(Opcodes.RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("list", "Ljava/util/List;", null, label0, label1, 0);
Set<String> usedParamNames = new HashSet<>();
usedParamNames.add("list");
int i;
for (i = 1; i < parameterArray.length; i++) {
Class<?> param = parameterArray[i];
methodVisitor.visitLocalVariable(getParamName(param, usedParamNames), param.descriptorString(), null, label0, label1, i);
}
methodVisitor.visitMaxs(i + 1, i + 1);
methodVisitor.visitEnd();
}
{
methodVisitor = node.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC,
"lambda$invoker$0", "(" + funcDesc + type + ")V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(2, label0);
methodVisitor.visitVarInsn(Opcodes.ALOAD, parameterArray.length - 1);
for (int i = 0; i < parameterArray.length - 1; i++) {
methodVisitor.visitVarInsn(Opcodes.ALOAD, i);
}
methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, handlerClass.getName().replace(".", "/"), functionalMethod.getName(),
"(" + funcDesc + ")V", true);
methodVisitor.visitInsn(Opcodes.RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
Set<String> usedLocalNames = new HashSet<>();
usedLocalNames.add("r");
int i;
for (i = 0; i < parameterArray.length - 1; i++) {
Class<?> param = parameterArray[i + 1];
methodVisitor.visitLocalVariable(getParamName(param, usedLocalNames), param.descriptorString(), null, label0, label1, i);
}
methodVisitor.visitLocalVariable("r", type, null, label0, label1, i);
methodVisitor.visitMaxs(i + 1, i + 1);
methodVisitor.visitEnd();
}
node.visitEnd();
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
node.accept(writer);
byte[] bytes = writer.toByteArray();
if (DEBUG_EXPORT) {
Path file = Paths.get(".frogmc/generated/" + name + ".class");
Files.createDirectories(file.getParent());
Files.write(file, bytes);
}
Class<?> generated = defineClass(name.replace("/", "."), bytes);
return MethodHandles.lookup().findStatic(generated, "invoker", MethodType.methodType(handlerClass, List.class));
}
@SuppressWarnings("unchecked")
private static <T> Class<T> defineClass(String name, byte[] bytes) {
try {
return (Class<T>) defineClass.invoke(EventGenerator.class.getClassLoader(), name, bytes, 0, bytes.length);
} catch (Throwable e) {
log.warn("Failed to generate class:", e);
throw new RuntimeException(e);
}
}
private static String getParamName(Class<?> cls, Set<String> usedLocalNames) {
String newName = convertDescriptor(cls.descriptorString());
int count = 1;
String nameCopy = newName;
while (usedLocalNames.contains(newName)) {
newName = nameCopy + ++count;
}
usedLocalNames.add(newName);
return newName;
}
private static String convertDescriptor(String descriptor) {
return switch (descriptor.charAt(0)) {
case 'B' -> "b";
case 'C' -> "c";
case 'D' -> "d";
case 'F' -> "f";
case 'I' -> "i";
case 'J' -> "l";
case 'S' -> "s";
case 'Z' -> "bl";
case '[' -> {
Type type = Type.getType(descriptor).getElementType();
yield type.getSort() == 10 ? convertDescriptor(type.getInternalName()) + "s" : type.getClassName() + "s";
}
default -> {
String name = cleanType(descriptor);
name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
yield name;
}
};
}
private static String cleanType(String type) {
String cleaned = type;
if (type.indexOf(60) != -1) {
cleaned = type.substring(0, type.indexOf(60));
}
if (cleaned.indexOf(47) != -1) {
cleaned = cleaned.substring(cleaned.lastIndexOf(47) + 1);
}
String var3 = cleaned.replaceAll("\\$\\d+;", "");
if (var3.indexOf(36) != -1) {
var3 = var3.substring(var3.lastIndexOf(36) + 1);
}
return var3.replaceAll("\\[]", "").replaceAll("(.*)\\d", "$1").replace(";", "");
}
}

View file

@ -0,0 +1,72 @@
package dev.frogmc.froglib.events.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import dev.frogmc.froglib.events.api.Event;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.ApiStatus;
@Slf4j
public class EventImpl<T> implements Event<T> {
private final List<T> listeners = new ArrayList<>();
private final Function<List<T>, T> invokerFactory;
private T cachedInvoker;
private EventImpl(Function<List<T>, T> invokerFactory) {
this.invokerFactory = invokerFactory;
}
public static <B> EventImpl<B> of(Function<List<B>, B> invokerFactory){
return new EventImpl<>(invokerFactory);
}
public static <B> EventImpl<B> of(Class<B> clazz) {
return of(list -> generateInvoker(list, clazz));
}
@SuppressWarnings("unchecked")
public static <B> EventImpl<B> of (B listener) {
EventImpl<B> event = of((Class<B>) listener.getClass().getInterfaces()[0]);
event.register(listener);
return event;
}
/**
* This constructor is only allowed if you can be sure that there is <strong>always</strong>
* at least one listener to this event (before the invoker method is called)
* @return a newly created Event
* @param <B> the type of this event
*/
@ApiStatus.Internal
@SuppressWarnings("unchecked")
public static <B> EventImpl<B> of(){
return of(list -> generateInvoker(list, (Class<B>) list.getFirst().getClass().getInterfaces()[0]));
}
public static EventImpl<Runnable> simple() {
return of(l -> () -> l.forEach(Runnable::run));
}
@SuppressWarnings("unchecked")
private static <B> B generateInvoker(List<B> list, Class<B> clazz) {
try {
return (B) EventGenerator.generateInvoker(clazz).invoke(list);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
@Override
public void register(T listener) {
listeners.add(listener);
}
@Override
public T invoker() {
return cachedInvoker == null ? cachedInvoker = invokerFactory.apply(listeners) : cachedInvoker;
}
}

View file

@ -0,0 +1,18 @@
package dev.frogmc.froglib.events.impl.mixin.client;
import dev.frogmc.froglib.events.api.Events;
import net.minecraft.client.Minecraft;
import net.minecraft.client.main.GameConfig;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Minecraft.class)
public class MinecraftMixin {
@Inject(method = "<init>", at = @At("TAIL"))
private void postClientInit(GameConfig gameConfig, CallbackInfo ci) {
Events.CLIENT_STARTUP.invoker().onClientInit((Minecraft) (Object) this, gameConfig);
}
}

View file

@ -0,0 +1,17 @@
package dev.frogmc.froglib.events.impl.mixin.server;
import dev.frogmc.froglib.events.api.Events;
import net.minecraft.server.dedicated.DedicatedServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(DedicatedServer.class)
public class DedicatedServerMixin {
@Inject(method = "initServer", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;)V", ordinal = 0))
private void initializeServer(CallbackInfoReturnable<Boolean> cir) {
Events.SERVER_STARTUP.invoker().onServerInit((DedicatedServer) (Object) this);
}
}

View file

@ -0,0 +1,23 @@
[frog]
format_version = "1.0.0"
[frog.mod]
id = "froglib"
name = "FrogLib"
version = "0.0.1"
license = "Apache-2.0"
credits = [
{ name = "FrogMC Team", roles = ["author"] }
]
[frog.dependencies]
depends = [
{ id = "minecraft", versions = "~1.20.6", name = "Minecraft", link = "https://frogmc.dev" }
]
[frog.extensions]
mixin_config = [
"froglib.entrypoints.mixins.json",
"froglib.events.mixins.json"
]

View file

@ -0,0 +1,15 @@
{
"required": true,
"minVersion": "0.8",
"package": "dev.frogmc.froglib.entrypoints.mixin",
"compatibilityLevel": "JAVA_21",
"server": [
"server.MinecraftServerMixin"
],
"client": [
"client.MainMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View file

@ -0,0 +1,18 @@
{
"required": true,
"minVersion": "0.8",
"package": "dev.frogmc.froglib.events.impl.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
],
"server": [
"server.DedicatedServerMixin"
],
"client": [
"client.MinecraftMixin"
],
"injectors": {
"defaultRequire": 1
}
}