From a1112ea2a031b4d1a02986a6301d3825191bebc7 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 24 Jan 2026 23:07:25 +0100 Subject: [PATCH 01/20] feat: introduce hook framework with processor, annotations, and integrations for Bukkit, Velocity, and core APIs --- .../surfapi/bukkit/test/BukkitPluginMain.kt | 11 +- .../surf/surfapi/bukkit/test/hook/TestHook.kt | 35 ++++ .../bukkit/server/hook/PaperHookService.kt | 42 +++++ .../impl/glow/entity/EntityGlowingData.kt | 2 +- .../server/reflection/JavaPluginProxy.kt | 12 ++ .../bukkit/server/reflection/Reflection.kt | 4 +- .../surfapi/core/api/hook/AbstractHook.kt | 53 ++++++ .../surf/surfapi/core/api/hook/HookMeta.kt | 7 + .../surf/surfapi/core/api/hook/SurfHookApi.kt | 22 +++ .../api/hook/requirement/DependsOnClass.kt | 8 + .../hook/requirement/DependsOnClassName.kt | 6 + .../hook/requirement/DependsOnOnePlugin.kt | 6 + .../api/hook/requirement/DependsOnPlugin.kt | 6 + .../reflection/SurfInvocationHandlerJava.java | 22 +-- .../surfapi/core/server/hook/HookService.kt | 118 +++++++++++++ .../core/server/hook/HookServiceFallback.kt | 29 ++++ .../core/server/hook/PluginHookMeta.kt | 20 +++ .../core/server/impl/hook/SurfHookApiImpl.kt | 48 ++++++ .../surf-api-processor/build.gradle.kts | 2 + .../autoservice/AutoServiceSymbolProcessor.kt | 14 +- .../processor/hook/HookSymbolProcessor.kt | 155 ++++++++++++++++++ .../hook/HookSymbolProcessorProvider.kt | 11 ++ .../surfapi/processor/hook/PluginHookMeta.kt | 20 +++ .../slne/surf/surfapi/processor/util/util.kt | 15 ++ ...ols.ksp.processing.SymbolProcessorProvider | 3 +- .../server/hook/VelocityHookService.kt | 45 +++++ 26 files changed, 687 insertions(+), 29 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperHookService.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/JavaPluginProxy.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/HookMeta.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin.kt create mode 100644 surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt create mode 100644 surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookServiceFallback.kt create mode 100644 surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/PluginHookMeta.kt create mode 100644 surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt create mode 100644 surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt create mode 100644 surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessorProvider.kt create mode 100644 surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt create mode 100644 surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt create mode 100644 surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt index 6d368cc9..af4c1a99 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt @@ -10,24 +10,29 @@ import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.TestInven import dev.slne.surf.surfapi.bukkit.test.command.subcommands.reflection.Reflection import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig import dev.slne.surf.surfapi.bukkit.test.listener.ChatListener +import dev.slne.surf.surfapi.core.api.hook.surfHookApi @OptIn(NmsUseWithCaution::class) class BukkitPluginMain : SuspendingJavaPlugin() { - override fun onLoad() { + override suspend fun onLoadAsync() { ModernTestConfig.init() ModernTestConfig.randomise() + surfHookApi.load(this) packetListenerApi.registerListeners(ChatListener()) TestInventoryView.register() } - override fun onEnable() { + override suspend fun onEnableAsync() { SurfApiTestCommand().register() Reflection::class.java.getClassLoader() // initialize Reflection + + surfHookApi.enable(this) } - override fun onDisable() { + override suspend fun onDisableAsync() { CommandAPI.unregister("surfapitest") + surfHookApi.disable(this) } companion object { diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt new file mode 100644 index 00000000..4b4f1c48 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt @@ -0,0 +1,35 @@ +package dev.slne.surf.surfapi.bukkit.test.hook + +import dev.slne.surf.surfapi.bukkit.test.BukkitPluginMain +import dev.slne.surf.surfapi.core.api.hook.AbstractHook +import dev.slne.surf.surfapi.core.api.hook.HookMeta +import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClass +import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClassName +import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnOnePlugin +import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnPlugin +import dev.slne.surf.surfapi.core.api.util.logger + +@HookMeta +@DependsOnClass(BukkitPluginMain::class) +@DependsOnClassName("dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig") +@DependsOnPlugin("SurfBukkitPluginTest") +@DependsOnOnePlugin(["SurfBukkitPlugin", "surf-bukkit-plugin", "SurfBukkitPluginTest"]) +class TestHook : AbstractHook() { + private val log = logger() + + override suspend fun onBootstrap() { + log.atInfo().log("TestHook bootstrapped") + } + + override suspend fun onLoad() { + log.atInfo().log("TestHook loaded") + } + + override suspend fun onEnable() { + log.atInfo().log("TestHook enabled") + } + + override suspend fun onDisable() { + log.atInfo().log("TestHook disabled") + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperHookService.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperHookService.kt new file mode 100644 index 00000000..d87c0a13 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperHookService.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.surfapi.bukkit.server.hook + +import com.google.auto.service.AutoService +import dev.slne.surf.surfapi.bukkit.api.extensions.pluginManager +import dev.slne.surf.surfapi.bukkit.server.reflection.Reflection +import dev.slne.surf.surfapi.core.server.hook.HookService +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import org.bukkit.plugin.java.JavaPlugin +import java.io.InputStream +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@AutoService(HookService::class) +class PaperHookService : HookService() { + override fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? { + ensureOwnerIsPlugin(owner) + return owner.getResource(fileName) + } + + override fun getClassloader(owner: Any): ClassLoader { + ensureOwnerIsPlugin(owner) + return Reflection.JAVA_PLUGIN_PROXY.getClassLoader(owner) + } + + override fun isPluginLoaded(pluginId: String): Boolean { + return pluginManager.getPlugin(pluginId) != null + } + + override fun getLogger(owner: Any): ComponentLogger { + ensureOwnerIsPlugin(owner) + return owner.componentLogger + } + + @OptIn(ExperimentalContracts::class) + private fun ensureOwnerIsPlugin(owner: Any): JavaPlugin { + contract { + returns() implies (owner is JavaPlugin) + } + + return owner as? JavaPlugin ?: error("Owner must be a JavaPlugin") + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/glow/entity/EntityGlowingData.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/glow/entity/EntityGlowingData.kt index 409263dd..8bfd098f 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/glow/entity/EntityGlowingData.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/glow/entity/EntityGlowingData.kt @@ -35,7 +35,7 @@ data class EntityGlowingData( @OptIn(NmsUseWithCaution::class) fun removeFromTeam(): PacketOperation { val color = color ?: return PacketOperationImpl.empty() - val teamData = TeamData.Companion.getByColorOrNull(color) ?: return PacketOperationImpl.empty() + val teamData = TeamData.getByColorOrNull(color) ?: return PacketOperationImpl.empty() val operation = PacketOperation.start() if (teamData.removeSeen(playerData.uuid)) { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/JavaPluginProxy.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/JavaPluginProxy.kt new file mode 100644 index 00000000..74b0ac76 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/JavaPluginProxy.kt @@ -0,0 +1,12 @@ +package dev.slne.surf.surfapi.bukkit.server.reflection + +import dev.slne.surf.surfapi.core.api.reflection.Name +import dev.slne.surf.surfapi.core.api.reflection.SurfProxy +import org.bukkit.plugin.java.JavaPlugin + +@SurfProxy(JavaPlugin::class) +interface JavaPluginProxy { + + @Name("getClassLoader") + fun getClassLoader(instance: JavaPlugin): ClassLoader +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/Reflection.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/Reflection.kt index 953ec7a6..02aece6a 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/Reflection.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/reflection/Reflection.kt @@ -11,16 +11,18 @@ object Reflection { val ITEM_PROXY: ItemProxy val ENTITY_PROXY: EntityProxy val SERVER_CONNECTION_LISTENER_PROXY: ServerConnectionListenerProxy + val JAVA_PLUGIN_PROXY: JavaPluginProxy init { val remapper = ReflectionRemapper.forReobfMappingsInPaperJar() val proxyFactory = - ReflectionProxyFactory.create(remapper, Reflection::class.java.getClassLoader()) + ReflectionProxyFactory.create(remapper, Reflection::class.java.classLoader) SERVER_STATS_COUNTER_PROXY = proxyFactory.reflectionProxy() ITEM_PROXY = surfReflection.createProxy() ENTITY_PROXY = proxyFactory.reflectionProxy() SERVER_CONNECTION_LISTENER_PROXY = proxyFactory.reflectionProxy() + JAVA_PLUGIN_PROXY = surfReflection.createProxy() // gc the remapper System.gc() diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt new file mode 100644 index 00000000..560e7330 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt @@ -0,0 +1,53 @@ +package dev.slne.surf.surfapi.core.api.hook + +import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import java.util.concurrent.atomic.AtomicBoolean + +abstract class AbstractHook : Comparable { + private val bootstrapped = AtomicBoolean(false) + private val loaded = AtomicBoolean(false) + private val enabled = AtomicBoolean(false) + private val disabled = AtomicBoolean(false) + + private val meta: HookMeta = javaClass.getAnnotation(HookMeta::class.java) + ?: error("HookMeta annotation is missing on hook class ${this::class.qualifiedName}") + + @InternalSurfApi + suspend fun bootstrap() { + if (bootstrapped.compareAndSet(false, true)) { + onBootstrap() + } + } + + @InternalSurfApi + suspend fun load() { + if (loaded.compareAndSet(false, true)) { + bootstrap() + onLoad() + } + } + + @InternalSurfApi + suspend fun enable() { + if (enabled.compareAndSet(false, true)) { + load() + onEnable() + } + } + + @InternalSurfApi + suspend fun disable() { + if (disabled.compareAndSet(false, true)) { + onDisable() + } + } + + final override fun compareTo(other: AbstractHook): Int { + return this.meta.priority.compareTo(other.meta.priority) + } + + protected open suspend fun onBootstrap() {} + protected open suspend fun onLoad() {} + protected open suspend fun onEnable() {} + protected open suspend fun onDisable() {} +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/HookMeta.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/HookMeta.kt new file mode 100644 index 00000000..08fbf6fc --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/HookMeta.kt @@ -0,0 +1,7 @@ +package dev.slne.surf.surfapi.core.api.hook + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class HookMeta( + val priority: Short = 0 +) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt new file mode 100644 index 00000000..fad36bba --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.surfapi.core.api.hook + +import dev.slne.surf.surfapi.core.api.util.requiredService + +interface SurfHookApi { + + suspend fun bootstrap(owner: Any) + suspend fun load(owner: Any) + suspend fun enable(owner: Any) + suspend fun disable(owner: Any) + + suspend fun hooksOfType(owner: Any, type: Class): List + suspend fun hooksOfType(type: Class): List + suspend fun hooks(owner: Any): List + + companion object { + val instance = requiredService() + } +} + +val surfHookApi get() = SurfHookApi.instance + diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass.kt new file mode 100644 index 00000000..47e816f0 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass.kt @@ -0,0 +1,8 @@ +package dev.slne.surf.surfapi.core.api.hook.requirement + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnClass(val clazz: KClass<*>) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName.kt new file mode 100644 index 00000000..b95ec58f --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName.kt @@ -0,0 +1,6 @@ +package dev.slne.surf.surfapi.core.api.hook.requirement + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnClassName(val className: String) \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin.kt new file mode 100644 index 00000000..3dc6c769 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin.kt @@ -0,0 +1,6 @@ +package dev.slne.surf.surfapi.core.api.hook.requirement + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnOnePlugin(val pluginIds: Array) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin.kt new file mode 100644 index 00000000..43441a86 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin.kt @@ -0,0 +1,6 @@ +package dev.slne.surf.surfapi.core.api.hook.requirement + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnPlugin(val pluginId: String) diff --git a/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java index 5ef069c5..ad4ceb85 100644 --- a/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java +++ b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java @@ -6,6 +6,12 @@ import dev.slne.surf.surfapi.core.api.util.SurfUtil; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -19,11 +25,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.reflect.FieldUtils; -import org.apache.commons.lang3.reflect.MethodUtils; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; @NullMarked public final class SurfInvocationHandlerJava implements InvocationHandler { @@ -98,6 +99,7 @@ private Invokable createInvokable(final Method method) { final var constructorAnnotation = method.getDeclaredAnnotation( dev.slne.surf.surfapi.core.api.reflection.Constructor.class); final var nameAnnotation = method.getDeclaredAnnotation(Name.class); + final var privateLookup = sneaky(() -> MethodHandles.privateLookupIn(proxiedClass, LOOKUP)); if (fieldAnnotation != null) { final String fieldName = getMethodName(method, nameAnnotation, fieldAnnotation, @@ -105,9 +107,9 @@ private Invokable createInvokable(final Method method) { final Field field = sneaky(() -> findField(proxiedClass, fieldName)); final boolean isGetter = fieldAnnotation.type() == Type.GETTER; final MethodHandle handleGetter = - isGetter ? sneaky(() -> LOOKUP.unreflectGetter(field)) : null; + isGetter ? sneaky(() -> privateLookup.unreflectGetter(field)) : null; final MethodHandle handleSetter = !isGetter && !fieldAnnotation.overrideFinal() - ? sneaky(() -> LOOKUP.unreflectSetter(field)) : null; + ? sneaky(() -> privateLookup.unreflectSetter(field)) : null; if (isGetter) { checkParamCount(method, staticAnnotation != null ? 0 : 1); @@ -125,7 +127,7 @@ private Invokable createInvokable(final Method method) { if (constructorAnnotation != null) { final var handle = sneaky( - () -> LOOKUP.unreflectConstructor(findConstructor(proxiedClass, method))); + () -> privateLookup.unreflectConstructor(findConstructor(proxiedClass, method))); return new HandleInvokable(normalizeMethodHandleType(handle)); } @@ -136,7 +138,7 @@ private Invokable createInvokable(final Method method) { final Method target = sneaky( () -> findMethod(proxiedClass, method, nameAnnotation, staticAnnotation)); - final MethodHandle handle = sneaky(() -> LOOKUP.unreflect(target)); + final MethodHandle handle = sneaky(() -> privateLookup.unreflect(target)); return new HandleInvokable(normalizeMethodHandleType(handle)); } @@ -176,7 +178,7 @@ private static Method findMethod( final Class[] paramTypes = Arrays.copyOfRange(original.getParameterTypes(), paramOffset, original.getParameterCount()); final String methodName = getMethodName(original, nameAnnotation, null, staticAnnotation, null); - final Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, paramTypes); + final Method method = MethodUtils.getMatchingMethod(clazz, methodName, paramTypes); if (method == null) { throw new NoSuchMethodException( "Method " + methodName + " with params " + Arrays.toString(paramTypes)); diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt new file mode 100644 index 00000000..60c3656b --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -0,0 +1,118 @@ +package dev.slne.surf.surfapi.core.server.hook + +import com.github.benmanes.caffeine.cache.Caffeine +import dev.slne.surf.surfapi.core.api.hook.AbstractHook +import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf +import dev.slne.surf.surfapi.core.api.util.requiredService +import kotlinx.serialization.json.Json +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import java.io.InputStream + +abstract class HookService { + + private val hookMetaCache = Caffeine.newBuilder() + .weakKeys() + .build { owner -> loadHooksMeta(owner) } + + private val hooksCache = Caffeine.newBuilder() + .weakKeys() + .build> { owner -> loadHooks(owner) } + + private fun loadHooksMeta(owner: Any): PluginHookMeta { + val rawStream = readHooksFileFromResources(owner, HOOKS_FILE_NAME) ?: return PluginHookMeta.empty() + val raw = rawStream.bufferedReader().use { it.readText() } + val decoded = Json.decodeFromString(raw) + return decoded + } + + private fun loadHooks(owner: Any): List { + val meta = hookMetaCache.get(owner) + val classLoader = getClassloader(owner) + val hooks = mutableObjectListOf() + + for (hookMeta in meta.hooks) { + val missingDependencies = mutableObject2ObjectMapOf>() + for (classDependency in hookMeta.classDependencies) { + try { + Class.forName(classDependency, false, classLoader) + } catch (_: ClassNotFoundException) { + missingDependencies.computeIfAbsent("Class") { mutableObjectSetOf() }.add(classDependency) + } + } + + for (pluginDependencyId in hookMeta.pluginDependencies) { + if (!isPluginLoaded(pluginDependencyId)) { + missingDependencies.computeIfAbsent("Plugin") { mutableObjectSetOf() }.add(pluginDependencyId) + } + } + + for (pluginDependenciesIds in hookMeta.pluginOneDependencies) { + if (pluginDependenciesIds.none { isPluginLoaded(it) }) { + missingDependencies.computeIfAbsent("Plugin (one of)") { mutableObjectSetOf() } + .add(pluginDependenciesIds.joinToString("|")) + } + } + + if (missingDependencies.isNotEmpty()) { + logMissingDependencies(owner, hookMeta.className, missingDependencies) + continue + } + + try { + val hookClass = Class.forName(hookMeta.className, false, classLoader) + val hookKClass = hookClass.kotlin + val objectInstance = hookKClass.objectInstance + if (objectInstance != null) { + require(objectInstance is AbstractHook) { "Hook class must implement AbstractHook" } + hooks.add(objectInstance) + } else { + val constructor = hookClass.getConstructor() + val instance = constructor.newInstance() + require(instance is AbstractHook) { "Hook class must implement AbstractHook" } + hooks.add(instance) + } + } catch (e: Exception) { + getLogger(owner).error("Failed to load hook ${hookMeta.className}", e) + } + } + + return hooks.sorted() + } + + private fun logMissingDependencies(owner: Any, hookClassName: String, missing: Map>) { + val logger = getLogger(owner) + + val lines = missing.entries + .sortedBy { it.key } + .joinToString(separator = System.lineSeparator()) { (type, ids) -> + val formattedIds = ids.toList().sorted().joinToString(", ") + " - $type: $formattedIds" + } + + logger.warn( + "Skipping hook $hookClassName due to missing dependencies:\n$lines" + ) + } + + fun getHooks(owner: Any): List { + return hooksCache.get(owner) + } + + fun getAllHooks(): List { + return hooksCache.asMap().values.flatten().sorted() + } + + abstract fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? + abstract fun getClassloader(owner: Any): ClassLoader + abstract fun isPluginLoaded(pluginId: String): Boolean + abstract fun getLogger(owner: Any): ComponentLogger + + companion object { + const val HOOKS_FILE_NAME = "surf-hooks.json" + + val instance = requiredService() + fun get() = instance + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookServiceFallback.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookServiceFallback.kt new file mode 100644 index 00000000..9d975531 --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookServiceFallback.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.surfapi.core.server.hook + +import com.google.auto.service.AutoService +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.kyori.adventure.util.Services +import java.io.InputStream + +@AutoService(HookService::class) +class HookServiceFallback : HookService(), Services.Fallback { + override fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? { + throwNotImplementedOnThisPlatform() + } + + override fun getClassloader(owner: Any): ClassLoader { + throwNotImplementedOnThisPlatform() + } + + override fun isPluginLoaded(pluginId: String): Boolean { + throwNotImplementedOnThisPlatform() + } + + override fun getLogger(owner: Any): ComponentLogger { + throwNotImplementedOnThisPlatform() + } + + private fun throwNotImplementedOnThisPlatform(): Nothing { + throw UnsupportedOperationException("This platform does not yet support hooks") + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/PluginHookMeta.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/PluginHookMeta.kt new file mode 100644 index 00000000..8d9fdccb --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/PluginHookMeta.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.surfapi.core.server.hook + +import kotlinx.serialization.Serializable + +@Serializable +data class PluginHookMeta(val hooks: List) { + + @Serializable + data class Hook( + val priority: Short, + val className: String, + val classDependencies: List, + val pluginDependencies: List, + val pluginOneDependencies: List>, + ) + + companion object { + fun empty() = PluginHookMeta(emptyList()) + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt new file mode 100644 index 00000000..a2c329ed --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt @@ -0,0 +1,48 @@ +package dev.slne.surf.surfapi.core.server.impl.hook + +import com.google.auto.service.AutoService +import dev.slne.surf.surfapi.core.api.hook.AbstractHook +import dev.slne.surf.surfapi.core.api.hook.SurfHookApi +import dev.slne.surf.surfapi.core.server.hook.HookService + +@AutoService(SurfHookApi::class) +class SurfHookApiImpl : SurfHookApi { + override suspend fun bootstrap(owner: Any) { + for (hook in hooks(owner)) { + hook.bootstrap() + } + } + + override suspend fun load(owner: Any) { + for (hook in hooks(owner)) { + hook.load() + } + } + + override suspend fun enable(owner: Any) { + for (hook in hooks(owner)) { + hook.enable() + } + } + + override suspend fun disable(owner: Any) { + for (hook in hooks(owner).reversed()) { + hook.disable() + } + } + + override suspend fun hooksOfType( + owner: Any, + type: Class + ): List { + return hooks(owner).filterIsInstance(type) + } + + override suspend fun hooksOfType(type: Class): List { + return HookService.get().getAllHooks().filterIsInstance(type) + } + + override suspend fun hooks(owner: Any): List { + return HookService.get().getHooks(owner) + } +} \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts b/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts index d296e209..b37faec9 100644 --- a/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts +++ b/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts @@ -6,6 +6,7 @@ val snapshot = (findProperty("snapshot") as String).toBooleanStrict() plugins { kotlin("jvm") + kotlin("plugin.serialization") `publish-convention` } @@ -19,6 +20,7 @@ version = buildString { dependencies { implementation(libs.ksp.api) implementation(libs.auto.service.annotations) + implementation(libs.kotlin.serialization.json) // https://mvnrepository.com/artifact/com.squareup/kotlinpoet implementation("com.squareup:kotlinpoet:2.2.0") diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/autoservice/AutoServiceSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/autoservice/AutoServiceSymbolProcessor.kt index eeaaa37d..59d40c32 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/autoservice/AutoServiceSymbolProcessor.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/autoservice/AutoServiceSymbolProcessor.kt @@ -3,7 +3,6 @@ package dev.slne.surf.surfapi.processor.autoservice import com.google.auto.service.AutoService import com.google.devtools.ksp.closestClassDeclaration import com.google.devtools.ksp.getAllSuperTypes -import com.google.devtools.ksp.isLocal import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor @@ -12,7 +11,7 @@ import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFile import com.google.devtools.ksp.symbol.KSType -import com.squareup.kotlinpoet.ClassName +import dev.slne.surf.surfapi.processor.util.toBinaryName import java.io.IOException class AutoServiceSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { @@ -143,17 +142,6 @@ class AutoServiceSymbolProcessor(environment: SymbolProcessorEnvironment) : Symb } } - - private fun KSClassDeclaration.toClassName(): ClassName { - require(!isLocal()) { "Local/anonymous classes are not supported!" } - val pkg = packageName.asString() - val typesString = qualifiedName!!.asString().removePrefix("$pkg.") - val simpleNames = typesString.split(".") - return ClassName(pkg, simpleNames) - } - - private fun KSClassDeclaration.toBinaryName(): String = toClassName().reflectionName() - private fun checkImplementer( implementer: KSClassDeclaration, providerType: KSType, diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt new file mode 100644 index 00000000..b01b1b94 --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt @@ -0,0 +1,155 @@ +package dev.slne.surf.surfapi.processor.hook + +import com.google.devtools.ksp.closestClassDeclaration +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import dev.slne.surf.surfapi.processor.util.toBinaryName +import kotlinx.serialization.json.Json +import java.io.IOException + +class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { + companion object { + private const val HOOK_ANNOTATION = "dev.slne.surf.surfapi.core.api.hook.HookMeta" + private const val DEPENDS_ON_CLASS_ANNOTATION = "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClass" + private const val DEPENDS_ON_CLASS_NAME_ANNOTATION = + "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClassName" + private const val DEPENDS_ON_ONE_PLUGIN_ANNOTATION = + "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnOnePlugin" + private const val DEPENDS_ON_PLUGIN_ANNOTATION = + "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnPlugin" + + private const val HOOKS_FILE_NAME = "surf-hooks.json" + + private val json = Json { prettyPrint = true } + } + + private val logger = environment.logger + private val codeGenerator = environment.codeGenerator + private val hooks = mutableSetOf() + + override fun process(resolver: Resolver): List { + val deferred = mutableListOf() + val hooksMetas = resolver.getSymbolsWithAnnotation(HOOK_ANNOTATION) + .filterIsInstance() + .mapNotNull { hookClass -> + val hookMeta = hookClass.annotations.findAnnotation(HOOK_ANNOTATION) ?: run { + logger.error("@HookMeta annotation not found on element", hookClass) + return@mapNotNull null + } + + val priority = hookMeta.arguments.find { it.name?.asString() == "priority" }?.value as? Short ?: 0 + val dependsOnClass = hookClass.annotations.findAnnotations(DEPENDS_ON_CLASS_ANNOTATION) + .mapNotNull { annotation -> + val clazzValue = annotation.arguments.find { it.name?.asString() == "clazz" }?.value as? KSType + if (clazzValue == null) { + logger.error("DependsOnClass annotation must have 'clazz' parameter", annotation) + return@mapNotNull null + } + + if (clazzValue.isError) { + deferred += hookClass + return@mapNotNull null + } + + val closestClass = clazzValue.declaration.closestClassDeclaration() + if (closestClass == null) { + deferred += hookClass + return@mapNotNull null + } + closestClass.toBinaryName() + } + + val dependsOnClassName = hookClass.annotations.findAnnotations(DEPENDS_ON_CLASS_NAME_ANNOTATION) + .mapNotNull { annotation -> + val classNameValue = + annotation.arguments.find { it.name?.asString() == "className" }?.value as? String + if (classNameValue == null) { + logger.error("@DependsOnClassName annotation must have 'className' parameter", annotation) + return@mapNotNull null + } + classNameValue + } + + val dependsOnOnePlugin = hookClass.annotations.findAnnotations(DEPENDS_ON_ONE_PLUGIN_ANNOTATION) + .mapNotNull { annotation -> + val argValue = annotation.arguments.find { it.name?.asString() == "pluginIds" }?.value + val pluginIds = when (argValue) { + is List<*> -> argValue.filterIsInstance() + is String -> listOf(argValue) + else -> emptyList() + } + + if (pluginIds.isEmpty()) { + logger.error("@DependsOnOnePlugin annotation must have 'pluginIds' parameter", annotation) + return@mapNotNull null + } + + pluginIds + } + + val dependsOnPlugin = hookClass.annotations.findAnnotations(DEPENDS_ON_PLUGIN_ANNOTATION) + .mapNotNull { annotation -> + val argValue = annotation.arguments.find { it.name?.asString() == "pluginId" }?.value + val pluginId = argValue as? String + if (pluginId == null) { + logger.error("@DependsOnPlugin annotation must have 'pluginId' parameter", annotation) + return@mapNotNull null + } + pluginId + } + + PluginHookMeta.Hook( + priority = priority, + className = hookClass.toBinaryName(), + classDependencies = dependsOnClass.toList() + dependsOnClassName.toList(), + pluginDependencies = dependsOnPlugin.toList(), + pluginOneDependencies = dependsOnOnePlugin.toList() + ) + }.toList() + + hooks.addAll(hooksMetas) + return deferred + } + + override fun finish() { + generatePluginHookFile() + } + + + private fun generatePluginHookFile() { + if (hooks.isEmpty()) { + return + } + + val hookMeta = PluginHookMeta(hooks.toList()) + try { + codeGenerator.createNewFileByPath(Dependencies(aggregating = true), HOOKS_FILE_NAME, "").bufferedWriter() + .use { writer -> + val json = json.encodeToString(hookMeta) + writer.write(json) + } + + logger.info("Wrote Hooks to: $HOOKS_FILE_NAME") + } catch (e: IOException) { + logger.error("Unable to create $HOOKS_FILE_NAME, $e") + } + } + + private fun Sequence.findAnnotation(annotationClassName: String): KSAnnotation? { + return this.firstOrNull { + it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationClassName + } + } + + private fun Sequence.findAnnotations(annotationClassName: String): Sequence { + return this.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationClassName + } + } +} \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessorProvider.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessorProvider.kt new file mode 100644 index 00000000..a9ffa9bf --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessorProvider.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.surfapi.processor.hook + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class HookSymbolProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return HookSymbolProcessor(environment) + } +} \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt new file mode 100644 index 00000000..047bd414 --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.surfapi.processor.hook + +import kotlinx.serialization.Serializable + +@Serializable +data class PluginHookMeta(val hooks: List) { + + @Serializable + data class Hook( + val priority: Short, + val className: String, + val classDependencies: List, + val pluginDependencies: List, + val pluginOneDependencies: List>, + ) + + companion object { + fun empty() = PluginHookMeta(emptyList()) + } +} \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt new file mode 100644 index 00000000..1088a85f --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt @@ -0,0 +1,15 @@ +package dev.slne.surf.surfapi.processor.util + +import com.google.devtools.ksp.isLocal +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.kotlinpoet.ClassName + +fun KSClassDeclaration.toClassName(): ClassName { + require(!isLocal()) { "Local/anonymous classes are not supported!" } + val pkg = packageName.asString() + val typesString = qualifiedName!!.asString().removePrefix("$pkg.") + val simpleNames = typesString.split(".") + return ClassName(pkg, simpleNames) +} + +fun KSClassDeclaration.toBinaryName(): String = toClassName().reflectionName() \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/surf-api-gradle-plugin/surf-api-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider index 82833035..6b2ab056 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -1 +1,2 @@ -dev.slne.surf.surfapi.processor.autoservice.AutoServiceSymbolProcessorProvider \ No newline at end of file +dev.slne.surf.surfapi.processor.autoservice.AutoServiceSymbolProcessorProvider +dev.slne.surf.surfapi.processor.hook.HookSymbolProcessorProvider \ No newline at end of file diff --git a/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt new file mode 100644 index 00000000..613f1e42 --- /dev/null +++ b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.surfapi.velocity.server.hook + +import com.google.auto.service.AutoService +import dev.slne.surf.surfapi.core.server.hook.HookService +import dev.slne.surf.surfapi.velocity.server.velocityMain +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import java.io.IOException +import java.io.InputStream +import kotlin.jvm.optionals.getOrNull + +@AutoService(HookService::class) +class VelocityHookService : HookService() { + override fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? { + val instance = getInstanceFromOwner(owner) + + return try { + val url = instance.javaClass.getResource(fileName) ?: return null + val connection = url.openConnection() + connection.useCaches = false + connection.getInputStream() + } catch (_: IOException) { + null + } + } + + override fun getClassloader(owner: Any): ClassLoader { + return getInstanceFromOwner(owner).javaClass.classLoader + } + + override fun isPluginLoaded(pluginId: String): Boolean { + return velocityMain.server.pluginManager.isLoaded(pluginId) + } + + override fun getLogger(owner: Any): ComponentLogger { + return ComponentLogger.logger(getPluginContainerFromOwner(owner).description.id) + } + + private fun getPluginContainerFromOwner(owner: Any) = + velocityMain.server.pluginManager.ensurePluginContainer(owner) + + private fun getInstanceFromOwner(owner: Any): Any { + return getPluginContainerFromOwner(owner).instance.getOrNull() + ?: error("Failed to get instance from owner: $owner") + } +} \ No newline at end of file From f4a0340caeb873dcc239eb30046b75352888eba8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Jan 2026 22:11:41 +0000 Subject: [PATCH 02/20] chore: update ABI and bump version [skip ci] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 102869b5..690b873b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=1.21.11 group=dev.slne.surf -version=1.21.11-2.55.1 +version=1.21.11-2.56.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false From c5619cf2242ea723534084466f61cf31a7c89d27 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 24 Jan 2026 23:14:39 +0100 Subject: [PATCH 03/20] feat: refactor VelocityHookService to use classloader for resource retrieval --- .../surf/surfapi/velocity/server/hook/VelocityHookService.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt index 613f1e42..4078f0e8 100644 --- a/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt +++ b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt @@ -11,10 +11,8 @@ import kotlin.jvm.optionals.getOrNull @AutoService(HookService::class) class VelocityHookService : HookService() { override fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? { - val instance = getInstanceFromOwner(owner) - return try { - val url = instance.javaClass.getResource(fileName) ?: return null + val url = getClassloader(owner).getResource(fileName) ?: return null val connection = url.openConnection() connection.useCaches = false connection.getInputStream() From f693a76196c4ed05235ab11bf046e43162918650 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:25:30 +0100 Subject: [PATCH 04/20] Fix hook metadata emission on unresolved deps --- .../surf/surfapi/processor/hook/HookSymbolProcessor.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt index b01b1b94..fb24a0b6 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt @@ -38,6 +38,7 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce val hooksMetas = resolver.getSymbolsWithAnnotation(HOOK_ANNOTATION) .filterIsInstance() .mapNotNull { hookClass -> + var hasUnresolvedClassDependency = false val hookMeta = hookClass.annotations.findAnnotation(HOOK_ANNOTATION) ?: run { logger.error("@HookMeta annotation not found on element", hookClass) return@mapNotNull null @@ -54,17 +55,23 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce if (clazzValue.isError) { deferred += hookClass + hasUnresolvedClassDependency = true return@mapNotNull null } val closestClass = clazzValue.declaration.closestClassDeclaration() if (closestClass == null) { deferred += hookClass + hasUnresolvedClassDependency = true return@mapNotNull null } closestClass.toBinaryName() } + if (hasUnresolvedClassDependency) { + return@mapNotNull null + } + val dependsOnClassName = hookClass.annotations.findAnnotations(DEPENDS_ON_CLASS_NAME_ANNOTATION) .mapNotNull { annotation -> val classNameValue = @@ -152,4 +159,4 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationClassName } } -} \ No newline at end of file +} From 153a7dc054f364546893147e1ad213503ba360e3 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 24 Jan 2026 23:28:58 +0100 Subject: [PATCH 05/20] Update ABI reference --- .../api/surf-api-core-api.api | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index fa0651a2..bdc7f601 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -6389,6 +6389,75 @@ public final class dev/slne/surf/surfapi/core/api/generated/VanillaAdvancementKe public static final field UPGRADE_TOOLS Lnet/kyori/adventure/key/Key; } +public abstract class dev/slne/surf/surfapi/core/api/hook/AbstractHook : java/lang/Comparable { + public fun ()V + public final fun bootstrap (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun compareTo (Ldev/slne/surf/surfapi/core/api/hook/AbstractHook;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun disable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun enable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun load (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected fun onBootstrap (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected fun onDisable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected fun onEnable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected fun onLoad (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/HookMeta : java/lang/annotation/Annotation { + public abstract fun priority ()S +} + +public abstract interface class dev/slne/surf/surfapi/core/api/hook/SurfHookApi { + public static final field Companion Ldev/slne/surf/surfapi/core/api/hook/SurfHookApi$Companion; + public abstract fun bootstrap (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun disable (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun enable (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun hooks (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun hooksOfType (Ljava/lang/Class;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun hooksOfType (Ljava/lang/Object;Ljava/lang/Class;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun load (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/surfapi/core/api/hook/SurfHookApi$Companion { + public final fun getInstance ()Ldev/slne/surf/surfapi/core/api/hook/SurfHookApi; +} + +public final class dev/slne/surf/surfapi/core/api/hook/SurfHookApiKt { + public static final fun getSurfHookApi ()Ldev/slne/surf/surfapi/core/api/hook/SurfHookApi; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass : java/lang/annotation/Annotation { + public abstract fun clazz ()Ljava/lang/Class; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName : java/lang/annotation/Annotation { + public abstract fun className ()Ljava/lang/String; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin : java/lang/annotation/Annotation { + public abstract fun pluginIds ()[Ljava/lang/String; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin : java/lang/annotation/Annotation { + public abstract fun pluginId ()Ljava/lang/String; +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin; +} + public final class dev/slne/surf/surfapi/core/api/math/VoxelLineTracer { public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/math/VoxelLineTracer; public final fun trace (Lorg/spongepowered/math/vector/Vector3d;Lorg/spongepowered/math/vector/Vector3d;)Lkotlin/sequences/Sequence; From cdfca6c631c68d09ed1a79647626b12ab6373414 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 24 Jan 2026 23:32:03 +0100 Subject: [PATCH 06/20] feat: improve error handling for hooks metadata loading --- .../slne/surf/surfapi/core/server/hook/HookService.kt | 9 +++++++-- .../surf/surfapi/processor/hook/HookSymbolProcessor.kt | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 60c3656b..14fcc8f3 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -6,6 +6,7 @@ import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf import dev.slne.surf.surfapi.core.api.util.requiredService +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import net.kyori.adventure.text.logger.slf4j.ComponentLogger import java.io.InputStream @@ -23,8 +24,12 @@ abstract class HookService { private fun loadHooksMeta(owner: Any): PluginHookMeta { val rawStream = readHooksFileFromResources(owner, HOOKS_FILE_NAME) ?: return PluginHookMeta.empty() val raw = rawStream.bufferedReader().use { it.readText() } - val decoded = Json.decodeFromString(raw) - return decoded + return try { + Json.decodeFromString(raw) + } catch (e: SerializationException) { + getLogger(owner).error("Failed to parse $HOOKS_FILE_NAME", e) + PluginHookMeta.empty() + } } private fun loadHooks(owner: Any): List { diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt index fb24a0b6..4e860bed 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt @@ -138,8 +138,8 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce try { codeGenerator.createNewFileByPath(Dependencies(aggregating = true), HOOKS_FILE_NAME, "").bufferedWriter() .use { writer -> - val json = json.encodeToString(hookMeta) - writer.write(json) + val jsonString = json.encodeToString(hookMeta) + writer.write(jsonString) } logger.info("Wrote Hooks to: $HOOKS_FILE_NAME") From b342e50a7454bdb6ef567eb5fc7dc3a259a21f75 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 15:05:06 +0100 Subject: [PATCH 07/20] feat: introduce shared hook metadata and configuration classes --- .../main/kotlin/core-convention.gradle.kts | 4 ++- gradle/wrapper/gradle-wrapper.jar | Bin 45457 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 +--- gradlew.bat | 3 +- settings.gradle.kts | 5 +++- .../surf/surfapi/bukkit/test/hook/TestHook.kt | 10 +++---- .../surf-api-core-api/build.gradle.kts | 10 +------ .../surfapi/core/api/hook/AbstractHook.kt | 1 + .../surf-api-core-server/build.gradle.kts | 1 + .../surfapi/core/server/hook/HookService.kt | 1 + .../surf-api-processor/build.gradle.kts | 2 +- .../processor/hook/HookSymbolProcessor.kt | 27 +++++++++--------- .../surfapi/processor/hook/PluginHookMeta.kt | 20 ------------- .../slne/surf/surfapi/processor/util/util.kt | 4 ++- surf-api-shared/build.gradle.kts | 3 ++ .../surf-api-shared-internal/build.gradle.kts | 7 +++++ .../shared/internal/hook/HooksConfig.kt | 8 ++++++ .../shared/internal}/hook/PluginHookMeta.kt | 2 +- .../surf-api-shared-public/build.gradle.kts | 16 +++++++++++ .../surf/surfapi/shared}/api/hook/HookMeta.kt | 2 +- .../api/hook/requirement/DependsOnClass.kt | 2 +- .../hook/requirement/DependsOnClassName.kt | 2 +- .../hook/requirement/DependsOnOnePlugin.kt | 2 +- .../api/hook/requirement/DependsOnPlugin.kt | 2 +- 25 files changed, 77 insertions(+), 64 deletions(-) delete mode 100644 surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt create mode 100644 surf-api-shared/build.gradle.kts create mode 100644 surf-api-shared/surf-api-shared-internal/build.gradle.kts create mode 100644 surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt rename {surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server => surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal}/hook/PluginHookMeta.kt (89%) create mode 100644 surf-api-shared/surf-api-shared-public/build.gradle.kts rename {surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core => surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared}/api/hook/HookMeta.kt (73%) rename {surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core => surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared}/api/hook/requirement/DependsOnClass.kt (74%) rename {surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core => surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared}/api/hook/requirement/DependsOnClassName.kt (71%) rename {surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core => surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared}/api/hook/requirement/DependsOnOnePlugin.kt (72%) rename {surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core => surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared}/api/hook/requirement/DependsOnPlugin.kt (70%) diff --git a/buildSrc/src/main/kotlin/core-convention.gradle.kts b/buildSrc/src/main/kotlin/core-convention.gradle.kts index 59d2424a..744f7e82 100644 --- a/buildSrc/src/main/kotlin/core-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/core-convention.gradle.kts @@ -27,7 +27,9 @@ repositories { dependencies { compileOnly(libs.auto.service.annotations) - ksp(project(":surf-api-gradle-plugin:surf-api-processor")) + if (!project.path.contains("surf-api-shared")) { + ksp(project(":surf-api-gradle-plugin:surf-api-processor")) + } compileOnlyApi("org.jetbrains:annotations:26.0.2-1") } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8bdaf60c75ab801e22807dde59e12a8735a34077..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39724 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ⅆA{k6{Sa_dPB>TF4Im=mf;z@rf2j7Tr#P>Bp9qL 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 delta 36423 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e111328..6e750047 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-milestone-5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index ef07e016..0fff279d 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/dd2cf7f3826b2da07d8d6de488a2d1bc5651ca7d/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index db3a6ac2..c4bdd3ab 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle.kts b/settings.gradle.kts index 49471d0f..c5d1fb12 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,4 +31,7 @@ if (!ci) { include(":surf-api-bukkit:surf-api-bukkit-plugin-test") // include("surf-api-generator") include("surf-api-modern-generator") -} \ No newline at end of file +} +include("surf-api-shared") +include("surf-api-shared:surf-api-shared-public") +include("surf-api-shared:surf-api-shared-internal") \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt index 4b4f1c48..d51b733a 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt @@ -2,12 +2,12 @@ package dev.slne.surf.surfapi.bukkit.test.hook import dev.slne.surf.surfapi.bukkit.test.BukkitPluginMain import dev.slne.surf.surfapi.core.api.hook.AbstractHook -import dev.slne.surf.surfapi.core.api.hook.HookMeta -import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClass -import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClassName -import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnOnePlugin -import dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnPlugin import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.shared.api.hook.HookMeta +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClass +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClassName +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnOnePlugin +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnPlugin @HookMeta @DependsOnClass(BukkitPluginMain::class) diff --git a/surf-api-core/surf-api-core-api/build.gradle.kts b/surf-api-core/surf-api-core-api/build.gradle.kts index 8ad624f2..d6b749f7 100644 --- a/surf-api-core/surf-api-core-api/build.gradle.kts +++ b/surf-api-core/surf-api-core-api/build.gradle.kts @@ -4,13 +4,7 @@ plugins { } dependencies { - compileOnlyApi(libs.adventure.api) - compileOnlyApi(libs.adventure.text.logger.slf4j) - compileOnlyApi(libs.adventure.text.minimessage) - compileOnlyApi(libs.adventure.serializer.gson) - compileOnlyApi(libs.adventure.serializer.legacy) - compileOnlyApi(libs.adventure.serializer.plain) - compileOnlyApi(libs.adventure.serializer.ansi) + api(project(":surf-api-shared:surf-api-shared-public")) api(libs.adventure.nbt) compileOnlyApi(libs.packetevents.api) compileOnlyApi(libs.dazzleconf) @@ -31,8 +25,6 @@ dependencies { api(libs.caffeine.courotines) api(libs.bundles.kotlin.coroutines) api(libs.bundles.reactor.netty) - api(libs.kotlin.reflect) - api(libs.bundles.kotlin.serialization) compileOnlyApi(libs.guava) compileOnlyApi(libs.caffeine) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt index 560e7330..c1afbb49 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt @@ -1,6 +1,7 @@ package dev.slne.surf.surfapi.core.api.hook import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.hook.HookMeta import java.util.concurrent.atomic.AtomicBoolean abstract class AbstractHook : Comparable { diff --git a/surf-api-core/surf-api-core-server/build.gradle.kts b/surf-api-core/surf-api-core-server/build.gradle.kts index 97f09634..3c474899 100644 --- a/surf-api-core/surf-api-core-server/build.gradle.kts +++ b/surf-api-core/surf-api-core-server/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { api(project(":surf-api-core:surf-api-core-api")) + api(project(":surf-api-shared:surf-api-shared-internal")) compileOnly(libs.packetevents.netty.common) } diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 14fcc8f3..6c20f68d 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -6,6 +6,7 @@ import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf import dev.slne.surf.surfapi.core.api.util.requiredService +import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import net.kyori.adventure.text.logger.slf4j.ComponentLogger diff --git a/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts b/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts index b37faec9..1117bcc2 100644 --- a/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts +++ b/surf-api-gradle-plugin/surf-api-processor/build.gradle.kts @@ -20,7 +20,7 @@ version = buildString { dependencies { implementation(libs.ksp.api) implementation(libs.auto.service.annotations) - implementation(libs.kotlin.serialization.json) + api(project(":surf-api-shared:surf-api-shared-internal")) // https://mvnrepository.com/artifact/com.squareup/kotlinpoet implementation("com.squareup:kotlinpoet:2.2.0") diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt index 4e860bed..c60e547d 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt @@ -9,24 +9,25 @@ import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSType +import dev.slne.surf.surfapi.processor.util.nameOf import dev.slne.surf.surfapi.processor.util.toBinaryName -import kotlinx.serialization.json.Json +import dev.slne.surf.surfapi.shared.api.hook.HookMeta +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClass +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClassName +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnOnePlugin +import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnPlugin +import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig.HOOKS_FILE_NAME +import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig.json +import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import java.io.IOException class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { companion object { - private const val HOOK_ANNOTATION = "dev.slne.surf.surfapi.core.api.hook.HookMeta" - private const val DEPENDS_ON_CLASS_ANNOTATION = "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClass" - private const val DEPENDS_ON_CLASS_NAME_ANNOTATION = - "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnClassName" - private const val DEPENDS_ON_ONE_PLUGIN_ANNOTATION = - "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnOnePlugin" - private const val DEPENDS_ON_PLUGIN_ANNOTATION = - "dev.slne.surf.surfapi.core.api.hook.requirement.DependsOnPlugin" - - private const val HOOKS_FILE_NAME = "surf-hooks.json" - - private val json = Json { prettyPrint = true } + private val HOOK_ANNOTATION = nameOf() + private val DEPENDS_ON_CLASS_ANNOTATION = nameOf() + private val DEPENDS_ON_CLASS_NAME_ANNOTATION = nameOf() + private val DEPENDS_ON_ONE_PLUGIN_ANNOTATION = nameOf() + private val DEPENDS_ON_PLUGIN_ANNOTATION = nameOf() } private val logger = environment.logger diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt deleted file mode 100644 index 047bd414..00000000 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/PluginHookMeta.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.slne.surf.surfapi.processor.hook - -import kotlinx.serialization.Serializable - -@Serializable -data class PluginHookMeta(val hooks: List) { - - @Serializable - data class Hook( - val priority: Short, - val className: String, - val classDependencies: List, - val pluginDependencies: List, - val pluginOneDependencies: List>, - ) - - companion object { - fun empty() = PluginHookMeta(emptyList()) - } -} \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt index 1088a85f..bda62ba2 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt @@ -12,4 +12,6 @@ fun KSClassDeclaration.toClassName(): ClassName { return ClassName(pkg, simpleNames) } -fun KSClassDeclaration.toBinaryName(): String = toClassName().reflectionName() \ No newline at end of file +fun KSClassDeclaration.toBinaryName(): String = toClassName().reflectionName() + +inline fun nameOf(): String = T::class.java.name \ No newline at end of file diff --git a/surf-api-shared/build.gradle.kts b/surf-api-shared/build.gradle.kts new file mode 100644 index 00000000..4b36bc3a --- /dev/null +++ b/surf-api-shared/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + `core-convention` +} \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-internal/build.gradle.kts b/surf-api-shared/surf-api-shared-internal/build.gradle.kts new file mode 100644 index 00000000..08b86c6e --- /dev/null +++ b/surf-api-shared/surf-api-shared-internal/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `core-convention` +} + +dependencies { + api(project(":surf-api-shared:surf-api-shared-public")) +} \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt new file mode 100644 index 00000000..0d62faa3 --- /dev/null +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt @@ -0,0 +1,8 @@ +package dev.slne.surf.surfapi.shared.internal.hook + +import kotlinx.serialization.json.Json + +object HooksConfig { + const val HOOKS_FILE_NAME = "surf-hooks.json" + val json = Json { prettyPrint = true } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/PluginHookMeta.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt similarity index 89% rename from surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/PluginHookMeta.kt rename to surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt index 8d9fdccb..cc5b5425 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/PluginHookMeta.kt +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.core.server.hook +package dev.slne.surf.surfapi.shared.internal.hook import kotlinx.serialization.Serializable diff --git a/surf-api-shared/surf-api-shared-public/build.gradle.kts b/surf-api-shared/surf-api-shared-public/build.gradle.kts new file mode 100644 index 00000000..5e640d5c --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `core-convention` +} + +dependencies { + compileOnlyApi(libs.adventure.api) + compileOnlyApi(libs.adventure.text.logger.slf4j) + compileOnlyApi(libs.adventure.text.minimessage) + compileOnlyApi(libs.adventure.serializer.gson) + compileOnlyApi(libs.adventure.serializer.legacy) + compileOnlyApi(libs.adventure.serializer.plain) + compileOnlyApi(libs.adventure.serializer.ansi) + + api(libs.kotlin.reflect) + api(libs.bundles.kotlin.serialization) +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/HookMeta.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/HookMeta.kt similarity index 73% rename from surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/HookMeta.kt rename to surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/HookMeta.kt index 08fbf6fc..b714a56f 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/HookMeta.kt +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/HookMeta.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.core.api.hook +package dev.slne.surf.surfapi.shared.api.hook @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass.kt similarity index 74% rename from surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass.kt rename to surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass.kt index 47e816f0..ec11e6e7 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass.kt +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.core.api.hook.requirement +package dev.slne.surf.surfapi.shared.api.hook.requirement import kotlin.reflect.KClass diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName.kt similarity index 71% rename from surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName.kt rename to surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName.kt index b95ec58f..9db89030 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName.kt +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.core.api.hook.requirement +package dev.slne.surf.surfapi.shared.api.hook.requirement @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin.kt similarity index 72% rename from surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin.kt rename to surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin.kt index 3dc6c769..c4290759 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin.kt +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.core.api.hook.requirement +package dev.slne.surf.surfapi.shared.api.hook.requirement @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin.kt similarity index 70% rename from surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin.kt rename to surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin.kt index 43441a86..73966d91 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin.kt +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.core.api.hook.requirement +package dev.slne.surf.surfapi.shared.api.hook.requirement @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) From 1c47e839ceb52ca3dfaac7e4fcb8370995cc1916 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 18:25:51 +0100 Subject: [PATCH 08/20] feat: refactor hook architecture to use shared Hook interface and add DependsOnHook annotation --- build.gradle.kts | 2 +- .../executors/SuspendCommandExecutors.kt | 2 +- .../surfapi/core/api/hook/AbstractHook.kt | 19 +- .../surf/surfapi/core/api/hook/SurfHookApi.kt | 3 +- .../pagination/InternalPaginationBridge.kt | 2 +- .../surfapi/core/api/nbt/InternalNbtBridge.kt | 2 +- .../surfapi/core/server/hook/HookService.kt | 192 ++++++++++++++---- .../processor/hook/HookSymbolProcessor.kt | 23 ++- .../hytale/api/coroutines/CoroutineSession.kt | 2 +- .../hytale/api/coroutines/HysCoroutine.kt | 2 +- .../shared/internal/hook/PluginHookMeta.kt | 1 + .../slne/surf/surfapi/shared/api/hook/Hook.kt | 9 + .../api/hook/requirement/DependsOnHook.kt | 9 + .../surfapi/shared}/api/util/internal-api.kt | 2 +- .../executors/SuspendCommandExecutors.kt | 2 +- 15 files changed, 209 insertions(+), 63 deletions(-) create mode 100644 surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/Hook.kt create mode 100644 surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook.kt rename {surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core => surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared}/api/util/internal-api.kt (77%) diff --git a/build.gradle.kts b/build.gradle.kts index ce1833ab..3e8951f5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,7 +49,7 @@ subprojects { afterEvaluate { extensions.findByType()?.apply { compilerOptions { - optIn.add("dev.slne.surf.surfapi.core.api.util.InternalSurfApi") + optIn.add("dev.slne.surf.surfapi.shared.api.util.InternalSurfApi") } } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/command/executors/SuspendCommandExecutors.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/command/executors/SuspendCommandExecutors.kt index 5e06e2da..dcaa0672 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/command/executors/SuspendCommandExecutors.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/command/executors/SuspendCommandExecutors.kt @@ -13,7 +13,7 @@ import dev.jorel.commandapi.kotlindsl.* import dev.jorel.commandapi.wrappers.NativeProxyCommandSender import dev.slne.surf.surfapi.core.api.messages.Colors import dev.slne.surf.surfapi.core.api.messages.adventure.text -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt index c1afbb49..c7c3c92c 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt @@ -1,10 +1,11 @@ package dev.slne.surf.surfapi.core.api.hook -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.hook.Hook import dev.slne.surf.surfapi.shared.api.hook.HookMeta +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import java.util.concurrent.atomic.AtomicBoolean -abstract class AbstractHook : Comparable { +abstract class AbstractHook : Hook { private val bootstrapped = AtomicBoolean(false) private val loaded = AtomicBoolean(false) private val enabled = AtomicBoolean(false) @@ -13,15 +14,17 @@ abstract class AbstractHook : Comparable { private val meta: HookMeta = javaClass.getAnnotation(HookMeta::class.java) ?: error("HookMeta annotation is missing on hook class ${this::class.qualifiedName}") + override val priority = meta.priority + @InternalSurfApi - suspend fun bootstrap() { + override suspend fun bootstrap() { if (bootstrapped.compareAndSet(false, true)) { onBootstrap() } } @InternalSurfApi - suspend fun load() { + override suspend fun load() { if (loaded.compareAndSet(false, true)) { bootstrap() onLoad() @@ -29,7 +32,7 @@ abstract class AbstractHook : Comparable { } @InternalSurfApi - suspend fun enable() { + override suspend fun enable() { if (enabled.compareAndSet(false, true)) { load() onEnable() @@ -37,14 +40,14 @@ abstract class AbstractHook : Comparable { } @InternalSurfApi - suspend fun disable() { + override suspend fun disable() { if (disabled.compareAndSet(false, true)) { onDisable() } } - final override fun compareTo(other: AbstractHook): Int { - return this.meta.priority.compareTo(other.meta.priority) + final override fun compareTo(other: Hook): Int { + return this.priority.compareTo(other.priority) } protected open suspend fun onBootstrap() {} diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt index fad36bba..6652759f 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt @@ -1,6 +1,7 @@ package dev.slne.surf.surfapi.core.api.hook import dev.slne.surf.surfapi.core.api.util.requiredService +import dev.slne.surf.surfapi.shared.api.hook.Hook interface SurfHookApi { @@ -11,7 +12,7 @@ interface SurfHookApi { suspend fun hooksOfType(owner: Any, type: Class): List suspend fun hooksOfType(type: Class): List - suspend fun hooks(owner: Any): List + suspend fun hooks(owner: Any): List companion object { val instance = requiredService() diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt index 3989961e..b5ad7d07 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt @@ -1,7 +1,7 @@ package dev.slne.surf.surfapi.core.api.messages.pagination -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi import dev.slne.surf.surfapi.core.api.util.requiredService +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi @InternalSurfApi interface InternalPaginationBridge { diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/InternalNbtBridge.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/InternalNbtBridge.kt index 6bf36284..623cd6e2 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/InternalNbtBridge.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/InternalNbtBridge.kt @@ -1,7 +1,7 @@ package dev.slne.surf.surfapi.core.api.nbt -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi import dev.slne.surf.surfapi.core.api.util.requiredService +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import net.kyori.adventure.nbt.CompoundBinaryTag @InternalSurfApi diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 6c20f68d..6f3748d8 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -1,11 +1,10 @@ package dev.slne.surf.surfapi.core.server.hook import com.github.benmanes.caffeine.cache.Caffeine -import dev.slne.surf.surfapi.core.api.hook.AbstractHook import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf -import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf import dev.slne.surf.surfapi.core.api.util.requiredService +import dev.slne.surf.surfapi.shared.api.hook.Hook import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -20,7 +19,7 @@ abstract class HookService { private val hooksCache = Caffeine.newBuilder() .weakKeys() - .build> { owner -> loadHooks(owner) } + .build> { owner -> loadHooks(owner) } private fun loadHooksMeta(owner: Any): PluginHookMeta { val rawStream = readHooksFileFromResources(owner, HOOKS_FILE_NAME) ?: return PluginHookMeta.empty() @@ -33,58 +32,167 @@ abstract class HookService { } } - private fun loadHooks(owner: Any): List { + private fun loadHooks(owner: Any): List { val meta = hookMetaCache.get(owner) val classLoader = getClassloader(owner) - val hooks = mutableObjectListOf() - - for (hookMeta in meta.hooks) { - val missingDependencies = mutableObject2ObjectMapOf>() - for (classDependency in hookMeta.classDependencies) { - try { - Class.forName(classDependency, false, classLoader) - } catch (_: ClassNotFoundException) { - missingDependencies.computeIfAbsent("Class") { mutableObjectSetOf() }.add(classDependency) - } + + val hooksWithMeta = meta.hooks.mapNotNull { hookMeta -> + val hook = instantiateHookIfValid(owner, hookMeta, classLoader) + if (hook != null) { + hookMeta to hook + } else { + null } + } - for (pluginDependencyId in hookMeta.pluginDependencies) { - if (!isPluginLoaded(pluginDependencyId)) { - missingDependencies.computeIfAbsent("Plugin") { mutableObjectSetOf() }.add(pluginDependencyId) - } + return topologicalSort(hooksWithMeta, owner) + } + + private fun instantiateHookIfValid( + owner: Any, + hookMeta: PluginHookMeta.Hook, + classLoader: ClassLoader + ): Hook? { + val missingDependencies = mutableObject2ObjectMapOf>() + for (classDependency in hookMeta.classDependencies) { + try { + Class.forName(classDependency, false, classLoader) + } catch (_: ClassNotFoundException) { + missingDependencies.computeIfAbsent("Class") { mutableObjectSetOf() }.add(classDependency) + } + } + + for (pluginDependencyId in hookMeta.pluginDependencies) { + if (!isPluginLoaded(pluginDependencyId)) { + missingDependencies.computeIfAbsent("Plugin") { mutableObjectSetOf() }.add(pluginDependencyId) } + } + + for (pluginDependenciesIds in hookMeta.pluginOneDependencies) { + if (pluginDependenciesIds.none { isPluginLoaded(it) }) { + missingDependencies.computeIfAbsent("Plugin (one of)") { mutableObjectSetOf() } + .add(pluginDependenciesIds.joinToString("|")) + } + } + + if (missingDependencies.isNotEmpty()) { + logMissingDependencies(owner, hookMeta.className, missingDependencies) + return null + } + + try { + val hookClass = Class.forName(hookMeta.className, false, classLoader) + val hookKClass = hookClass.kotlin + val objectInstance = hookKClass.objectInstance + if (objectInstance != null) { + require(objectInstance is Hook) { "Hook class must implement Hook" } + return objectInstance + } else { + val constructor = hookClass.getConstructor() + val instance = constructor.newInstance() + require(instance is Hook) { "Hook class must implement Hook" } + return instance + } + } catch (e: Exception) { + getLogger(owner).error("Failed to load hook ${hookMeta.className}", e) + } + + return null + } + + private fun topologicalSort( + hooksWithMeta: List>, + owner: Any + ): List { + // If no hooks depend on other hooks, simply sort by priority + if (hooksWithMeta.none { it.first.hookDependencies.isNotEmpty() }) { + return hooksWithMeta.map { it.second }.sorted() + } - for (pluginDependenciesIds in hookMeta.pluginOneDependencies) { - if (pluginDependenciesIds.none { isPluginLoaded(it) }) { - missingDependencies.computeIfAbsent("Plugin (one of)") { mutableObjectSetOf() } - .add(pluginDependenciesIds.joinToString("|")) + val hooksByClassName = hooksWithMeta.associate { (meta, hook) -> + meta.className to hook + } + + val metaByClassName = hooksWithMeta.associate { (meta, _) -> + meta.className to meta + } + + val missingHookDeps = mutableMapOf>() + for ((meta, _) in hooksWithMeta) { + for (depClassName in meta.hookDependencies) { + if (depClassName !in hooksByClassName) { + missingHookDeps.computeIfAbsent(meta.className) { mutableSetOf() } + .add(depClassName) } } + } - if (missingDependencies.isNotEmpty()) { - logMissingDependencies(owner, hookMeta.className, missingDependencies) - continue + if (missingHookDeps.isNotEmpty()) { + val logger = getLogger(owner) + for ((hookClassName, missingDeps) in missingHookDeps) { + logger.warn( + "Hook $hookClassName depends on hooks that are not loaded: ${missingDeps.joinToString(", ")}" + ) } + } - try { - val hookClass = Class.forName(hookMeta.className, false, classLoader) - val hookKClass = hookClass.kotlin - val objectInstance = hookKClass.objectInstance - if (objectInstance != null) { - require(objectInstance is AbstractHook) { "Hook class must implement AbstractHook" } - hooks.add(objectInstance) - } else { - val constructor = hookClass.getConstructor() - val instance = constructor.newInstance() - require(instance is AbstractHook) { "Hook class must implement AbstractHook" } - hooks.add(instance) + val validHooks = hooksWithMeta.filter { (meta, _) -> + meta.className !in missingHookDeps + } + + if (validHooks.isEmpty()) { + return emptyList() + } + + val sorted = mutableListOf() + val visited = mutableSetOf() + val visiting = mutableSetOf() + + fun visit(className: String) { + if (className in visited) return + + if (className in visiting) { + val chain = buildCircularDependencyChain(className, metaByClassName, visiting) + throw IllegalStateException( + "Circular hook dependency detected: ${chain.joinToString(" -> ")}" + ) + } + + visiting.add(className) + + val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList() + for (depClassName in dependencies) { + if (depClassName in hooksByClassName) { + visit(depClassName) } - } catch (e: Exception) { - getLogger(owner).error("Failed to load hook ${hookMeta.className}", e) } + + visiting.remove(className) + visited.add(className) + + hooksByClassName[className]?.let { sorted.add(it) } + } + + validHooks.forEach { (meta, _) -> visit(meta.className) } + return sorted.sortedBy { it.priority } + } + + private fun buildCircularDependencyChain( + startClassName: String, + metaByClassName: Map, + visiting: Set + ): List { + val chain = mutableListOf() + var current = startClassName + + while (current !in chain) { + chain.add(current) + val deps = metaByClassName[current]?.hookDependencies ?: break + current = deps.firstOrNull { it in visiting } ?: break } - return hooks.sorted() + chain.add(startClassName) + return chain } private fun logMissingDependencies(owner: Any, hookClassName: String, missing: Map>) { @@ -102,11 +210,11 @@ abstract class HookService { ) } - fun getHooks(owner: Any): List { + fun getHooks(owner: Any): List { return hooksCache.get(owner) } - fun getAllHooks(): List { + fun getAllHooks(): List { return hooksCache.asMap().values.flatten().sorted() } diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt index c60e547d..027793d1 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt @@ -12,10 +12,7 @@ import com.google.devtools.ksp.symbol.KSType import dev.slne.surf.surfapi.processor.util.nameOf import dev.slne.surf.surfapi.processor.util.toBinaryName import dev.slne.surf.surfapi.shared.api.hook.HookMeta -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClass -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClassName -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnOnePlugin -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnPlugin +import dev.slne.surf.surfapi.shared.api.hook.requirement.* import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig.HOOKS_FILE_NAME import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig.json import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta @@ -28,6 +25,7 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce private val DEPENDS_ON_CLASS_NAME_ANNOTATION = nameOf() private val DEPENDS_ON_ONE_PLUGIN_ANNOTATION = nameOf() private val DEPENDS_ON_PLUGIN_ANNOTATION = nameOf() + private val DEPENDS_ON_HOOK_ANNOTATION = nameOf() } private val logger = environment.logger @@ -112,6 +110,23 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce pluginId } + val dependsOnHook = hookClass.annotations.findAnnotations(DEPENDS_ON_HOOK_ANNOTATION) + .mapNotNull { annotation -> + val hookValue = annotation.arguments.find { it.name?.asString() == "hook" }?.value as? KSType + if (hookValue == null) { + logger.error("@DependsOnHook annotation must have 'hook' parameter", annotation) + return@mapNotNull null + } + + if (hookValue.isError) { + deferred += hookClass + hasUnresolvedClassDependency = true + return@mapNotNull null + } + + hookValue.declaration.closestClassDeclaration()?.toBinaryName() + } + PluginHookMeta.Hook( priority = priority, className = hookClass.toBinaryName(), diff --git a/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/CoroutineSession.kt b/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/CoroutineSession.kt index fbe5c2fb..b2cfc61b 100644 --- a/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/CoroutineSession.kt +++ b/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/CoroutineSession.kt @@ -1,6 +1,6 @@ package dev.slne.surf.surfapi.hytale.api.coroutines -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import kotlinx.coroutines.CoroutineScope import kotlin.coroutines.CoroutineContext diff --git a/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/HysCoroutine.kt b/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/HysCoroutine.kt index 5706e62e..69248f13 100644 --- a/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/HysCoroutine.kt +++ b/surf-api-hytale/surf-api-hytale-api/src/main/kotlin/dev/slne/surf/surfapi/hytale/api/coroutines/HysCoroutine.kt @@ -1,7 +1,7 @@ package dev.slne.surf.surfapi.hytale.api.coroutines import com.hypixel.hytale.server.core.plugin.JavaPlugin -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job diff --git a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt index cc5b5425..f3a7b49a 100644 --- a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt @@ -12,6 +12,7 @@ data class PluginHookMeta(val hooks: List) { val classDependencies: List, val pluginDependencies: List, val pluginOneDependencies: List>, + val hookDependencies: List, ) companion object { diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/Hook.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/Hook.kt new file mode 100644 index 00000000..6a87e4d3 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/Hook.kt @@ -0,0 +1,9 @@ +package dev.slne.surf.surfapi.shared.api.hook + +interface Hook : Comparable { + val priority: Short + suspend fun bootstrap() + suspend fun load() + suspend fun enable() + suspend fun disable() +} \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook.kt new file mode 100644 index 00000000..1b984f7d --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook.kt @@ -0,0 +1,9 @@ +package dev.slne.surf.surfapi.shared.api.hook.requirement + +import dev.slne.surf.surfapi.shared.api.hook.Hook +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnHook(val hook: KClass) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/internal-api.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/util/internal-api.kt similarity index 77% rename from surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/internal-api.kt rename to surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/util/internal-api.kt index ba98c93c..27d4fe90 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/internal-api.kt +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/util/internal-api.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.core.api.util +package dev.slne.surf.surfapi.shared.api.util @RequiresOptIn( "This API is internal and should not be used from outside the library", diff --git a/surf-api-velocity/surf-api-velocity-api/src/main/kotlin/dev/slne/surf/surfapi/velocity/api/command/executors/SuspendCommandExecutors.kt b/surf-api-velocity/surf-api-velocity-api/src/main/kotlin/dev/slne/surf/surfapi/velocity/api/command/executors/SuspendCommandExecutors.kt index c57ded33..cf5c73b3 100644 --- a/surf-api-velocity/surf-api-velocity-api/src/main/kotlin/dev/slne/surf/surfapi/velocity/api/command/executors/SuspendCommandExecutors.kt +++ b/surf-api-velocity/surf-api-velocity-api/src/main/kotlin/dev/slne/surf/surfapi/velocity/api/command/executors/SuspendCommandExecutors.kt @@ -14,7 +14,7 @@ import dev.jorel.commandapi.kotlindsl.consoleExecutor import dev.jorel.commandapi.kotlindsl.playerExecutor import dev.slne.surf.surfapi.core.api.messages.Colors import dev.slne.surf.surfapi.core.api.messages.adventure.text -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import kotlinx.coroutines.* import net.kyori.adventure.text.logger.slf4j.ComponentLogger From e12ea89cf4f2723a3df99d9ac17669a65e36b9af Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 18:39:50 +0100 Subject: [PATCH 09/20] feat: add conditional hook evaluation with custom conditions support --- .../surf/surfapi/core/api/hook/SurfHookApi.kt | 6 ++- .../surfapi/core/server/hook/HookService.kt | 53 +++++++++++++++++-- .../core/server/impl/hook/SurfHookApiImpl.kt | 19 ++++++- .../processor/hook/HookSymbolProcessor.kt | 11 +++- .../shared/internal/hook/HooksConfig.kt | 5 +- .../shared/internal/hook/PluginHookMeta.kt | 9 ++-- .../api/hook/condition/HookCondition.kt | 5 ++ .../hook/condition/HookConditionContext.kt | 11 ++++ .../hook/requirement/ConditionalOnCustom.kt | 9 ++++ 9 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition.kt create mode 100644 surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt create mode 100644 surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom.kt diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt index 6652759f..d65cb1f3 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt @@ -11,8 +11,12 @@ interface SurfHookApi { suspend fun disable(owner: Any) suspend fun hooksOfType(owner: Any, type: Class): List - suspend fun hooksOfType(type: Class): List + fun hooksOfTypeLoaded(owner: Any, type: Class): List + suspend fun hooksOfType(type: Class): List + fun hooksOfTypeLoaded(type: Class): List + suspend fun hooks(owner: Any): List + fun hooksLoaded(owner: Any): List companion object { val instance = requiredService() diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 6f3748d8..0b70f23c 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -1,10 +1,13 @@ package dev.slne.surf.surfapi.core.server.hook import com.github.benmanes.caffeine.cache.Caffeine +import com.sksamuel.aedile.core.asLoadingCache import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf import dev.slne.surf.surfapi.core.api.util.requiredService import dev.slne.surf.surfapi.shared.api.hook.Hook +import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition +import dev.slne.surf.surfapi.shared.api.hook.condition.HookConditionContext import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -19,7 +22,7 @@ abstract class HookService { private val hooksCache = Caffeine.newBuilder() .weakKeys() - .build> { owner -> loadHooks(owner) } + .asLoadingCache { owner -> loadHooks(owner) } private fun loadHooksMeta(owner: Any): PluginHookMeta { val rawStream = readHooksFileFromResources(owner, HOOKS_FILE_NAME) ?: return PluginHookMeta.empty() @@ -32,7 +35,7 @@ abstract class HookService { } } - private fun loadHooks(owner: Any): List { + private suspend fun loadHooks(owner: Any): List { val meta = hookMetaCache.get(owner) val classLoader = getClassloader(owner) @@ -48,7 +51,7 @@ abstract class HookService { return topologicalSort(hooksWithMeta, owner) } - private fun instantiateHookIfValid( + private suspend fun instantiateHookIfValid( owner: Any, hookMeta: PluginHookMeta.Hook, classLoader: ClassLoader @@ -80,6 +83,8 @@ abstract class HookService { return null } + if (!evaluateConditions(owner, hookMeta, classLoader)) return null + try { val hookClass = Class.forName(hookMeta.className, false, classLoader) val hookKClass = hookClass.kotlin @@ -100,6 +105,36 @@ abstract class HookService { return null } + @Suppress("UNCHECKED_CAST") + private suspend fun evaluateConditions( + owner: Any, + hookMeta: PluginHookMeta.Hook, + classLoader: ClassLoader + ): Boolean { + for (conditionClassName in hookMeta.customConditions) { + try { + val conditionClass = Class.forName(conditionClassName, false, classLoader) + val condition = conditionClass.getConstructor().newInstance() as HookCondition + val logger = getLogger(owner) + + val context = HookConditionContext( + owner = owner, + logger = logger, + hookClass = Class.forName(hookMeta.className, false, classLoader) as Class + ) + + if (!condition.evaluate(context)) { + logger.debug("Hook ${hookMeta.className} skipped due to condition $conditionClassName") + return false + } + } catch (e: Exception) { + getLogger(owner).error("Failed to evaluate condition $conditionClassName", e) + return false + } + } + return true + } + private fun topologicalSort( hooksWithMeta: List>, owner: Any @@ -210,14 +245,22 @@ abstract class HookService { ) } - fun getHooks(owner: Any): List { + suspend fun getHooks(owner: Any): List { return hooksCache.get(owner) } - fun getAllHooks(): List { + fun getHooksLoaded(owner: Any): List { + return hooksCache.underlying().asMap()[owner]?.getNow(emptyList()) ?: emptyList() + } + + suspend fun getAllHooks(): List { return hooksCache.asMap().values.flatten().sorted() } + fun getAllHooksLoaded(): List { + return hooksCache.underlying().asMap().values.flatMap { it.getNow(emptyList()) }.sorted() + } + abstract fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? abstract fun getClassloader(owner: Any): ClassLoader abstract fun isPluginLoaded(pluginId: String): Boolean diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt index a2c329ed..da704bdf 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt @@ -1,9 +1,9 @@ package dev.slne.surf.surfapi.core.server.impl.hook import com.google.auto.service.AutoService -import dev.slne.surf.surfapi.core.api.hook.AbstractHook import dev.slne.surf.surfapi.core.api.hook.SurfHookApi import dev.slne.surf.surfapi.core.server.hook.HookService +import dev.slne.surf.surfapi.shared.api.hook.Hook @AutoService(SurfHookApi::class) class SurfHookApiImpl : SurfHookApi { @@ -38,11 +38,26 @@ class SurfHookApiImpl : SurfHookApi { return hooks(owner).filterIsInstance(type) } + override fun hooksOfTypeLoaded( + owner: Any, + type: Class + ): List { + return hooksLoaded(owner).filterIsInstance(type) + } + override suspend fun hooksOfType(type: Class): List { return HookService.get().getAllHooks().filterIsInstance(type) } - override suspend fun hooks(owner: Any): List { + override fun hooksOfTypeLoaded(type: Class): List { + return HookService.get().getAllHooksLoaded().filterIsInstance(type) + } + + override suspend fun hooks(owner: Any): List { return HookService.get().getHooks(owner) } + + override fun hooksLoaded(owner: Any): List { + return HookService.get().getHooksLoaded(owner) + } } \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt index 027793d1..48310fbe 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt @@ -26,6 +26,7 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce private val DEPENDS_ON_ONE_PLUGIN_ANNOTATION = nameOf() private val DEPENDS_ON_PLUGIN_ANNOTATION = nameOf() private val DEPENDS_ON_HOOK_ANNOTATION = nameOf() + private val CONDITIONAL_ON_CUSTOM_ANNOTATION = nameOf() } private val logger = environment.logger @@ -127,12 +128,20 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce hookValue.declaration.closestClassDeclaration()?.toBinaryName() } + val customConditions = hookClass.annotations.findAnnotations(CONDITIONAL_ON_CUSTOM_ANNOTATION) + .mapNotNull { annotation -> + val conditionValue = annotation.arguments.find { it.name?.asString() == "condition" }?.value as? KSType + conditionValue?.declaration?.closestClassDeclaration()?.toBinaryName() + } + PluginHookMeta.Hook( priority = priority, className = hookClass.toBinaryName(), classDependencies = dependsOnClass.toList() + dependsOnClassName.toList(), pluginDependencies = dependsOnPlugin.toList(), - pluginOneDependencies = dependsOnOnePlugin.toList() + pluginOneDependencies = dependsOnOnePlugin.toList(), + hookDependencies = dependsOnHook.toList(), + customConditions = customConditions.toList() ) }.toList() diff --git a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt index 0d62faa3..6e7f4595 100644 --- a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt @@ -4,5 +4,8 @@ import kotlinx.serialization.json.Json object HooksConfig { const val HOOKS_FILE_NAME = "surf-hooks.json" - val json = Json { prettyPrint = true } + val json = Json { + prettyPrint = true + encodeDefaults = false + } } \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt index f3a7b49a..89255aab 100644 --- a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt @@ -9,10 +9,11 @@ data class PluginHookMeta(val hooks: List) { data class Hook( val priority: Short, val className: String, - val classDependencies: List, - val pluginDependencies: List, - val pluginOneDependencies: List>, - val hookDependencies: List, + val classDependencies: List = emptyList(), + val pluginDependencies: List = emptyList(), + val pluginOneDependencies: List> = emptyList(), + val hookDependencies: List = emptyList(), + val customConditions: List = emptyList(), ) companion object { diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition.kt new file mode 100644 index 00000000..24f6f4dc --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition.kt @@ -0,0 +1,5 @@ +package dev.slne.surf.surfapi.shared.api.hook.condition + +interface HookCondition { + suspend fun evaluate(context: HookConditionContext): Boolean +} \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt new file mode 100644 index 00000000..6fd146be --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.surfapi.shared.api.hook.condition + +import dev.slne.surf.surfapi.shared.api.hook.Hook +import net.kyori.adventure.text.logger.slf4j.ComponentLogger + +data class HookConditionContext( + val owner: Any, + val hookClass: Class, + val logger: ComponentLogger, + val environment: Map = emptyMap() +) \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom.kt new file mode 100644 index 00000000..2b249f71 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom.kt @@ -0,0 +1,9 @@ +package dev.slne.surf.surfapi.shared.api.hook.requirement + +import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class ConditionalOnCustom(val condition: KClass) \ No newline at end of file From 41f5bdd36cb622d5ebb72efd2d71e92e4d00f016 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 18:49:08 +0100 Subject: [PATCH 10/20] feat: implement conditional hook support and enhance configuration handling --- .../bukkit/test/hook/PrimaryTestHook.kt | 29 +++++++++++++++++++ .../surf/surfapi/bukkit/test/hook/TestHook.kt | 6 ++-- .../test/hook/condition/EnabledCondition.kt | 11 +++++++ .../surfapi/core/api/hook/AbstractHook.kt | 10 +++---- .../surfapi/core/server/hook/HookService.kt | 10 +++---- .../shared/internal/hook/HooksConfig.kt | 2 ++ 6 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/PrimaryTestHook.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/condition/EnabledCondition.kt diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/PrimaryTestHook.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/PrimaryTestHook.kt new file mode 100644 index 00000000..feb9071f --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/PrimaryTestHook.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.surfapi.bukkit.test.hook + +import dev.slne.surf.surfapi.bukkit.test.hook.condition.EnabledCondition +import dev.slne.surf.surfapi.core.api.hook.AbstractHook +import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.shared.api.hook.HookMeta +import dev.slne.surf.surfapi.shared.api.hook.requirement.ConditionalOnCustom + +@ConditionalOnCustom(EnabledCondition::class) +@HookMeta +class PrimaryTestHook : AbstractHook() { + private val log = logger() + + override suspend fun onBootstrap() { + log.atInfo().log("PrimaryTestHook bootstrapped") + } + + override suspend fun onLoad() { + log.atInfo().log("PrimaryTestHook loaded") + } + + override suspend fun onEnable() { + log.atInfo().log("PrimaryTestHook enabled") + } + + override suspend fun onDisable() { + log.atInfo().log("PrimaryTestHook disabled") + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt index d51b733a..db168398 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt @@ -4,16 +4,14 @@ import dev.slne.surf.surfapi.bukkit.test.BukkitPluginMain import dev.slne.surf.surfapi.core.api.hook.AbstractHook import dev.slne.surf.surfapi.core.api.util.logger import dev.slne.surf.surfapi.shared.api.hook.HookMeta -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClass -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnClassName -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnOnePlugin -import dev.slne.surf.surfapi.shared.api.hook.requirement.DependsOnPlugin +import dev.slne.surf.surfapi.shared.api.hook.requirement.* @HookMeta @DependsOnClass(BukkitPluginMain::class) @DependsOnClassName("dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig") @DependsOnPlugin("SurfBukkitPluginTest") @DependsOnOnePlugin(["SurfBukkitPlugin", "surf-bukkit-plugin", "SurfBukkitPluginTest"]) +@DependsOnHook(PrimaryTestHook::class) class TestHook : AbstractHook() { private val log = logger() diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/condition/EnabledCondition.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/condition/EnabledCondition.kt new file mode 100644 index 00000000..aa98ca8b --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/condition/EnabledCondition.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.surfapi.bukkit.test.hook.condition + +import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig +import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition +import dev.slne.surf.surfapi.shared.api.hook.condition.HookConditionContext + +class EnabledCondition : HookCondition { + override suspend fun evaluate(context: HookConditionContext): Boolean { + return ModernTestConfig.getConfig().enabled + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt index c7c3c92c..e4a3a698 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt @@ -14,17 +14,17 @@ abstract class AbstractHook : Hook { private val meta: HookMeta = javaClass.getAnnotation(HookMeta::class.java) ?: error("HookMeta annotation is missing on hook class ${this::class.qualifiedName}") - override val priority = meta.priority + final override val priority = meta.priority @InternalSurfApi - override suspend fun bootstrap() { + final override suspend fun bootstrap() { if (bootstrapped.compareAndSet(false, true)) { onBootstrap() } } @InternalSurfApi - override suspend fun load() { + final override suspend fun load() { if (loaded.compareAndSet(false, true)) { bootstrap() onLoad() @@ -32,7 +32,7 @@ abstract class AbstractHook : Hook { } @InternalSurfApi - override suspend fun enable() { + final override suspend fun enable() { if (enabled.compareAndSet(false, true)) { load() onEnable() @@ -40,7 +40,7 @@ abstract class AbstractHook : Hook { } @InternalSurfApi - override suspend fun disable() { + final override suspend fun disable() { if (disabled.compareAndSet(false, true)) { onDisable() } diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 0b70f23c..3c26b785 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -8,9 +8,9 @@ import dev.slne.surf.surfapi.core.api.util.requiredService import dev.slne.surf.surfapi.shared.api.hook.Hook import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition import dev.slne.surf.surfapi.shared.api.hook.condition.HookConditionContext +import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json import net.kyori.adventure.text.logger.slf4j.ComponentLogger import java.io.InputStream @@ -25,12 +25,12 @@ abstract class HookService { .asLoadingCache { owner -> loadHooks(owner) } private fun loadHooksMeta(owner: Any): PluginHookMeta { - val rawStream = readHooksFileFromResources(owner, HOOKS_FILE_NAME) ?: return PluginHookMeta.empty() + val rawStream = readHooksFileFromResources(owner, HooksConfig.HOOKS_FILE_NAME) ?: return PluginHookMeta.empty() val raw = rawStream.bufferedReader().use { it.readText() } return try { - Json.decodeFromString(raw) + HooksConfig.json.decodeFromString(raw) } catch (e: SerializationException) { - getLogger(owner).error("Failed to parse $HOOKS_FILE_NAME", e) + getLogger(owner).error("Failed to parse ${HooksConfig.HOOKS_FILE_NAME}", e) PluginHookMeta.empty() } } @@ -267,8 +267,6 @@ abstract class HookService { abstract fun getLogger(owner: Any): ComponentLogger companion object { - const val HOOKS_FILE_NAME = "surf-hooks.json" - val instance = requiredService() fun get() = instance } diff --git a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt index 6e7f4595..b78875e6 100644 --- a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt @@ -7,5 +7,7 @@ object HooksConfig { val json = Json { prettyPrint = true encodeDefaults = false + ignoreUnknownKeys = true + prettyPrintIndent = " " } } \ No newline at end of file From e4f973698c79b4e22c11cbddda4ee196306d4c65 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 18:56:54 +0100 Subject: [PATCH 11/20] feat: add shared Hook interface and enhance hook condition handling --- .../api/surf-api-core-api.api | 47 ++-------- .../api/surf-api-shared-public.api | 85 +++++++++++++++++++ .../surf-api-shared-public/build.gradle.kts | 10 +++ .../hook/condition/HookConditionContext.kt | 1 + 4 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 surf-api-shared/surf-api-shared-public/api/surf-api-shared-public.api diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index bdc7f601..9688cc31 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -6389,13 +6389,14 @@ public final class dev/slne/surf/surfapi/core/api/generated/VanillaAdvancementKe public static final field UPGRADE_TOOLS Lnet/kyori/adventure/key/Key; } -public abstract class dev/slne/surf/surfapi/core/api/hook/AbstractHook : java/lang/Comparable { +public abstract class dev/slne/surf/surfapi/core/api/hook/AbstractHook : dev/slne/surf/surfapi/shared/api/hook/Hook { public fun ()V public final fun bootstrap (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun compareTo (Ldev/slne/surf/surfapi/core/api/hook/AbstractHook;)I + public final fun compareTo (Ldev/slne/surf/surfapi/shared/api/hook/Hook;)I public synthetic fun compareTo (Ljava/lang/Object;)I public final fun disable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun enable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getPriority ()S public final fun load (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; protected fun onBootstrap (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; protected fun onDisable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -6403,18 +6404,17 @@ public abstract class dev/slne/surf/surfapi/core/api/hook/AbstractHook : java/la protected fun onLoad (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/HookMeta : java/lang/annotation/Annotation { - public abstract fun priority ()S -} - public abstract interface class dev/slne/surf/surfapi/core/api/hook/SurfHookApi { public static final field Companion Ldev/slne/surf/surfapi/core/api/hook/SurfHookApi$Companion; public abstract fun bootstrap (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun disable (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun enable (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun hooks (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun hooksLoaded (Ljava/lang/Object;)Ljava/util/List; public abstract fun hooksOfType (Ljava/lang/Class;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun hooksOfType (Ljava/lang/Object;Ljava/lang/Class;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun hooksOfTypeLoaded (Ljava/lang/Class;)Ljava/util/List; + public abstract fun hooksOfTypeLoaded (Ljava/lang/Object;Ljava/lang/Class;)Ljava/util/List; public abstract fun load (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -6426,38 +6426,6 @@ public final class dev/slne/surf/surfapi/core/api/hook/SurfHookApiKt { public static final fun getSurfHookApi ()Ldev/slne/surf/surfapi/core/api/hook/SurfHookApi; } -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass : java/lang/annotation/Annotation { - public abstract fun clazz ()Ljava/lang/Class; -} - -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass$Container : java/lang/annotation/Annotation { - public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClass; -} - -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName : java/lang/annotation/Annotation { - public abstract fun className ()Ljava/lang/String; -} - -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName$Container : java/lang/annotation/Annotation { - public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnClassName; -} - -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin : java/lang/annotation/Annotation { - public abstract fun pluginIds ()[Ljava/lang/String; -} - -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin$Container : java/lang/annotation/Annotation { - public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnOnePlugin; -} - -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin : java/lang/annotation/Annotation { - public abstract fun pluginId ()Ljava/lang/String; -} - -public abstract interface annotation class dev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin$Container : java/lang/annotation/Annotation { - public abstract fun value ()[Ldev/slne/surf/surfapi/core/api/hook/requirement/DependsOnPlugin; -} - public final class dev/slne/surf/surfapi/core/api/math/VoxelLineTracer { public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/math/VoxelLineTracer; public final fun trace (Lorg/spongepowered/math/vector/Vector3d;Lorg/spongepowered/math/vector/Vector3d;)Lkotlin/sequences/Sequence; @@ -10283,9 +10251,6 @@ public final class dev/slne/surf/surfapi/core/api/util/Fast_util_utilKt { public static final fun toShortSet ([Ljava/lang/Short;)Lit/unimi/dsi/fastutil/shorts/ShortSet; } -public abstract interface annotation class dev/slne/surf/surfapi/core/api/util/InternalSurfApi : java/lang/annotation/Annotation { -} - public abstract interface class dev/slne/surf/surfapi/core/api/util/ItemStackFactory { public static final field Companion Ldev/slne/surf/surfapi/core/api/util/ItemStackFactory$Companion; } diff --git a/surf-api-shared/surf-api-shared-public/api/surf-api-shared-public.api b/surf-api-shared/surf-api-shared-public/api/surf-api-shared-public.api new file mode 100644 index 00000000..2525d77d --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/api/surf-api-shared-public.api @@ -0,0 +1,85 @@ +public abstract interface class dev/slne/surf/surfapi/shared/api/hook/Hook : java/lang/Comparable { + public abstract fun bootstrap (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun disable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun enable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getPriority ()S + public abstract fun load (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/HookMeta : java/lang/annotation/Annotation { + public abstract fun priority ()S +} + +public abstract interface class dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition { + public abstract fun evaluate (Ldev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext : java/lang/Record { + public fun (Ljava/lang/Object;Ljava/lang/Class;Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/Object;Ljava/lang/Class;Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Object; + public final fun component2 ()Ljava/lang/Class; + public final fun component3 ()Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger; + public final fun component4 ()Ljava/util/Map; + public final fun copy (Ljava/lang/Object;Ljava/lang/Class;Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Ljava/util/Map;)Ldev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext; + public static synthetic fun copy$default (Ldev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext;Ljava/lang/Object;Ljava/lang/Class;Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Ljava/util/Map;ILjava/lang/Object;)Ldev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext; + public final fun environment ()Ljava/util/Map; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public final fun hookClass ()Ljava/lang/Class; + public final fun logger ()Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger; + public final fun owner ()Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom : java/lang/annotation/Annotation { + public abstract fun condition ()Ljava/lang/Class; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass : java/lang/annotation/Annotation { + public abstract fun clazz ()Ljava/lang/Class; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName : java/lang/annotation/Annotation { + public abstract fun className ()Ljava/lang/String; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook : java/lang/annotation/Annotation { + public abstract fun hook ()Ljava/lang/Class; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin : java/lang/annotation/Annotation { + public abstract fun pluginIds ()[Ljava/lang/String; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin : java/lang/annotation/Annotation { + public abstract fun pluginId ()Ljava/lang/String; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin$Container : java/lang/annotation/Annotation { + public abstract fun value ()[Ldev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin; +} + +public abstract interface annotation class dev/slne/surf/surfapi/shared/api/util/InternalSurfApi : java/lang/annotation/Annotation { +} + diff --git a/surf-api-shared/surf-api-shared-public/build.gradle.kts b/surf-api-shared/surf-api-shared-public/build.gradle.kts index 5e640d5c..df0fffb8 100644 --- a/surf-api-shared/surf-api-shared-public/build.gradle.kts +++ b/surf-api-shared/surf-api-shared-public/build.gradle.kts @@ -1,7 +1,17 @@ +@file:OptIn(ExperimentalAbiValidation::class) + +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + plugins { `core-convention` } +kotlin { + abiValidation { + enabled = true + } +} + dependencies { compileOnlyApi(libs.adventure.api) compileOnlyApi(libs.adventure.text.logger.slf4j) diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt index 6fd146be..f3ce80d4 100644 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt @@ -3,6 +3,7 @@ package dev.slne.surf.surfapi.shared.api.hook.condition import dev.slne.surf.surfapi.shared.api.hook.Hook import net.kyori.adventure.text.logger.slf4j.ComponentLogger +@JvmRecord data class HookConditionContext( val owner: Any, val hookClass: Class, From ef61b306e85da0d396dd93a489b4802edbf85981 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 20:34:02 +0100 Subject: [PATCH 12/20] feat: implement topological sort algorithm for directed graphs --- .../api/algorithms/kahn-topological-sort.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/algorithms/kahn-topological-sort.kt diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/algorithms/kahn-topological-sort.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/algorithms/kahn-topological-sort.kt new file mode 100644 index 00000000..0f9e0a3d --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/algorithms/kahn-topological-sort.kt @@ -0,0 +1,51 @@ +package dev.slne.surf.surfapi.core.api.algorithms + +import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf + +private typealias Graph = Map> + +fun Graph.topologicalSortSafe(): Result> { + val graph = this + val incomingEdges = mutableObject2IntMapOf() + for ((vertex, successors) in graph) { + if (vertex !in incomingEdges) { + incomingEdges[vertex] = 0 + } + for (successor in successors) { + incomingEdges.mergeInt(successor, 1, Int::plus) + } + } + + val queue = ArrayDeque() + incomingEdges.object2IntEntrySet().fastForEach {entry -> + val vertex = entry.key + val edges = entry.intValue + if (edges == 0) queue += vertex + + } + + val result = mutableObjectListOf() + + while (queue.isNotEmpty()) { + val vertex = queue.removeFirst() + result += vertex + + for (successor in graph[vertex].orEmpty()) { + incomingEdges.mergeInt(successor, -1, Int::minus) + if (incomingEdges.getInt(successor) == 0) { + queue += successor + } + } + } + + if (result.size != incomingEdges.size) { + return Result.failure(IllegalStateException("Graph contains a cycle, topological sort not possible!")) + } + + return Result.success(result) +} + +fun Graph.topologicalSort(): List { + return topologicalSortSafe().getOrThrow() +} From 0471e83b545fc36d82e239b822d0e8c314c7d01b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:37:27 +0000 Subject: [PATCH 13/20] Initial plan From 4d18670a2ee92ef01674952712b2ec2448bf1cc4 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 20:38:13 +0100 Subject: [PATCH 14/20] feat: handle unresolved class dependencies in hook processing --- .../slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt index 48310fbe..1a588250 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt @@ -128,6 +128,10 @@ class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProce hookValue.declaration.closestClassDeclaration()?.toBinaryName() } + if (hasUnresolvedClassDependency) { + return@mapNotNull null + } + val customConditions = hookClass.annotations.findAnnotations(CONDITIONAL_ON_CUSTOM_ANNOTATION) .mapNotNull { annotation -> val conditionValue = annotation.arguments.find { it.name?.asString() == "condition" }?.value as? KSType From 16cc998021f329e216daaf6c3f3a2fae47696bd3 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 20:39:02 +0100 Subject: [PATCH 15/20] feat: remove unused Kotlin compiler options for InternalSurfApi --- surf-api-core/surf-api-core-api/build.gradle.kts | 6 ------ surf-api-core/surf-api-core-server/build.gradle.kts | 7 ------- 2 files changed, 13 deletions(-) diff --git a/surf-api-core/surf-api-core-api/build.gradle.kts b/surf-api-core/surf-api-core-api/build.gradle.kts index d6b749f7..9cfc75e1 100644 --- a/surf-api-core/surf-api-core-api/build.gradle.kts +++ b/surf-api-core/surf-api-core-api/build.gradle.kts @@ -38,12 +38,6 @@ dependencies { api(libs.datafixerupper) { isTransitive = false } } -kotlin { - compilerOptions { - optIn.add("dev.slne.surf.surfapi.core.api.util.InternalSurfApi") - } -} - tasks { shadowJar { val relocationPrefix: String by project diff --git a/surf-api-core/surf-api-core-server/build.gradle.kts b/surf-api-core/surf-api-core-server/build.gradle.kts index 3c474899..ccf9857b 100644 --- a/surf-api-core/surf-api-core-server/build.gradle.kts +++ b/surf-api-core/surf-api-core-server/build.gradle.kts @@ -8,11 +8,4 @@ dependencies { compileOnly(libs.packetevents.netty.common) } - -kotlin { - compilerOptions { - optIn.add("dev.slne.surf.surfapi.core.api.util.InternalSurfApi") - } -} - description = "surf-api-core-server" From ea677c5b9d43a8919b53a6c75b84b3efcfb05787 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:41:44 +0000 Subject: [PATCH 16/20] fix: preserve topological order by using priority as tie-breaker during DFS Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../slne/surf/surfapi/core/server/hook/HookService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 3c26b785..84d3fef3 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -196,7 +196,11 @@ abstract class HookService { visiting.add(className) val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList() - for (depClassName in dependencies) { + // Sort dependencies by priority to use priority as a tie-breaker + val sortedDeps = dependencies.sortedBy { depClassName -> + hooksByClassName[depClassName]?.priority ?: Short.MAX_VALUE + } + for (depClassName in sortedDeps) { if (depClassName in hooksByClassName) { visit(depClassName) } @@ -208,8 +212,9 @@ abstract class HookService { hooksByClassName[className]?.let { sorted.add(it) } } - validHooks.forEach { (meta, _) -> visit(meta.className) } - return sorted.sortedBy { it.priority } + // Sort validHooks by priority before visiting to use priority as a tie-breaker + validHooks.sortedBy { (_, hook) -> hook.priority }.forEach { (meta, _) -> visit(meta.className) } + return sorted } private fun buildCircularDependencyChain( From 85b4f1b911657ace7920e951c0656ee379b9f29a Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 20:42:27 +0100 Subject: [PATCH 17/20] chore: dump abi --- surf-api-core/surf-api-core-api/api/surf-api-core-api.api | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index 9688cc31..4a7e58da 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -23,6 +23,11 @@ public final class dev/slne/surf/surfapi/core/api/algorithms/ConvexHull2DKt { public static final fun convexHull2D ([Lorg/spongepowered/math/vector/Vectord;)Lit/unimi/dsi/fastutil/objects/ObjectList; } +public final class dev/slne/surf/surfapi/core/api/algorithms/Kahn_topological_sortKt { + public static final fun topologicalSort (Ljava/util/Map;)Ljava/util/List; + public static final fun topologicalSortSafe (Ljava/util/Map;)Ljava/lang/Object; +} + public final class dev/slne/surf/surfapi/core/api/collection/TransformingObjectSet : it/unimi/dsi/fastutil/objects/ObjectSet { public fun (Lit/unimi/dsi/fastutil/objects/ObjectSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V public fun add (Ljava/lang/Object;)Z From 1a1a0d3e64d7513806d731ff46e41d3e3fef9200 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:43:16 +0000 Subject: [PATCH 18/20] refactor: filter dependencies before sorting by priority Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../slne/surf/surfapi/core/server/hook/HookService.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 84d3fef3..5f98d2dc 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -197,13 +197,12 @@ abstract class HookService { val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList() // Sort dependencies by priority to use priority as a tie-breaker - val sortedDeps = dependencies.sortedBy { depClassName -> - hooksByClassName[depClassName]?.priority ?: Short.MAX_VALUE - } + // Only sort dependencies that are actually loaded + val sortedDeps = dependencies + .filter { it in hooksByClassName } + .sortedBy { hooksByClassName[it]!!.priority } for (depClassName in sortedDeps) { - if (depClassName in hooksByClassName) { - visit(depClassName) - } + visit(depClassName) } visiting.remove(className) From 0d30fd4e539f34873337baa0ae90d7b0e6fcaecf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:47:50 +0000 Subject: [PATCH 19/20] refactor: use Kahn's algorithm with priority queue for topological sort Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../surfapi/core/server/hook/HookService.kt | 106 ++++++++++++------ 1 file changed, 70 insertions(+), 36 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index 5f98d2dc..e2bfd2a2 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -2,7 +2,9 @@ package dev.slne.surf.surfapi.core.server.hook import com.github.benmanes.caffeine.cache.Caffeine import com.sksamuel.aedile.core.asLoadingCache +import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf import dev.slne.surf.surfapi.core.api.util.requiredService import dev.slne.surf.surfapi.shared.api.hook.Hook @@ -13,6 +15,7 @@ import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import kotlinx.serialization.SerializationException import net.kyori.adventure.text.logger.slf4j.ComponentLogger import java.io.InputStream +import java.util.PriorityQueue abstract class HookService { @@ -179,58 +182,89 @@ abstract class HookService { return emptyList() } - val sorted = mutableListOf() - val visited = mutableSetOf() - val visiting = mutableSetOf() - - fun visit(className: String) { - if (className in visited) return + // Build dependency graph: className -> list of dependents (successors) + // Note: In Kahn's algorithm, edges go from dependency to dependent + val graph = mutableObject2ObjectMapOf>() + for ((meta, _) in validHooks) { + // Ensure all nodes exist in the graph + if (meta.className !in graph) { + graph[meta.className] = mutableListOf() + } + // Add edges from dependencies to this hook + for (depClassName in meta.hookDependencies) { + if (depClassName in hooksByClassName) { + graph.computeIfAbsent(depClassName) { mutableListOf() }.add(meta.className) + } + } + } - if (className in visiting) { - val chain = buildCircularDependencyChain(className, metaByClassName, visiting) - throw IllegalStateException( - "Circular hook dependency detected: ${chain.joinToString(" -> ")}" - ) + // Kahn's algorithm with priority queue for tie-breaking + val incomingEdges = mutableObject2IntMapOf() + for ((vertex, successors) in graph) { + if (vertex !in incomingEdges) { + incomingEdges[vertex] = 0 + } + for (successor in successors) { + incomingEdges.mergeInt(successor, 1, Int::plus) } + } - visiting.add(className) + // Use a priority queue ordered by hook priority (lower priority value = higher priority) + val queue = PriorityQueue(compareBy { className -> + hooksByClassName[className]?.priority ?: Short.MAX_VALUE + }) + + incomingEdges.object2IntEntrySet().fastForEach { entry -> + val vertex = entry.key + val edges = entry.intValue + if (edges == 0) queue += vertex + } - val dependencies = metaByClassName[className]?.hookDependencies ?: emptyList() - // Sort dependencies by priority to use priority as a tie-breaker - // Only sort dependencies that are actually loaded - val sortedDeps = dependencies - .filter { it in hooksByClassName } - .sortedBy { hooksByClassName[it]!!.priority } - for (depClassName in sortedDeps) { - visit(depClassName) - } + val result = mutableObjectListOf() - visiting.remove(className) - visited.add(className) + while (queue.isNotEmpty()) { + val vertex = queue.poll() + hooksByClassName[vertex]?.let { result += it } - hooksByClassName[className]?.let { sorted.add(it) } + for (successor in graph[vertex].orEmpty()) { + incomingEdges.mergeInt(successor, -1, Int::minus) + if (incomingEdges.getInt(successor) == 0) { + queue += successor + } + } } - // Sort validHooks by priority before visiting to use priority as a tie-breaker - validHooks.sortedBy { (_, hook) -> hook.priority }.forEach { (meta, _) -> visit(meta.className) } - return sorted + if (result.size != incomingEdges.size) { + val chain = findCyclicDependency(graph, incomingEdges) + throw IllegalStateException( + "Circular hook dependency detected: ${chain.joinToString(" -> ")}" + ) + } + + return result } - private fun buildCircularDependencyChain( - startClassName: String, - metaByClassName: Map, - visiting: Set + private fun findCyclicDependency( + graph: Map>, + incomingEdges: Map ): List { + // Find a node that still has incoming edges (part of cycle) + val cycleNode = incomingEdges.entries.firstOrNull { it.value > 0 }?.key + ?: return emptyList() + + // Trace back through dependencies to find the cycle + val visited = mutableSetOf() val chain = mutableListOf() - var current = startClassName + var current = cycleNode - while (current !in chain) { + while (current !in visited) { + visited.add(current) chain.add(current) - val deps = metaByClassName[current]?.hookDependencies ?: break - current = deps.firstOrNull { it in visiting } ?: break + // Find a successor that still has incoming edges (part of cycle) + current = graph[current]?.firstOrNull { incomingEdges[it] ?: 0 > 0 } ?: break } - chain.add(startClassName) + chain.add(current) return chain } From db1645afdb4a20f7a06ff84d7bd0900752051e3a Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 25 Jan 2026 20:53:30 +0100 Subject: [PATCH 20/20] fix: ensure safe null handling for incoming edges in hook processing --- .../slne/surf/surfapi/core/server/hook/HookService.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt index e2bfd2a2..fd29e71a 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt @@ -2,11 +2,7 @@ package dev.slne.surf.surfapi.core.server.hook import com.github.benmanes.caffeine.cache.Caffeine import com.sksamuel.aedile.core.asLoadingCache -import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf -import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf -import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf -import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf -import dev.slne.surf.surfapi.core.api.util.requiredService +import dev.slne.surf.surfapi.core.api.util.* import dev.slne.surf.surfapi.shared.api.hook.Hook import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition import dev.slne.surf.surfapi.shared.api.hook.condition.HookConditionContext @@ -15,7 +11,7 @@ import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta import kotlinx.serialization.SerializationException import net.kyori.adventure.text.logger.slf4j.ComponentLogger import java.io.InputStream -import java.util.PriorityQueue +import java.util.* abstract class HookService { @@ -261,7 +257,7 @@ abstract class HookService { visited.add(current) chain.add(current) // Find a successor that still has incoming edges (part of cycle) - current = graph[current]?.firstOrNull { incomingEdges[it] ?: 0 > 0 } ?: break + current = graph[current]?.firstOrNull { (incomingEdges[it] ?: 0) > 0 } ?: break } chain.add(current)