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 af4c1a99..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,7 +10,7 @@ import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.TestInven import dev.slne.surf.surfapi.bukkit.test.command.subcommands.reflection.Reflection import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig import dev.slne.surf.surfapi.bukkit.test.listener.ChatListener -import dev.slne.surf.surfapi.core.api.hook.surfHookApi +import dev.slne.surf.surfapi.core.api.component.surfComponentApi @OptIn(NmsUseWithCaution::class) class BukkitPluginMain : SuspendingJavaPlugin() { @@ -18,7 +18,7 @@ class BukkitPluginMain : SuspendingJavaPlugin() { ModernTestConfig.init() ModernTestConfig.randomise() - surfHookApi.load(this) + surfComponentApi.load(this) packetListenerApi.registerListeners(ChatListener()) TestInventoryView.register() } @@ -27,12 +27,12 @@ class BukkitPluginMain : SuspendingJavaPlugin() { SurfApiTestCommand().register() Reflection::class.java.getClassLoader() // initialize Reflection - surfHookApi.enable(this) + surfComponentApi.enable(this) } override suspend fun onDisableAsync() { CommandAPI.unregister("surfapitest") - surfHookApi.disable(this) + 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/component/PrimaryTestComponent.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/PrimaryTestComponent.kt new file mode 100644 index 00000000..8d8e41bf --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/PrimaryTestComponent.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.surfapi.bukkit.test.component + +import dev.slne.surf.surfapi.bukkit.test.component.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 PrimaryTestComponent : AbstractComponent() { + private val log = logger() + + override suspend fun onBootstrap() { + log.atInfo().log("PrimaryTestComponent bootstrapped") + } + + override suspend fun onLoad() { + log.atInfo().log("PrimaryTestComponent loaded") + } + + override suspend fun onEnable() { + log.atInfo().log("PrimaryTestComponent enabled") + } + + override suspend fun onDisable() { + log.atInfo().log("PrimaryTestComponent disabled") + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/TestComponent.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/TestComponent.kt new file mode 100644 index 00000000..a24bb7fd --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/TestComponent.kt @@ -0,0 +1,35 @@ +package dev.slne.surf.surfapi.bukkit.test.component + +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.Priority +import dev.slne.surf.surfapi.shared.api.component.requirement.* + +@ComponentMeta +@Priority(10) +@DependsOnClass(BukkitPluginMain::class) +@DependsOnClassName("dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig") +@DependsOnPlugin("SurfBukkitPluginTest") +@DependsOnOnePlugin(["SurfBukkitPlugin", "surf-bukkit-plugin", "SurfBukkitPluginTest"]) +@DependsOnComponent(PrimaryTestComponent::class) +class TestComponent : AbstractComponent() { + private val log = logger() + + override suspend fun onBootstrap() { + log.atInfo().log("TestComponent bootstrapped") + } + + override suspend fun onLoad() { + log.atInfo().log("TestComponent loaded") + } + + override suspend fun onEnable() { + log.atInfo().log("TestComponent enabled") + } + + override suspend fun onDisable() { + log.atInfo().log("TestComponent disabled") + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/condition/EnabledCondition.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/condition/EnabledCondition.kt new file mode 100644 index 00000000..572a875c --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/condition/EnabledCondition.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.surfapi.bukkit.test.component.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 + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/processor/TestLoggingPostProcessor.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/processor/TestLoggingPostProcessor.kt new file mode 100644 index 00000000..3fbc6e5f --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/component/processor/TestLoggingPostProcessor.kt @@ -0,0 +1,33 @@ +package dev.slne.surf.surfapi.bukkit.test.component.processor + +import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.shared.api.component.Component +import dev.slne.surf.surfapi.shared.api.component.processor.ComponentContext +import dev.slne.surf.surfapi.shared.api.component.processor.ComponentPostProcessor + +/** + * Example ComponentPostProcessor implementation that is auto-discovered. + * No annotation needed - just implementing the interface is enough. + */ +class TestLoggingPostProcessor : ComponentPostProcessor { + private val log = logger() + + override val priority: Int = 5 + + override suspend fun postProcessAfterInitialization( + component: Component, + componentName: String, + context: ComponentContext + ): Component { + log.atInfo().log("Component initialized: $componentName") + return component + } + + override suspend fun postProcessBeforeDestruction( + component: Component, + componentName: String, + context: ComponentContext + ) { + log.atInfo().log("Component being destroyed: $componentName") + } +} 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 deleted file mode 100644 index feb9071f..00000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/PrimaryTestHook.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.test.hook - -import dev.slne.surf.surfapi.bukkit.test.hook.condition.EnabledCondition -import dev.slne.surf.surfapi.core.api.hook.AbstractHook -import dev.slne.surf.surfapi.core.api.util.logger -import dev.slne.surf.surfapi.shared.api.hook.HookMeta -import dev.slne.surf.surfapi.shared.api.hook.requirement.ConditionalOnCustom - -@ConditionalOnCustom(EnabledCondition::class) -@HookMeta -class PrimaryTestHook : AbstractHook() { - private val log = logger() - - override suspend fun onBootstrap() { - log.atInfo().log("PrimaryTestHook bootstrapped") - } - - override suspend fun onLoad() { - log.atInfo().log("PrimaryTestHook loaded") - } - - override suspend fun onEnable() { - log.atInfo().log("PrimaryTestHook enabled") - } - - override suspend fun onDisable() { - log.atInfo().log("PrimaryTestHook disabled") - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt deleted file mode 100644 index db168398..00000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/TestHook.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.test.hook - -import dev.slne.surf.surfapi.bukkit.test.BukkitPluginMain -import dev.slne.surf.surfapi.core.api.hook.AbstractHook -import dev.slne.surf.surfapi.core.api.util.logger -import dev.slne.surf.surfapi.shared.api.hook.HookMeta -import dev.slne.surf.surfapi.shared.api.hook.requirement.* - -@HookMeta -@DependsOnClass(BukkitPluginMain::class) -@DependsOnClassName("dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig") -@DependsOnPlugin("SurfBukkitPluginTest") -@DependsOnOnePlugin(["SurfBukkitPlugin", "surf-bukkit-plugin", "SurfBukkitPluginTest"]) -@DependsOnHook(PrimaryTestHook::class) -class TestHook : AbstractHook() { - private val log = logger() - - 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 deleted file mode 100644 index aa98ca8b..00000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/hook/condition/EnabledCondition.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.test.hook.condition - -import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig -import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition -import dev.slne.surf.surfapi.shared.api.hook.condition.HookConditionContext - -class EnabledCondition : HookCondition { - override suspend fun evaluate(context: HookConditionContext): Boolean { - return ModernTestConfig.getConfig().enabled - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperHookService.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/component/PaperComponentService.kt similarity index 79% rename from surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperHookService.kt rename to surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/component/PaperComponentService.kt index d87c0a13..05c8d484 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/hook/PaperHookService.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/component/PaperComponentService.kt @@ -1,18 +1,18 @@ -package dev.slne.surf.surfapi.bukkit.server.hook +package dev.slne.surf.surfapi.bukkit.server.component import com.google.auto.service.AutoService import dev.slne.surf.surfapi.bukkit.api.extensions.pluginManager import dev.slne.surf.surfapi.bukkit.server.reflection.Reflection -import dev.slne.surf.surfapi.core.server.hook.HookService +import 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(HookService::class) -class PaperHookService : HookService() { - override fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? { +@AutoService(ComponentService::class) +class PaperComponentService : ComponentService() { + override fun readComponentsFileFromResources(owner: Any, fileName: String): InputStream? { ensureOwnerIsPlugin(owner) return owner.getResource(fileName) } @@ -39,4 +39,4 @@ class PaperHookService : HookService() { return owner as? JavaPlugin ?: error("Owner must be a JavaPlugin") } -} \ 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/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..02773d2c --- /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,136 @@ +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 + .mapNotNull { it.annotationClass.java.getAnnotation(T::class.java) } + .firstOrNull() + } + + @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() {} +} 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..3f3a0d73 --- /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,133 @@ +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 : JavaPlugin() { + * override fun onLoad() { + * runBlocking { surfComponentApi.load(this@MyPlugin) } + * } + * + * override fun onEnable() { + * runBlocking { surfComponentApi.enable(this@MyPlugin) } + * } + * + * override fun onDisable() { + * runBlocking { surfComponentApi.disable(this@MyPlugin) } + * } + * } + * ``` + * + * @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 (typically 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 (typically 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 (typically 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 (typically 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 { + /** + * The singleton instance of the component API. + */ + val instance = requiredService() + } +} + +/** + * Convenience property to access the [SurfComponentApi] instance. + */ +val surfComponentApi get() = SurfComponentApi.instance diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt deleted file mode 100644 index e4a3a698..00000000 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/AbstractHook.kt +++ /dev/null @@ -1,57 +0,0 @@ -package dev.slne.surf.surfapi.core.api.hook - -import dev.slne.surf.surfapi.shared.api.hook.Hook -import dev.slne.surf.surfapi.shared.api.hook.HookMeta -import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi -import java.util.concurrent.atomic.AtomicBoolean - -abstract class AbstractHook : Hook { - private val bootstrapped = AtomicBoolean(false) - private val loaded = AtomicBoolean(false) - private val enabled = AtomicBoolean(false) - private val disabled = AtomicBoolean(false) - - private val meta: HookMeta = javaClass.getAnnotation(HookMeta::class.java) - ?: error("HookMeta annotation is missing on hook class ${this::class.qualifiedName}") - - final override val priority = meta.priority - - @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: Hook): Int { - return this.priority.compareTo(other.priority) - } - - protected open suspend fun onBootstrap() {} - protected open suspend fun onLoad() {} - protected open suspend fun onEnable() {} - protected open suspend fun onDisable() {} -} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt deleted file mode 100644 index d65cb1f3..00000000 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/hook/SurfHookApi.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.slne.surf.surfapi.core.api.hook - -import dev.slne.surf.surfapi.core.api.util.requiredService -import dev.slne.surf.surfapi.shared.api.hook.Hook - -interface SurfHookApi { - - suspend fun bootstrap(owner: Any) - suspend fun load(owner: Any) - suspend fun enable(owner: Any) - suspend fun disable(owner: Any) - - suspend fun hooksOfType(owner: Any, type: Class): List - fun hooksOfTypeLoaded(owner: Any, type: Class): List - suspend fun hooksOfType(type: Class): List - fun hooksOfTypeLoaded(type: Class): List - - suspend fun hooks(owner: Any): List - fun hooksLoaded(owner: Any): List - - companion object { - val instance = requiredService() - } -} - -val surfHookApi get() = SurfHookApi.instance - 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..b878ef16 --- /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,572 @@ +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.component.ComponentsConfig +import dev.slne.surf.surfapi.shared.internal.component.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() + .asLoadingCache { 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 component = instantiateComponentIfValid(owner, componentMeta, classLoader) + if (component != null) { + componentMeta to component + } else { + null + } + } + + val sortedComponents = topologicalSort(componentsWithMeta, owner) + + // Load and apply post-processors + val postProcessors = postProcessorsCache.get(owner) + return applyPostProcessors(sortedComponents, postProcessors, owner) + } + + private suspend 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 = componentsCache.underlying().asMap()[owner]?.getNow(emptyList()) ?: return + val postProcessors = postProcessorsCache.underlying().asMap()[owner]?.getNow(emptyList()) ?: 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 componentClass = Class.forName(componentMeta.className, false, classLoader) + val componentKClass = componentClass.kotlin + val objectInstance = componentKClass.objectInstance + if (objectInstance != null) { + require(objectInstance is Component) { "Component class must implement Component" } + return objectInstance + } else { + val constructor = componentClass.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 = mutableMapOf() + val lowLink = mutableMapOf() + val onStack = mutableSetOf() + val stack = ArrayDeque() + val sccs = mutableListOf>() + + 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[node]!!, lowLink[dependency]!!) + } else if (dependency in onStack) { + lowLink[node] = minOf(lowLink[node]!!, nodeIndex[dependency]!!) + } + } + + // If node is a root node, pop the stack and generate an SCC + if (lowLink[node] == nodeIndex[node]) { + val scc = mutableListOf() + 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 = mutableListOf>() + 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.toSet() + + // Try to find a cycle starting from each node in the SCC + for (startNode in scc) { + val path = mutableListOf() + val visited = mutableSetOf() + + 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 getComponents(owner: Any): List { + return componentsCache.get(owner) + } + + fun getComponentsLoaded(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 + } +} diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookServiceFallback.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/component/ComponentServiceFallback.kt similarity index 70% rename from surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookServiceFallback.kt rename to surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/component/ComponentServiceFallback.kt index 9d975531..19365c53 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookServiceFallback.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/component/ComponentServiceFallback.kt @@ -1,13 +1,13 @@ -package dev.slne.surf.surfapi.core.server.hook +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(HookService::class) -class HookServiceFallback : HookService(), Services.Fallback { - override fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? { +@AutoService(ComponentService::class) +class ComponentServiceFallback : ComponentService(), Services.Fallback { + override fun readComponentsFileFromResources(owner: Any, fileName: String): InputStream? { throwNotImplementedOnThisPlatform() } @@ -24,6 +24,6 @@ class HookServiceFallback : HookService(), Services.Fallback { } private fun throwNotImplementedOnThisPlatform(): Nothing { - throw UnsupportedOperationException("This platform does not yet support hooks") + 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/hook/HookService.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt deleted file mode 100644 index c270eb7b..00000000 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/hook/HookService.kt +++ /dev/null @@ -1,381 +0,0 @@ -package dev.slne.surf.surfapi.core.server.hook - -import com.github.benmanes.caffeine.cache.Caffeine -import com.sksamuel.aedile.core.asLoadingCache -import dev.slne.surf.surfapi.core.api.util.* -import dev.slne.surf.surfapi.shared.api.hook.Hook -import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition -import dev.slne.surf.surfapi.shared.api.hook.condition.HookConditionContext -import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig -import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta -import 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 HookService { - - private val hookMetaCache = Caffeine.newBuilder() - .weakKeys() - .build { owner -> loadHooksMeta(owner) } - - private val hooksCache = Caffeine.newBuilder() - .weakKeys() - .asLoadingCache { owner -> loadHooks(owner) } - - private fun loadHooksMeta(owner: Any): PluginHookMeta { - val classloader = getClassloader(owner) - val logger = getLogger(owner) - var meta = PluginHookMeta.empty() - - try { - val resources = classloader.getResources(HooksConfig.HOOKS_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(HooksConfig.HOOKS_DIRECTORY) && it.name.endsWith(".json") } - .forEach { entry -> - try { - val raw = jarFile.getInputStream(entry).bufferedReader().use { it.readText() } - val decoded = HooksConfig.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 = HooksConfig.json.decodeFromString(raw) - meta += decoded - } catch (e: Exception) { - logger.error("Failed to parse ${file.name}", e) - } - } - } - } - } catch (e: Exception) { - logger.warn("No hooks directory found or error reading hooks", e) - } - - return meta - -// val rawStream = readHooksFileFromResources(owner, HooksConfig.HOOKS_FILE_PATH) ?: return PluginHookMeta.empty() -// val raw = rawStream.bufferedReader().use { it.readText() } -// return try { -// HooksConfig.json.decodeFromString(raw) -// } catch (e: SerializationException) { -// getLogger(owner).error("Failed to parse ${HooksConfig.HOOKS_FILE_PATH}", e) -// PluginHookMeta.empty() -// } - } - - private suspend fun loadHooks(owner: Any): List { - val meta = hookMetaCache.get(owner) - val classLoader = getClassloader(owner) - - val hooksWithMeta = meta.hooks.mapNotNull { hookMeta -> - val hook = instantiateHookIfValid(owner, hookMeta, classLoader) - if (hook != null) { - hookMeta to hook - } else { - null - } - } - - return topologicalSort(hooksWithMeta, owner) - } - - private suspend fun instantiateHookIfValid( - owner: Any, - hookMeta: PluginHookMeta.Hook, - classLoader: ClassLoader - ): Hook? { - val missingDependencies = mutableObject2ObjectMapOf>() - for (classDependency in hookMeta.classDependencies) { - try { - Class.forName(classDependency, false, classLoader) - } catch (_: ClassNotFoundException) { - missingDependencies.computeIfAbsent("Class") { mutableObjectSetOf() }.add(classDependency) - } - } - - for (pluginDependencyId in hookMeta.pluginDependencies) { - if (!isPluginLoaded(pluginDependencyId)) { - missingDependencies.computeIfAbsent("Plugin") { mutableObjectSetOf() }.add(pluginDependencyId) - } - } - - for (pluginDependenciesIds in hookMeta.pluginOneDependencies) { - if (pluginDependenciesIds.none { isPluginLoaded(it) }) { - missingDependencies.computeIfAbsent("Plugin (one of)") { mutableObjectSetOf() } - .add(pluginDependenciesIds.joinToString("|")) - } - } - - if (missingDependencies.isNotEmpty()) { - logMissingDependencies(owner, hookMeta.className, missingDependencies) - return null - } - - if (!evaluateConditions(owner, hookMeta, classLoader)) return null - - try { - val hookClass = Class.forName(hookMeta.className, false, classLoader) - val hookKClass = hookClass.kotlin - val objectInstance = hookKClass.objectInstance - if (objectInstance != null) { - require(objectInstance is Hook) { "Hook class must implement Hook" } - return objectInstance - } else { - val constructor = hookClass.getConstructor() - val instance = constructor.newInstance() - require(instance is Hook) { "Hook class must implement Hook" } - return instance - } - } catch (e: Exception) { - getLogger(owner).error("Failed to load hook ${hookMeta.className}", e) - } - - return null - } - - @Suppress("UNCHECKED_CAST") - private suspend fun evaluateConditions( - owner: Any, - hookMeta: PluginHookMeta.Hook, - classLoader: ClassLoader - ): Boolean { - for (conditionClassName in hookMeta.customConditions) { - try { - val conditionClass = Class.forName(conditionClassName, false, classLoader) - val condition = conditionClass.getConstructor().newInstance() as HookCondition - val logger = getLogger(owner) - - val context = HookConditionContext( - owner = owner, - logger = logger, - hookClass = Class.forName(hookMeta.className, false, classLoader) as Class - ) - - if (!condition.evaluate(context)) { - logger.debug("Hook ${hookMeta.className} skipped due to condition $conditionClassName") - return false - } - } catch (e: Exception) { - getLogger(owner).error("Failed to evaluate condition $conditionClassName", e) - return false - } - } - return true - } - - private fun topologicalSort( - hooksWithMeta: List>, - owner: Any - ): List { - // If no hooks depend on other hooks, simply sort by priority - if (hooksWithMeta.none { it.first.hookDependencies.isNotEmpty() }) { - return hooksWithMeta.map { it.second }.sorted() - } - - val hooksByClassName = hooksWithMeta.associate { (meta, hook) -> - meta.className to hook - } - - val metaByClassName = hooksWithMeta.associate { (meta, _) -> - meta.className to meta - } - - val missingHookDeps = mutableMapOf>() - for ((meta, _) in hooksWithMeta) { - for (depClassName in meta.hookDependencies) { - if (depClassName !in hooksByClassName) { - missingHookDeps.computeIfAbsent(meta.className) { mutableSetOf() } - .add(depClassName) - } - } - } - - if (missingHookDeps.isNotEmpty()) { - val logger = getLogger(owner) - for ((hookClassName, missingDeps) in missingHookDeps) { - logger.warn( - "Hook $hookClassName depends on hooks that are not loaded: ${missingDeps.joinToString(", ")}" - ) - } - } - - val validHooks = hooksWithMeta.filter { (meta, _) -> - meta.className !in missingHookDeps - } - - if (validHooks.isEmpty()) { - return emptyList() - } - - // Build dependency graph: className -> list of dependents (successors) - // Note: In Kahn's algorithm, edges go from dependency to dependent - val graph = mutableObject2ObjectMapOf>() - for ((meta, _) in validHooks) { - // Ensure all nodes exist in the graph - if (meta.className !in graph) { - graph[meta.className] = mutableListOf() - } - // Add edges from dependencies to this hook - for (depClassName in meta.hookDependencies) { - if (depClassName in hooksByClassName) { - graph.computeIfAbsent(depClassName) { mutableListOf() }.add(meta.className) - } - } - } - - // 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 hook priority (lower priority value = higher priority) - val queue = PriorityQueue(compareBy { className -> - hooksByClassName[className]?.priority ?: Short.MAX_VALUE - }) - - incomingEdges.object2IntEntrySet().fastForEach { entry -> - val vertex = entry.key - val edges = entry.intValue - if (edges == 0) queue += vertex - } - - val result = mutableObjectListOf() - - while (queue.isNotEmpty()) { - val vertex = queue.poll() - hooksByClassName[vertex]?.let { result += it } - - for (successor in graph[vertex].orEmpty()) { - incomingEdges.mergeInt(successor, -1, Int::minus) - if (incomingEdges.getInt(successor) == 0) { - queue += successor - } - } - } - - if (result.size != incomingEdges.size) { - val chain = findCyclicDependency(graph, incomingEdges) - throw IllegalStateException( - "Circular hook dependency detected: ${chain.joinToString(" -> ")}" - ) - } - - return result - } - - private fun findCyclicDependency( - graph: Map>, - incomingEdges: Map - ): List { - // Find nodes that are part of a cycle (still have incoming edges after topological sort) - val nodesInCycle = incomingEdges.filter { it.value > 0 }.keys - if (nodesInCycle.isEmpty()) return emptyList() - - // Use DFS to find an actual cycle path - val visited = mutableSetOf() - val recursionStack = mutableSetOf() - val path = mutableListOf() - - fun dfs(node: String): List? { - if (node in recursionStack) { - // Found a cycle! Build the cycle path - val cycleStart = path.indexOf(node) - return if (cycleStart >= 0) { - path.subList(cycleStart, path.size) + node - } else { - listOf(node) - } - } - - if (node in visited) return null - - visited.add(node) - recursionStack.add(node) - path.add(node) - - // Explore successors - for (successor in graph[node].orEmpty()) { - val cycle = dfs(successor) - if (cycle != null) return cycle - } - - recursionStack.remove(node) - path.removeAt(path.lastIndex) - return null - } - - // Start DFS from any node that's part of a cycle - for (startNode in nodesInCycle) { - if (startNode !in visited) { - val cycle = dfs(startNode) - if (cycle != null) return cycle - } - } - - // Fallback: if no cycle found via DFS, return the nodes with remaining edges - return nodesInCycle.toList() - } - - - private fun logMissingDependencies(owner: Any, hookClassName: String, missing: Map>) { - val logger = getLogger(owner) - - val lines = missing.entries - .sortedBy { it.key } - .joinToString(separator = System.lineSeparator()) { (type, ids) -> - val formattedIds = ids.toList().sorted().joinToString(", ") - " - $type: $formattedIds" - } - - logger.warn( - "Skipping hook $hookClassName due to missing dependencies:\n$lines" - ) - } - - suspend fun getHooks(owner: Any): List { - return hooksCache.get(owner) - } - - fun getHooksLoaded(owner: Any): List { - return hooksCache.underlying().asMap()[owner]?.getNow(emptyList()) ?: emptyList() - } - - suspend fun getAllHooks(): List { - return hooksCache.asMap().values.flatten().sorted() - } - - fun getAllHooksLoaded(): List { - return hooksCache.underlying().asMap().values.flatMap { it.getNow(emptyList()) }.sorted() - } - - abstract fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? - abstract fun getClassloader(owner: Any): ClassLoader - abstract fun isPluginLoaded(pluginId: String): Boolean - 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/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..4549cf3f --- /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,66 @@ +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) { + // Invoke post-processors before destruction + 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().getComponents(owner) + } + + override fun componentsLoaded(owner: Any): List { + return ComponentService.get().getComponentsLoaded(owner) + } +} diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt deleted file mode 100644 index da704bdf..00000000 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/hook/SurfHookApiImpl.kt +++ /dev/null @@ -1,63 +0,0 @@ -package dev.slne.surf.surfapi.core.server.impl.hook - -import com.google.auto.service.AutoService -import dev.slne.surf.surfapi.core.api.hook.SurfHookApi -import dev.slne.surf.surfapi.core.server.hook.HookService -import dev.slne.surf.surfapi.shared.api.hook.Hook - -@AutoService(SurfHookApi::class) -class SurfHookApiImpl : SurfHookApi { - override suspend fun bootstrap(owner: Any) { - for (hook in hooks(owner)) { - hook.bootstrap() - } - } - - override suspend fun load(owner: Any) { - for (hook in hooks(owner)) { - hook.load() - } - } - - override suspend fun enable(owner: Any) { - for (hook in hooks(owner)) { - hook.enable() - } - } - - override suspend fun disable(owner: Any) { - for (hook in hooks(owner).reversed()) { - hook.disable() - } - } - - override suspend fun hooksOfType( - owner: Any, - type: Class - ): List { - return hooks(owner).filterIsInstance(type) - } - - override fun hooksOfTypeLoaded( - owner: Any, - type: Class - ): List { - return hooksLoaded(owner).filterIsInstance(type) - } - - override suspend fun hooksOfType(type: Class): List { - return HookService.get().getAllHooks().filterIsInstance(type) - } - - override fun hooksOfTypeLoaded(type: Class): List { - return HookService.get().getAllHooksLoaded().filterIsInstance(type) - } - - override suspend fun hooks(owner: Any): List { - return HookService.get().getHooks(owner) - } - - override fun hooksLoaded(owner: Any): List { - return HookService.get().getHooksLoaded(owner) - } -} \ No newline at end of file diff --git a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/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..4a47a1e4 --- /dev/null +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessor.kt @@ -0,0 +1,386 @@ +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.component.ComponentsConfig.COMPONENTS_DIRECTORY +import dev.slne.surf.surfapi.shared.internal.component.ComponentsConfig.json +import dev.slne.surf.surfapi.shared.internal.component.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/hook/HookSymbolProcessorProvider.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessorProvider.kt similarity index 62% rename from surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessorProvider.kt rename to surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessorProvider.kt index a9ffa9bf..be454a33 100644 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessorProvider.kt +++ b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/component/ComponentSymbolProcessorProvider.kt @@ -1,11 +1,11 @@ -package dev.slne.surf.surfapi.processor.hook +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 HookSymbolProcessorProvider : SymbolProcessorProvider { +class ComponentSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - return HookSymbolProcessor(environment) + 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/hook/HookSymbolProcessor.kt b/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt deleted file mode 100644 index 6a5ebbf4..00000000 --- a/surf-api-gradle-plugin/surf-api-processor/src/main/kotlin/dev/slne/surf/surfapi/processor/hook/HookSymbolProcessor.kt +++ /dev/null @@ -1,201 +0,0 @@ -package dev.slne.surf.surfapi.processor.hook - -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.closestClassDeclaration -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.processing.Resolver -import com.google.devtools.ksp.processing.SymbolProcessor -import com.google.devtools.ksp.processing.SymbolProcessorEnvironment -import com.google.devtools.ksp.symbol.KSAnnotated -import com.google.devtools.ksp.symbol.KSAnnotation -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.symbol.KSType -import dev.slne.surf.surfapi.processor.util.nameOf -import dev.slne.surf.surfapi.processor.util.toBinaryName -import dev.slne.surf.surfapi.shared.api.hook.HookMeta -import dev.slne.surf.surfapi.shared.api.hook.requirement.* -import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig.HOOKS_DIRECTORY -import dev.slne.surf.surfapi.shared.internal.hook.HooksConfig.json -import dev.slne.surf.surfapi.shared.internal.hook.PluginHookMeta -import java.io.IOException - -class HookSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { - companion object { - private val HOOK_ANNOTATION = nameOf() - private val DEPENDS_ON_CLASS_ANNOTATION = nameOf() - private val DEPENDS_ON_CLASS_NAME_ANNOTATION = nameOf() - private val DEPENDS_ON_ONE_PLUGIN_ANNOTATION = nameOf() - private val DEPENDS_ON_PLUGIN_ANNOTATION = nameOf() - private val DEPENDS_ON_HOOK_ANNOTATION = nameOf() - private val CONDITIONAL_ON_CUSTOM_ANNOTATION = nameOf() - } - - private val logger = environment.logger - private val codeGenerator = environment.codeGenerator - private val hooks = mutableMapOf>() - - @OptIn(KspExperimental::class) - override fun process(resolver: Resolver): List { - val moduleName = resolver.getModuleName().asString() - val deferred = mutableListOf() - val hooksMetas = resolver.getSymbolsWithAnnotation(HOOK_ANNOTATION) - .filterIsInstance() - .mapNotNull { hookClass -> - var hasUnresolvedClassDependency = false - val hookMeta = hookClass.annotations.findAnnotation(HOOK_ANNOTATION) ?: run { - logger.error("@HookMeta annotation not found on element", hookClass) - return@mapNotNull null - } - - val priority = hookMeta.arguments.find { it.name?.asString() == "priority" }?.value as? Short ?: 0 - val dependsOnClass = hookClass.annotations.findAnnotations(DEPENDS_ON_CLASS_ANNOTATION) - .mapNotNull { annotation -> - val clazzValue = annotation.arguments.find { it.name?.asString() == "clazz" }?.value as? KSType - if (clazzValue == null) { - logger.error("DependsOnClass annotation must have 'clazz' parameter", annotation) - return@mapNotNull null - } - - if (clazzValue.isError) { - deferred += hookClass - hasUnresolvedClassDependency = true - return@mapNotNull null - } - - val closestClass = clazzValue.declaration.closestClassDeclaration() - if (closestClass == null) { - deferred += hookClass - hasUnresolvedClassDependency = true - return@mapNotNull null - } - closestClass.toBinaryName() - } - - if (hasUnresolvedClassDependency) { - return@mapNotNull null - } - - val dependsOnClassName = hookClass.annotations.findAnnotations(DEPENDS_ON_CLASS_NAME_ANNOTATION) - .mapNotNull { annotation -> - val classNameValue = - annotation.arguments.find { it.name?.asString() == "className" }?.value as? String - if (classNameValue == null) { - logger.error("@DependsOnClassName annotation must have 'className' parameter", annotation) - return@mapNotNull null - } - classNameValue - } - - val dependsOnOnePlugin = hookClass.annotations.findAnnotations(DEPENDS_ON_ONE_PLUGIN_ANNOTATION) - .mapNotNull { annotation -> - val argValue = annotation.arguments.find { it.name?.asString() == "pluginIds" }?.value - val pluginIds = when (argValue) { - is List<*> -> argValue.filterIsInstance() - is String -> listOf(argValue) - else -> emptyList() - } - - if (pluginIds.isEmpty()) { - logger.error("@DependsOnOnePlugin annotation must have 'pluginIds' parameter", annotation) - return@mapNotNull null - } - - pluginIds - } - - val dependsOnPlugin = hookClass.annotations.findAnnotations(DEPENDS_ON_PLUGIN_ANNOTATION) - .mapNotNull { annotation -> - val argValue = annotation.arguments.find { it.name?.asString() == "pluginId" }?.value - val pluginId = argValue as? String - if (pluginId == null) { - logger.error("@DependsOnPlugin annotation must have 'pluginId' parameter", annotation) - return@mapNotNull null - } - pluginId - } - - val dependsOnHook = hookClass.annotations.findAnnotations(DEPENDS_ON_HOOK_ANNOTATION) - .mapNotNull { annotation -> - val hookValue = annotation.arguments.find { it.name?.asString() == "hook" }?.value as? KSType - if (hookValue == null) { - logger.error("@DependsOnHook annotation must have 'hook' parameter", annotation) - return@mapNotNull null - } - - if (hookValue.isError) { - deferred += hookClass - hasUnresolvedClassDependency = true - return@mapNotNull null - } - - hookValue.declaration.closestClassDeclaration()?.toBinaryName() - } - - if (hasUnresolvedClassDependency) { - return@mapNotNull null - } - - val customConditions = hookClass.annotations.findAnnotations(CONDITIONAL_ON_CUSTOM_ANNOTATION) - .mapNotNull { annotation -> - val conditionValue = - annotation.arguments.find { it.name?.asString() == "condition" }?.value as? KSType - conditionValue?.declaration?.closestClassDeclaration()?.toBinaryName() - } - - PluginHookMeta.Hook( - priority = priority, - className = hookClass.toBinaryName(), - classDependencies = dependsOnClass.toList() + dependsOnClassName.toList(), - pluginDependencies = dependsOnPlugin.toList(), - pluginOneDependencies = dependsOnOnePlugin.toList(), - hookDependencies = dependsOnHook.toList(), - customConditions = customConditions.toList() - ) - }.toList() - - hooks.getOrPut(moduleName) { mutableSetOf() }.addAll(hooksMetas) - - return deferred - } - - override fun finish() { - generatePluginHookFile() - hooks.clear() - } - - - private fun generatePluginHookFile() { - if (hooks.isEmpty()) { - return - } - - for ((moduleName, moduleHooks) in hooks) { - val hookMeta = PluginHookMeta(moduleHooks.toList()) - val filePath = "$HOOKS_DIRECTORY/$moduleName.json" - try { - codeGenerator.createNewFileByPath(Dependencies(aggregating = true), filePath, "") - .bufferedWriter() - .use { writer -> - val jsonString = json.encodeToString(hookMeta) - writer.write(jsonString) - } - - logger.info("Wrote Hooks to: $filePath") - } catch (e: IOException) { - logger.error("Unable to create $filePath, $e") - } - } - } - - private fun Sequence.findAnnotation(annotationClassName: String): KSAnnotation? { - return this.firstOrNull { - it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationClassName - } - } - - private fun Sequence.findAnnotations(annotationClassName: String): Sequence { - return this.filter { - it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationClassName - } - } -} 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 6b2ab056..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,2 +1,2 @@ dev.slne.surf.surfapi.processor.autoservice.AutoServiceSymbolProcessorProvider -dev.slne.surf.surfapi.processor.hook.HookSymbolProcessorProvider \ No newline at end of file +dev.slne.surf.surfapi.processor.component.ComponentSymbolProcessorProvider \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/component/ComponentsConfig.kt similarity index 55% rename from surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt rename to surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/component/ComponentsConfig.kt index e244e46a..dcbfea4f 100644 --- a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/HooksConfig.kt +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/component/ComponentsConfig.kt @@ -1,13 +1,13 @@ -package dev.slne.surf.surfapi.shared.internal.hook +package dev.slne.surf.surfapi.shared.internal.component import kotlinx.serialization.json.Json -object HooksConfig { - const val HOOKS_DIRECTORY = "META-INF/surf-api/hooks" +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/component/PluginComponentMeta.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/component/PluginComponentMeta.kt new file mode 100644 index 00000000..66ef4b7d --- /dev/null +++ b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/component/PluginComponentMeta.kt @@ -0,0 +1,57 @@ +package dev.slne.surf.surfapi.shared.internal.component + +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()) + } +} diff --git a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt b/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt deleted file mode 100644 index 51bc93dc..00000000 --- a/surf-api-shared/surf-api-shared-internal/src/main/kotlin/dev/slne/surf/surfapi/shared/internal/hook/PluginHookMeta.kt +++ /dev/null @@ -1,37 +0,0 @@ -package dev.slne.surf.surfapi.shared.internal.hook - -import kotlinx.serialization.Serializable - -@Serializable -data class PluginHookMeta(val hooks: List) { - - @Serializable - data class Hook( - val priority: Short, - val className: String, - val classDependencies: List = emptyList(), - val pluginDependencies: List = emptyList(), - val pluginOneDependencies: List> = emptyList(), - val hookDependencies: List = emptyList(), - val customConditions: List = emptyList(), - ) - - fun mergeWith(other: PluginHookMeta): PluginHookMeta { - val mergedHooks = ArrayList(this.hooks.size + other.hooks.size) - mergedHooks.addAll(this.hooks) - for (hook in other.hooks) { - if (!mergedHooks.any { it.className == hook.className }) { - mergedHooks.add(hook) - } else { - throw IllegalStateException("Duplicate hook className found during merge: ${hook.className}") - } - } - return PluginHookMeta(mergedHooks) - } - - operator fun plus(other: PluginHookMeta) = mergeWith(other) - - companion object { - fun empty() = PluginHookMeta(emptyList()) - } -} \ 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..ab3da9e3 --- /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,56 @@ +package dev.slne.surf.surfapi.shared.api.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 + */ +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() +} 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..5839a791 --- /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,48 @@ +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..afdf9895 --- /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 +) 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..99f1b897 --- /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,36 @@ +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 +} 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..48364f17 --- /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.Component +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() +) 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..7744452f --- /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,22 @@ +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 + */ +data class ComponentContext( + val owner: Any, + val allComponents: List, + val environment: Map = emptyMap() +) 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..cf547e52 --- /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.Component + +/** + * 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: Component, + componentName: String, + context: ComponentContext + ): Component + + /** + * 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: Component, + componentName: String, + context: ComponentContext + ) { + // default empty - override if cleanup is needed + } +} 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..92bb466b --- /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) 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..56d1f625 --- /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) 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..aec32491 --- /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 +) 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..78b73257 --- /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<*>) 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..3b768c48 --- /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,30 @@ +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) 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..ae807920 --- /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,36 @@ +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) 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..1d4fc792 --- /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) 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..e9a3f6aa --- /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) diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/Hook.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/Hook.kt deleted file mode 100644 index 6a87e4d3..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/Hook.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook - -interface Hook : Comparable { - val priority: Short - suspend fun bootstrap() - suspend fun load() - suspend fun enable() - suspend fun disable() -} \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/HookMeta.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/HookMeta.kt deleted file mode 100644 index b714a56f..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/HookMeta.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class HookMeta( - val priority: Short = 0 -) diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition.kt deleted file mode 100644 index 24f6f4dc..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookCondition.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.condition - -interface HookCondition { - suspend fun evaluate(context: HookConditionContext): Boolean -} \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt deleted file mode 100644 index f3ce80d4..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/condition/HookConditionContext.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.condition - -import dev.slne.surf.surfapi.shared.api.hook.Hook -import net.kyori.adventure.text.logger.slf4j.ComponentLogger - -@JvmRecord -data class HookConditionContext( - val owner: Any, - val hookClass: Class, - val logger: ComponentLogger, - val environment: Map = emptyMap() -) \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom.kt deleted file mode 100644 index 2b249f71..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/ConditionalOnCustom.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.requirement - -import dev.slne.surf.surfapi.shared.api.hook.condition.HookCondition -import kotlin.reflect.KClass - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@Repeatable -annotation class ConditionalOnCustom(val condition: KClass) \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass.kt deleted file mode 100644 index ec11e6e7..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClass.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.requirement - -import kotlin.reflect.KClass - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@Repeatable -annotation class DependsOnClass(val clazz: KClass<*>) diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName.kt deleted file mode 100644 index 9db89030..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnClassName.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.requirement - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@Repeatable -annotation class DependsOnClassName(val className: String) \ No newline at end of file diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook.kt deleted file mode 100644 index 1b984f7d..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnHook.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.requirement - -import dev.slne.surf.surfapi.shared.api.hook.Hook -import kotlin.reflect.KClass - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@Repeatable -annotation class DependsOnHook(val hook: KClass) diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin.kt deleted file mode 100644 index c4290759..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnOnePlugin.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.requirement - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@Repeatable -annotation class DependsOnOnePlugin(val pluginIds: Array) diff --git a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin.kt b/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin.kt deleted file mode 100644 index 73966d91..00000000 --- a/surf-api-shared/surf-api-shared-public/src/main/kotlin/dev/slne/surf/surfapi/shared/api/hook/requirement/DependsOnPlugin.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slne.surf.surfapi.shared.api.hook.requirement - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@Repeatable -annotation class DependsOnPlugin(val pluginId: String) diff --git a/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/component/VelocityComponentService.kt similarity index 80% rename from surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt rename to surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/component/VelocityComponentService.kt index 4078f0e8..b59e12c7 100644 --- a/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/hook/VelocityHookService.kt +++ b/surf-api-velocity/surf-api-velocity-server/src/main/kotlin/dev/slne/surf/surfapi/velocity/server/component/VelocityComponentService.kt @@ -1,16 +1,16 @@ -package dev.slne.surf.surfapi.velocity.server.hook +package dev.slne.surf.surfapi.velocity.server.component import com.google.auto.service.AutoService -import dev.slne.surf.surfapi.core.server.hook.HookService +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(HookService::class) -class VelocityHookService : HookService() { - override fun readHooksFileFromResources(owner: Any, fileName: String): InputStream? { +@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() @@ -40,4 +40,4 @@ class VelocityHookService : HookService() { return getPluginContainerFromOwner(owner).instance.getOrNull() ?: error("Failed to get instance from owner: $owner") } -} \ No newline at end of file +}