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/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.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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8bdaf60c..d997cfc6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e111328..00af0816 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-snapshots/gradle-9.4.0-20260128061951+0000-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index ef07e016..e2ee8b61 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/8b952b8a1c2911b98b123cac54f718edc0e20d43/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-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-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..e54569ad 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.component.surfComponentApi @OptIn(NmsUseWithCaution::class) class BukkitPluginMain : SuspendingJavaPlugin() { - override fun onLoad() { + override suspend fun onLoadAsync() { ModernTestConfig.init() ModernTestConfig.randomise() + surfComponentApi.load(this) packetListenerApi.registerListeners(ChatListener()) TestInventoryView.register() } - override fun onEnable() { + override suspend fun onEnableAsync() { SurfApiTestCommand().register() Reflection::class.java.getClassLoader() // initialize Reflection + + surfComponentApi.enable(this) } - override fun onDisable() { + override suspend fun onDisableAsync() { CommandAPI.unregister("surfapitest") + surfComponentApi.disable(this) } companion object { diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt index 4fc41189..96b1807a 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt @@ -18,7 +18,7 @@ data class ModernTestConfig( fun randomise() = edit { message = "Random Message ${Math.random()}" number = (Math.random() * 100).toInt() - enabled = Math.random() > 0.5 +// enabled = Math.random() > 0.5 } } } \ 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/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..e44ec9d7 --- /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.component.AbstractComponent +import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.shared.api.component.ComponentMeta +import dev.slne.surf.surfapi.shared.api.component.requirement.ConditionalOn + +@ConditionalOn(EnabledCondition::class) +@ComponentMeta +class PrimaryTestHook : AbstractComponent() { + 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 new file mode 100644 index 00000000..e1462c19 --- /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.component.AbstractComponent +import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.shared.api.component.ComponentMeta +import dev.slne.surf.surfapi.shared.api.component.requirement.DependsOnClass +import dev.slne.surf.surfapi.shared.api.component.requirement.DependsOnClassName +import dev.slne.surf.surfapi.shared.api.component.requirement.DependsOnComponent + +@ComponentMeta +@DependsOnClass(BukkitPluginMain::class) +@DependsOnClassName("dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig") +//@DependsOnPlugin("SurfBukkitPluginTest") +//@DependsOnOnePlugin(["SurfBukkitPlugin", "surf-bukkit-plugin", "SurfBukkitPluginTest"]) +@DependsOnComponent(PrimaryTestHook::class) +class TestHook : AbstractComponent() { + 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-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..df2302f3 --- /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.component.condition.ComponentCondition +import dev.slne.surf.surfapi.shared.api.component.condition.ComponentConditionContext + +class EnabledCondition : ComponentCondition { + override suspend fun evaluate(context: ComponentConditionContext): Boolean { + return ModernTestConfig.getConfig().enabled + } +} \ 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/PaperComponentService.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperComponentService.kt new file mode 100644 index 00000000..9fc77782 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperComponentService.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.component.ComponentService +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(ComponentService::class) +class PaperComponentService : ComponentService() { + override fun readComponentsFileFromResources(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/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index fa0651a2..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 @@ -6389,6 +6394,43 @@ 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 : 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/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; + protected fun onEnable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected fun onLoad (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +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; +} + +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 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; @@ -10214,9 +10256,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-core/surf-api-core-api/build.gradle.kts b/surf-api-core/surf-api-core-api/build.gradle.kts index 8ad624f2..9cfc75e1 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) @@ -46,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-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() +} diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/component/AbstractComponent.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/component/AbstractComponent.kt new file mode 100644 index 00000000..640a7fba --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/component/AbstractComponent.kt @@ -0,0 +1,134 @@ +package dev.slne.surf.surfapi.core.api.component + +import dev.slne.surf.surfapi.shared.api.component.Component +import dev.slne.surf.surfapi.shared.api.component.ComponentMeta +import dev.slne.surf.surfapi.shared.api.component.Priority +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Abstract base class for components that provides lifecycle management. + * + * This class handles the component lifecycle by ensuring each phase is only + * executed once and in the correct order. Subclasses should override the + * protected `on*` methods to implement their functionality. + * + * The priority is determined by the [@Priority][Priority] annotation on the class + * or its meta-annotations. If no priority is specified, the default is 0. + * + * Example: + * ```kotlin + * @ComponentMeta + * @Priority(10) + * class MyComponent : AbstractComponent() { + * override suspend fun onEnable() { + * // Initialize component + * } + * + * override suspend fun onDisable() { + * // Cleanup + * } + * } + * ``` + * + * @see Component + * @see ComponentMeta + * @see Priority + */ +abstract class AbstractComponent : Component { + private val bootstrapped = AtomicBoolean(false) + private val loaded = AtomicBoolean(false) + private val enabled = AtomicBoolean(false) + private val disabled = AtomicBoolean(false) + + init { + // Validate that the class has @ComponentMeta (directly or via meta-annotation) + val hasComponentMeta = javaClass.getAnnotation(ComponentMeta::class.java) != null + || findMetaAnnotation() != null + if (!hasComponentMeta) { + error("ComponentMeta annotation is missing on component class ${this::class.qualifiedName}") + } + } + + final override val priority: Short = findPriority() + + /** + * Finds the priority from @Priority annotation on the class or its meta-annotations. + * Direct annotations take precedence over meta-annotations. + */ + private fun findPriority(): Short { + // First check for direct @Priority annotation + javaClass.getAnnotation(Priority::class.java)?.let { return it.value } + + // Then check meta-annotations for @Priority + findMetaAnnotation()?.let { return it.value } + + // Default priority + return 0 + } + + /** + * Searches for an annotation on the class's annotations (meta-annotation support) + */ + private inline fun findMetaAnnotation(): T? { + return javaClass.annotations.firstNotNullOfOrNull { it.annotationClass.java.getAnnotation(T::class.java) } + } + + @InternalSurfApi + final override suspend fun bootstrap() { + if (bootstrapped.compareAndSet(false, true)) { + onBootstrap() + } + } + + @InternalSurfApi + final override suspend fun load() { + if (loaded.compareAndSet(false, true)) { + bootstrap() + onLoad() + } + } + + @InternalSurfApi + final override suspend fun enable() { + if (enabled.compareAndSet(false, true)) { + load() + onEnable() + } + } + + @InternalSurfApi + final override suspend fun disable() { + if (disabled.compareAndSet(false, true)) { + onDisable() + } + } + + final override fun compareTo(other: Component): Int { + return this.priority.compareTo(other.priority) + } + + /** + * Called during the bootstrap phase. + * Override to perform early initialization. + */ + protected open suspend fun onBootstrap() {} + + /** + * Called during the load phase. + * Override to load configuration and resources. + */ + protected open suspend fun onLoad() {} + + /** + * Called during the enable phase. + * Override to activate component functionality. + */ + protected open suspend fun onEnable() {} + + /** + * Called during the disable phase. + * Override to clean up resources. + */ + 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/component/SurfComponentApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/component/SurfComponentApi.kt new file mode 100644 index 00000000..b67b4316 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/component/SurfComponentApi.kt @@ -0,0 +1,128 @@ +package dev.slne.surf.surfapi.core.api.component + +import dev.slne.surf.surfapi.core.api.util.requiredService +import dev.slne.surf.surfapi.shared.api.component.Component + +/** + * Main API for managing the component lifecycle. + * + * This API provides methods to trigger lifecycle phases for components + * and to query loaded components. Components are loaded lazily when + * their lifecycle methods are first called. + * + * Typical usage in a plugin: + * ```kotlin + * class MyPlugin : SuspendingJavaPlugin() { + * override suspend fun onLoadAsync() { + * surfComponentApi.load(this) + * } + * + * override suspend fun onEnableAsync() { + * surfComponentApi.enable(this) + * } + * + * override suspend fun onDisableAsync() { + * surfComponentApi.disable(this) + * } + * } + * ``` + * + * @see Component + * @see AbstractComponent + */ +interface SurfComponentApi { + + /** + * Triggers the bootstrap phase for all components owned by the given owner. + * Components are loaded lazily if not already loaded. + * + * @param owner The owner of the components (a plugin instance) + */ + suspend fun bootstrap(owner: Any) + + /** + * Triggers the load phase for all components owned by the given owner. + * This also triggers the bootstrap phase if not already done. + * + * @param owner The owner of the components (a plugin instance) + */ + suspend fun load(owner: Any) + + /** + * Triggers the enable phase for all components owned by the given owner. + * This also triggers the load phase if not already done. + * + * @param owner The owner of the components (a plugin instance) + */ + suspend fun enable(owner: Any) + + /** + * Triggers the disable phase for all components owned by the given owner. + * Components are disabled in reverse order of their initialization. + * Post-processor destruction callbacks are invoked before disabling. + * + * @param owner The owner of the components (a plugin instance) + */ + suspend fun disable(owner: Any) + + /** + * Returns all components of the specified type for the given owner. + * Components are loaded lazily if not already loaded. + * + * @param owner The owner of the components + * @param type The class of components to filter for + * @return List of components matching the specified type + */ + suspend fun componentsOfType(owner: Any, type: Class): List + + /** + * Returns all already-loaded components of the specified type for the given owner. + * Does not trigger lazy loading. + * + * @param owner The owner of the components + * @param type The class of components to filter for + * @return List of loaded components matching the specified type + */ + fun componentsOfTypeLoaded(owner: Any, type: Class): List + + /** + * Returns all components of the specified type across all owners. + * + * @param type The class of components to filter for + * @return List of components matching the specified type + */ + suspend fun componentsOfType(type: Class): List + + /** + * Returns all already-loaded components of the specified type across all owners. + * + * @param type The class of components to filter for + * @return List of loaded components matching the specified type + */ + fun componentsOfTypeLoaded(type: Class): List + + /** + * Returns all components for the given owner. + * Components are loaded lazily if not already loaded. + * + * @param owner The owner of the components + * @return List of all components for the owner + */ + suspend fun components(owner: Any): List + + /** + * Returns all already-loaded components for the given owner. + * Does not trigger lazy loading. + * + * @param owner The owner of the components + * @return List of loaded components for the owner + */ + fun componentsLoaded(owner: Any): List + + companion object : SurfComponentApi by surfComponentApi { + val instance = surfComponentApi + } +} + +val surfComponentApi = 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/build.gradle.kts b/surf-api-core/surf-api-core-server/build.gradle.kts index 97f09634..ccf9857b 100644 --- a/surf-api-core/surf-api-core-server/build.gradle.kts +++ b/surf-api-core/surf-api-core-server/build.gradle.kts @@ -4,14 +4,8 @@ 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) } - -kotlin { - compilerOptions { - optIn.add("dev.slne.surf.surfapi.core.api.util.InternalSurfApi") - } -} - description = "surf-api-core-server" 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/component/ComponentService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/component/ComponentService.kt new file mode 100644 index 00000000..6a7a9428 --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/component/ComponentService.kt @@ -0,0 +1,576 @@ +package dev.slne.surf.surfapi.core.server.component + +import com.github.benmanes.caffeine.cache.Caffeine +import com.sksamuel.aedile.core.asLoadingCache +import dev.slne.surf.surfapi.core.api.util.* +import dev.slne.surf.surfapi.shared.api.component.Component +import dev.slne.surf.surfapi.shared.api.component.condition.ComponentCondition +import dev.slne.surf.surfapi.shared.api.component.condition.ComponentConditionContext +import dev.slne.surf.surfapi.shared.api.component.processor.ComponentContext +import dev.slne.surf.surfapi.shared.api.component.processor.ComponentPostProcessor +import dev.slne.surf.surfapi.shared.internal.hook.ComponentsConfig +import dev.slne.surf.surfapi.shared.internal.hook.PluginComponentMeta +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import java.io.File +import java.io.InputStream +import java.net.URI +import java.util.* +import java.util.jar.JarFile + +abstract class ComponentService { + + private val componentMetaCache = Caffeine.newBuilder() + .weakKeys() + .build { owner -> loadComponentsMeta(owner) } + + private val componentsCache = Caffeine.newBuilder() + .weakKeys() + .asLoadingCache { owner -> loadComponents(owner) } + + private val postProcessorsCache = Caffeine.newBuilder() + .weakKeys() + .build> { owner -> loadPostProcessors(owner) } + + private fun loadComponentsMeta(owner: Any): PluginComponentMeta { + val classloader = getClassloader(owner) + val logger = getLogger(owner) + var meta = PluginComponentMeta.empty() + + try { + val resources = classloader.getResources(ComponentsConfig.COMPONENTS_DIRECTORY) + while (resources.hasMoreElements()) { + val url = resources.nextElement() + + if (url.protocol == "jar") { + val jarPath = url.path.substringBefore("!") + val jarFile = JarFile(File(URI(jarPath))) + jarFile.entries().asSequence() + .filter { it.name.startsWith(ComponentsConfig.COMPONENTS_DIRECTORY) && it.name.endsWith(".json") } + .forEach { entry -> + try { + val raw = jarFile.getInputStream(entry).bufferedReader().use { it.readText() } + val decoded = ComponentsConfig.json.decodeFromString(raw) + meta += decoded + } catch (e: Exception) { + logger.error("Failed to parse ${entry.name}", e) + } + } + } else { + val dir = File(url.toURI()) + dir.listFiles { file -> file.extension == "json" }?.forEach { file -> + try { + val raw = file.readText() + val decoded = ComponentsConfig.json.decodeFromString(raw) + meta += decoded + } catch (e: Exception) { + logger.error("Failed to parse ${file.name}", e) + } + } + } + } + } catch (e: Exception) { + logger.warn("No components directory found or error reading components", e) + } + + return meta + } + + private suspend fun loadComponents(owner: Any): List { + val meta = componentMetaCache.get(owner) + val classLoader = getClassloader(owner) + + val componentsWithMeta = meta.components.mapNotNull { componentMeta -> + val hook = instantiateComponentIfValid(owner, componentMeta, classLoader) + if (hook != null) { + componentMeta to hook + } else { + null + } + } + + val sortedComponents = topologicalSort(componentsWithMeta, owner) + + // Load and apply post-processors + val postProcessors = postProcessorsCache.get(owner) + return applyPostProcessors(sortedComponents, postProcessors, owner) + } + + private fun loadPostProcessors(owner: Any): List { + val meta = componentMetaCache.get(owner) + val classLoader = getClassloader(owner) + val logger = getLogger(owner) + + return meta.postProcessors + .mapNotNull { postProcessorMeta -> + try { + val clazz = Class.forName(postProcessorMeta.className, false, classLoader) + val kClass = clazz.kotlin + val objectInstance = kClass.objectInstance + if (objectInstance != null) { + require(objectInstance is ComponentPostProcessor) { "Post processor must implement ComponentPostProcessor" } + objectInstance + } else { + val constructor = clazz.getConstructor() + val instance = constructor.newInstance() + require(instance is ComponentPostProcessor) { "Post processor must implement ComponentPostProcessor" } + instance + } + } catch (e: Exception) { + logger.error("Failed to load post processor ${postProcessorMeta.className}", e) + null + } + } + .sortedBy { it.priority } + } + + private suspend fun applyPostProcessors( + components: List, + postProcessors: List, + owner: Any + ): List { + if (postProcessors.isEmpty()) { + return components + } + + val context = ComponentContext( + owner = owner, + allComponents = components + ) + + return components.map { component -> + var processedComponent = component + for (processor in postProcessors) { + processedComponent = processor.postProcessAfterInitialization( + processedComponent, + processedComponent::class.qualifiedName ?: processedComponent::class.java.name, + context + ) + } + processedComponent + } + } + + suspend fun invokePostProcessorsBeforeDestruction(owner: Any) { + val components = getLoadedComponents(owner) + val postProcessors = postProcessorsCache.getIfPresent(owner) ?: return + + if (postProcessors.isEmpty() || components.isEmpty()) { + return + } + + val context = ComponentContext( + owner = owner, + allComponents = components + ) + + // Invoke in reverse priority order + val reversedProcessors = postProcessors.reversed() + for (component in components.reversed()) { + for (processor in reversedProcessors) { + processor.postProcessBeforeDestruction( + component, + component::class.qualifiedName ?: component::class.java.name, + context + ) + } + } + } + + private suspend fun instantiateComponentIfValid( + owner: Any, + componentMeta: PluginComponentMeta.Component, + classLoader: ClassLoader + ): Component? { + val missingDependencies = mutableObject2ObjectMapOf>() + for (classDependency in componentMeta.classDependencies) { + try { + Class.forName(classDependency, false, classLoader) + } catch (_: ClassNotFoundException) { + missingDependencies.computeIfAbsent("Class") { mutableObjectSetOf() }.add(classDependency) + } + } + + for (pluginDependencyId in componentMeta.pluginDependencies) { + if (!isPluginLoaded(pluginDependencyId)) { + missingDependencies.computeIfAbsent("Plugin") { mutableObjectSetOf() }.add(pluginDependencyId) + } + } + + for (pluginDependenciesIds in componentMeta.pluginOneDependencies) { + if (pluginDependenciesIds.none { isPluginLoaded(it) }) { + missingDependencies.computeIfAbsent("Plugin (one of)") { mutableObjectSetOf() } + .add(pluginDependenciesIds.joinToString("|")) + } + } + + if (missingDependencies.isNotEmpty()) { + logMissingDependencies(owner, componentMeta.className, missingDependencies) + return null + } + + if (!evaluateConditions(owner, componentMeta, classLoader)) return null + + try { + val hookClass = Class.forName(componentMeta.className, false, classLoader) + val hookKClass = hookClass.kotlin + val objectInstance = hookKClass.objectInstance + if (objectInstance != null) { + require(objectInstance is Component) { "Component class must implement Component" } + return objectInstance + } else { + val constructor = hookClass.getConstructor() + val instance = constructor.newInstance() + require(instance is Component) { "Component class must implement Component" } + return instance + } + } catch (e: Exception) { + getLogger(owner).error("Failed to load component ${componentMeta.className}", e) + } + + return null + } + + @Suppress("UNCHECKED_CAST") + private suspend fun evaluateConditions( + owner: Any, + componentMeta: PluginComponentMeta.Component, + classLoader: ClassLoader + ): Boolean { + for (conditionClassName in componentMeta.customConditions) { + try { + val conditionClass = Class.forName(conditionClassName, false, classLoader) + val condition = conditionClass.getConstructor().newInstance() as ComponentCondition + val logger = getLogger(owner) + + val context = ComponentConditionContext( + owner = owner, + logger = logger, + componentClass = Class.forName(componentMeta.className, false, classLoader) as Class + ) + + if (!condition.evaluate(context)) { + logger.debug("Component ${componentMeta.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( + componentsWithMeta: List>, + owner: Any + ): List { + // If no components depend on other components, simply sort by priority + if (componentsWithMeta.none { it.first.componentDependencies.isNotEmpty() }) { + return componentsWithMeta.map { it.second }.sorted() + } + + val componentsByClassName = componentsWithMeta.associate { (meta, component) -> + meta.className to component + } + + val metaByClassName = componentsWithMeta.associate { (meta, _) -> + meta.className to meta + } + + val missingComponentDeps = mutableMapOf>() + for ((meta, _) in componentsWithMeta) { + for (depClassName in meta.componentDependencies) { + if (depClassName !in componentsByClassName) { + missingComponentDeps.computeIfAbsent(meta.className) { mutableSetOf() } + .add(depClassName) + } + } + } + + if (missingComponentDeps.isNotEmpty()) { + val logger = getLogger(owner) + for ((componentClassName, missingDeps) in missingComponentDeps) { + logger.warn( + "Component $componentClassName depends on components that are not loaded: ${ + missingDeps.joinToString( + ", " + ) + }" + ) + } + } + + val validComponents = componentsWithMeta.filter { (meta, _) -> + meta.className !in missingComponentDeps + } + + if (validComponents.isEmpty()) { + return emptyList() + } + + // Build dependency graph: className -> list of dependents (successors) + val graph = mutableObject2ObjectMapOf>() + val dependencyMap = mutableObject2ObjectMapOf>() + + for ((meta, _) in validComponents) { + if (meta.className !in graph) { + graph[meta.className] = mutableListOf() + } + if (meta.className !in dependencyMap) { + dependencyMap[meta.className] = mutableListOf() + } + for (depClassName in meta.componentDependencies) { + if (depClassName in componentsByClassName) { + graph.computeIfAbsent(depClassName) { mutableListOf() }.add(meta.className) + dependencyMap.computeIfAbsent(meta.className) { mutableListOf() }.add(depClassName) + } + } + } + + // Check for cycles using Tarjan's SCC algorithm + val cycles = findCyclesWithTarjan(graph, dependencyMap) + if (cycles.isNotEmpty()) { + val errorMessage = buildCycleErrorMessage(cycles, metaByClassName) + throw IllegalStateException(errorMessage) + } + + // 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) + } + } + + // Use a priority queue ordered by component priority (lower priority value = higher priority) + val queue = PriorityQueue(compareBy { className -> + componentsByClassName[className]?.priority ?: Short.MAX_VALUE + }) + + 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.poll() + componentsByClassName[vertex]?.let { result += it } + + for (successor in graph[vertex].orEmpty()) { + incomingEdges.mergeInt(successor, -1, Int::minus) + if (incomingEdges.getInt(successor) == 0) { + queue += successor + } + } + } + + return result + } + + /** + * Finds all cycles in the dependency graph using Tarjan's Strongly Connected Components algorithm. + * + * @param graph The forward dependency graph (dependency -> dependents) + * @param dependencyMap The reverse dependency map (component -> its dependencies) + * @return A list of cycles, where each cycle is a list of class names forming the cycle + */ + private fun findCyclesWithTarjan( + graph: Map>, + dependencyMap: Map> + ): List> { + val nodes = graph.keys.toSet() + if (nodes.isEmpty()) return emptyList() + + var index = 0 + val nodeIndex = mutableObject2IntMapOf().apply { defaultReturnValue(-1) } + val lowLink = mutableObject2IntMapOf().apply { defaultReturnValue(-1) } + val onStack = mutableObjectSetOf() + val stack = ArrayDeque() + val sccs = mutableObjectListOf>() + + fun strongConnect(node: String) { + nodeIndex[node] = index + lowLink[node] = index + index++ + stack.addFirst(node) + onStack.add(node) + + // Consider all successors (nodes that this node depends on) + for (dependency in dependencyMap[node].orEmpty()) { + if (dependency !in nodes) continue + + if (dependency !in nodeIndex) { + strongConnect(dependency) + lowLink[node] = minOf(lowLink.getInt(node), lowLink.getInt(dependency)) + } else if (dependency in onStack) { + lowLink[node] = minOf(lowLink.getInt(node), nodeIndex.getInt(dependency)) + } + } + + // If node is a root node, pop the stack and generate an SCC + if (lowLink.getInt(node) == nodeIndex.getInt(node)) { + val scc = mutableObjectListOf() + do { + val w = stack.removeFirst() + onStack.remove(w) + scc.add(w) + } while (w != node) + sccs.add(scc) + } + } + + for (node in nodes) { + if (node !in nodeIndex) { + strongConnect(node) + } + } + + // Filter for SCCs that represent cycles (size > 1 or self-loop) + val cycles = mutableObjectListOf>() + for (scc in sccs) { + if (scc.size > 1) { + // Reconstruct the cycle path + val cyclePath = reconstructCyclePath(scc, dependencyMap) + cycles.add(cyclePath) + } else if (scc.size == 1) { + // Check for self-loop + val node = scc[0] + if (dependencyMap[node]?.contains(node) == true) { + cycles.add(listOf(node, node)) + } + } + } + + return cycles + } + + /** + * Reconstructs a readable cycle path from an SCC. + * Returns a list where the first and last elements are the same, representing the cycle. + */ + private fun reconstructCyclePath( + scc: List, + dependencyMap: Map> + ): List { + if (scc.size <= 1) { + // Self-loop case + return if (scc.isNotEmpty()) listOf(scc[0], scc[0]) else scc + } + + val sccSet = scc.toObjectSet() + + // Try to find a cycle starting from each node in the SCC + for (startNode in scc) { + val path = mutableObjectListOf() + val visited = mutableObjectSetOf() + + fun dfs(node: String): Boolean { + if (node == startNode && path.isNotEmpty()) { + path.add(node) // Complete the cycle + return true + } + if (node in visited) return false + + visited.add(node) + path.add(node) + + for (dep in dependencyMap[node].orEmpty()) { + if (dep in sccSet) { + if (dfs(dep)) return true + } + } + + path.removeAt(path.lastIndex) + visited.remove(node) + return false + } + + if (dfs(startNode) && path.size > 1) { + return path + } + } + + // Fallback: construct a simple representation from the SCC + return scc + scc[0] + } + + /** + * Builds a detailed error message for detected cycles. + */ + private fun buildCycleErrorMessage( + cycles: List>, + metaByClassName: Map + ): String { + val sb = StringBuilder() + sb.appendLine("Circular component dependencies detected:") + sb.appendLine() + + cycles.forEachIndexed { index, cycle -> + // Show short names in summary for readability + val cycleDisplay = cycle.map { it.substringAfterLast('.') } + sb.appendLine(" Cycle ${index + 1}: ${cycleDisplay.joinToString(" → ")}") + } + + sb.appendLine() + sb.appendLine("Details (full class names):") + + for (cycle in cycles) { + for (i in 0 until cycle.size - 1) { + val from = cycle[i] + val to = cycle[i + 1] + sb.appendLine(" — $from") + sb.appendLine(" depends on $to (via @DependsOnComponent)") + } + sb.appendLine() + } + + return sb.toString() + } + + private fun logMissingDependencies(owner: Any, componentClassName: 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 component $componentClassName due to missing dependencies:\n$lines" + ) + } + + suspend fun getOrLoadComponents(owner: Any): List { + return componentsCache.get(owner) + } + + fun getLoadedComponents(owner: Any): List { + return componentsCache.underlying().asMap()[owner]?.getNow(emptyList()) ?: emptyList() + } + + suspend fun getAllComponents(): List { + return componentsCache.asMap().values.flatten().sorted() + } + + fun getAllComponentsLoaded(): List { + return componentsCache.underlying().asMap().values.flatMap { it.getNow(emptyList()) }.sorted() + } + + abstract fun readComponentsFileFromResources(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 { + 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/component/ComponentServiceFallback.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/component/ComponentServiceFallback.kt new file mode 100644 index 00000000..25d5cc87 --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/component/ComponentServiceFallback.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.surfapi.core.server.component + +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(ComponentService::class) +class ComponentServiceFallback : ComponentService(), Services.Fallback { + override fun readComponentsFileFromResources(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 components") + } +} \ 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/component/SurfComponentApiImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/component/SurfComponentApiImpl.kt new file mode 100644 index 00000000..2c7925e3 --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/component/SurfComponentApiImpl.kt @@ -0,0 +1,65 @@ +package dev.slne.surf.surfapi.core.server.impl.component + +import com.google.auto.service.AutoService +import dev.slne.surf.surfapi.core.api.component.SurfComponentApi +import dev.slne.surf.surfapi.core.server.component.ComponentService +import dev.slne.surf.surfapi.shared.api.component.Component + +@AutoService(SurfComponentApi::class) +class SurfComponentApiImpl : SurfComponentApi { + override suspend fun bootstrap(owner: Any) { + for (component in components(owner)) { + component.bootstrap() + } + } + + override suspend fun load(owner: Any) { + for (component in components(owner)) { + component.load() + } + } + + override suspend fun enable(owner: Any) { + for (component in components(owner)) { + component.enable() + } + } + + override suspend fun disable(owner: Any) { + ComponentService.get().invokePostProcessorsBeforeDestruction(owner) + + for (component in components(owner).reversed()) { + component.disable() + } + } + + override suspend fun componentsOfType( + owner: Any, + type: Class + ): List { + return components(owner).filterIsInstance(type) + } + + override fun componentsOfTypeLoaded( + owner: Any, + type: Class + ): List { + return componentsLoaded(owner).filterIsInstance(type) + } + + override suspend fun componentsOfType(type: Class): List { + return ComponentService.get().getAllComponents().filterIsInstance(type) + } + + override fun componentsOfTypeLoaded(type: Class): List { + return ComponentService.get().getAllComponentsLoaded().filterIsInstance(type) + } + + override suspend fun components(owner: Any): List { + return ComponentService.get().getOrLoadComponents(owner) + } + + override fun componentsLoaded(owner: Any): List { + return ComponentService.get().getLoadedComponents(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..1117bcc2 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) + 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/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/component/ComponentSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessor.kt new file mode 100644 index 00000000..f1434e2f --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessor.kt @@ -0,0 +1,393 @@ +package dev.slne.surf.surfapi.processor.component + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.closestClassDeclaration +import com.google.devtools.ksp.getAllSuperTypes +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.nameOf +import dev.slne.surf.surfapi.processor.util.toBinaryName +import dev.slne.surf.surfapi.shared.api.component.ComponentMeta +import dev.slne.surf.surfapi.shared.api.component.Priority +import dev.slne.surf.surfapi.shared.api.component.processor.ComponentPostProcessor +import dev.slne.surf.surfapi.shared.api.component.requirement.* +import dev.slne.surf.surfapi.shared.internal.hook.ComponentsConfig.COMPONENTS_DIRECTORY +import dev.slne.surf.surfapi.shared.internal.hook.ComponentsConfig.json +import dev.slne.surf.surfapi.shared.internal.hook.PluginComponentMeta +import java.io.IOException + +class ComponentSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { + companion object { + private val COMPONENT_ANNOTATION = nameOf() + private val PRIORITY_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 DEPENDS_ON_COMPONENT_ANNOTATION = nameOf() + private val CONDITIONAL_ON_ANNOTATION = nameOf() + private val COMPONENT_POST_PROCESSOR_NAME = nameOf() + } + + private val logger = environment.logger + private val codeGenerator = environment.codeGenerator + private val components = mutableMapOf>() + private val postProcessors = mutableMapOf>() + + @OptIn(KspExperimental::class) + override fun process(resolver: Resolver): List { + val moduleName = resolver.getModuleName().asString() + val deferred = mutableListOf() + + // Process components (both direct @ComponentMeta and meta-annotations) + processComponents(resolver, moduleName, deferred) + + // Process ComponentPostProcessor implementations + processPostProcessors(resolver, moduleName) + + return deferred + } + + + private fun processComponents( + resolver: Resolver, + moduleName: String, + deferred: MutableList + ) { + // Get all classes with direct @ComponentMeta annotation + val directlyAnnotated = resolver.getSymbolsWithAnnotation(COMPONENT_ANNOTATION) + .filterIsInstance() + .toList() + + // Get all classes that have annotations which are themselves annotated with @ComponentMeta (meta-annotations) + val metaAnnotated = resolver.getAllFiles() + .flatMap { it.declarations } + .filterIsInstance() + .filter { classDecl -> + // Check if any annotation on this class is itself annotated with @ComponentMeta + classDecl.annotations.any { annotation -> + hasComponentMetaAnnotation(annotation) + } + } + .toList() + + val allComponentClasses = (directlyAnnotated + metaAnnotated).distinctBy { it.qualifiedName?.asString() } + + val componentMetas = allComponentClasses.mapNotNull { componentClass -> + processComponentClass(componentClass, deferred) + } + + components.getOrPut(moduleName) { mutableSetOf() }.addAll(componentMetas) + } + + /** + * Checks if an annotation is annotated with @ComponentMeta (meta-annotation support) + */ + private fun hasComponentMetaAnnotation(annotation: KSAnnotation): Boolean { + val annotationType = annotation.annotationType.resolve() + val annotationDecl = annotationType.declaration as? KSClassDeclaration ?: return false + + return annotationDecl.annotations.any { + it.annotationType.resolve().declaration.qualifiedName?.asString() == COMPONENT_ANNOTATION + } + } + + /** + * Checks if a class has @ComponentMeta (directly or via meta-annotation) + * and returns the priority from @Priority annotation + */ + private fun findComponentMeta(classDecl: KSClassDeclaration): Pair? { + // First, check for direct @ComponentMeta + val directMeta = classDecl.annotations.find { + it.annotationType.resolve().declaration.qualifiedName?.asString() == COMPONENT_ANNOTATION + } + + if (directMeta != null) { + val priority = findPriority(classDecl) + return directMeta to priority + } + + // Check for meta-annotations + for (annotation in classDecl.annotations) { + val annotationType = annotation.annotationType.resolve() + val annotationDecl = annotationType.declaration as? KSClassDeclaration ?: continue + + val componentMetaOnAnnotation = annotationDecl.annotations.find { + it.annotationType.resolve().declaration.qualifiedName?.asString() == COMPONENT_ANNOTATION + } + + if (componentMetaOnAnnotation != null) { + val priority = findPriority(classDecl, annotationDecl) + return annotation to priority + } + } + + return null + } + + /** + * Finds the priority for a component class. + * Direct @Priority on the class takes precedence over meta-annotation @Priority. + */ + private fun findPriority(classDecl: KSClassDeclaration, metaAnnotationDecl: KSClassDeclaration? = null): Short { + // First check for direct @Priority annotation on the class + val directPriority = classDecl.annotations.find { + it.annotationType.resolve().declaration.qualifiedName?.asString() == PRIORITY_ANNOTATION + } + if (directPriority != null) { + return directPriority.arguments.find { it.name?.asString() == "value" }?.value as? Short ?: 0 + } + + // Then check for @Priority on the meta-annotation + if (metaAnnotationDecl != null) { + val metaPriority = metaAnnotationDecl.annotations.find { + it.annotationType.resolve().declaration.qualifiedName?.asString() == PRIORITY_ANNOTATION + } + if (metaPriority != null) { + return metaPriority.arguments.find { it.name?.asString() == "value" }?.value as? Short ?: 0 + } + } + + // Default priority + return 0 + } + + private fun processComponentClass( + componentClass: KSClassDeclaration, + deferred: MutableList + ): PluginComponentMeta.Component? { + var hasUnresolvedClassDependency = false + + val componentMetaInfo = findComponentMeta(componentClass) + if (componentMetaInfo == null) { + logger.error("@ComponentMeta annotation not found on element", componentClass) + return null + } + + val priority = componentMetaInfo.second + + // Collect all annotations from the class and its meta-annotations + val allAnnotations = collectAllAnnotations(componentClass) + + val dependsOnClass = allAnnotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == 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 += componentClass + hasUnresolvedClassDependency = true + return@mapNotNull null + } + + val closestClass = clazzValue.declaration.closestClassDeclaration() + if (closestClass == null) { + deferred += componentClass + hasUnresolvedClassDependency = true + return@mapNotNull null + } + closestClass.toBinaryName() + } + + if (hasUnresolvedClassDependency) { + return null + } + + val dependsOnClassName = allAnnotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == 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 = allAnnotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == 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 = allAnnotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == 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 + } + + val dependsOnComponent = allAnnotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == DEPENDS_ON_COMPONENT_ANNOTATION + }.mapNotNull { annotation -> + val componentValue = annotation.arguments.find { it.name?.asString() == "component" }?.value as? KSType + if (componentValue == null) { + logger.error("@DependsOnComponent annotation must have 'component' parameter", annotation) + return@mapNotNull null + } + + if (componentValue.isError) { + deferred += componentClass + hasUnresolvedClassDependency = true + return@mapNotNull null + } + + componentValue.declaration.closestClassDeclaration()?.toBinaryName() + } + + if (hasUnresolvedClassDependency) { + return null + } + + val customConditions = allAnnotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == CONDITIONAL_ON_ANNOTATION + }.mapNotNull { annotation -> + val conditionValue = annotation.arguments.find { it.name?.asString() == "condition" }?.value as? KSType + conditionValue?.declaration?.closestClassDeclaration()?.toBinaryName() + } + + return PluginComponentMeta.Component( + priority = priority, + className = componentClass.toBinaryName(), + classDependencies = dependsOnClass.toList() + dependsOnClassName.toList(), + pluginDependencies = dependsOnPlugin.toList(), + pluginOneDependencies = dependsOnOnePlugin.toList(), + componentDependencies = dependsOnComponent.toList(), + customConditions = customConditions.toList() + ) + } + + /** + * Collects all annotations from a class, including annotations from meta-annotations + */ + private fun collectAllAnnotations(classDecl: KSClassDeclaration): List { + val result = mutableListOf() + + // Add direct annotations + result.addAll(classDecl.annotations.toList()) + + // Add annotations from meta-annotations (annotations on the class's annotations) + for (annotation in classDecl.annotations) { + val annotationType = annotation.annotationType.resolve() + val annotationDecl = annotationType.declaration as? KSClassDeclaration ?: continue + + // Add annotations from the annotation declaration (excluding @ComponentMeta itself to avoid duplication) + for (metaAnnotation in annotationDecl.annotations) { + val metaAnnotationName = metaAnnotation.annotationType.resolve().declaration.qualifiedName?.asString() + if (metaAnnotationName != COMPONENT_ANNOTATION && + metaAnnotationName != "kotlin.annotation.Target" && + metaAnnotationName != "kotlin.annotation.Retention" && + metaAnnotationName != "kotlin.annotation.Repeatable" + ) { + result.add(metaAnnotation) + } + } + } + + return result + } + + private fun processPostProcessors(resolver: Resolver, moduleName: String) { + // Find all classes that implement ComponentPostProcessor + val postProcessorClasses = resolver.getAllFiles() + .flatMap { it.declarations } + .filterIsInstance() + .filter { classDecl -> + // Check if this class implements ComponentPostProcessor + classDecl.getAllSuperTypes().any { superType -> + superType.declaration.qualifiedName?.asString() == COMPONENT_POST_PROCESSOR_NAME + } + } + .filter { classDecl -> + // Exclude the interface itself + classDecl.qualifiedName?.asString() != COMPONENT_POST_PROCESSOR_NAME + } + .toList() + + val postProcessorMetas = postProcessorClasses.mapNotNull { postProcessorClass -> + try { + // Try to find priority from the class (default to 0) + // Note: Priority is a property, so we'll use default 0 and let runtime determine actual priority + PluginComponentMeta.PostProcessor( + className = postProcessorClass.toBinaryName(), + priority = 0 // Runtime will get actual priority from the instance + ) + } catch (e: Exception) { + logger.error( + "Failed to process post processor ${postProcessorClass.qualifiedName?.asString()}", + postProcessorClass + ) + null + } + } + + postProcessors.getOrPut(moduleName) { mutableSetOf() }.addAll(postProcessorMetas) + } + + + override fun finish() { + generatePluginComponentFile() + components.clear() + postProcessors.clear() + } + + + private fun generatePluginComponentFile() { + val allModules = (components.keys + postProcessors.keys).toSet() + + if (allModules.isEmpty()) { + return + } + + for (moduleName in allModules) { + val moduleComponents = components[moduleName]?.toList() ?: emptyList() + val modulePostProcessors = postProcessors[moduleName]?.toList() ?: emptyList() + + if (moduleComponents.isEmpty() && modulePostProcessors.isEmpty()) { + continue + } + + val componentMeta = PluginComponentMeta(moduleComponents, modulePostProcessors) + val filePath = "$COMPONENTS_DIRECTORY/$moduleName.json" + try { + codeGenerator.createNewFileByPath(Dependencies(aggregating = true), filePath, "") + .bufferedWriter() + .use { writer -> + val jsonString = json.encodeToString(componentMeta) + writer.write(jsonString) + } + + logger.info("Wrote Components to: $filePath") + } catch (e: IOException) { + logger.error("Unable to create $filePath, $e") + } + } + } +} diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessorProvider.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessorProvider.kt new file mode 100644 index 00000000..2b0b9712 --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessorProvider.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.surfapi.processor.component + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class ComponentSymbolProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return ComponentSymbolProcessor(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/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..bda62ba2 --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/util/util.kt @@ -0,0 +1,17 @@ +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() + +inline fun nameOf(): String = T::class.java.name \ 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..9404702a 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.component.ComponentSymbolProcessorProvider \ No newline at end of file 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/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/ComponentsConfig.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/ComponentsConfig.kt new file mode 100644 index 00000000..57355778 --- /dev/null +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/ComponentsConfig.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.surfapi.shared.internal.hook + +import kotlinx.serialization.json.Json + +object ComponentsConfig { + const val COMPONENTS_DIRECTORY = "META-INF/surf-api/components" + val json = Json { + prettyPrint = true + encodeDefaults = false + ignoreUnknownKeys = true + prettyPrintIndent = " " + } +} \ 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/PluginComponentMeta.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginComponentMeta.kt new file mode 100644 index 00000000..b5b1e85d --- /dev/null +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginComponentMeta.kt @@ -0,0 +1,57 @@ +package dev.slne.surf.surfapi.shared.internal.hook + +import kotlinx.serialization.Serializable + +@Serializable +data class PluginComponentMeta( + val components: List, + val postProcessors: List = emptyList() +) { + + @Serializable + data class Component( + val priority: Short, + val className: String, + val classDependencies: List = emptyList(), + val pluginDependencies: List = emptyList(), + val pluginOneDependencies: List> = emptyList(), + val componentDependencies: List = emptyList(), + val customConditions: List = emptyList(), + ) + + @Serializable + data class PostProcessor( + val className: String, + val priority: Int = 0 + ) + + fun mergeWith(other: PluginComponentMeta): PluginComponentMeta { + val mergedComponents = ArrayList(this.components.size + other.components.size) + mergedComponents.addAll(this.components) + for (component in other.components) { + if (!mergedComponents.any { it.className == component.className }) { + mergedComponents.add(component) + } else { + throw IllegalStateException("Duplicate component className found during merge: ${component.className}") + } + } + + val mergedPostProcessors = ArrayList(this.postProcessors.size + other.postProcessors.size) + mergedPostProcessors.addAll(this.postProcessors) + for (postProcessor in other.postProcessors) { + if (!mergedPostProcessors.any { it.className == postProcessor.className }) { + mergedPostProcessors.add(postProcessor) + } else { + throw IllegalStateException("Duplicate post processor className found during merge: ${postProcessor.className}") + } + } + + return PluginComponentMeta(mergedComponents, mergedPostProcessors) + } + + operator fun plus(other: PluginComponentMeta) = mergeWith(other) + + companion object { + fun empty() = PluginComponentMeta(emptyList(), emptyList()) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..df0fffb8 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/build.gradle.kts @@ -0,0 +1,26 @@ +@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) + 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-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/Component.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/Component.kt new file mode 100644 index 00000000..6aec7dd6 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/Component.kt @@ -0,0 +1,69 @@ +package dev.slne.surf.surfapi.shared.api.component + +import org.jetbrains.annotations.ApiStatus + +/** + * Type alias for the [Component] interface, providing an alternative name for usage within the system. + * + * This alias helps avoid naming conflicts with other libraries that may define + * a `Component` interface, such as the Adventure API. + * + * @see Component + */ +typealias SurfComponent = Component + +/** + * Core interface for all components in the component system. + * + * Components are modular units of functionality that follow a defined lifecycle. + * They are automatically discovered at compile time and instantiated at runtime + * based on their dependencies and conditions. + * + * The lifecycle of a component consists of four phases: + * 1. [bootstrap] - Initial bootstrap phase, called first + * 2. [load] - Loading phase, called after bootstrap + * 3. [enable] - Enable phase, called after load + * 4. [disable] - Disable phase, called during shutdown + * + * Components are sorted by [priority] and dependency order. Lower priority values + * are initialized first. Dependencies declared via [@DependsOnComponent][dev.slne.surf.surfapi.shared.api.component.requirement.DependsOnComponent] + * are guaranteed to be initialized before their dependents. + * + * @see AbstractComponent + * @see ComponentMeta + */ +@ApiStatus.NonExtendable +interface Component : Comparable { + /** + * The priority of this component for ordering purposes. + * Lower values indicate higher priority (initialized first). + * Components with the same priority are ordered by their dependencies. + */ + val priority: Short + + /** + * Called during the bootstrap phase. + * This is the first lifecycle method called on the component. + * Use this for early initialization that doesn't depend on other components. + */ + suspend fun bootstrap() + + /** + * Called during the load phase, after [bootstrap]. + * Use this for loading configuration, resources, or other data. + */ + suspend fun load() + + /** + * Called during the enable phase, after [load]. + * Use this to activate the component's functionality. + */ + suspend fun enable() + + /** + * Called during the disable phase, during shutdown. + * Use this to clean up resources and deactivate functionality. + * Components are disabled in reverse order of their initialization. + */ + 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/component/ComponentMeta.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/ComponentMeta.kt new file mode 100644 index 00000000..81d4dfa1 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/ComponentMeta.kt @@ -0,0 +1,49 @@ +package dev.slne.surf.surfapi.shared.api.component + + +/** + * Annotation to mark a class as a component or to create meta-annotations. + * + * When applied to a class, it marks that class as a component that will be + * automatically discovered and managed by the component system. + * + * When applied to another annotation class (meta-annotation), any class annotated + * with that annotation will also be treated as a component. This enables creating + * domain-specific component annotations. + * + * Example of direct usage: + * ```kotlin + * @ComponentMeta + * @Priority(10) + * class MyComponent : AbstractComponent() { + * override suspend fun onEnable() { + * // Component logic + * } + * } + * ``` + * + * Example of meta-annotation usage: + * ```kotlin + * // Define a custom annotation + * @ComponentMeta + * @Priority(100) // Default priority for all @Service components + * @Target(AnnotationTarget.CLASS) + * annotation class Service + * + * // Use the custom annotation - class is automatically a component with priority 100 + * @Service + * class MyService : AbstractComponent() { ... } + * + * // Override the default priority + * @Service + * @Priority(50) + * class ImportantService : AbstractComponent() { ... } + * ``` + * + * @see Component + * @see AbstractComponent + * @see Priority + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class ComponentMeta diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/Priority.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/Priority.kt new file mode 100644 index 00000000..57452ac5 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/Priority.kt @@ -0,0 +1,48 @@ +package dev.slne.surf.surfapi.shared.api.component + +/** + * Specifies the priority of a component for initialization ordering. + * + * Components with lower priority values are initialized first. + * If two components have the same priority, their order is determined + * by their dependencies (via [@DependsOnComponent][dev.slne.surf.surfapi.shared.api.component.requirement.DependsOnComponent]). + * + * This annotation can be used on component classes directly or on meta-annotations. + * When used on a meta-annotation, it provides a default priority for all components + * using that meta-annotation. A direct `@Priority` on the component class overrides + * the meta-annotation's priority. + * + * Example: + * ```kotlin + * @ComponentMeta + * @Priority(10) + * class LowPriorityComponent : AbstractComponent() { ... } + * + * @ComponentMeta + * @Priority(-10) + * class HighPriorityComponent : AbstractComponent() { ... } + * ``` + * + * Example with meta-annotation: + * ```kotlin + * @ComponentMeta + * @Priority(100) // Default priority for all @Service components + * annotation class Service + * + * @Service // Will have priority 100 + * class MyService : AbstractComponent() { ... } + * + * @Service + * @Priority(50) // Overrides the default + * class ImportantService : AbstractComponent() { ... } + * ``` + * + * @property value The priority value. Lower values are initialized first. Default is 0. + * + * @see ComponentMeta + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Priority( + val value: Short = 0 +) \ 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/component/condition/ComponentCondition.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/condition/ComponentCondition.kt new file mode 100644 index 00000000..a9f20aba --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/condition/ComponentCondition.kt @@ -0,0 +1,37 @@ +package dev.slne.surf.surfapi.shared.api.component.condition + +/** + * Interface for defining custom conditions that control component instantiation. + * + * Conditions are evaluated at runtime before a component is instantiated. + * If the condition returns `false`, the component will not be loaded. + * + * Conditions are specified using the [@ConditionalOn][dev.slne.surf.surfapi.shared.api.component.requirement.ConditionalOn] + * annotation on component classes. + * + * Example implementation: + * ```kotlin + * class FeatureEnabledCondition : ComponentCondition { + * override suspend fun evaluate(context: ComponentConditionContext): Boolean { + * return MyConfig.isFeatureEnabled() + * } + * } + * + * @ConditionalOn(FeatureEnabledCondition::class) + * @ComponentMeta + * class MyFeatureComponent : AbstractComponent() { ... } + * ``` + * + * @see ComponentConditionContext + * @see dev.slne.surf.surfapi.shared.api.component.requirement.ConditionalOn + */ +interface ComponentCondition { + + /** + * Evaluates whether the component should be instantiated. + * + * @param context The context providing information about the component and environment + * @return `true` if the component should be instantiated, `false` to skip it + */ + suspend fun evaluate(context: ComponentConditionContext): 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/component/condition/ComponentConditionContext.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/condition/ComponentConditionContext.kt new file mode 100644 index 00000000..6fd9360a --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/condition/ComponentConditionContext.kt @@ -0,0 +1,23 @@ +package dev.slne.surf.surfapi.shared.api.component.condition + +import dev.slne.surf.surfapi.shared.api.component.SurfComponent +import net.kyori.adventure.text.logger.slf4j.ComponentLogger + +/** + * Context object passed to [ComponentCondition.evaluate] providing information + * about the component being evaluated and its environment. + * + * @property owner The owner of the component (e.g., the plugin instance) + * @property componentClass The class of the component being evaluated + * @property logger A logger that can be used for diagnostic output + * @property environment Additional environment variables or configuration + * + * @see ComponentCondition + */ +@JvmRecord +data class ComponentConditionContext( + val owner: Any, + val componentClass: 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/component/processor/ComponentContext.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/processor/ComponentContext.kt new file mode 100644 index 00000000..4b8127d9 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/processor/ComponentContext.kt @@ -0,0 +1,24 @@ +package dev.slne.surf.surfapi.shared.api.component.processor + +import dev.slne.surf.surfapi.shared.api.component.Component + + +/** + * Context object passed to [ComponentPostProcessor] methods providing information + * about the processing environment. + * + * @property owner The owner of the components (e.g., the plugin instance) + * @property allComponents List of all components that have been instantiated. + * Note: During postProcessAfterInitialization, this list + * contains the original components before any post-processing. + * @property environment Additional environment variables or configuration that + * can be used by post-processors + * + * @see ComponentPostProcessor + */ +@JvmRecord +data class ComponentContext( + val owner: Any, + val allComponents: List, + 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/component/processor/ComponentPostProcessor.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/processor/ComponentPostProcessor.kt new file mode 100644 index 00000000..6b030b8a --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/processor/ComponentPostProcessor.kt @@ -0,0 +1,87 @@ +package dev.slne.surf.surfapi.shared.api.component.processor + +import dev.slne.surf.surfapi.shared.api.component.SurfComponent + +/** + * Interface for post-processing components after initialization. + * + * Implementations are auto-discovered by the symbol processor without requiring any annotation. + * Simply implement this interface and the processor will register it automatically. + * + * Post-processors are executed in the following order: + * 1. Component instantiation + * 2. For each post-processor (sorted by priority, lower first): postProcessAfterInitialization() + * 3. Component.bootstrap() + * 4. Component.load() + * 5. Component.enable() + * 6. ... + * 7. For each post-processor (reverse priority order): postProcessBeforeDestruction() + * 8. Component.disable() + * + * Example usage: + * ```kotlin + * class LoggingPostProcessor : ComponentPostProcessor { + * override val priority: Int = 10 + * + * override suspend fun postProcessAfterInitialization( + * component: Component, + * componentName: String, + * context: ComponentContext + * ): Component { + * println("Initialized: $componentName") + * return component + * } + * } + * ``` + */ +interface ComponentPostProcessor { + /** + * Priority for ordering multiple processors. + * Lower values are executed first during initialization. + * During destruction, the order is reversed (higher values first). + * Default is 0. + */ + val priority: Int get() = 0 + + /** + * Called after component initialization but before the component's bootstrap phase. + * + * This method can be used for: + * - Logging component initialization + * - Injecting dependencies + * - Wrapping or proxying the component + * - Validating component configuration + * + * @param component The component that was just initialized + * @param componentName The fully qualified class name of the component + * @param context Context containing the owner, all components, and environment + * @return The component (possibly wrapped or modified). Return the same component if no modification is needed. + */ + suspend fun postProcessAfterInitialization( + component: SurfComponent, + componentName: String, + context: ComponentContext + ): SurfComponent + + /** + * Called before component shutdown (before the component's disable phase). + * + * This method can be used for: + * - Logging component destruction + * - Cleanup of resources injected during initialization + * - Unwrapping proxied components + * + * The default implementation does nothing. Override only if cleanup is needed. + * + * @param component The component that is about to be destroyed + * @param componentName The fully qualified class name of the component + * @param context Context containing the owner, all components, and environment + */ + suspend fun postProcessBeforeDestruction( + component: SurfComponent, + componentName: String, + context: ComponentContext + ) { + // default empty - override if cleanup is needed + } +} \ 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/component/requirement/ConditionalOn.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOn.kt new file mode 100644 index 00000000..2166da8a --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOn.kt @@ -0,0 +1,35 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +import dev.slne.surf.surfapi.shared.api.component.condition.ComponentCondition +import kotlin.reflect.KClass + +/** + * Specifies a custom condition that must be satisfied for the component to be loaded. + * + * The condition class must implement [ComponentCondition] and will be instantiated + * and evaluated at runtime. If the condition returns `false`, the component will not be loaded. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple conditions to be specified. + * + * Example: + * ```kotlin + * class FeatureEnabledCondition : ComponentCondition { + * override suspend fun evaluate(context: ComponentConditionContext): Boolean { + * return Config.isFeatureEnabled() + * } + * } + * + * @ConditionalOn(FeatureEnabledCondition::class) + * @ComponentMeta + * class MyComponent : AbstractComponent() { ... } + * ``` + * + * @property condition The condition class that determines if the component should be loaded + * + * @see ComponentCondition + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class ConditionalOn(val condition: KClass) \ 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/component/requirement/ConditionalOnEnvironment.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOnEnvironment.kt new file mode 100644 index 00000000..eb210de3 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOnEnvironment.kt @@ -0,0 +1,25 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +/** + * Specifies that the component should only be loaded in certain environments. + * + * The component will only be instantiated if the current environment matches + * one of the specified environment names. This is useful for components that + * should only run in development, production, or test environments. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple environment constraints to be specified. + * + * Example: + * ```kotlin + * @ConditionalOnEnvironment(environments = ["development", "test"]) + * @ComponentMeta + * class DebugComponent : AbstractComponent() { ... } + * ``` + * + * @property environments Array of environment names in which the component should be loaded + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class ConditionalOnEnvironment(val environments: Array) diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOnMissingComponent.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOnMissingComponent.kt new file mode 100644 index 00000000..8c884bb1 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOnMissingComponent.kt @@ -0,0 +1,28 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +import dev.slne.surf.surfapi.shared.api.component.Component +import kotlin.reflect.KClass + +/** + * Specifies that the component should only be loaded if another component is NOT present. + * + * This is useful for providing fallback implementations or default behavior + * that should only be active when a more specific component is not available. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple missing component constraints to be specified. + * + * Example: + * ```kotlin + * // This component is only loaded if CustomLogger is not present + * @ConditionalOnMissingComponent(CustomLogger::class) + * @ComponentMeta + * class DefaultLogger : AbstractComponent() { ... } + * ``` + * + * @property component The component class that must NOT be present for this component to load + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class ConditionalOnMissingComponent(val component: KClass) \ 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/component/requirement/ConditionalOnProperty.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOnProperty.kt new file mode 100644 index 00000000..9de84bc4 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/ConditionalOnProperty.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +/** + * Specifies that the component should only be loaded if a configuration property matches. + * + * This annotation allows conditional loading based on configuration values. + * The property is looked up in the environment configuration at runtime. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple property conditions to be specified. + * + * Example: + * ```kotlin + * @ConditionalOnProperty(key = "feature.enabled", havingValue = "true") + * @ComponentMeta + * class FeatureComponent : AbstractComponent() { ... } + * + * // Load if property is missing or equals "default" + * @ConditionalOnProperty(key = "mode", havingValue = "default", matchIfMissing = true) + * @ComponentMeta + * class DefaultModeComponent : AbstractComponent() { ... } + * ``` + * + * @property key The property key to check + * @property havingValue The expected value. If empty, just checks for property existence. + * @property matchIfMissing If true, the condition matches when the property is not set. + * Default is false. + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class ConditionalOnProperty( + val key: String, + val havingValue: String = "", + val matchIfMissing: Boolean = false +) \ 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/component/requirement/DependsOnClass.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnClass.kt new file mode 100644 index 00000000..7d8dd92b --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnClass.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +import kotlin.reflect.KClass + +/** + * Specifies that the component depends on a class being available at runtime. + * + * The component will only be loaded if the specified class can be found in the classpath. + * This is useful for optional integrations with other libraries or plugins. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple class dependencies to be specified. + * + * Example: + * ```kotlin + * // Only load if LuckPerms API is available + * @DependsOnClass(net.luckperms.api.LuckPerms::class) + * @ComponentMeta + * class LuckPermsIntegration : AbstractComponent() { ... } + * ``` + * + * @property clazz The class that must be available for the component to load + * + * @see DependsOnClassName + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnClass(val clazz: KClass<*>) \ 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/component/requirement/DependsOnClassName.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnClassName.kt new file mode 100644 index 00000000..c0e3f6a1 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnClassName.kt @@ -0,0 +1,31 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + + +/** + * Specifies that the component depends on a class being available at runtime, using the class name as a string. + * + * This is an alternative to [@DependsOnClass][DependsOnClass] that uses a string class name + * instead of a class reference. This is useful when the dependency class is not available + * at compile time. + * + * The component will only be loaded if the specified class can be found in the classpath. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple class dependencies to be specified. + * + * Example: + * ```kotlin + * // Only load if a specific class is available + * @DependsOnClassName("com.example.optional.OptionalFeature") + * @ComponentMeta + * class OptionalIntegration : AbstractComponent() { ... } + * ``` + * + * @property className The fully qualified class name that must be available + * + * @see DependsOnClass + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnClassName(val className: String) \ 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/component/requirement/DependsOnComponent.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnComponent.kt new file mode 100644 index 00000000..e16f17cd --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnComponent.kt @@ -0,0 +1,37 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +import dev.slne.surf.surfapi.shared.api.component.Component +import kotlin.reflect.KClass + + +/** + * Specifies that the component depends on another component being loaded first. + * + * This creates a dependency relationship that affects the loading order. + * The dependent component will not be instantiated until all its dependencies + * have been successfully loaded. + * + * Circular dependencies are detected at runtime and will cause an error with + * a detailed message showing the cycle. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple component dependencies to be specified. + * + * Example: + * ```kotlin + * @ComponentMeta + * class DatabaseComponent : AbstractComponent() { ... } + * + * @DependsOnComponent(DatabaseComponent::class) + * @ComponentMeta + * class UserService : AbstractComponent() { + * // DatabaseComponent is guaranteed to be loaded before this + * } + * ``` + * + * @property component The component class that must be loaded before this component + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnComponent(val component: KClass) \ 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/component/requirement/DependsOnOnePlugin.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnOnePlugin.kt new file mode 100644 index 00000000..a896983f --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnOnePlugin.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +/** + * Specifies that the component depends on at least one of the specified plugins being loaded. + * + * The component will only be loaded if at least one of the specified plugins is present. + * This is useful when a component can work with multiple alternative plugins. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple "one of" plugin groups to be specified. + * + * Example: + * ```kotlin + * // Load if either Vault or another economy plugin is present + * @DependsOnOnePlugin(["Vault", "EssentialsX", "CMI"]) + * @ComponentMeta + * class EconomyIntegration : AbstractComponent() { ... } + * ``` + * + * @property pluginIds Array of plugin IDs where at least one must be loaded + * + * @see DependsOnPlugin + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnOnePlugin(val pluginIds: Array) \ 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/component/requirement/DependsOnPlugin.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnPlugin.kt new file mode 100644 index 00000000..13d669d2 --- /dev/null +++ b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/component/requirement/DependsOnPlugin.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.surfapi.shared.api.component.requirement + +/** + * Specifies that the component depends on a plugin being loaded. + * + * The component will only be loaded if the specified plugin is present and loaded. + * This is useful for creating optional integrations with other plugins. + * + * This annotation can be used on component classes directly or on meta-annotations. + * It is repeatable, allowing multiple plugin dependencies to be specified. + * + * Example: + * ```kotlin + * // Only load if Vault plugin is present + * @DependsOnPlugin("Vault") + * @ComponentMeta + * class VaultEconomy : AbstractComponent() { ... } + * ``` + * + * @property pluginId The plugin ID that must be loaded + * + * @see DependsOnOnePlugin + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class DependsOnPlugin(val pluginId: 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/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 diff --git a/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityComponentService.kt b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityComponentService.kt new file mode 100644 index 00000000..b1ecfffe --- /dev/null +++ b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityComponentService.kt @@ -0,0 +1,43 @@ +package dev.slne.surf.surfapi.velocity.server.hook + +import com.google.auto.service.AutoService +import dev.slne.surf.surfapi.core.server.component.ComponentService +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(ComponentService::class) +class VelocityComponentService : ComponentService() { + override fun readComponentsFileFromResources(owner: Any, fileName: String): InputStream? { + return try { + val url = getClassloader(owner).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