From 6d6b767dab6385fc13e63e5852858497bb17c812 Mon Sep 17 00:00:00 2001 From: Liam Sage Date: Wed, 14 Jan 2026 09:35:44 +0100 Subject: [PATCH 1/3] Remove all source and docs; simplify build config Deleted all source files, documentation, and module build scripts, leaving only the root build configuration and minimal settings. The root build.gradle.kts was rewritten to simplify plugin and dependency management, and gradle.properties was updated to clarify Kotlin and JVM target configuration. This change effectively removes the KTale codebase and documentation, retaining only a minimal project skeleton. --- .woodpecker/build.yml | 1 + build.gradle.kts | 132 +++------- docs/auto-registration.mdx | 40 --- docs/commands.mdx | 52 ---- docs/configs.mdx | 24 -- docs/dependency-resolution.mdx | 36 --- docs/dokka-and-pages.mdx | 33 --- docs/events.mdx | 40 --- docs/fake-platform-testing.mdx | 32 --- docs/getting-started.mdx | 61 ----- docs/gradle-plugin.mdx | 57 ----- docs/index.mdx | 29 --- docs/modules.mdx | 64 ----- docs/plugin-developer-workflow.mdx | 233 ------------------ docs/plugins-java.mdx | 44 ---- docs/plugins-kotlin.mdx | 40 --- docs/standalone-host.mdx | 31 --- gradle.properties | 10 +- ktale-api/build.gradle.kts | 7 - .../ktale/api/autoregister/AutoCommand.java | 18 -- .../api/autoregister/SubscribeEvent.java | 25 -- .../src/main/kotlin/ktale/api/KtalePlugin.kt | 38 --- .../main/kotlin/ktale/api/PluginContext.kt | 55 ----- .../ktale/api/commands/CommandContext.kt | 21 -- .../ktale/api/commands/CommandDefinition.kt | 28 --- .../ktale/api/commands/CommandRegistry.kt | 32 --- .../ktale/api/commands/CommandResult.kt | 31 --- .../ktale/api/commands/CommandSender.kt | 23 -- .../kotlin/ktale/api/commands/Permission.kt | 23 -- .../kotlin/ktale/api/config/ConfigCodec.kt | 20 -- .../main/kotlin/ktale/api/config/ConfigKey.kt | 33 --- .../kotlin/ktale/api/config/ConfigManager.kt | 26 -- .../ktale/api/config/ConfigMigration.kt | 26 -- .../kotlin/ktale/api/entities/EntityRef.kt | 24 -- .../kotlin/ktale/api/entities/HasModel.kt | 25 -- .../kotlin/ktale/api/events/Cancellable.kt | 15 -- .../src/main/kotlin/ktale/api/events/Event.kt | 11 - .../main/kotlin/ktale/api/events/EventBus.kt | 57 ----- .../kotlin/ktale/api/events/EventListener.kt | 13 - .../kotlin/ktale/api/events/EventPriority.kt | 37 --- .../ktale/api/events/EventSubscription.kt | 13 - .../src/main/kotlin/ktale/api/identity/Key.kt | 30 --- .../kotlin/ktale/api/inventory/Container.kt | 27 -- .../ktale/api/inventory/ContainerKind.kt | 20 -- .../main/kotlin/ktale/api/items/ItemStack.kt | 20 -- .../main/kotlin/ktale/api/items/ItemType.kt | 15 -- .../kotlin/ktale/api/logging/KtaleLogger.kt | 55 ----- .../main/kotlin/ktale/api/logging/LogLevel.kt | 14 -- .../main/kotlin/ktale/api/prefabs/Prefab.kt | 18 -- .../kotlin/ktale/api/prefabs/PrefabStore.kt | 29 --- .../kotlin/ktale/api/scheduler/Scheduler.kt | 45 ---- .../ktale/api/scheduler/SchedulerKotlin.kt | 27 -- .../kotlin/ktale/api/scheduler/TaskHandle.kt | 16 -- .../ktale/api/services/ServiceRegistry.kt | 46 ---- .../main/kotlin/ktale/api/stats/Attribute.kt | 23 -- .../kotlin/ktale/api/stats/HasAttributes.kt | 19 -- ktale-core/build.gradle.kts | 16 -- .../main/kotlin/ktale/core/PluginContexts.kt | 33 --- .../ktale/core/autoregister/AutoRegistrar.kt | 100 -------- .../core/commands/BridgedCommandRegistry.kt | 38 --- .../ktale/core/commands/CommandExecutor.kt | 16 -- .../kotlin/ktale/core/commands/Commands.kt | 102 -------- .../core/commands/SimpleCommandRegistry.kt | 67 ----- .../ktale/core/config/ConfigTextStore.kt | 16 -- .../ktale/core/config/CoreConfigManager.kt | 87 ------- .../ktale/core/config/FileConfigTextStore.kt | 44 ---- .../core/config/InMemoryConfigTextStore.kt | 18 -- .../ktale/core/config/yaml/DefaultYaml.kt | 21 -- .../ktale/core/config/yaml/YamlConfigCodec.kt | 22 -- .../ktale/core/events/SimpleEventBus.kt | 66 ----- .../ktale/core/logging/SimpleConsoleLogger.kt | 28 --- .../kotlin/ktale/core/runtime/CoreRuntime.kt | 61 ----- .../core/scheduler/HookBackedScheduler.kt | 35 --- .../core/services/SimpleServiceRegistry.kt | 37 --- .../ktale/core/threading/ThreadGuards.kt | 30 --- .../commands/BridgedCommandRegistryTest.kt | 64 ----- .../commands/SimpleCommandRegistryTest.kt | 86 ------- .../core/config/CoreConfigManagerYamlTest.kt | 92 ------- .../ktale/core/events/SimpleEventBusTest.kt | 52 ---- .../services/SimpleServiceRegistryTest.kt | 40 --- ktale-gradle-plugin/build.gradle.kts | 23 -- .../modlabs/ktale/gradle/KtaleDepsPlugin.kt | 165 ------------- ktale-platform-fake/build.gradle.kts | 9 - .../ktale/platform/fake/DeterministicClock.kt | 39 --- .../ktale/platform/fake/FakeCommandBridge.kt | 41 --- .../ktale/platform/fake/FakePlatform.kt | 36 --- .../kotlin/ktale/platform/fake/FakePlayer.kt | 31 --- .../ktale/platform/fake/FakeSchedulerHooks.kt | 112 --------- .../kotlin/ktale/platform/fake/FakeServer.kt | 48 ---- .../ktale/platform/fake/FakeTaskHandle.kt | 17 -- .../kotlin/ktale/platform/fake/FakeWorld.kt | 15 -- .../examples/JavaExamplePluginUsage.java | 60 ----- .../examples/KotlinExamplePluginUsage.kt | 58 ----- .../platform/fake/AutoRegistrarSmokeTest.kt | 56 ----- .../platform/fake/FakeSchedulerHooksTest.kt | 48 ---- .../FakeServerEndToEndConfigSchedulerTest.kt | 61 ----- .../platform/fake/FakeServerSmokeTest.kt | 47 ---- ktale-platform-hytale/build.gradle.kts | 8 - .../hypothesis/CommandRoutingHypothesis.kt | 27 -- .../hypothesis/ServerLifecycleHypothesis.kt | 30 --- .../hypothesis/ThreadingHypothesis.kt | 27 -- .../hytale/HytalePlatformPlaceholder.kt | 82 ------ ktale-platform/build.gradle.kts | 7 - .../main/kotlin/ktale/platform/Platform.kt | 47 ---- .../kotlin/ktale/platform/PlatformClock.kt | 24 -- .../ktale/platform/PlatformCommandBridge.kt | 47 ---- .../ktale/platform/PlatformLoggerFactory.kt | 19 -- .../ktale/platform/PlatformSchedulerHooks.kt | 26 -- ktale-runtime-deps/build.gradle.kts | 11 - .../ktale/runtime/deps/DependencyManifest.kt | 34 --- .../runtime/deps/DependencyManifestReader.kt | 53 ---- .../runtime/deps/MavenDependencyResolver.kt | 99 -------- .../deps/DependencyManifestReaderTest.kt | 47 ---- ktale-runtime-host/build.gradle.kts | 10 - .../runtime/host/JarPluginDescriptorReader.kt | 39 --- .../ktale/runtime/host/PluginDescriptor.kt | 15 -- .../StandalonePluginClassLoaderFactory.kt | 25 -- .../runtime/host/StandalonePluginHost.kt | 109 -------- .../host/JarPluginDescriptorReaderTest.kt | 24 -- settings.gradle.kts | 13 +- 120 files changed, 42 insertions(+), 4782 deletions(-) delete mode 100644 docs/auto-registration.mdx delete mode 100644 docs/commands.mdx delete mode 100644 docs/configs.mdx delete mode 100644 docs/dependency-resolution.mdx delete mode 100644 docs/dokka-and-pages.mdx delete mode 100644 docs/events.mdx delete mode 100644 docs/fake-platform-testing.mdx delete mode 100644 docs/getting-started.mdx delete mode 100644 docs/gradle-plugin.mdx delete mode 100644 docs/index.mdx delete mode 100644 docs/modules.mdx delete mode 100644 docs/plugin-developer-workflow.mdx delete mode 100644 docs/plugins-java.mdx delete mode 100644 docs/plugins-kotlin.mdx delete mode 100644 docs/standalone-host.mdx delete mode 100644 ktale-api/build.gradle.kts delete mode 100644 ktale-api/src/main/java/ktale/api/autoregister/AutoCommand.java delete mode 100644 ktale-api/src/main/java/ktale/api/autoregister/SubscribeEvent.java delete mode 100644 ktale-api/src/main/kotlin/ktale/api/KtalePlugin.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/PluginContext.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/commands/CommandContext.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/commands/CommandDefinition.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/commands/CommandRegistry.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/commands/CommandResult.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/commands/CommandSender.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/commands/Permission.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/config/ConfigCodec.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/config/ConfigKey.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/config/ConfigManager.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/config/ConfigMigration.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/entities/EntityRef.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/entities/HasModel.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/events/Cancellable.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/events/Event.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/events/EventBus.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/events/EventListener.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/events/EventPriority.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/events/EventSubscription.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/identity/Key.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/inventory/Container.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/inventory/ContainerKind.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/items/ItemStack.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/items/ItemType.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/logging/KtaleLogger.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/logging/LogLevel.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/prefabs/Prefab.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/prefabs/PrefabStore.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/scheduler/Scheduler.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/scheduler/SchedulerKotlin.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/scheduler/TaskHandle.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/services/ServiceRegistry.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/stats/Attribute.kt delete mode 100644 ktale-api/src/main/kotlin/ktale/api/stats/HasAttributes.kt delete mode 100644 ktale-core/build.gradle.kts delete mode 100644 ktale-core/src/main/kotlin/ktale/core/PluginContexts.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/autoregister/AutoRegistrar.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/commands/BridgedCommandRegistry.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/commands/CommandExecutor.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/commands/Commands.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/commands/SimpleCommandRegistry.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/config/ConfigTextStore.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/config/CoreConfigManager.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/config/FileConfigTextStore.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/config/InMemoryConfigTextStore.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/config/yaml/DefaultYaml.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/config/yaml/YamlConfigCodec.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/events/SimpleEventBus.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/logging/SimpleConsoleLogger.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/runtime/CoreRuntime.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/scheduler/HookBackedScheduler.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/services/SimpleServiceRegistry.kt delete mode 100644 ktale-core/src/main/kotlin/ktale/core/threading/ThreadGuards.kt delete mode 100644 ktale-core/src/test/kotlin/ktale/core/commands/BridgedCommandRegistryTest.kt delete mode 100644 ktale-core/src/test/kotlin/ktale/core/commands/SimpleCommandRegistryTest.kt delete mode 100644 ktale-core/src/test/kotlin/ktale/core/config/CoreConfigManagerYamlTest.kt delete mode 100644 ktale-core/src/test/kotlin/ktale/core/events/SimpleEventBusTest.kt delete mode 100644 ktale-core/src/test/kotlin/ktale/core/services/SimpleServiceRegistryTest.kt delete mode 100644 ktale-gradle-plugin/build.gradle.kts delete mode 100644 ktale-gradle-plugin/src/main/kotlin/cc/modlabs/ktale/gradle/KtaleDepsPlugin.kt delete mode 100644 ktale-platform-fake/build.gradle.kts delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/DeterministicClock.kt delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeCommandBridge.kt delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlatform.kt delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlayer.kt delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeSchedulerHooks.kt delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeServer.kt delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeTaskHandle.kt delete mode 100644 ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeWorld.kt delete mode 100644 ktale-platform-fake/src/test/java/ktale/examples/JavaExamplePluginUsage.java delete mode 100644 ktale-platform-fake/src/test/kotlin/ktale/examples/KotlinExamplePluginUsage.kt delete mode 100644 ktale-platform-fake/src/test/kotlin/ktale/platform/fake/AutoRegistrarSmokeTest.kt delete mode 100644 ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeSchedulerHooksTest.kt delete mode 100644 ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerEndToEndConfigSchedulerTest.kt delete mode 100644 ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerSmokeTest.kt delete mode 100644 ktale-platform-hytale/build.gradle.kts delete mode 100644 ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/CommandRoutingHypothesis.kt delete mode 100644 ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ServerLifecycleHypothesis.kt delete mode 100644 ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ThreadingHypothesis.kt delete mode 100644 ktale-platform-hytale/src/main/kotlin/ktale/platform/hytale/HytalePlatformPlaceholder.kt delete mode 100644 ktale-platform/build.gradle.kts delete mode 100644 ktale-platform/src/main/kotlin/ktale/platform/Platform.kt delete mode 100644 ktale-platform/src/main/kotlin/ktale/platform/PlatformClock.kt delete mode 100644 ktale-platform/src/main/kotlin/ktale/platform/PlatformCommandBridge.kt delete mode 100644 ktale-platform/src/main/kotlin/ktale/platform/PlatformLoggerFactory.kt delete mode 100644 ktale-platform/src/main/kotlin/ktale/platform/PlatformSchedulerHooks.kt delete mode 100644 ktale-runtime-deps/build.gradle.kts delete mode 100644 ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifest.kt delete mode 100644 ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifestReader.kt delete mode 100644 ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/MavenDependencyResolver.kt delete mode 100644 ktale-runtime-deps/src/test/kotlin/ktale/runtime/deps/DependencyManifestReaderTest.kt delete mode 100644 ktale-runtime-host/build.gradle.kts delete mode 100644 ktale-runtime-host/src/main/kotlin/ktale/runtime/host/JarPluginDescriptorReader.kt delete mode 100644 ktale-runtime-host/src/main/kotlin/ktale/runtime/host/PluginDescriptor.kt delete mode 100644 ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginClassLoaderFactory.kt delete mode 100644 ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginHost.kt delete mode 100644 ktale-runtime-host/src/test/kotlin/ktale/runtime/host/JarPluginDescriptorReaderTest.kt diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 0949d9e..2f15741 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -16,3 +16,4 @@ steps: - ./gradlew build --stacktrace --no-daemon + diff --git a/build.gradle.kts b/build.gradle.kts index 5282252..45a7b22 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,114 +1,44 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.testing.Test -import java.util.Calendar -import java.util.TimeZone - plugins { - // Root is an aggregator. Subprojects apply Kotlin + publishing. - kotlin("jvm") version "2.2.21" apply false - kotlin("plugin.serialization") version "2.2.21" apply false - id("org.jetbrains.dokka") version "2.0.0" + // Match real plugin projects: allow Kotlin version to be controlled externally (e.g. via gradle.properties systemProp.kotlin_version). + val kotlin_version: String by System.getProperties() + kotlin("jvm").version(kotlin_version) + `java-library` } -allprojects { - group = "cc.modlabs" - version = System.getenv("VERSION_OVERRIDE") ?: Calendar.getInstance(TimeZone.getTimeZone("UTC")).run { - "${get(Calendar.YEAR)}.${get(Calendar.MONTH) + 1}.${get(Calendar.DAY_OF_MONTH)}.${ - String.format("%02d%02d", get(Calendar.HOUR_OF_DAY), get(Calendar.MINUTE)) - }" - } +group = "cc.modlabs" +version = System.getenv("VERSION_OVERRIDE") ?: "1.0-SNAPSHOT" - repositories { - maven("https://nexus.modlabs.cc/repository/maven-mirrors/") - } +repositories { + maven("https://nexus.modlabs.cc/repository/maven-mirrors/") } -subprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") - apply(plugin = "java-library") - apply(plugin = "maven-publish") - apply(plugin = "org.jetbrains.dokka") - // Root build only wires compilation + testing. - // Publishing is configured per-module later (kept out of root to avoid Gradle plugin ordering pitfalls). - - dependencies { - add("testImplementation", kotlin("test")) - add("testImplementation", "io.kotest:kotest-runner-junit5:5.9.1") - add("testImplementation", "io.kotest:kotest-assertions-core:5.9.1") - add("testImplementation", "io.mockk:mockk:1.13.14") - } - - tasks.withType { - options.encoding = "UTF-8" - options.release.set(21) - } +dependencies { + // Provided by the Hytale server runtime; we only compile against it. + compileOnly("com.hypixel.hytale:Server:2026.01.13-dcad8778f") - tasks.withType { - compilerOptions.jvmTarget.set(JvmTarget.JVM_21) - } - - extensions.configure { - jvmToolchain(21) - } + testImplementation(kotlin("test")) + testImplementation("io.kotest:kotest-runner-junit5:5.9.1") + testImplementation("io.kotest:kotest-assertions-core:5.9.1") + testImplementation("io.mockk:mockk:1.13.14") +} - tasks.withType().configureEach { - useJUnitPlatform() - } +tasks.test { + useJUnitPlatform() +} - // Publishing: keep it CI-safe by always enabling `mavenLocal()`, and only adding remote repos when creds exist. - extensions.configure { - withSourcesJar() +kotlin { + jvmToolchain(25) + compilerOptions { + // Kotlin's supported JVM targets lag behind Java toolchains. Use string target for maximum compatibility. + // If your Kotlin version supports 25, this will work; otherwise set systemProp.ktale_jvm_target (e.g. 21). + val target: String = (System.getProperty("ktale_jvm_target") ?: "25").trim() + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(target)) } +} - extensions.configure { - repositories { - mavenLocal() - val user = System.getenv("NEXUS_USER") - val pass = System.getenv("NEXUS_PASS") - if (!user.isNullOrBlank() && !pass.isNullOrBlank()) { - maven { - name = "ModLabs" - url = uri("https://nexus.modlabs.cc/repository/maven-public/") - credentials { - username = user - password = pass - } - } - } - } - - publications { - create("maven") { - from(components["java"]) - pom { - name.set("KTale") - description.set("A speculative, adapter-based Kotlin server SDK foundation for Hytale (Day-1 oriented).") - url.set("https://github.com/ModLabsCC/ktale") - licenses { - license { - name.set("GPL-3.0") - url.set("https://github.com/ModLabsCC/ktale/blob/main/LICENSE") - } - } - developers { - developer { - id.set("ModLabsCC") - name.set("ModLabsCC") - email.set("contact@modlabs.cc") - } - } - scm { - connection.set("scm:git:git://github.com/ModLabsCC/ktale.git") - developerConnection.set("scm:git:git@github.com:ModLabsCC/ktale.git") - url.set("https://github.com/ModLabsCC/ktale") - } - } - } - } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) } + withSourcesJar() } \ No newline at end of file diff --git a/docs/auto-registration.mdx b/docs/auto-registration.mdx deleted file mode 100644 index 5004b11..0000000 --- a/docs/auto-registration.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Auto-registration ---- - -Auto-registration is a **plug-and-play convenience** primarily for the standalone host model. - -## Commands - -Annotate command classes with `@AutoCommand`: - -```java -@AutoCommand -public final class Ping implements CommandDefinition { - public String getName() { return "ping"; } - public java.util.Set getAliases() { return java.util.Collections.emptySet(); } - public String getDescription() { return null; } - public Permission getPermission() { return null; } - public CommandResult execute(CommandContext ctx) { return CommandResult.Success.INSTANCE; } -} -``` - -## Events - -Annotate listener methods with `@SubscribeEvent`: - -```java -public final class Listeners { - @SubscribeEvent(MyEvent.class) - public void on(MyEvent e) { } -} -``` - -## What performs scanning? - -- `ktale-core` provides `ktale.core.autoregister.AutoRegistrar` -- `ktale-runtime-host` calls it automatically during plugin enable - -This feature is optional and host-specific; platform adapters may implement a different strategy. - - diff --git a/docs/commands.mdx b/docs/commands.mdx deleted file mode 100644 index 730b3e8..0000000 --- a/docs/commands.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Commands ---- - -## Contracts - -- `CommandRegistry` is logic-only (no IO). -- Platforms/hosts bridge inbound command text to `CommandContext`. - -## Define commands (Kotlin DSL) - -```kotlin -import ktale.core.commands.Commands -import ktale.api.commands.CommandResult - -context.commands.register( - Commands.command("ping") { - aliases("p") - execute { CommandResult.Success } - } -) -``` - -## Define commands (Java fluent) - -```java -ctx.getCommands().register( - Commands.command("ping") - .aliases("p") - .executor(c -> CommandResult.Success.INSTANCE) - .build() -); -``` - -## Auto-registration (standalone host) - -If using the standalone host, annotate command classes: - -```java -@AutoCommand -public final class Ping implements CommandDefinition { - public String getName() { return "ping"; } - public java.util.Set getAliases() { return java.util.Collections.emptySet(); } - public String getDescription() { return null; } - public Permission getPermission() { return null; } - public CommandResult execute(CommandContext ctx) { return CommandResult.Success.INSTANCE; } -} -``` - -See [Auto-registration](./auto-registration.mdx). - - diff --git a/docs/configs.mdx b/docs/configs.mdx deleted file mode 100644 index f0ea301..0000000 --- a/docs/configs.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Configs ---- - -## Contracts - -- `ConfigManager` loads/saves typed configs based on `ConfigKey` -- migrations are explicit and versioned (`ConfigMigration`) -- storage is host/platform-specific - -## Core default - -`ktale-core` provides a format-agnostic `CoreConfigManager` that: - -- stores text with a small header `ktaleConfigVersion: ` -- applies migrations to raw text before decoding - -### YAML helper - -`ktale-core` includes `YamlConfigCodec` (kotlinx.serialization + Kaml). - -If you need a different format (TOML/JSON/Jackson), provide your own `ConfigCodec`. - - diff --git a/docs/dependency-resolution.mdx b/docs/dependency-resolution.mdx deleted file mode 100644 index ad937ad..0000000 --- a/docs/dependency-resolution.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Runtime dependency resolution ---- - -This doc applies only if KTale is used as a **standalone host runtime** (bundled server model). - -## Why - -- plugins stay clean (no shading) -- dependencies are downloaded into a cache at runtime - -## Manifest format - -### `.dependencies` - -One Maven coordinate per line: - -``` -com.foo:bar:1.2.3 -``` - -### `.repositories` - -- unauthenticated: - -``` -id https://repo.example.com/maven/ -``` - -- authenticated (env var names only; secrets not stored in jars): - -``` -id https://repo.example.com/maven/ REPO_USER_ENV REPO_PASS_ENV -``` - - diff --git a/docs/dokka-and-pages.mdx b/docs/dokka-and-pages.mdx deleted file mode 100644 index ef1db20..0000000 --- a/docs/dokka-and-pages.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Dokka + GitHub Pages ---- - -KTale generates API docs via Dokka and publishes them to GitHub Pages. - -## Local build - -```bash -./gradlew dokkaHtmlMultiModule -``` - -Output: - -- `build/dokka/htmlMultiModule` - -## GitHub Actions - -The workflow lives at: - -- `.github/workflows/docs.yml` - -It: - -- runs `./gradlew dokkaHtmlMultiModule` -- uploads `build/dokka/htmlMultiModule` -- deploys to GitHub Pages - -You must enable Pages once in repo settings: - -- **Settings → Pages → Source: GitHub Actions** - - diff --git a/docs/events.mdx b/docs/events.mdx deleted file mode 100644 index 4805208..0000000 --- a/docs/events.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Events ---- - -## Contracts - -- `EventBus` is synchronous and deterministic by default (in `ktale-core`). -- Events are plain objects implementing `ktale.api.events.Event`. -- Cancellation is capability-based (`Cancellable`). - -## Subscribe (Kotlin) - -```kotlin -context.events.subscribe { e -> - // ... -} -``` - -## Subscribe (Java) - -```java -ctx.getEvents().subscribe(MyEvent.class, EventPriority.NORMAL, (EventListener) e -> { - // ... -}); -``` - -## Auto-registration (standalone host) - -If using the standalone host, you can annotate methods with `@SubscribeEvent` and let the host auto-register: - -```java -public final class MyListeners { - @SubscribeEvent(value = MyEvent.class, priority = EventPriority.NORMAL) - public void on(MyEvent e) { } -} -``` - -See [Auto-registration](./auto-registration.mdx). - - diff --git a/docs/fake-platform-testing.mdx b/docs/fake-platform-testing.mdx deleted file mode 100644 index 8c0612d..0000000 --- a/docs/fake-platform-testing.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Fake platform & testing ---- - -KTale ships a deterministic fake platform so you can test plugins without any real server: - -- deterministic clock -- controllable scheduler execution -- simulated command dispatch -- event simulation via `EventBus` - -## Example (Kotlin) - -```kotlin -import ktale.platform.fake.FakeServer -import ktale.api.KtalePlugin -import ktale.api.PluginContext - -val server = FakeServer() - -val plugin = object : KtalePlugin { - override fun onLoad(context: PluginContext) {} - override fun onEnable(context: PluginContext) { /* register stuff */ } - override fun onDisable(context: PluginContext) {} -} - -server.runPlugin("demo", plugin) { ctx -> - // drive fake scheduler or dispatch commands here -} -``` - - diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx deleted file mode 100644 index e83fb57..0000000 --- a/docs/getting-started.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Getting started ---- - -## Install (Gradle) - -KTale is a multi-module library. Most plugins will depend on: - -- `ktale-api` (interfaces/contracts) -- `ktale-core` (default implementations) - -Example (Kotlin DSL): - -```kotlin -dependencies { - implementation("cc.modlabs:ktale-api:") - implementation("cc.modlabs:ktale-core:") -} -``` - -## Gradle plugin repositories (if using `cc.modlabs.ktale-deps`) - -If you use the KTale Gradle plugin (`cc.modlabs.ktale-deps`), ensure your build can resolve it: - -```kotlin -// settings.gradle.kts -pluginManagement { - repositories { - gradlePluginPortal() - maven("https://nexus.modlabs.cc/repository/maven-public/") - mavenLocal() - } -} -``` - -## Your first plugin - -KTale plugin entrypoint is `ktale.api.KtalePlugin`: - -```kotlin -import ktale.api.KtalePlugin -import ktale.api.PluginContext - -class MyPlugin : KtalePlugin { - override fun onLoad(context: PluginContext) {} - override fun onEnable(context: PluginContext) {} - override fun onDisable(context: PluginContext) {} -} -``` - -### Java plugins - -All major contracts are Java-first: - -- schedulers use `Runnable` + `java.time.Duration` -- event subscription uses `Class` + `EventListener` -- services use `Class` for lookup/registration - -See [Java usage](./plugins-java.mdx). - - diff --git a/docs/gradle-plugin.mdx b/docs/gradle-plugin.mdx deleted file mode 100644 index e996e6f..0000000 --- a/docs/gradle-plugin.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Gradle plugin (dependency manifest) ---- - -KTale includes a Gradle plugin that writes dependency manifests into your jar so a standalone host can download them. - -## Plugin id - -`cc.modlabs.ktale-deps` - -## Usage (Gradle Kotlin DSL) - -### Resolve the plugin - -Add the ModLabs repo to `pluginManagement`: - -```kotlin -// settings.gradle.kts -pluginManagement { - repositories { - gradlePluginPortal() - maven("https://nexus.modlabs.cc/repository/maven-public/") - mavenLocal() - } -} -``` - -### Configure `ktaleDeps` - -```kotlin -plugins { - id("cc.modlabs.ktale-deps") -} - -ktaleDeps { - // extra deps to record - deliver("com.foo:bar:1.2.3") - - // extra repos - repository("jitpack", "https://jitpack.io/") - - // repo with auth (env var names only) - repositoryWithAuth("private", "https://repo.example.com/maven/", "REPO_USER", "REPO_PASS") - - // optional: standalone host descriptor - pluginId.set("my-plugin") - mainClass.set("com.example.MyPlugin") -} -``` - -## Files emitted into the jar - -- `.dependencies` -- optional `.repositories` -- optional `ktale-plugin.properties` (if `pluginId` + `mainClass` are set) - - diff --git a/docs/index.mdx b/docs/index.mdx deleted file mode 100644 index ebb37ff..0000000 --- a/docs/index.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: KTale Documentation -description: Day-1-ready, speculative Kotlin server SDK foundation for Hytale ---- - -## What KTale is - -KTale is a **speculative** Kotlin server SDK foundation for Hytale. The Hytale server API does not exist (for us) yet, so KTale is designed around: - -- **Adapter-based architecture**: stable core + replaceable platform adapters -- **Capabilities over inheritance**: small, composable interfaces -- **Testability without a real server**: a deterministic fake platform -- **Honest uncertainty**: anything speculative is isolated and removable - -## Where to start - -- [Plugin developer workflow (end-to-end)](./plugin-developer-workflow.mdx) -- [Getting started](./getting-started.mdx) -- [Modules](./modules.mdx) -- [Events](./events.mdx) -- [Commands](./commands.mdx) -- [Configs](./configs.mdx) -- [Fake platform & testing](./fake-platform-testing.mdx) -- [Standalone host (bundled server model)](./standalone-host.mdx) -- [Gradle plugin: dependency manifest](./gradle-plugin.mdx) -- [Auto-registration](./auto-registration.mdx) -- [Dokka + GitHub Pages](./dokka-and-pages.mdx) - - diff --git a/docs/modules.mdx b/docs/modules.mdx deleted file mode 100644 index 12b1382..0000000 --- a/docs/modules.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Modules ---- - -KTale is a Kotlin multi-module project: - -## `ktale-api` - -Stable public **contracts only**: - -- plugin lifecycle (`KtalePlugin`) -- events (`EventBus`) -- scheduler (`Scheduler`) -- commands (`CommandRegistry`) -- configs (`ConfigManager`) -- logging (`KtaleLogger`) -- service registry (`ServiceRegistry`) -- capability contracts like models/prefabs/attributes/containers (keyed, not enum-based) - -## `ktale-core` - -Minimal default implementations (platform-neutral): - -- `SimpleEventBus` -- `SimpleCommandRegistry` + `Commands` builder -- `CoreConfigManager` (+ YAML codec helpers) -- `SimpleServiceRegistry` -- `CoreRuntime` wiring helper -- `AutoRegistrar` (optional convenience) - -## `ktale-platform` - -Platform boundary interfaces (no game logic): - -- `Platform`, `PlatformClock`, `PlatformSchedulerHooks`, `PlatformCommandBridge`, logger factory - -## `ktale-platform-fake` - -Deterministic fake platform for tests and demos: - -- `FakeServer`, `FakePlatform` -- deterministic clock + controllable scheduler -- command bridge for inbound dispatch simulation - -## `ktale-platform-hytale` - -Placeholder module: - -- `HytalePlatformPlaceholder` (fails loudly; TODO-only) -- `ktale.experimental.hypothesis` package (explicit speculation) - -## `ktale-runtime-deps` - -Runtime dependency manifest + Maven resolver utilities for the “standalone host” model. - -## `ktale-runtime-host` - -Standalone host utilities that can load plugin jars and build isolated classloaders. - -## `ktale-gradle-plugin` - -Gradle plugin that writes `.dependencies` / `.repositories` (and optionally `ktale-plugin.properties`) into jars. - - diff --git a/docs/plugin-developer-workflow.mdx b/docs/plugin-developer-workflow.mdx deleted file mode 100644 index 4d7aa54..0000000 --- a/docs/plugin-developer-workflow.mdx +++ /dev/null @@ -1,233 +0,0 @@ ---- -title: Plugin developer workflow (end-to-end) ---- - -This guide is a **full workflow** for KTale plugin developers: - -1) install dependencies -2) create an entrypoint -3) register commands + listeners (manual or plug-and-play) -4) use features (prefabs, models, attributes, containers, items) -5) test everything without a real server -6) optional: package for the standalone host model (dependency manifest + runtime resolution) - -## 0) Choose your host model (important) - -KTale supports two host models: - -- **Plugin/mod SDK hosted by an official server runtime** (future) -- **Bundled/standalone server distribution that embeds KTale** (available today via `ktale-runtime-host`) - -This guide shows both. If you are targeting a future official server, focus on `ktale-api` + `ktale-core` + `ktale-platform-*` and skip the “standalone host” sections. - -## 1) Install (Gradle) - -Most plugins depend on: - -- `ktale-api` -- `ktale-core` - -Example (Gradle Kotlin DSL): - -```kotlin -dependencies { - implementation("cc.modlabs:ktale-api:") - implementation("cc.modlabs:ktale-core:") -} -``` - -### Optional (standalone host packaging) - -If you want a clean, unshaded jar and let the standalone host download deps: - -#### Resolve the Gradle plugin - -```kotlin -// settings.gradle.kts -pluginManagement { - repositories { - gradlePluginPortal() - maven("https://nexus.modlabs.cc/repository/maven-public/") - mavenLocal() - } -} -``` - -```kotlin -plugins { - id("cc.modlabs.ktale-deps") -} - -ktaleDeps { - // Standalone host descriptor (required for ktale-runtime-host) - pluginId.set("my-plugin") - mainClass.set("com.example.MyPlugin") - - // Extra dependencies to record (in addition to first-level runtime deps) - deliver("com.foo:bar:1.2.3") - - // Extra repos - repository("jitpack", "https://jitpack.io/") - - // Repo with auth using env var names (secrets are NOT stored in the jar) - repositoryWithAuth("private", "https://repo.example.com/maven/", "REPO_USER", "REPO_PASS") -} -``` - -This emits into your jar: - -- `.dependencies` -- optional `.repositories` -- `ktale-plugin.properties` - -## 2) Create a plugin entrypoint - -Implement `ktale.api.KtalePlugin`: - -```kotlin -import ktale.api.KtalePlugin -import ktale.api.PluginContext - -class MyPlugin : KtalePlugin { - override fun onLoad(context: PluginContext) {} - override fun onEnable(context: PluginContext) {} - override fun onDisable(context: PluginContext) {} -} -``` - -## 3) Commands - -### Kotlin (DSL) - -```kotlin -import ktale.core.commands.Commands -import ktale.api.commands.CommandResult - -context.commands.register( - Commands.command("ping") { - aliases("p") - execute { CommandResult.Success } - } -) -``` - -### Java (fluent) - -```java -ctx.getCommands().register( - Commands.command("ping") - .aliases("p") - .executor(c -> CommandResult.Success.INSTANCE) - .build() -); -``` - -### Plug-and-play (standalone host) - -If you use the standalone host, you can avoid manual registration: - -```java -@AutoCommand -public final class Ping implements CommandDefinition { - public String getName() { return "ping"; } - public java.util.Set getAliases() { return java.util.Collections.emptySet(); } - public String getDescription() { return null; } - public Permission getPermission() { return null; } - public CommandResult execute(CommandContext ctx) { return CommandResult.Success.INSTANCE; } -} -``` - -The host scans and registers these automatically. - -## 4) Events / listeners - -### Manual subscription - -Kotlin: - -```kotlin -import ktale.api.events.subscribe - -context.events.subscribe { e -> - context.logger.info("got event") -} -``` - -Java: - -```java -ctx.getEvents().subscribe(MyEvent.class, EventPriority.NORMAL, (EventListener) e -> { - ctx.getLogger().info("got event"); -}); -``` - -### Plug-and-play listeners (standalone host) - -```java -public final class MyListeners { - @SubscribeEvent(MyEvent.class) - public void on(MyEvent e) { - // ... - } -} -``` - -## 5) Prefabs - -Prefabs are **host-dependent** and exposed as an optional capability: - -```kotlin -val store = context.prefabs -if (store != null) { - val ids = store.list() - // load/save/delete prefabs by Key -} -``` - -The standalone host currently does not provide a prefab store by default (it can be added later as a platform capability). - -## 6) Models, attributes, containers, items (capabilities) - -KTale models these features without committing to a specific server API: - -- **IDs are opaque strings** via `ktale.api.identity.Key` - - Example ID: `Cloth_Block_Wool_Blue` - - KTale preserves the string verbatim. -- **Models** via `HasModel` (keyed) -- **Stats** via `HasAttributes` / `Attribute` (health/mana are expected keys) -- **Containers** via `Container` / `ContainerKind` (equipment/backpack/brewery bag are kinds) -- **Items** via `ItemStack` (type is a `Key`) - -## 7) Testing without a real server - -Use the deterministic fake platform: - -```kotlin -import ktale.platform.fake.FakeServer - -val server = FakeServer() -val ctx = server.createContext("demo") - -// register commands/events/schedules -// advance clock / run scheduler deterministically -``` - -This is how you keep plugin logic testable even before a real Hytale server exists. - -## 8) Standalone host run (optional) - -If you’re experimenting with KTale as a **bundled server distribution**, the standalone host can load jars and auto-register: - -- reads `ktale-plugin.properties` -- resolves `.dependencies` / `.repositories` - - supports repo auth via env var names -- builds an isolated classloader -- runs lifecycle and auto-registration - -See: - -- [Standalone host](./standalone-host.mdx) -- [Dependency resolution](./dependency-resolution.mdx) -- [Auto-registration](./auto-registration.mdx) - - diff --git a/docs/plugins-java.mdx b/docs/plugins-java.mdx deleted file mode 100644 index 0d34038..0000000 --- a/docs/plugins-java.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Java plugin usage ---- - -KTale is designed so Java plugins can use the core APIs without Kotlin-only types. - -## Example usage - -```java -import ktale.api.PluginContext; -import ktale.api.commands.CommandContext; -import ktale.api.commands.CommandResult; -import ktale.api.events.EventListener; -import ktale.api.events.EventPriority; -import ktale.core.commands.Commands; - -import java.time.Duration; - -public final class JavaUsage { - public static void demo(PluginContext ctx) { - // Services - ctx.getServices().register(String.class, "hello"); - - // Events (Class + EventListener) - ctx.getEvents().subscribe(MyEvent.class, EventPriority.NORMAL, (EventListener) e -> - ctx.getLogger().info("event!") - ); - - // Commands (fluent builder in ktale-core) - ctx.getCommands().register( - Commands.command("ping") - .executor((CommandContext c) -> CommandResult.Success.INSTANCE) - .build() - ); - - // Scheduler (Runnable + java.time.Duration) - ctx.getScheduler().runSyncDelayed(Duration.ofMillis(250), () -> ctx.getLogger().info("tick")); - } - - public static final class MyEvent implements ktale.api.events.Event {} -} -``` - - diff --git a/docs/plugins-kotlin.mdx b/docs/plugins-kotlin.mdx deleted file mode 100644 index 888841e..0000000 --- a/docs/plugins-kotlin.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Kotlin plugin usage ---- - -## Core idea - -Plugins receive a `PluginContext` and interact only with capabilities: - -```kotlin -import ktale.api.KtalePlugin -import ktale.api.PluginContext -import ktale.api.events.subscribe -import ktale.core.commands.Commands -import java.time.Duration - -class DemoPlugin : KtalePlugin { - override fun onLoad(context: PluginContext) {} - - override fun onEnable(context: PluginContext) { - // Events - context.events.subscribe { context.logger.info("event!") } - - // Commands - context.commands.register( - Commands.command("ping") { - execute { CommandResult.Success } - } - ) - - // Scheduler - context.scheduler.runSyncDelayed(Duration.ofSeconds(1)) { - context.logger.info("tick") - } - } - - override fun onDisable(context: PluginContext) {} -} -``` - - diff --git a/docs/standalone-host.mdx b/docs/standalone-host.mdx deleted file mode 100644 index 2430663..0000000 --- a/docs/standalone-host.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Standalone host (bundled server model) ---- - -KTale supports an alternate “host model” where KTale is part of a **bundled/custom server distribution**. -In this model, plugins can remain **unshaded** and the host downloads dependencies at runtime. - -## Plugin packaging - -Standalone host expects plugin jars to contain: - -- `ktale-plugin.properties` - - `id=` - - `main=` (implements `ktale.api.KtalePlugin`) -- optional `.dependencies` / `.repositories` - -## Runtime pieces - -- `ktale-runtime-deps`: reads manifests + resolves Maven deps into a cache -- `ktale-runtime-host`: loads jars, builds isolated classloaders, instantiates `KtalePlugin` - -## Auto-registration - -Standalone host enables plug-and-play: - -- `@AutoCommand` on `CommandDefinition` classes -- `@SubscribeEvent` on listener methods - -See [Auto-registration](./auto-registration.mdx). - - diff --git a/gradle.properties b/gradle.properties index 36704cb..b243a32 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,10 @@ kotlin.code.style=official -org.gradle.configuration-cache=true \ No newline at end of file +org.gradle.configuration-cache=true + +# Required by build.gradle.kts (mirrors your plugin build convention). +# You can override with: ./gradlew -Dkotlin_version=... +systemProp.kotlin_version=2.2.21 + +# Kotlin may not support JVM target 25 yet, even if you compile with JDK 25. +# Set to 25 when your Kotlin version supports it; otherwise keep at 21. +systemProp.ktale_jvm_target=21 \ No newline at end of file diff --git a/ktale-api/build.gradle.kts b/ktale-api/build.gradle.kts deleted file mode 100644 index eaa0da3..0000000 --- a/ktale-api/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -description = "KTale stable public contracts (interfaces only)." - -dependencies { - // No implementations in this module by design. -} - - diff --git a/ktale-api/src/main/java/ktale/api/autoregister/AutoCommand.java b/ktale-api/src/main/java/ktale/api/autoregister/AutoCommand.java deleted file mode 100644 index 553ecbe..0000000 --- a/ktale-api/src/main/java/ktale/api/autoregister/AutoCommand.java +++ /dev/null @@ -1,18 +0,0 @@ -package ktale.api.autoregister; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a {@code ktale.api.commands.CommandDefinition} class as eligible for auto-registration. - * - *

Standalone hosts can discover these classes in a plugin jar and register them without manual wiring. - * The class must have a public no-arg constructor.

- */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface AutoCommand {} - - diff --git a/ktale-api/src/main/java/ktale/api/autoregister/SubscribeEvent.java b/ktale-api/src/main/java/ktale/api/autoregister/SubscribeEvent.java deleted file mode 100644 index 0bf5521..0000000 --- a/ktale-api/src/main/java/ktale/api/autoregister/SubscribeEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -package ktale.api.autoregister; - -import ktale.api.events.Event; -import ktale.api.events.EventPriority; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as an event subscriber for auto-registration. - * - *

Host runtimes may scan plugin jars and register these methods automatically. - * This annotation is intentionally in Java so both Java and Kotlin can use it cleanly.

- */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface SubscribeEvent { - Class value(); - EventPriority priority() default EventPriority.NORMAL; - boolean ignoreCancelled() default false; -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/KtalePlugin.kt b/ktale-api/src/main/kotlin/ktale/api/KtalePlugin.kt deleted file mode 100644 index 8095911..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/KtalePlugin.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ktale.api - -/** - * A KTale plugin entrypoint. - * - * ## Design note (intentional constraint) - * KTale treats "unknown server APIs" as a first-class constraint. - * - * This interface is intentionally small and stable: - * - Platform adapters provide a [PluginContext]. - * - Plugin code talks to contracts in `ktale-api`, not to platform types. - * - No assumptions are made about threading, tick loops, or IO models. - */ -public interface KtalePlugin { - /** - * Called when the plugin is discovered and constructed, before it is enabled. - * - * This phase is for wiring services and reading static metadata. - * Avoid registering listeners that assume a running server. - */ - public fun onLoad(context: PluginContext) - - /** - * Called when the plugin becomes active. - * - * This phase is for registering listeners, commands, and starting scheduled tasks. - */ - public fun onEnable(context: PluginContext) - - /** - * Called when the plugin is being disabled or unloaded. - * - * Implementations should cancel tasks and release resources. - */ - public fun onDisable(context: PluginContext) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/PluginContext.kt b/ktale-api/src/main/kotlin/ktale/api/PluginContext.kt deleted file mode 100644 index 0bdcb57..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/PluginContext.kt +++ /dev/null @@ -1,55 +0,0 @@ -package ktale.api - -import ktale.api.commands.CommandRegistry -import ktale.api.config.ConfigManager -import ktale.api.events.EventBus -import ktale.api.inventory.Container -import ktale.api.logging.KtaleLogger -import ktale.api.prefabs.PrefabStore -import ktale.api.scheduler.Scheduler -import ktale.api.services.ServiceRegistry - -/** - * Per-plugin access to platform-provided facilities. - * - * ## Stability rules - * - This is the *only* object a plugin needs to keep around. - * - It is platform-agnostic: implementations live in platform adapters. - * - It is intentionally capability-oriented instead of exposing a giant "Server" object. - */ -public interface PluginContext { - /** A human-readable plugin identifier (stable across reloads). */ - public val pluginId: String - - /** A logger scoped to this plugin. */ - public val logger: KtaleLogger - - /** Event publishing and subscription. */ - public val events: EventBus - - /** Scheduling API for sync/async/delayed/repeating work. */ - public val scheduler: Scheduler - - /** Command registration and dispatch (logic only; IO bridges are platform-specific). */ - public val commands: CommandRegistry - - /** Typed configuration access with versioned migrations. */ - public val configs: ConfigManager - - /** - * Service registry used as "DI-light". - * - * This is intentionally runtime-based (not compile-time DI) to remain adaptable - * to unknown server/container lifecycles. - */ - public val services: ServiceRegistry - - /** - * Prefab storage/hosting, if the platform supports it. - * - * Platforms that do not expose prefabs can return `null`. - */ - public val prefabs: PrefabStore? -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/commands/CommandContext.kt b/ktale-api/src/main/kotlin/ktale/api/commands/CommandContext.kt deleted file mode 100644 index bd74613..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/commands/CommandContext.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ktale.api.commands - -/** - * Context for a command execution. - * - * ## Design note - * Parsing is intentionally not part of this contract. - * A platform adapter may provide only tokenized args, or pre-parsed structured args. - */ -public interface CommandContext { - /** Sender of the command. */ - public val sender: CommandSender - - /** The label used to invoke the command (may be an alias). */ - public val label: String - - /** Tokenized arguments after the label (no quoting guarantees). */ - public val args: List -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/commands/CommandDefinition.kt b/ktale-api/src/main/kotlin/ktale/api/commands/CommandDefinition.kt deleted file mode 100644 index 402ae6e..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/commands/CommandDefinition.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ktale.api.commands - -/** - * Declarative command definition. - * - * ## Design note - * This is a *pure definition type* (no IO). - * A DSL-friendly builder is expected to live in `ktale-core`, but this contract is shaped - * so that builders can target it without depending on any platform details. - */ -public interface CommandDefinition { - /** Primary name for the command (as registered). */ - public val name: String - - /** Alternative names that map to this definition. */ - public val aliases: Set - - /** Human-readable description. */ - public val description: String? - - /** Permission required to execute this command, if any. */ - public val permission: Permission? - - /** Executes the command. */ - public fun execute(context: CommandContext): CommandResult -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/commands/CommandRegistry.kt b/ktale-api/src/main/kotlin/ktale/api/commands/CommandRegistry.kt deleted file mode 100644 index b0c45a8..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/commands/CommandRegistry.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ktale.api.commands - -/** - * Registry for command definitions. - * - * Implementations must not perform IO directly; platform adapters bridge registration and input. - */ -public interface CommandRegistry { - /** - * Registers [definition]. - * - * @throws IllegalArgumentException if the command name or alias is invalid or conflicts. - */ - public fun register(definition: CommandDefinition) - - /** - * Unregisters a command by its primary name. - * - * Implementations should also remove aliases that point to the definition. - */ - public fun unregister(name: String) - - /** - * Dispatches a command execution. - * - * Platform adapters typically call this after receiving user input, providing a [context] - * that contains tokens and sender info. - */ - public fun dispatch(context: CommandContext): CommandResult -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/commands/CommandResult.kt b/ktale-api/src/main/kotlin/ktale/api/commands/CommandResult.kt deleted file mode 100644 index b5f95c1..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/commands/CommandResult.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ktale.api.commands - -/** - * Result of executing a command. - * - * This is intentionally lightweight and does not prescribe a UX. - * Platforms may map these results to their own messaging conventions. - */ -public sealed interface CommandResult { - /** Indicates success. */ - public data object Success : CommandResult - - /** - * Indicates failure with a message suitable for end users. - * - * Platforms may choose to hide or transform messages. - */ - public interface Failure : CommandResult { - public val message: String - } - - /** Sender lacks required permission. */ - public data object NoPermission : Failure { - override val message: String = "You do not have permission to use that command." - } - - /** Provided arguments are invalid for the chosen command route. */ - public interface UsageError : Failure -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/commands/CommandSender.kt b/ktale-api/src/main/kotlin/ktale/api/commands/CommandSender.kt deleted file mode 100644 index 84f20ce..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/commands/CommandSender.kt +++ /dev/null @@ -1,23 +0,0 @@ -package ktale.api.commands - -/** - * Entity that can execute commands (e.g. player, console, remote admin, script). - * - * KTale intentionally models this as a capability surface, not as a concrete actor hierarchy. - */ -public interface CommandSender { - /** Display name of the sender. */ - public val name: String - - /** Sends a message to the sender. */ - public fun sendMessage(message: String) - - /** - * Checks whether the sender has [permission]. - * - * Platforms decide whether permissions are hierarchical, wildcard-based, etc. - */ - public fun hasPermission(permission: Permission): Boolean -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/commands/Permission.kt b/ktale-api/src/main/kotlin/ktale/api/commands/Permission.kt deleted file mode 100644 index 9acaae2..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/commands/Permission.kt +++ /dev/null @@ -1,23 +0,0 @@ -package ktale.api.commands - -/** - * Abstract permission identifier. - * - * KTale does not assume any specific permission engine. A platform adapter decides how - * permissions are checked and represented. - */ -public class Permission public constructor(public val value: String) { - override fun toString(): String = value - - override fun equals(other: Any?): Boolean = other is Permission && other.value == value - - override fun hashCode(): Int = value.hashCode() - - public companion object { - /** Java-friendly factory. */ - @JvmStatic - public fun of(value: String): Permission = Permission(value) - } -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/config/ConfigCodec.kt b/ktale-api/src/main/kotlin/ktale/api/config/ConfigCodec.kt deleted file mode 100644 index aee106e..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/config/ConfigCodec.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ktale.api.config - -/** - * Encodes and decodes a typed configuration object. - * - * ## Design note - * The codec is separated from the store to avoid committing KTale to a specific format (YAML/TOML/JSON), - * while still enabling typed config objects. - * - * Codecs can be replaced without changing plugin code that uses [ConfigKey]. - */ -public interface ConfigCodec { - /** Parses [text] into a typed config object. */ - public fun decode(text: String): T - - /** Serializes [value] into text suitable for storage. */ - public fun encode(value: T): String -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/config/ConfigKey.kt b/ktale-api/src/main/kotlin/ktale/api/config/ConfigKey.kt deleted file mode 100644 index b73ddb8..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/config/ConfigKey.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ktale.api.config - -/** - * Typed handle for a configuration file/document. - * - * @param T strongly-typed config object type - */ -public interface ConfigKey { - /** - * Stable identifier for the config (often a filename without extension). - * - * Platform adapters may map this to a file, database row, or other store. - */ - public val id: String - - /** Current schema version for this config. */ - public val version: Int - - /** Codec used to parse and serialize this config. */ - public val codec: ConfigCodec - - /** Default value used when config is missing or cannot be loaded. */ - public fun defaultValue(): T - - /** - * Migrations to apply when stored config version is older than [version]. - * - * Implementations should list migrations in ascending order. - */ - public val migrations: List -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/config/ConfigManager.kt b/ktale-api/src/main/kotlin/ktale/api/config/ConfigManager.kt deleted file mode 100644 index d59df90..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/config/ConfigManager.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ktale.api.config - -/** - * Typed configuration access. - * - * Implementations are responsible for: - * - locating storage - * - applying [ConfigMigration] steps - * - decoding via [ConfigCodec] - * - * The core module provides a default file-backed implementation; platforms may override. - */ -public interface ConfigManager { - /** - * Loads a configuration for [key], applying migrations if needed. - * - * Implementations should be resilient: when loading fails, return [ConfigKey.defaultValue] - * and log a diagnostic (platform-defined logging). - */ - public fun load(key: ConfigKey): T - - /** Saves [value] for [key] using [ConfigKey.codec]. */ - public fun save(key: ConfigKey, value: T) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/config/ConfigMigration.kt b/ktale-api/src/main/kotlin/ktale/api/config/ConfigMigration.kt deleted file mode 100644 index 9a264e9..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/config/ConfigMigration.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ktale.api.config - -/** - * A versioned migration from one schema version to the next. - * - * ## Design note - * Migrations operate on *text* to avoid locking KTale into a particular parsing model. - * Core may provide higher-level helpers for structured migrations, but the public contract stays minimal. - */ -public interface ConfigMigration { - /** Schema version this migration expects. */ - public val fromVersion: Int - - /** Schema version after this migration is applied. Usually `fromVersion + 1`. */ - public val toVersion: Int - - /** - * Applies the migration. - * - * @param oldText config content at [fromVersion] - * @return migrated config content at [toVersion] - */ - public fun migrate(oldText: String): String -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/entities/EntityRef.kt b/ktale-api/src/main/kotlin/ktale/api/entities/EntityRef.kt deleted file mode 100644 index 9834fdb..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/entities/EntityRef.kt +++ /dev/null @@ -1,24 +0,0 @@ -package ktale.api.entities - -import ktale.api.identity.Key - -/** - * Minimal reference to an entity-like thing in the host runtime. - * - * ## Design note - * KTale intentionally does not model a full entity system. - * This is just a stable handle plugins can pass around. - */ -public interface EntityRef { - /** Stable identifier for this entity within the host runtime. */ - public val id: Key - - /** - * Broad kind/type identifier for routing/logging. - * - * This is not a class hierarchy; it's a registry-style key. - */ - public val kind: Key -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/entities/HasModel.kt b/ktale-api/src/main/kotlin/ktale/api/entities/HasModel.kt deleted file mode 100644 index cde6446..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/entities/HasModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ktale.api.entities - -import ktale.api.identity.Key - -/** - * Capability: the entity can expose and/or change its "model". - * - * ## Design note - * The word "model" is used because that's what we *expect* the game UI exposes, - * but the actual host mapping is unknown. This capability stays generic: - * a model is just a [Key] into a host-provided catalog. - */ -public interface HasModel { - /** Current model key. */ - public fun model(): Key - - /** - * Requests a model change. - * - * Platforms decide validation rules and whether this is instant or eventual. - */ - public fun setModel(model: Key) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/events/Cancellable.kt b/ktale-api/src/main/kotlin/ktale/api/events/Cancellable.kt deleted file mode 100644 index e8b1cb9..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/events/Cancellable.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ktale.api.events - -/** - * Capability for events that can be cancelled. - * - * Cancellation semantics are event-defined. - * For example, a platform adapter might treat cancellation as "do not execute default behavior", - * while other events may use it as a hint to later phases. - */ -public interface Cancellable { - /** Whether the event has been cancelled. */ - public var isCancelled: Boolean -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/events/Event.kt b/ktale-api/src/main/kotlin/ktale/api/events/Event.kt deleted file mode 100644 index 29e6904..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/events/Event.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ktale.api.events - -/** - * Marker interface for events. - * - * Events are plain objects; KTale makes no assumptions about inheritance trees, - * and avoids "entity-model" coupling. - */ -public interface Event - - diff --git a/ktale-api/src/main/kotlin/ktale/api/events/EventBus.kt b/ktale-api/src/main/kotlin/ktale/api/events/EventBus.kt deleted file mode 100644 index c5f1b89..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/events/EventBus.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ktale.api.events - -/** - * Event publishing and subscription. - * - * ## Key constraints - * - No dependency on a real server runtime. - * - No assumptions about threading: platforms decide what "sync" means. - * - Subscription is type-based and capability-oriented (e.g. [Cancellable]). - * - * Implementations should be deterministic and test-friendly. - */ -public interface EventBus { - /** - * Publishes an event to all listeners. - * - * @return the same [event] instance for convenience. - */ - public fun post(event: E): E - - /** - * Subscribes a listener for a specific event type. - * - * @param type the exact event class to subscribe to - * @param priority relative order of invocation - * @param ignoreCancelled if `true`, the listener is skipped when [event] is [Cancellable] and cancelled - */ - public fun subscribe( - type: Class, - listener: EventListener, - priority: EventPriority, - ignoreCancelled: Boolean, - ): EventSubscription - - /** - * Java-friendly overload: defaults to [EventPriority.NORMAL], not ignoring cancelled events. - */ - public fun subscribe(type: Class, listener: EventListener): EventSubscription = - subscribe(type, listener, EventPriority.NORMAL, ignoreCancelled = false) - - /** - * Java-friendly overload: defaults to not ignoring cancelled events. - */ - public fun subscribe(type: Class, priority: EventPriority, listener: EventListener): EventSubscription = - subscribe(type, listener, priority, ignoreCancelled = false) -} - -/** - * Kotlin convenience overload. - */ -public inline fun EventBus.subscribe( - priority: EventPriority = EventPriority.NORMAL, - ignoreCancelled: Boolean = false, - noinline listener: (E) -> Unit, -): EventSubscription = subscribe(E::class.java, EventListener(listener), priority, ignoreCancelled) - - diff --git a/ktale-api/src/main/kotlin/ktale/api/events/EventListener.kt b/ktale-api/src/main/kotlin/ktale/api/events/EventListener.kt deleted file mode 100644 index ea2d986..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/events/EventListener.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ktale.api.events - -/** - * Java-friendly event listener functional interface. - * - * Kotlin users can still pass lambdas naturally; Java users can pass lambdas or method references - * without touching Kotlin function types. - */ -public fun interface EventListener { - public fun onEvent(event: E) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/events/EventPriority.kt b/ktale-api/src/main/kotlin/ktale/api/events/EventPriority.kt deleted file mode 100644 index af8fe69..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/events/EventPriority.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ktale.api.events - -/** - * Relative ordering for event listeners. - * - * ## Design note - * This is intentionally *not* modeled after any existing game plugin API. - * The goal is to provide a generic ordering mechanism that can map onto - * any future platform behavior. - */ -public enum class EventPriority { - /** - * Earliest observers. - * - * Typical use: validation, cheap pre-checks, early routing. - */ - EARLY, - - /** Default priority for most listeners. */ - NORMAL, - - /** - * Later observers. - * - * Typical use: modifications that should see effects from NORMAL listeners. - */ - LATE, - - /** - * Last observers. - * - * Typical use: metrics, logging, state mirroring (avoid mutating event here). - */ - FINAL, -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/events/EventSubscription.kt b/ktale-api/src/main/kotlin/ktale/api/events/EventSubscription.kt deleted file mode 100644 index 3d13292..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/events/EventSubscription.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ktale.api.events - -/** - * Handle for a listener registration. - * - * Implementations must make [unsubscribe] idempotent. - */ -public interface EventSubscription { - /** Unregisters the listener. Safe to call multiple times. */ - public fun unsubscribe() -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/identity/Key.kt b/ktale-api/src/main/kotlin/ktale/api/identity/Key.kt deleted file mode 100644 index f064ed8..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/identity/Key.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ktale.api.identity - -/** - * Opaque identifier for game-facing registries (items, blocks, materials, models, prefabs, etc.). - * - * ## Design note - * - This is intentionally *not* an enum: registries may be huge and may evolve. - * - This is intentionally *not* tied to any host API: it can map to strings, hashes, resource locations, etc. - * - Java-friendly: this is a normal class (not a Kotlin value class). - * - * ## Example - * IDs can be plain strings such as `Cloth_Block_Wool_Blue` (as seen in Hytale UI/tooling). - * KTale preserves the string verbatim; it does not enforce casing or separators. - */ -public class Key public constructor(public val value: String) { - init { - require(value.isNotBlank()) { "Key must not be blank" } - } - - override fun toString(): String = value - override fun equals(other: Any?): Boolean = other is Key && other.value == value - override fun hashCode(): Int = value.hashCode() - - public companion object { - @JvmStatic - public fun of(value: String): Key = Key(value) - } -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/inventory/Container.kt b/ktale-api/src/main/kotlin/ktale/api/inventory/Container.kt deleted file mode 100644 index 006eead..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/inventory/Container.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ktale.api.inventory - -import ktale.api.identity.Key -import ktale.api.items.ItemStack - -/** - * Minimal container/inventory abstraction. - * - * ## Design note - * KTale doesn't assume a particular slot model (grid vs list vs equipment). This is a generic interface - * that can represent backpacks, equipment, bags, etc. through [ContainerKind] and slot indexing. - */ -public interface Container { - /** Stable identifier for this container (best-effort). */ - public val id: Key - - /** Kind identifier (e.g. "equipment", "backpack", "brewery_bag"). */ - public val kind: ContainerKind - - /** Number of slots in this container. */ - public fun size(): Int - - public fun get(slot: Int): ItemStack? - public fun set(slot: Int, stack: ItemStack?) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/inventory/ContainerKind.kt b/ktale-api/src/main/kotlin/ktale/api/inventory/ContainerKind.kt deleted file mode 100644 index 6edc5b5..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/inventory/ContainerKind.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ktale.api.inventory - -import ktale.api.identity.Key - -/** - * A container kind identifier. - * - * This is a [Key]-backed identifier to avoid hardcoding all possible container types. - */ -public class ContainerKind public constructor(public val key: Key) { - override fun toString(): String = key.toString() - override fun equals(other: Any?): Boolean = other is ContainerKind && other.key == key - override fun hashCode(): Int = key.hashCode() - - public companion object { - @JvmStatic public fun of(value: String): ContainerKind = ContainerKind(Key.of(value)) - } -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/items/ItemStack.kt b/ktale-api/src/main/kotlin/ktale/api/items/ItemStack.kt deleted file mode 100644 index 6eb0a21..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/items/ItemStack.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ktale.api.items - -import ktale.api.identity.Key - -/** - * Minimal stack of an [ItemType]. - * - * ## Design note - * Metadata is modeled as a string-keyed map to avoid committing to a host NBT/JSON/etc system. - * - * ## ID note - * The [type] key is an opaque string identifier; IDs like `Cloth_Block_Wool_Blue` are valid. - */ -public interface ItemStack { - public val type: Key - public val amount: Int - public val meta: Map -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/items/ItemType.kt b/ktale-api/src/main/kotlin/ktale/api/items/ItemType.kt deleted file mode 100644 index 0016b78..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/items/ItemType.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ktale.api.items - -import ktale.api.identity.Key - -/** - * Item/material/block identifiers are modeled as [Key]s instead of enums. - * - * This avoids rewriting the SDK when the host's registry shape is known. - */ -public interface ItemType { - public val key: Key - public val displayName: String? -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/logging/KtaleLogger.kt b/ktale-api/src/main/kotlin/ktale/api/logging/KtaleLogger.kt deleted file mode 100644 index 1e9d735..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/logging/KtaleLogger.kt +++ /dev/null @@ -1,55 +0,0 @@ -package ktale.api.logging - -/** - * Logging abstraction. - * - * ## Design note - * KTale avoids prescribing a logging backend. Platforms may route logs to: - * - console - * - file - * - structured telemetry - * - remote sinks - * - * Implementations should treat [context] as *optional structured data*. - */ -public interface KtaleLogger { - /** Logs a message at [level] with optional structured [context]. */ - public fun log(level: LogLevel, message: String, throwable: Throwable?, context: Map) - - /** Java-friendly overload. */ - public fun log(level: LogLevel, message: String) { - log(level, message, null, emptyMap()) - } - - /** Java-friendly overload. */ - public fun log(level: LogLevel, message: String, throwable: Throwable?) { - log(level, message, throwable, emptyMap()) - } - - public fun trace(message: String) = log(LogLevel.TRACE, message) - public fun trace(message: String, throwable: Throwable?) = log(LogLevel.TRACE, message, throwable) - public fun trace(message: String, throwable: Throwable?, context: Map) = - log(LogLevel.TRACE, message, throwable, context) - - public fun debug(message: String) = log(LogLevel.DEBUG, message) - public fun debug(message: String, throwable: Throwable?) = log(LogLevel.DEBUG, message, throwable) - public fun debug(message: String, throwable: Throwable?, context: Map) = - log(LogLevel.DEBUG, message, throwable, context) - - public fun info(message: String) = log(LogLevel.INFO, message) - public fun info(message: String, throwable: Throwable?) = log(LogLevel.INFO, message, throwable) - public fun info(message: String, throwable: Throwable?, context: Map) = - log(LogLevel.INFO, message, throwable, context) - - public fun warn(message: String) = log(LogLevel.WARN, message) - public fun warn(message: String, throwable: Throwable?) = log(LogLevel.WARN, message, throwable) - public fun warn(message: String, throwable: Throwable?, context: Map) = - log(LogLevel.WARN, message, throwable, context) - - public fun error(message: String) = log(LogLevel.ERROR, message) - public fun error(message: String, throwable: Throwable?) = log(LogLevel.ERROR, message, throwable) - public fun error(message: String, throwable: Throwable?, context: Map) = - log(LogLevel.ERROR, message, throwable, context) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/logging/LogLevel.kt b/ktale-api/src/main/kotlin/ktale/api/logging/LogLevel.kt deleted file mode 100644 index 1aa9f85..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/logging/LogLevel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ktale.api.logging - -/** - * Logging severity. - */ -public enum class LogLevel { - TRACE, - DEBUG, - INFO, - WARN, - ERROR, -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/prefabs/Prefab.kt b/ktale-api/src/main/kotlin/ktale/api/prefabs/Prefab.kt deleted file mode 100644 index 9517140..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/prefabs/Prefab.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ktale.api.prefabs - -import ktale.api.identity.Key - -/** - * Opaque prefab payload. - * - * ## Design note - * KTale does not assume the prefab format (binary/JSON/custom). - * The payload is treated as opaque bytes plus a best-effort [formatHint]. - */ -public interface Prefab { - public val id: Key - public val formatHint: String? - public val bytes: ByteArray -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/prefabs/PrefabStore.kt b/ktale-api/src/main/kotlin/ktale/api/prefabs/PrefabStore.kt deleted file mode 100644 index a853238..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/prefabs/PrefabStore.kt +++ /dev/null @@ -1,29 +0,0 @@ -package ktale.api.prefabs - -import ktale.api.identity.Key - -/** - * Storage/hosting for prefabs. - * - * ## Design note - * We know "prefabs exist" as a concept, but we do not know: - * - whether the host stores them on disk, in memory, or streams them - * - whether they are world-scoped, server-scoped, or per-player - * - * This contract stays minimal and uses opaque [Prefab] data. - */ -public interface PrefabStore { - /** Returns known prefab ids. */ - public fun list(): List - - /** Loads a prefab by id, or `null` if missing. */ - public fun load(id: Key): Prefab? - - /** Saves or overwrites a prefab. */ - public fun save(prefab: Prefab) - - /** Deletes a prefab if present. */ - public fun delete(id: Key) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/scheduler/Scheduler.kt b/ktale-api/src/main/kotlin/ktale/api/scheduler/Scheduler.kt deleted file mode 100644 index acdad93..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/scheduler/Scheduler.kt +++ /dev/null @@ -1,45 +0,0 @@ -package ktale.api.scheduler - -import java.time.Duration - -/** - * Schedules work in sync/async contexts. - * - * ## Design note - * KTale does not assume a tick loop, thread affinity, or coroutine availability. - * "Sync" and "async" are *platform-defined* concepts; a platform adapter decides what - * constitutes the main thread (if any), and how async is executed. - */ -public interface Scheduler { - /** Runs [task] as soon as possible in the platform's "sync" context. */ - public fun runSync(task: Runnable): TaskHandle - - /** Runs [task] as soon as possible in the platform's "async" context. */ - public fun runAsync(task: Runnable): TaskHandle - - /** Runs [task] once after [delay] in the platform's "sync" context. */ - public fun runSyncDelayed(delay: Duration, task: Runnable): TaskHandle - - /** Runs [task] once after [delay] in the platform's "async" context. */ - public fun runAsyncDelayed(delay: Duration, task: Runnable): TaskHandle - - /** - * Runs [task] repeatedly with the given [interval] in the platform's "sync" context. - * - * Implementations should attempt to avoid drift, but exact semantics are platform-defined. - */ - public fun runSyncRepeating( - initialDelay: Duration, - interval: Duration, - task: Runnable, - ): TaskHandle - - /** Async variant of [runSyncRepeating]. */ - public fun runAsyncRepeating( - initialDelay: Duration, - interval: Duration, - task: Runnable, - ): TaskHandle -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/scheduler/SchedulerKotlin.kt b/ktale-api/src/main/kotlin/ktale/api/scheduler/SchedulerKotlin.kt deleted file mode 100644 index 57c078f..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/scheduler/SchedulerKotlin.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ktale.api.scheduler - -import java.time.Duration - -/** - * Kotlin convenience overloads for [Scheduler]. - * - * These helpers exist so that the *public contracts* can remain Java-first (Runnable / java.time.Duration), - * while Kotlin call sites still feel natural. - */ -public fun Scheduler.runSync(task: () -> Unit): TaskHandle = runSync(Runnable(task)) - -public fun Scheduler.runAsync(task: () -> Unit): TaskHandle = runAsync(Runnable(task)) - -public fun Scheduler.runSyncDelayed(delay: Duration, task: () -> Unit): TaskHandle = - runSyncDelayed(delay, Runnable(task)) - -public fun Scheduler.runAsyncDelayed(delay: Duration, task: () -> Unit): TaskHandle = - runAsyncDelayed(delay, Runnable(task)) - -public fun Scheduler.runSyncRepeating(initialDelay: Duration, interval: Duration, task: () -> Unit): TaskHandle = - runSyncRepeating(initialDelay, interval, Runnable(task)) - -public fun Scheduler.runAsyncRepeating(initialDelay: Duration, interval: Duration, task: () -> Unit): TaskHandle = - runAsyncRepeating(initialDelay, interval, Runnable(task)) - - diff --git a/ktale-api/src/main/kotlin/ktale/api/scheduler/TaskHandle.kt b/ktale-api/src/main/kotlin/ktale/api/scheduler/TaskHandle.kt deleted file mode 100644 index ae1f831..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/scheduler/TaskHandle.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ktale.api.scheduler - -/** - * Handle for a scheduled task. - * - * Implementations must make [cancel] idempotent. - */ -public interface TaskHandle { - /** Cancels the task. Safe to call multiple times. */ - public fun cancel() - - /** Whether [cancel] has been requested. */ - public val isCancelled: Boolean -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/services/ServiceRegistry.kt b/ktale-api/src/main/kotlin/ktale/api/services/ServiceRegistry.kt deleted file mode 100644 index 0c7dfda..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/services/ServiceRegistry.kt +++ /dev/null @@ -1,46 +0,0 @@ -package ktale.api.services - -/** - * Minimal service registry ("DI-light"). - * - * ## Design note - * A runtime registry keeps KTale adaptable to unknown host/container lifecycles. - * This avoids forcing a DI framework choice on Day 1. - */ -public interface ServiceRegistry { - /** - * Registers a service instance. - * - * @param replace if `true`, replaces any existing service of the same [type] - * @throws IllegalStateException if a service already exists and [replace] is `false` - */ - public fun register(type: Class, instance: T, replace: Boolean) - - /** Java-friendly overload (does not replace). */ - public fun register(type: Class, instance: T) { - register(type, instance, replace = false) - } - - /** Returns a service instance if registered, otherwise `null`. */ - public fun get(type: Class): T? - - /** Returns a service instance or throws if missing. */ - public fun require(type: Class): T - - /** Unregisters a service by type. */ - public fun unregister(type: Class) -} - -public inline fun ServiceRegistry.register(instance: T, replace: Boolean = false) { - register(T::class.java, instance, replace) -} - -public inline fun ServiceRegistry.get(): T? = get(T::class.java) - -public inline fun ServiceRegistry.require(): T = require(T::class.java) - -public inline fun ServiceRegistry.unregister() { - unregister(T::class.java) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/stats/Attribute.kt b/ktale-api/src/main/kotlin/ktale/api/stats/Attribute.kt deleted file mode 100644 index 2019a18..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/stats/Attribute.kt +++ /dev/null @@ -1,23 +0,0 @@ -package ktale.api.stats - -import ktale.api.identity.Key - -/** - * Minimal numeric attribute. - * - * ## Examples - * - health (current/max) - * - mana (current/max) - * - * ## Design note - * The host may represent these differently; this is a capability surface for plugins. - */ -public interface Attribute { - public val key: Key - public fun current(): Double - public fun max(): Double? - public fun setCurrent(value: Double) - public fun setMax(value: Double) -} - - diff --git a/ktale-api/src/main/kotlin/ktale/api/stats/HasAttributes.kt b/ktale-api/src/main/kotlin/ktale/api/stats/HasAttributes.kt deleted file mode 100644 index 863c67e..0000000 --- a/ktale-api/src/main/kotlin/ktale/api/stats/HasAttributes.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ktale.api.stats - -import ktale.api.identity.Key - -/** - * Capability: exposes a set of numeric attributes (health, mana, etc.). - * - * ## Design note - * We avoid hardcoding a full stat system; attributes are resolved by [Key]. - */ -public interface HasAttributes { - /** Returns an attribute by key, or `null` if not supported. */ - public fun attribute(key: Key): Attribute? - - /** Returns supported attribute keys (best-effort). */ - public fun attributes(): List -} - - diff --git a/ktale-core/build.gradle.kts b/ktale-core/build.gradle.kts deleted file mode 100644 index 6d504de..0000000 --- a/ktale-core/build.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -description = "KTale minimal default implementations (platform-neutral)." - -plugins { - kotlin("plugin.serialization") -} - -dependencies { - implementation(project(":ktale-api")) - implementation(project(":ktale-platform")) - - // Core must stay platform-agnostic; config parsing lives here behind KTale codecs. - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0") - implementation("com.charleskorn.kaml:kaml:0.76.0") -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/PluginContexts.kt b/ktale-core/src/main/kotlin/ktale/core/PluginContexts.kt deleted file mode 100644 index 6bdf9e1..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/PluginContexts.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ktale.core - -import ktale.api.PluginContext -import ktale.api.commands.CommandRegistry -import ktale.api.config.ConfigManager -import ktale.api.events.EventBus -import ktale.api.prefabs.PrefabStore -import ktale.api.logging.KtaleLogger -import ktale.api.scheduler.Scheduler -import ktale.api.services.ServiceRegistry -import ktale.platform.Platform - -/** - * Default core implementation of [PluginContext]. - * - * ## Design note - * This is intentionally "dumb wiring": - * it composes capability interfaces without introducing extra lifecycle assumptions. - */ -public class DefaultPluginContext( - override val pluginId: String, - platform: Platform, - override val events: EventBus, - override val scheduler: Scheduler, - override val commands: CommandRegistry, - override val configs: ConfigManager, - override val services: ServiceRegistry, - override val prefabs: PrefabStore? = null, -) : PluginContext { - override val logger: KtaleLogger = platform.loggers.logger(pluginId) -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/autoregister/AutoRegistrar.kt b/ktale-core/src/main/kotlin/ktale/core/autoregister/AutoRegistrar.kt deleted file mode 100644 index f58ba77..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/autoregister/AutoRegistrar.kt +++ /dev/null @@ -1,100 +0,0 @@ -package ktale.core.autoregister - -import ktale.api.PluginContext -import ktale.api.autoregister.AutoCommand -import ktale.api.autoregister.SubscribeEvent -import ktale.api.commands.CommandDefinition -import ktale.api.events.Event -import ktale.api.events.EventListener -import ktale.api.events.EventPriority -import java.lang.reflect.Method -import java.nio.file.Path -import java.util.jar.JarFile - -/** - * Plug-and-play auto registration for commands and event listeners. - * - * ## Design note - * This lives in `ktale-core` because it is a convenience implementation, not a stable contract. - * It is safe to omit in other host runtimes. - * - * ## Rules (conventions) - * - Commands: - * - classes that implement [CommandDefinition] - * - annotated with [AutoCommand] - * - public no-arg constructor - * - Event listeners: - * - any class with methods annotated [SubscribeEvent] - * - listener class must have a public no-arg constructor - */ -public object AutoRegistrar { - public fun registerAllFromJar(pluginJar: Path, classLoader: ClassLoader, context: PluginContext) { - val classNames = readClassNames(pluginJar) - val classes = classNames.mapNotNull { name -> - try { - Class.forName(name, true, classLoader) - } catch (_: Throwable) { - null - } - } - registerAllFromClasses(classes, context) - } - - public fun registerAllFromClasses(classes: List>, context: PluginContext) { - classes.forEach { clazz -> - tryRegisterCommand(clazz, context) - tryRegisterListener(clazz, context) - } - } - - private fun readClassNames(jar: Path): List = - JarFile(jar.toFile()).use { jf -> - jf.entries().asSequence() - .filter { !it.isDirectory } - .map { it.name } - .filter { it.endsWith(".class") } - .filter { !it.contains('$') } // skip inner/anonymous classes by default - .map { it.removeSuffix(".class").replace('/', '.') } - .toList() - } - - private fun tryRegisterCommand(clazz: Class<*>, context: PluginContext) { - if (!CommandDefinition::class.java.isAssignableFrom(clazz)) return - if (!clazz.isAnnotationPresent(AutoCommand::class.java)) return - val ctor = clazz.getDeclaredConstructor() - ctor.isAccessible = true - val def = ctor.newInstance() as CommandDefinition - context.commands.register(def) - } - - private fun tryRegisterListener(clazz: Class<*>, context: PluginContext) { - val methods = clazz.declaredMethods.filter { it.isAnnotationPresent(SubscribeEvent::class.java) } - if (methods.isEmpty()) return - - val ctor = clazz.getDeclaredConstructor() - ctor.isAccessible = true - val instance = ctor.newInstance() - for (m in methods) { - registerMethodListener(instance, m, context) - } - } - - private fun registerMethodListener(instance: Any, method: Method, context: PluginContext) { - val ann = method.getAnnotation(SubscribeEvent::class.java) - val eventType = ann.value.java - val priority: EventPriority = ann.priority - val ignoreCancelled: Boolean = ann.ignoreCancelled - - method.isAccessible = true - - @Suppress("UNCHECKED_CAST") - context.events.subscribe( - eventType as Class, - EventListener { e -> method.invoke(instance, e) }, - priority, - ignoreCancelled, - ) - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/commands/BridgedCommandRegistry.kt b/ktale-core/src/main/kotlin/ktale/core/commands/BridgedCommandRegistry.kt deleted file mode 100644 index 2b4f668..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/commands/BridgedCommandRegistry.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ktale.core.commands - -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandRegistry -import ktale.api.commands.CommandResult -import ktale.platform.PlatformCommandBridge - -/** - * Command registry wrapper that notifies a [PlatformCommandBridge] on registration changes. - * - * ## Design note - * `ktale-api` intentionally keeps command contracts IO-free. This wrapper is the point where - * core can *optionally* bridge command registration into a host runtime without polluting - * the logic-only registry itself. - */ -public class BridgedCommandRegistry( - private val delegate: CommandRegistry, - private val bridge: PlatformCommandBridge, -) : CommandRegistry { - init { - bridge.bind(this) - } - - override fun register(definition: CommandDefinition) { - delegate.register(definition) - bridge.onRegister(definition) - } - - override fun unregister(name: String) { - delegate.unregister(name) - bridge.onUnregister(name) - } - - override fun dispatch(context: CommandContext): CommandResult = delegate.dispatch(context) -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/commands/CommandExecutor.kt b/ktale-core/src/main/kotlin/ktale/core/commands/CommandExecutor.kt deleted file mode 100644 index bbd5a2d..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/commands/CommandExecutor.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ktale.core.commands - -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandResult - -/** - * Java-friendly command executor functional interface. - * - * `ktale-api` exposes [ktale.api.commands.CommandDefinition.execute] as a Kotlin method, - * but Java plugins typically prefer functional interfaces for wiring. - */ -public fun interface CommandExecutor { - public fun execute(context: CommandContext): CommandResult -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/commands/Commands.kt b/ktale-core/src/main/kotlin/ktale/core/commands/Commands.kt deleted file mode 100644 index b2f7192..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/commands/Commands.kt +++ /dev/null @@ -1,102 +0,0 @@ -package ktale.core.commands - -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandResult -import ktale.api.commands.Permission - -/** - * Command builders for both Kotlin and Java. - * - * ## Design note - * - This lives in `ktale-core` because it is an implementation detail (builders create [CommandDefinition] instances). - * - `ktale-api` stays implementation-free. - */ -public object Commands { - /** - * Java-friendly fluent builder entrypoint. - * - * Example (Java): - * `Commands.command("ping").executor(ctx -> CommandResult.Success).build()` - */ - @JvmStatic - public fun command(name: String): FluentBuilder = FluentBuilder(name) - - /** - * Kotlin DSL entrypoint. - * - * Example (Kotlin): - * `command("ping") { execute { CommandResult.Success } }` - */ - public fun command(name: String, block: DslBuilder.() -> Unit): CommandDefinition = - DslBuilder(name).apply(block).build() - - public class FluentBuilder internal constructor( - private val name: String, - ) { - private val aliases: MutableSet = linkedSetOf() - private var description: String? = null - private var permission: Permission? = null - private var executor: CommandExecutor? = null - - public fun aliases(vararg aliases: String): FluentBuilder = apply { this.aliases += aliases } - public fun description(description: String?): FluentBuilder = apply { this.description = description } - public fun permission(permission: Permission?): FluentBuilder = apply { this.permission = permission } - public fun executor(executor: CommandExecutor): FluentBuilder = apply { this.executor = executor } - - public fun build(): CommandDefinition { - val exec = executor ?: error("Commands.command('$name') is missing an executor") - return SimpleCommandDefinition( - name = name, - aliases = aliases.toSet(), - description = description, - permission = permission, - executor = exec, - ) - } - } - - @DslMarker - public annotation class CommandDsl - - @CommandDsl - public class DslBuilder internal constructor( - private val name: String, - ) { - private val aliases: MutableSet = linkedSetOf() - public var description: String? = null - public var permission: Permission? = null - private var executor: CommandExecutor? = null - - public fun aliases(vararg aliases: String) { - this.aliases += aliases - } - - public fun execute(block: (CommandContext) -> CommandResult) { - executor = CommandExecutor(block) - } - - public fun build(): CommandDefinition { - val exec = executor ?: error("command('$name') is missing an execute { ... } block") - return SimpleCommandDefinition( - name = name, - aliases = aliases.toSet(), - description = description, - permission = permission, - executor = exec, - ) - } - } - - private class SimpleCommandDefinition( - override val name: String, - override val aliases: Set, - override val description: String?, - override val permission: Permission?, - private val executor: CommandExecutor, - ) : CommandDefinition { - override fun execute(context: CommandContext): CommandResult = executor.execute(context) - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/commands/SimpleCommandRegistry.kt b/ktale-core/src/main/kotlin/ktale/core/commands/SimpleCommandRegistry.kt deleted file mode 100644 index f08f103..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/commands/SimpleCommandRegistry.kt +++ /dev/null @@ -1,67 +0,0 @@ -package ktale.core.commands - -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandRegistry -import ktale.api.commands.CommandResult -import ktale.api.commands.Permission -import java.util.concurrent.ConcurrentHashMap - -/** - * Minimal in-memory command registry and dispatcher (logic only). - * - * ## Design note - * - No IO: platforms handle input and output routing. - * - No parsing engine: dispatch uses [CommandContext.args] as already-tokenized input. - * - No hierarchy assumptions: commands are flat and may implement their own sub-routing. - */ -public class SimpleCommandRegistry : CommandRegistry { - private val byName = ConcurrentHashMap() - private val aliasToName = ConcurrentHashMap() - - override fun register(definition: CommandDefinition) { - val name = normalize(definition.name) - require(name.isNotBlank()) { "Command name must not be blank" } - - if (byName.putIfAbsent(name, definition) != null) { - throw IllegalArgumentException("Command already registered: $name") - } - - for (aliasRaw in definition.aliases) { - val alias = normalize(aliasRaw) - require(alias.isNotBlank()) { "Alias must not be blank" } - if (alias == name) continue - val existing = aliasToName.putIfAbsent(alias, name) - if (existing != null) { - byName.remove(name) - aliasToName.entries.removeIf { it.value == name } - throw IllegalArgumentException("Alias '$alias' already registered for '$existing'") - } - } - } - - override fun unregister(name: String) { - val key = normalize(name) - byName.remove(key) - aliasToName.entries.removeIf { it.value == key } - } - - override fun dispatch(context: CommandContext): CommandResult { - val key = normalize(context.label) - val name = aliasToName[key] ?: key - val def = byName[name] ?: return UnknownCommand(name) - - val perm = def.permission - if (perm != null && !context.sender.hasPermission(perm)) return CommandResult.NoPermission - - return def.execute(context) - } - - private fun normalize(s: String): String = s.trim().lowercase() - - private data class UnknownCommand(val name: String) : CommandResult.UsageError { - override val message: String = "Unknown command: $name" - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/config/ConfigTextStore.kt b/ktale-core/src/main/kotlin/ktale/core/config/ConfigTextStore.kt deleted file mode 100644 index bd8dc4b..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/config/ConfigTextStore.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ktale.core.config - -/** - * Text-based config storage boundary used by core config implementations. - * - * ## Design note - * This is internal to core on purpose: - * - `ktale-api` stays minimal and platform-agnostic. - * - Platforms can swap the storage mechanism without altering plugin-facing contracts. - */ -public interface ConfigTextStore { - public fun read(id: String): String? - public fun write(id: String, text: String) -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/config/CoreConfigManager.kt b/ktale-core/src/main/kotlin/ktale/core/config/CoreConfigManager.kt deleted file mode 100644 index 876b005..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/config/CoreConfigManager.kt +++ /dev/null @@ -1,87 +0,0 @@ -package ktale.core.config - -import ktale.api.config.ConfigKey -import ktale.api.config.ConfigManager -import ktale.api.logging.KtaleLogger - -/** - * Minimal config manager that loads/saves text configs via a [ConfigTextStore]. - * - * ## Versioning - * This implementation stores configs as text and applies [ConfigKey.migrations] on the raw text. - * Version detection is intentionally simple and format-agnostic; it relies on a tiny convention: - * - * - The first non-empty, non-comment line may be: `ktaleConfigVersion: ` - * - * Platforms and plugins are free to ignore this convention and provide their own ConfigManager. - */ -public class CoreConfigManager( - private val store: ConfigTextStore, - private val logger: KtaleLogger, -) : ConfigManager { - override fun load(key: ConfigKey): T { - val existing = store.read(key.id) - if (existing == null) { - val default = key.defaultValue() - save(key, default) - return default - } - - val (storedVersion, textWithoutHeader) = parseVersionHeader(existing) - var version = storedVersion ?: 0 - var migratedText = textWithoutHeader - - if (version < key.version) { - val migrations = key.migrations.sortedBy { it.fromVersion } - for (m in migrations) { - if (m.fromVersion != version) continue - migratedText = m.migrate(migratedText) - version = m.toVersion - } - } - - if (version != key.version) { - logger.warn("Config '${key.id}' could not be fully migrated (have=$version want=${key.version}); using best-effort decode.") - } - - return try { - key.codec.decode(migratedText) - } catch (t: Throwable) { - logger.error("Failed to decode config '${key.id}', falling back to defaults.", t) - val default = key.defaultValue() - save(key, default) - default - }.also { - // Persist migrations (if any) along with header. - store.write(key.id, renderWithHeader(key.version, key.codec.encode(it))) - } - } - - override fun save(key: ConfigKey, value: T) { - val encoded = key.codec.encode(value) - store.write(key.id, renderWithHeader(key.version, encoded)) - } - - private fun renderWithHeader(version: Int, body: String): String = - "ktaleConfigVersion: $version\n$body" - - private fun parseVersionHeader(text: String): Pair { - val lines = text.lines() - for ((idx, raw) in lines.withIndex()) { - val line = raw.trim() - if (line.isEmpty()) continue - if (line.startsWith("#")) continue - if (line.startsWith("//")) continue - val prefix = "ktaleConfigVersion:" - if (line.startsWith(prefix)) { - val v = line.removePrefix(prefix).trim().toIntOrNull() - val rest = lines.drop(idx + 1).joinToString("\n") - return v to rest - } - break - } - return null to text - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/config/FileConfigTextStore.kt b/ktale-core/src/main/kotlin/ktale/core/config/FileConfigTextStore.kt deleted file mode 100644 index dd3fe54..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/config/FileConfigTextStore.kt +++ /dev/null @@ -1,44 +0,0 @@ -package ktale.core.config - -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path - -/** - * Simple file-backed config store. - * - * ## Design note - * This is platform-neutral and intended for standalone hosts or platform adapters that store configs on disk. - * The [id] passed in is treated as a relative filename. No path traversal is allowed. - */ -public class FileConfigTextStore( - private val baseDir: Path, -) : ConfigTextStore { - init { - Files.createDirectories(baseDir) - } - - override fun read(id: String): String? { - val path = resolveSafe(id) ?: return null - if (!Files.exists(path)) return null - return Files.readString(path, StandardCharsets.UTF_8) - } - - override fun write(id: String, text: String) { - val path = resolveSafe(id) ?: return - Files.createDirectories(path.parent) - Files.writeString(path, text, StandardCharsets.UTF_8) - } - - private fun resolveSafe(id: String): Path? { - val trimmed = id.trim() - if (trimmed.isEmpty()) return null - // Extremely conservative: disallow absolute paths and parent traversal. - if (trimmed.contains("..")) return null - val p = baseDir.resolve(trimmed).normalize() - if (!p.startsWith(baseDir.normalize())) return null - return p - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/config/InMemoryConfigTextStore.kt b/ktale-core/src/main/kotlin/ktale/core/config/InMemoryConfigTextStore.kt deleted file mode 100644 index a8713af..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/config/InMemoryConfigTextStore.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ktale.core.config - -import java.util.concurrent.ConcurrentHashMap - -/** - * Simple in-memory config storage used for tests and fake platforms. - */ -public class InMemoryConfigTextStore : ConfigTextStore { - private val map = ConcurrentHashMap() - - override fun read(id: String): String? = map[id] - - override fun write(id: String, text: String) { - map[id] = text - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/config/yaml/DefaultYaml.kt b/ktale-core/src/main/kotlin/ktale/core/config/yaml/DefaultYaml.kt deleted file mode 100644 index 7f3a0a9..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/config/yaml/DefaultYaml.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ktale.core.config.yaml - -import com.charleskorn.kaml.Yaml -import com.charleskorn.kaml.YamlConfiguration - -/** - * Default YAML configuration used by KTale core. - * - * ## Design note - * This is intentionally conservative. Plugins that need different YAML behavior should supply their own [Yaml]. - */ -public object DefaultYaml { - public val instance: Yaml = Yaml( - configuration = YamlConfiguration( - encodeDefaults = true, - strictMode = false, - ) - ) -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/config/yaml/YamlConfigCodec.kt b/ktale-core/src/main/kotlin/ktale/core/config/yaml/YamlConfigCodec.kt deleted file mode 100644 index 28ef5c7..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/config/yaml/YamlConfigCodec.kt +++ /dev/null @@ -1,22 +0,0 @@ -package ktale.core.config.yaml - -import com.charleskorn.kaml.Yaml -import ktale.api.config.ConfigCodec -import kotlinx.serialization.KSerializer - -/** - * YAML codec backed by kotlinx.serialization + Kaml. - * - * ## Java compatibility note - * Java plugins can still use typed configs by providing their own [ConfigCodec] implementation - * (e.g. Jackson). This codec is provided as a batteries-included default for Kotlin users. - */ -public class YamlConfigCodec( - private val serializer: KSerializer, - private val yaml: Yaml = DefaultYaml.instance, -) : ConfigCodec { - override fun decode(text: String): T = yaml.decodeFromString(serializer, text) - override fun encode(value: T): String = yaml.encodeToString(serializer, value) -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/events/SimpleEventBus.kt b/ktale-core/src/main/kotlin/ktale/core/events/SimpleEventBus.kt deleted file mode 100644 index 7271fb5..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/events/SimpleEventBus.kt +++ /dev/null @@ -1,66 +0,0 @@ -package ktale.core.events - -import ktale.api.events.Cancellable -import ktale.api.events.Event -import ktale.api.events.EventBus -import ktale.api.events.EventListener -import ktale.api.events.EventPriority -import ktale.api.events.EventSubscription -import java.util.concurrent.CopyOnWriteArrayList - -/** - * A minimal, deterministic, in-memory event bus. - * - * ## Design note - * - Uses type-exact subscriptions (no class hierarchy walking) to avoid surprising dispatch costs. - * - Is synchronous: event dispatch happens on the calling thread. - * - Thread-safe subscription/unsubscription for typical plugin usage. - */ -public class SimpleEventBus : EventBus { - private data class RegisteredListener( - val type: Class, - val priority: EventPriority, - val ignoreCancelled: Boolean, - val listener: EventListener, - val token: Any = Any(), - ) - - private val listeners = CopyOnWriteArrayList>() - - override fun post(event: E): E { - @Suppress("UNCHECKED_CAST") - val typed = listeners - .asSequence() - .filter { it.type == event.javaClass } - .map { it as RegisteredListener } - .sortedBy { it.priority.ordinal } - .toList() - - val cancelled = (event as? Cancellable)?.isCancelled == true - for (reg in typed) { - if (reg.ignoreCancelled && cancelled) continue - reg.listener.onEvent(event) - } - return event - } - - override fun subscribe( - type: Class, - listener: EventListener, - priority: EventPriority, - ignoreCancelled: Boolean, - ): EventSubscription { - val reg = RegisteredListener(type, priority, ignoreCancelled, listener) - listeners.add(reg) - return object : EventSubscription { - private var unsubscribed = false - override fun unsubscribe() { - if (unsubscribed) return - unsubscribed = true - listeners.removeIf { it.token == reg.token } - } - } - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/logging/SimpleConsoleLogger.kt b/ktale-core/src/main/kotlin/ktale/core/logging/SimpleConsoleLogger.kt deleted file mode 100644 index ea5ed02..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/logging/SimpleConsoleLogger.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ktale.core.logging - -import ktale.api.logging.KtaleLogger -import ktale.api.logging.LogLevel -import java.time.Instant - -/** - * Minimal console logger used by the fake platform and as a fallback. - */ -public class SimpleConsoleLogger( - private val name: String, - private val nowEpochMillis: () -> Long = { System.currentTimeMillis() }, -) : KtaleLogger { - override fun log(level: LogLevel, message: String, throwable: Throwable?, context: Map) { - val ts = Instant.ofEpochMilli(nowEpochMillis()).toString() - val ctx = if (context.isEmpty()) "" else " $context" - val line = "[$ts] [$level] [$name] $message$ctx" - if (level >= LogLevel.WARN) { - System.err.println(line) - throwable?.printStackTrace(System.err) - } else { - println(line) - throwable?.printStackTrace(System.out) - } - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/runtime/CoreRuntime.kt b/ktale-core/src/main/kotlin/ktale/core/runtime/CoreRuntime.kt deleted file mode 100644 index 559eb7a..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/runtime/CoreRuntime.kt +++ /dev/null @@ -1,61 +0,0 @@ -package ktale.core.runtime - -import ktale.api.PluginContext -import ktale.api.commands.CommandRegistry -import ktale.api.config.ConfigManager -import ktale.api.events.EventBus -import ktale.api.prefabs.PrefabStore -import ktale.api.scheduler.Scheduler -import ktale.api.services.ServiceRegistry -import ktale.core.DefaultPluginContext -import ktale.core.commands.BridgedCommandRegistry -import ktale.core.commands.SimpleCommandRegistry -import ktale.core.config.ConfigTextStore -import ktale.core.config.CoreConfigManager -import ktale.core.events.SimpleEventBus -import ktale.core.scheduler.HookBackedScheduler -import ktale.core.services.SimpleServiceRegistry -import ktale.core.threading.ThreadGuards -import ktale.platform.Platform - -/** - * Minimal "Day-1" core runtime wiring helper. - * - * ## Design note - * This is *optional* glue: - * - platform adapters may choose to build their own contexts - * - fake servers can use this to reduce boilerplate - * - * This class intentionally does not guess storage locations; callers provide a [ConfigTextStore]. - */ -public class CoreRuntime( - private val platform: Platform, - private val configStore: ConfigTextStore, -) { - public val prefabs: PrefabStore? = null - public val services: ServiceRegistry = SimpleServiceRegistry() - public val events: EventBus = SimpleEventBus() - public val scheduler: Scheduler = HookBackedScheduler(platform.scheduler) - - private val baseCommands: CommandRegistry = SimpleCommandRegistry() - public val commands: CommandRegistry = BridgedCommandRegistry(baseCommands, platform.commands) - - public val configs: ConfigManager = CoreConfigManager(configStore, platform.loggers.logger("ktale-config")) - - public fun threadGuards(pluginId: String): ThreadGuards = - ThreadGuards(platform.loggers.logger("$pluginId-threading")) - - public fun pluginContext(pluginId: String): PluginContext = - DefaultPluginContext( - pluginId = pluginId, - platform = platform, - events = events, - scheduler = scheduler, - commands = commands, - configs = configs, - services = services, - prefabs = prefabs, - ) -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/scheduler/HookBackedScheduler.kt b/ktale-core/src/main/kotlin/ktale/core/scheduler/HookBackedScheduler.kt deleted file mode 100644 index e32c032..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/scheduler/HookBackedScheduler.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ktale.core.scheduler - -import ktale.api.scheduler.Scheduler -import ktale.api.scheduler.TaskHandle -import ktale.platform.PlatformSchedulerHooks -import java.time.Duration - -/** - * Scheduler implementation backed by [PlatformSchedulerHooks]. - * - * ## Design note - * The KTale public contract uses Java-first types (Runnable / java.time.Duration) so Java plugins - * can use the SDK without friction. Kotlin ergonomics are provided via extension helpers in `ktale-api`. - * - * "Sync" vs "async" semantics remain platform-defined; this class is a thin adapter. - */ -public class HookBackedScheduler( - private val hooks: PlatformSchedulerHooks, -) : Scheduler { - override fun runSync(task: Runnable): TaskHandle = hooks.runSync(task) - - override fun runAsync(task: Runnable): TaskHandle = hooks.runAsync(task) - - override fun runSyncDelayed(delay: Duration, task: Runnable): TaskHandle = hooks.runSyncDelayed(delay, task) - - override fun runAsyncDelayed(delay: Duration, task: Runnable): TaskHandle = hooks.runAsyncDelayed(delay, task) - - override fun runSyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable): TaskHandle = - hooks.runSyncRepeating(initialDelay, interval, task) - - override fun runAsyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable): TaskHandle = - hooks.runAsyncRepeating(initialDelay, interval, task) -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/services/SimpleServiceRegistry.kt b/ktale-core/src/main/kotlin/ktale/core/services/SimpleServiceRegistry.kt deleted file mode 100644 index 5a65513..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/services/SimpleServiceRegistry.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ktale.core.services - -import ktale.api.services.ServiceRegistry -import java.util.concurrent.ConcurrentHashMap - -/** - * Minimal thread-safe service registry. - * - * ## Design note - * This is intentionally tiny and runtime-oriented to keep KTale adaptable to unknown host lifecycles. - */ -public class SimpleServiceRegistry : ServiceRegistry { - private val services = ConcurrentHashMap, Any>() - - override fun register(type: Class, instance: T, replace: Boolean) { - if (!replace) { - val existing = services.putIfAbsent(type, instance) - if (existing != null) { - throw IllegalStateException("Service already registered for type ${type.name}") - } - } else { - services[type] = instance - } - } - - @Suppress("UNCHECKED_CAST") - override fun get(type: Class): T? = services[type] as T? - - override fun require(type: Class): T = - get(type) ?: throw NoSuchElementException("Missing service for type ${type.name}") - - override fun unregister(type: Class) { - services.remove(type) - } -} - - diff --git a/ktale-core/src/main/kotlin/ktale/core/threading/ThreadGuards.kt b/ktale-core/src/main/kotlin/ktale/core/threading/ThreadGuards.kt deleted file mode 100644 index b78da87..0000000 --- a/ktale-core/src/main/kotlin/ktale/core/threading/ThreadGuards.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ktale.core.threading - -import ktale.api.logging.KtaleLogger - -/** - * Conceptual thread guards (not enforced). - * - * ## Why this exists - * We don't know the eventual server threading model (if any). - * This is a lightweight mechanism that can be used to *document* expectations - * and to optionally log diagnostics when expectations are violated. - * - * ## Non-goal - * This does not attempt to control or enforce threads. - */ -public class ThreadGuards( - private val logger: KtaleLogger, -) { - /** Marker for "expected sync context". */ - public fun expectSync(note: String) { - logger.debug("ThreadGuard(sync): $note") - } - - /** Marker for "expected async context". */ - public fun expectAsync(note: String) { - logger.debug("ThreadGuard(async): $note") - } -} - - diff --git a/ktale-core/src/test/kotlin/ktale/core/commands/BridgedCommandRegistryTest.kt b/ktale-core/src/test/kotlin/ktale/core/commands/BridgedCommandRegistryTest.kt deleted file mode 100644 index c44334c..0000000 --- a/ktale-core/src/test/kotlin/ktale/core/commands/BridgedCommandRegistryTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package ktale.core.commands - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandResult -import ktale.api.commands.CommandSender -import ktale.platform.PlatformCommandBridge - -class BridgedCommandRegistryTest : FunSpec({ - test("register/unregister notify PlatformCommandBridge") { - val bridge = mockk(relaxed = true) - val base = SimpleCommandRegistry() - val reg = BridgedCommandRegistry(base, bridge) - - val def = object : CommandDefinition { - override val name = "ping" - override val aliases = emptySet() - override val description: String? = null - override val permission = null - override fun execute(context: CommandContext): CommandResult = CommandResult.Success - } - - reg.register(def) - verify { bridge.onRegister(def) } - - reg.unregister("ping") - verify { bridge.onUnregister("ping") } - } - - test("dispatch still delegates to underlying registry") { - val bridge = mockk(relaxed = true) - val base = SimpleCommandRegistry() - val reg = BridgedCommandRegistry(base, bridge) - - val def = object : CommandDefinition { - override val name = "ping" - override val aliases = emptySet() - override val description: String? = null - override val permission = null - override fun execute(context: CommandContext): CommandResult = CommandResult.Success - } - reg.register(def) - - val sender = mockk() - every { sender.name } returns "Tester" - every { sender.sendMessage(any()) } returns Unit - every { sender.hasPermission(any()) } returns true - - val ctx = object : CommandContext { - override val sender: CommandSender = sender - override val label: String = "ping" - override val args: List = emptyList() - } - - reg.dispatch(ctx) shouldBe CommandResult.Success - } -}) - - diff --git a/ktale-core/src/test/kotlin/ktale/core/commands/SimpleCommandRegistryTest.kt b/ktale-core/src/test/kotlin/ktale/core/commands/SimpleCommandRegistryTest.kt deleted file mode 100644 index 356b241..0000000 --- a/ktale-core/src/test/kotlin/ktale/core/commands/SimpleCommandRegistryTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package ktale.core.commands - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandResult -import ktale.api.commands.CommandSender -import ktale.api.commands.Permission - -class SimpleCommandRegistryTest : FunSpec({ - test("dispatch resolves aliases and checks permissions") { - val registry = SimpleCommandRegistry() - - val sender = mockk() - every { sender.name } returns "Tester" - every { sender.sendMessage(any()) } returns Unit - every { sender.hasPermission(any()) } returns false - - val ctx = object : CommandContext { - override val sender: CommandSender = sender - override val label: String = "KT" - override val args: List = emptyList() - } - - val perm = Permission.of("ktale.test") - registry.register( - object : CommandDefinition { - override val name: String = "kt" - override val aliases: Set = setOf("KT", "kT2") - override val description: String? = null - override val permission: Permission? = perm - override fun execute(context: CommandContext): CommandResult = CommandResult.Success - } - ) - - registry.dispatch(ctx) shouldBe CommandResult.NoPermission - - every { sender.hasPermission(perm) } returns true - registry.dispatch(ctx) shouldBe CommandResult.Success - } - - test("unknown command returns usage error") { - val registry = SimpleCommandRegistry() - val sender = mockk() - every { sender.name } returns "Tester" - every { sender.sendMessage(any()) } returns Unit - every { sender.hasPermission(any()) } returns true - - val ctx = object : CommandContext { - override val sender: CommandSender = sender - override val label: String = "doesnotexist" - override val args: List = emptyList() - } - - val res = registry.dispatch(ctx) - (res is CommandResult.UsageError) shouldBe true - } - - test("Commands Kotlin DSL builder produces a usable definition") { - val registry = SimpleCommandRegistry() - val sender = mockk() - every { sender.name } returns "Tester" - every { sender.sendMessage(any()) } returns Unit - every { sender.hasPermission(any()) } returns true - - registry.register( - Commands.command("ping") { - aliases("p") - execute { CommandResult.Success } - } - ) - - val ctx = object : CommandContext { - override val sender: CommandSender = sender - override val label: String = "p" - override val args: List = emptyList() - } - - registry.dispatch(ctx) shouldBe CommandResult.Success - } -}) - - diff --git a/ktale-core/src/test/kotlin/ktale/core/config/CoreConfigManagerYamlTest.kt b/ktale-core/src/test/kotlin/ktale/core/config/CoreConfigManagerYamlTest.kt deleted file mode 100644 index 3995413..0000000 --- a/ktale-core/src/test/kotlin/ktale/core/config/CoreConfigManagerYamlTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -package ktale.core.config - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import ktale.api.config.ConfigKey -import ktale.api.config.ConfigMigration -import ktale.core.config.yaml.YamlConfigCodec -import ktale.core.logging.SimpleConsoleLogger -import kotlinx.serialization.Serializable - -class CoreConfigManagerYamlTest : FunSpec({ - test("missing config loads defaults and persists with header") { - val store = InMemoryConfigTextStore() - val logger = SimpleConsoleLogger("test") - val mgr = CoreConfigManager(store, logger) - - val key = object : ConfigKey { - override val id = "test.yml" - override val version = 1 - override val codec = YamlConfigCodec(TestCfg.serializer()) - override fun defaultValue(): TestCfg = TestCfg(foo = "bar") - override val migrations: List = emptyList() - } - - mgr.load(key) shouldBe TestCfg(foo = "bar") - val raw = store.read("test.yml")!! - raw.lines().first().trim() shouldBe "ktaleConfigVersion: 1" - } - - test("migrations run on text before decode") { - val store = InMemoryConfigTextStore() - val logger = SimpleConsoleLogger("test") - val mgr = CoreConfigManager(store, logger) - - // Stored v0: foo: old - store.write( - "test.yml", - """ - ktaleConfigVersion: 0 - foo: old - """.trimIndent() - ) - - val key = object : ConfigKey { - override val id = "test.yml" - override val version = 1 - override val codec = YamlConfigCodec(TestCfg.serializer()) - override fun defaultValue(): TestCfg = TestCfg(foo = "default") - override val migrations: List = listOf( - object : ConfigMigration { - override val fromVersion: Int = 0 - override val toVersion: Int = 1 - override fun migrate(oldText: String): String = oldText.replace("foo: old", "foo: migrated") - } - ) - } - - mgr.load(key) shouldBe TestCfg(foo = "migrated") - store.read("test.yml")!!.lines().first().trim() shouldBe "ktaleConfigVersion: 1" - } - - test("decode failure falls back to defaults and overwrites stored value") { - val store = InMemoryConfigTextStore() - val logger = SimpleConsoleLogger("test") - val mgr = CoreConfigManager(store, logger) - - store.write( - "test.yml", - """ - ktaleConfigVersion: 1 - this is not yaml: - """.trimIndent() - ) - - val key = object : ConfigKey { - override val id = "test.yml" - override val version = 1 - override val codec = YamlConfigCodec(TestCfg.serializer()) - override fun defaultValue(): TestCfg = TestCfg(foo = "safe") - override val migrations: List = emptyList() - } - - mgr.load(key) shouldBe TestCfg(foo = "safe") - val rewritten = store.read("test.yml")!! - key.codec.decode(rewritten.lines().drop(1).joinToString("\n")) shouldBe TestCfg(foo = "safe") - } -}) - -@Serializable -private data class TestCfg(val foo: String) - - diff --git a/ktale-core/src/test/kotlin/ktale/core/events/SimpleEventBusTest.kt b/ktale-core/src/test/kotlin/ktale/core/events/SimpleEventBusTest.kt deleted file mode 100644 index 2edc511..0000000 --- a/ktale-core/src/test/kotlin/ktale/core/events/SimpleEventBusTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package ktale.core.events - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import ktale.api.events.Cancellable -import ktale.api.events.Event -import ktale.api.events.EventListener -import ktale.api.events.EventPriority - -class SimpleEventBusTest : FunSpec({ - test("dispatches in priority order") { - val bus = SimpleEventBus() - val order = mutableListOf() - - bus.subscribe(TestEvent::class.java, EventPriority.LATE, EventListener { order += "late" }) - bus.subscribe(TestEvent::class.java, EventPriority.EARLY, EventListener { order += "early" }) - bus.subscribe(TestEvent::class.java, EventPriority.NORMAL, EventListener { order += "normal" }) - bus.subscribe(TestEvent::class.java, EventPriority.FINAL, EventListener { order += "final" }) - - bus.post(TestEvent()) - - order shouldBe listOf("early", "normal", "late", "final") - } - - test("ignoreCancelled skips listeners") { - val bus = SimpleEventBus() - val order = mutableListOf() - - bus.subscribe( - TestCancellableEvent::class.java, - EventListener { order += "ignored" }, - EventPriority.NORMAL, - ignoreCancelled = true, - ) - bus.subscribe( - TestCancellableEvent::class.java, - EventListener { order += "always" }, - EventPriority.NORMAL, - ignoreCancelled = false, - ) - - bus.post(TestCancellableEvent(isCancelled = true)) - - order shouldBe listOf("always") - } -}) - -private class TestEvent : Event - -private data class TestCancellableEvent(override var isCancelled: Boolean) : Event, Cancellable - - diff --git a/ktale-core/src/test/kotlin/ktale/core/services/SimpleServiceRegistryTest.kt b/ktale-core/src/test/kotlin/ktale/core/services/SimpleServiceRegistryTest.kt deleted file mode 100644 index 7657ad2..0000000 --- a/ktale-core/src/test/kotlin/ktale/core/services/SimpleServiceRegistryTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ktale.core.services - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.assertions.throwables.shouldThrow - -class SimpleServiceRegistryTest : FunSpec({ - test("register/get/require/unregister works") { - val reg = SimpleServiceRegistry() - - reg.get(String::class.java) shouldBe null - shouldThrow { reg.require(String::class.java) } - - reg.register(String::class.java, "hello") - reg.get(String::class.java) shouldBe "hello" - reg.require(String::class.java) shouldBe "hello" - - reg.unregister(String::class.java) - reg.get(String::class.java) shouldBe null - } - - test("register without replace refuses overwrite") { - val reg = SimpleServiceRegistry() - reg.register(String::class.java, "a") - shouldThrow { - reg.register(String::class.java, "b", replace = false) - } - reg.get(String::class.java) shouldNotBe "b" - } - - test("register with replace overwrites") { - val reg = SimpleServiceRegistry() - reg.register(String::class.java, "a") - reg.register(String::class.java, "b", replace = true) - reg.get(String::class.java) shouldBe "b" - } -}) - - diff --git a/ktale-gradle-plugin/build.gradle.kts b/ktale-gradle-plugin/build.gradle.kts deleted file mode 100644 index 0769e4a..0000000 --- a/ktale-gradle-plugin/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -description = "Gradle plugin: emits .dependencies/.repositories resources for KTale runtime dependency resolution." - -plugins { - `java-gradle-plugin` -} - -dependencies { - // Compiled against Gradle APIs, provided by Gradle at runtime. - compileOnly(gradleApi()) -} - -gradlePlugin { - plugins { - create("ktaleDeps") { - id = "cc.modlabs.ktale-deps" - implementationClass = "cc.modlabs.ktale.gradle.KtaleDepsPlugin" - displayName = "KTale dependency manifest generator" - description = "Generates .dependencies/.repositories resources for runtime dependency resolution." - } - } -} - - diff --git a/ktale-gradle-plugin/src/main/kotlin/cc/modlabs/ktale/gradle/KtaleDepsPlugin.kt b/ktale-gradle-plugin/src/main/kotlin/cc/modlabs/ktale/gradle/KtaleDepsPlugin.kt deleted file mode 100644 index 352c448..0000000 --- a/ktale-gradle-plugin/src/main/kotlin/cc/modlabs/ktale/gradle/KtaleDepsPlugin.kt +++ /dev/null @@ -1,165 +0,0 @@ -package cc.modlabs.ktale.gradle - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property -import java.io.File -import java.net.URI - -/** - * Gradle plugin that emits `.dependencies` and optional `.repositories` resources into the jar. - * - * ## Purpose - * When KTale is used as a standalone/bundled server runtime, it can: - * - load a plugin jar - * - read `.dependencies` / `.repositories` - * - download those dependencies into a cache - * - build a plugin classloader without shading - * - * ## Design note - * This is based on the same idea as your `KPaperGradle` plugin, but intentionally does not - * generate any server-specific bootstrap classes. - * - * See: [KPaperGradle reference implementation](https://raw.githubusercontent.com/ModLabsCC/KPaperGradle/main/src/main/kotlin/cc/modlabs/kpapergradle/KPaperGradlePlugin.kt) - */ -public class KtaleDepsPlugin : Plugin { - override fun apply(project: Project) { - val ext = project.extensions.create("ktaleDeps", KtaleDepsExtension::class.java, project.objects) - - // Default mirror for resolving runtime dependencies - project.repositories.maven { it.url = URI.create("https://nexus.modlabs.cc/repository/maven-mirrors/") } - - val generateTask = project.tasks.register("generateKtaleDependencyManifest") { t -> - t.group = "build" - t.description = "Generates .dependencies and optional .repositories into resources for runtime resolution" - t.inputs.property("deliver", ext.deliverDependencies) - t.inputs.property("repos", ext.customRepositories) - t.inputs.property("pluginId", ext.pluginId.orNull) - t.inputs.property("mainClass", ext.mainClass.orNull) - - t.doLast { - val buildDir = project.layout.buildDirectory.asFile.get() - val genResDir = File(buildDir, "generated-resources/ktale-deps") - genResDir.mkdirs() - - val coords = mutableSetOf() - - // Prefer runtimeClasspath when present (typical for JVM artifacts) - val config = project.configurations.findByName("runtimeClasspath") - ?: project.configurations.findByName("compileClasspath") - - if (config != null && config.isCanBeResolved) { - config.resolvedConfiguration.firstLevelModuleDependencies.forEach { dep -> - val g = dep.moduleGroup - val n = dep.moduleName - val v = dep.moduleVersion - if (!g.isNullOrBlank() && !n.isNullOrBlank() && !v.isNullOrBlank()) { - coords += "$g:$n:$v" - } - } - } - - coords += ext.deliverDependencies - - File(genResDir, ".dependencies").writeText(coords.sorted().joinToString("\n") + "\n") - - val reposFile = File(genResDir, ".repositories") - if (ext.customRepositories.isNotEmpty()) { - val lines = ext.customRepositories.map { r -> - buildString { - append(r.id).append(' ').append(r.url) - if (!r.usernameEnv.isNullOrBlank() && !r.passwordEnv.isNullOrBlank()) { - append(' ').append(r.usernameEnv).append(' ').append(r.passwordEnv) - } - } - } - reposFile.writeText(lines.joinToString("\n") + "\n") - } else { - if (reposFile.exists()) reposFile.delete() - } - - // Optional: generate ktale-plugin.properties for standalone hosts - val pid = ext.pluginId.orNull?.trim().orEmpty() - val main = ext.mainClass.orNull?.trim().orEmpty() - val descriptor = File(genResDir, "ktale-plugin.properties") - if (pid.isNotBlank() && main.isNotBlank()) { - descriptor.writeText("id=$pid\nmain=$main\n") - } else { - if (descriptor.exists()) descriptor.delete() - } - } - } - - // Copy into the main resources directory so it ends up inside the jar. - project.tasks.matching { it.name == "processResources" }.configureEach { task -> - task.dependsOn(generateTask) - task.doLast { - val buildDir = project.layout.buildDirectory.asFile.get() - val genResDir = File(buildDir, "generated-resources/ktale-deps") - val resourcesDir = File(buildDir, "resources/main") - resourcesDir.mkdirs() - - val deps = File(genResDir, ".dependencies") - if (deps.exists()) deps.copyTo(File(resourcesDir, ".dependencies"), overwrite = true) - - val repos = File(genResDir, ".repositories") - if (repos.exists()) repos.copyTo(File(resourcesDir, ".repositories"), overwrite = true) - - val desc = File(genResDir, "ktale-plugin.properties") - if (desc.exists()) desc.copyTo(File(resourcesDir, "ktale-plugin.properties"), overwrite = true) - } - } - } -} - -public open class KtaleDepsExtension(objects: ObjectFactory) { - /** - * Extra dependencies that should be emitted even if they are not first-level runtime deps. - * - * Example: `deliver("com.foo:bar:1.2.3")` - */ - public val deliverDependencies: MutableList = mutableListOf() - - internal val customRepositories: MutableList = mutableListOf() - - /** Optional: plugin id to write into `ktale-plugin.properties`. */ - public val pluginId: Property = objects.property(String::class.java) - - /** Optional: main class to write into `ktale-plugin.properties`. */ - public val mainClass: Property = objects.property(String::class.java) - - public fun deliver(vararg deps: String) { - deliverDependencies += deps - } - - /** DSL: repository("https://repo1.maven.org/maven2/") */ - public fun repository(url: String) { - val host = try { URI(url).host ?: url } catch (_: Exception) { url } - val id = host.replace(Regex("[^a-zA-Z0-9-_]"), "-") - customRepositories += RepoSpec(id, url) - } - - /** DSL: repository("myRepo", "https://repo.example.com/maven/") */ - public fun repository(id: String, url: String) { - customRepositories += RepoSpec(id, url) - } - - /** - * DSL: repositoryWithAuth("private", "https://repo.example.com/maven/", "REPO_USER", "REPO_PASS") - * - * IMPORTANT: only the environment variable names are written into the jar. - */ - public fun repositoryWithAuth(id: String, url: String, userEnvVar: String, passEnvVar: String) { - customRepositories += RepoSpec(id, url, userEnvVar, passEnvVar) - } - - internal data class RepoSpec( - val id: String, - val url: String, - val usernameEnv: String? = null, - val passwordEnv: String? = null, - ) -} - - diff --git a/ktale-platform-fake/build.gradle.kts b/ktale-platform-fake/build.gradle.kts deleted file mode 100644 index 63992ef..0000000 --- a/ktale-platform-fake/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -description = "KTale deterministic fake platform for tests and demos (no real server required)." - -dependencies { - implementation(project(":ktale-api")) - implementation(project(":ktale-platform")) - implementation(project(":ktale-core")) -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/DeterministicClock.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/DeterministicClock.kt deleted file mode 100644 index 474b0d9..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/DeterministicClock.kt +++ /dev/null @@ -1,39 +0,0 @@ -package ktale.platform.fake - -import ktale.platform.PlatformClock -import java.time.Duration -import java.util.concurrent.atomic.AtomicLong - -/** - * Deterministic clock for tests and fake servers. - * - * ## Design note - * Both clocks advance together: - * - [nowEpochMillis] is derived from [epochMillis]. - * - [monotonicNanos] is derived from [monoNanos]. - */ -public class DeterministicClock( - startEpochMillis: Long = 0L, -) : PlatformClock { - private val epochMillis = AtomicLong(startEpochMillis) - private val monoNanos = AtomicLong(0L) - - override fun nowEpochMillis(): Long = epochMillis.get() - - override fun monotonicNanos(): Long = monoNanos.get() - - /** Advances the clock by [duration]. */ - public fun advanceBy(duration: Duration) { - val millis = duration.toMillis() - val nanos = duration.toNanos() - epochMillis.addAndGet(millis) - monoNanos.addAndGet(nanos) - } - - /** Sets absolute epoch millis (monotonic clock is unaffected). */ - public fun setEpochMillis(value: Long) { - epochMillis.set(value) - } -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeCommandBridge.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeCommandBridge.kt deleted file mode 100644 index 1dc0d75..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeCommandBridge.kt +++ /dev/null @@ -1,41 +0,0 @@ -package ktale.platform.fake - -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandRegistry -import ktale.api.commands.CommandResult -import ktale.platform.PlatformCommandBridge -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicReference - -/** - * Minimal command bridge for the fake platform. - * - * This bridge captures registrations for introspection and dispatches inbound commands into the bound registry. - */ -public class FakeCommandBridge : PlatformCommandBridge { - private val byName = ConcurrentHashMap() - private val registryRef = AtomicReference(null) - - override fun onRegister(definition: CommandDefinition) { - byName[definition.name.lowercase()] = definition - } - - override fun onUnregister(name: String) { - byName.remove(name.lowercase()) - } - - override fun bind(registry: CommandRegistry) { - registryRef.set(registry) - } - - override fun dispatchInbound(context: CommandContext): CommandResult { - val registry = registryRef.get() ?: error("FakeCommandBridge.bind(registry) was not called") - return registry.dispatch(context) - } - - /** Returns whether a command is registered (by primary name). */ - public fun isRegistered(name: String): Boolean = byName.containsKey(name.lowercase()) -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlatform.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlatform.kt deleted file mode 100644 index 340169f..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlatform.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ktale.platform.fake - -import ktale.api.logging.KtaleLogger -import ktale.core.logging.SimpleConsoleLogger -import ktale.platform.Platform -import ktale.platform.PlatformClock -import ktale.platform.PlatformCommandBridge -import ktale.platform.PlatformLoggerFactory -import ktale.platform.PlatformSchedulerHooks - -/** - * Fake platform implementation for tests and demos. - * - * ## Design note - * This platform is intentionally *fully controllable* and does not mimic any real server behavior. - * The goal is to let KTale core behavior be tested without a real server runtime. - */ -public class FakePlatform( - override val clock: DeterministicClock = DeterministicClock(), - public val schedulerHooks: FakeSchedulerHooks = FakeSchedulerHooks(clock), - public val commandBridge: FakeCommandBridge = FakeCommandBridge(), -) : Platform { - override val platformId: String = "fake" - - override val loggers: PlatformLoggerFactory = object : PlatformLoggerFactory { - override fun logger(name: String): KtaleLogger = SimpleConsoleLogger(name, nowEpochMillis = clock::nowEpochMillis) - } - - override val scheduler: PlatformSchedulerHooks - get() = schedulerHooks - - override val commands: PlatformCommandBridge - get() = commandBridge -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlayer.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlayer.kt deleted file mode 100644 index feffa84..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakePlayer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ktale.platform.fake - -import ktale.api.commands.CommandSender -import ktale.api.commands.Permission - -/** - * Minimal fake player. - * - * This implements [CommandSender] so commands can be tested without a real server. - */ -public class FakePlayer( - override val name: String, - private val permissions: MutableSet, -) : CommandSender { - public constructor(name: String) : this(name, mutableSetOf()) - - public val messages: MutableList = mutableListOf() - - override fun sendMessage(message: String) { - messages += message - } - - override fun hasPermission(permission: Permission): Boolean = - permissions.contains(permission.value) - - public fun grant(permission: String) { - permissions += permission - } -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeSchedulerHooks.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeSchedulerHooks.kt deleted file mode 100644 index d717ddd..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeSchedulerHooks.kt +++ /dev/null @@ -1,112 +0,0 @@ -package ktale.platform.fake - -import ktale.api.scheduler.TaskHandle -import ktale.platform.PlatformSchedulerHooks -import java.time.Duration -import java.util.PriorityQueue -import java.util.concurrent.atomic.AtomicLong - -/** - * Fully controllable scheduler hooks backed by a [DeterministicClock]. - * - * ## Control surface - * The platform API only exposes scheduling *hooks*; this fake exposes extra methods for tests: - * - [runDueSync] / [runDueAsync] - * - [advanceBy] (optionally auto-running due tasks) - * - * ## Semantics - * - "sync" and "async" are modeled as two independent queues. - * - Tasks only run when the test/server calls a control method. - */ -public class FakeSchedulerHooks( - private val clock: DeterministicClock, -) : PlatformSchedulerHooks { - private data class Scheduled( - val dueNanos: Long, - val intervalNanos: Long?, - val task: Runnable, - val handle: FakeTaskHandle, - val id: Long, - ) - - private val idSeq = AtomicLong(0L) - - private val syncQueue = PriorityQueue(compareBy({ it.dueNanos }, { it.id })) - private val asyncQueue = PriorityQueue(compareBy({ it.dueNanos }, { it.id })) - - override fun runSync(task: Runnable): TaskHandle = - schedule(syncQueue, Duration.ZERO, null, task) - - override fun runAsync(task: Runnable): TaskHandle = - schedule(asyncQueue, Duration.ZERO, null, task) - - override fun runSyncDelayed(delay: Duration, task: Runnable): TaskHandle = - schedule(syncQueue, delay, null, task) - - override fun runAsyncDelayed(delay: Duration, task: Runnable): TaskHandle = - schedule(asyncQueue, delay, null, task) - - override fun runSyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable): TaskHandle = - schedule(syncQueue, initialDelay, interval, task) - - override fun runAsyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable): TaskHandle = - schedule(asyncQueue, initialDelay, interval, task) - - private fun schedule( - q: PriorityQueue, - delay: Duration, - interval: Duration?, - task: Runnable, - ): TaskHandle { - val handle = FakeTaskHandle() - val due = clock.monotonicNanos() + delay.toNanos() - val id = idSeq.incrementAndGet() - q.add( - Scheduled( - dueNanos = due, - intervalNanos = interval?.toNanos(), - task = task, - handle = handle, - id = id, - ) - ) - return handle - } - - /** Runs all due sync tasks at the current clock time. */ - public fun runDueSync() { - runDue(syncQueue) - } - - /** Runs all due async tasks at the current clock time. */ - public fun runDueAsync() { - runDue(asyncQueue) - } - - /** Advances the clock by [duration]. */ - public fun advanceBy(duration: Duration, runDueAfterAdvance: Boolean = true) { - clock.advanceBy(duration) - if (runDueAfterAdvance) { - runDueSync() - runDueAsync() - } - } - - private fun runDue(q: PriorityQueue) { - val now = clock.monotonicNanos() - while (true) { - val next = q.peek() ?: return - if (next.dueNanos > now) return - q.poll() - if (!next.handle.isCancelled) { - next.task.run() - } - val interval = next.intervalNanos - if (interval != null && !next.handle.isCancelled) { - q.add(next.copy(dueNanos = next.dueNanos + interval)) - } - } - } -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeServer.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeServer.kt deleted file mode 100644 index d1e8dae..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeServer.kt +++ /dev/null @@ -1,48 +0,0 @@ -package ktale.platform.fake - -import ktale.api.KtalePlugin -import ktale.api.PluginContext -import ktale.api.events.Event -// DefaultPluginContext is composed by CoreRuntime. -import ktale.core.events.SimpleEventBus -import ktale.core.runtime.CoreRuntime - -/** - * Fully controllable fake server runtime. - * - * ## Purpose - * - Allows testing KTale-based plugins without any real server. - * - Provides deterministic time + manual scheduler execution. - * - Provides event simulation by posting into the core event bus. - */ -public class FakeServer( - public val platform: FakePlatform = FakePlatform(), -) { - private val runtime = CoreRuntime(platform, ktale.core.config.InMemoryConfigTextStore()) - - public val events: SimpleEventBus get() = runtime.events as SimpleEventBus - public val commands get() = runtime.commands - public val scheduler get() = runtime.scheduler - public val services get() = runtime.services - public val configs get() = runtime.configs - - public fun createContext(pluginId: String): PluginContext = - runtime.pluginContext(pluginId) - - /** Simulates the plugin lifecycle using the fake runtime. */ - public fun runPlugin(pluginId: String, plugin: KtalePlugin, block: (PluginContext) -> Unit = {}) { - val ctx = createContext(pluginId) - plugin.onLoad(ctx) - plugin.onEnable(ctx) - try { - block(ctx) - } finally { - plugin.onDisable(ctx) - } - } - - /** Posts an event into the core event bus. */ - public fun post(event: E): E = events.post(event) -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeTaskHandle.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeTaskHandle.kt deleted file mode 100644 index eb27a28..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeTaskHandle.kt +++ /dev/null @@ -1,17 +0,0 @@ -package ktale.platform.fake - -import ktale.api.scheduler.TaskHandle -import java.util.concurrent.atomic.AtomicBoolean - -internal class FakeTaskHandle : TaskHandle { - private val cancelled = AtomicBoolean(false) - - override fun cancel() { - cancelled.set(true) - } - - override val isCancelled: Boolean - get() = cancelled.get() -} - - diff --git a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeWorld.kt b/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeWorld.kt deleted file mode 100644 index 260719d..0000000 --- a/ktale-platform-fake/src/main/kotlin/ktale/platform/fake/FakeWorld.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ktale.platform.fake - -/** - * Minimal fake world. - * - * ## Design note - * This is intentionally tiny: KTale does not model full game entities/world state. - * Plugins can store their own metadata externally if needed. - */ -public data class FakeWorld( - public val id: String, - public val name: String = id, -) - - diff --git a/ktale-platform-fake/src/test/java/ktale/examples/JavaExamplePluginUsage.java b/ktale-platform-fake/src/test/java/ktale/examples/JavaExamplePluginUsage.java deleted file mode 100644 index 8d89dd5..0000000 --- a/ktale-platform-fake/src/test/java/ktale/examples/JavaExamplePluginUsage.java +++ /dev/null @@ -1,60 +0,0 @@ -package ktale.examples; - -import ktale.api.PluginContext; -import ktale.api.commands.CommandContext; -import ktale.api.commands.CommandResult; -import ktale.api.events.Event; -import ktale.api.events.EventListener; -import ktale.api.events.EventPriority; -import ktale.core.commands.Commands; -import ktale.platform.fake.FakePlayer; -import ktale.platform.fake.FakeServer; - -import java.time.Duration; - -/** - * This class exists to prove "Java-first" KTale API ergonomics at compile time. - * - * It is not a runtime demo and does not depend on any test framework. - */ -public final class JavaExamplePluginUsage { - private JavaExamplePluginUsage() {} - - public static void compileOnlyExample() { - FakeServer server = new FakeServer(); - PluginContext ctx = server.createContext("java-demo"); - - // Service registry - ctx.getServices().register(String.class, "hello"); - - // Events (Java-first subscribe) - ctx.getEvents().subscribe(TestEvent.class, EventPriority.NORMAL, new EventListener() { - @Override - public void onEvent(TestEvent event) { - ctx.getLogger().info("Received event from Java"); - } - }); - - // Commands (Java fluent builder from ktale-core) - ctx.getCommands().register( - Commands.command("ping") - .executor((CommandContext c) -> CommandResult.Success.INSTANCE) - .build() - ); - - // Scheduler (Java Runnable + java.time.Duration) - ctx.getScheduler().runSyncDelayed(Duration.ofMillis(10), () -> ctx.getLogger().info("tick")); - - // Fake inbound command execution - FakePlayer player = new FakePlayer("Alice"); - server.getPlatform().getCommandBridge().dispatchInbound(new CommandContext() { - @Override public ktale.api.commands.CommandSender getSender() { return player; } - @Override public String getLabel() { return "ping"; } - @Override public java.util.List getArgs() { return java.util.Collections.emptyList(); } - }); - } - - public static final class TestEvent implements Event {} -} - - diff --git a/ktale-platform-fake/src/test/kotlin/ktale/examples/KotlinExamplePluginUsage.kt b/ktale-platform-fake/src/test/kotlin/ktale/examples/KotlinExamplePluginUsage.kt deleted file mode 100644 index 038949b..0000000 --- a/ktale-platform-fake/src/test/kotlin/ktale/examples/KotlinExamplePluginUsage.kt +++ /dev/null @@ -1,58 +0,0 @@ -package ktale.examples - -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandResult -import ktale.api.events.Event -import ktale.api.events.subscribe -import ktale.api.services.register -import ktale.core.commands.Commands -import ktale.platform.fake.FakePlayer -import ktale.platform.fake.FakeServer -import java.time.Duration - -/** - * This file exists to prove Kotlin-first ergonomics at compile time. - * - * It is not a runtime demo and does not depend on any test framework. - */ -object KotlinExamplePluginUsage { - fun compileOnlyExample() { - val server = FakeServer() - val ctx = server.createContext("kotlin-demo") - - // Service registry (Kotlin reified helpers) - ctx.services.register("hello", replace = true) - - // Events (Kotlin reified subscribe) - ctx.events.subscribe { - ctx.logger.info("Received event from Kotlin") - } - - // Commands (Kotlin DSL builder from ktale-core) - ctx.commands.register( - Commands.command("ping") { - aliases("p") - execute { _: CommandContext -> CommandResult.Success } - } - ) - - // Scheduler (Kotlin extension helpers live in ktale-api) - ctx.scheduler.runSyncDelayed(Duration.ofMillis(10)) { - ctx.logger.info("tick") - } - - // Fake inbound command execution - val player = FakePlayer("Alice") - server.platform.commandBridge.dispatchInbound( - object : CommandContext { - override val sender = player - override val label = "ping" - override val args = emptyList() - } - ) - } - - class TestEvent : Event -} - - diff --git a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/AutoRegistrarSmokeTest.kt b/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/AutoRegistrarSmokeTest.kt deleted file mode 100644 index c3f10ce..0000000 --- a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/AutoRegistrarSmokeTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package ktale.platform.fake - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import ktale.api.autoregister.AutoCommand -import ktale.api.autoregister.SubscribeEvent -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandResult -import ktale.api.events.Event -import ktale.core.autoregister.AutoRegistrar - -class AutoRegistrarSmokeTest : FunSpec({ - test("auto-registers commands (by interface+annotation) and event listeners (by annotation)") { - val server = FakeServer() - val ctx = server.createContext("autoreg") - - AutoRegistrar.registerAllFromClasses( - listOf(AutoPingCommand::class.java, TestListener::class.java), - ctx, - ) - - // Command got bridged into FakeCommandBridge. - server.platform.commandBridge.isRegistered("ping") shouldBe true - - // Event listener is subscribed and receives events. - server.post(TestEvent()) - TestListener.seen shouldBe 1 - } -}) - -private class TestEvent : Event - -@AutoCommand -private class AutoPingCommand : CommandDefinition { - override val name: String = "ping" - override val aliases: Set = emptySet() - override val description: String? = null - override val permission = null - override fun execute(context: CommandContext): CommandResult = CommandResult.Success -} - -private class TestListener { - companion object { - @JvmStatic - var seen: Int = 0 - } - - @SubscribeEvent(TestEvent::class) - fun onTest(e: TestEvent) { - // Increment a static counter so we can assert that reflection invocation worked. - seen++ - } -} - - diff --git a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeSchedulerHooksTest.kt b/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeSchedulerHooksTest.kt deleted file mode 100644 index f0c9d67..0000000 --- a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeSchedulerHooksTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package ktale.platform.fake - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import java.time.Duration - -class FakeSchedulerHooksTest : FunSpec({ - test("delayed sync task runs only after time advance") { - val platform = FakePlatform() - val scheduler = platform.schedulerHooks - - var ran = 0 - scheduler.runSyncDelayed(Duration.ofMillis(50), Runnable { ran++ }) - - scheduler.runDueSync() - ran shouldBe 0 - - scheduler.advanceBy(Duration.ofMillis(49), runDueAfterAdvance = true) - ran shouldBe 0 - - scheduler.advanceBy(Duration.ofMillis(1), runDueAfterAdvance = true) - ran shouldBe 1 - } - - test("repeating task repeats at interval and can be cancelled") { - val platform = FakePlatform() - val scheduler = platform.schedulerHooks - - var ran = 0 - val handle = scheduler.runSyncRepeating( - initialDelay = Duration.ZERO, - interval = Duration.ofMillis(10), - task = Runnable { ran++ }, - ) - - scheduler.runDueSync() - ran shouldBe 1 - - scheduler.advanceBy(Duration.ofMillis(10), runDueAfterAdvance = true) - ran shouldBe 2 - - handle.cancel() - scheduler.advanceBy(Duration.ofMillis(100), runDueAfterAdvance = true) - ran shouldBe 2 - } -}) - - diff --git a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerEndToEndConfigSchedulerTest.kt b/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerEndToEndConfigSchedulerTest.kt deleted file mode 100644 index de4430e..0000000 --- a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerEndToEndConfigSchedulerTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -package ktale.platform.fake - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import ktale.api.KtalePlugin -import ktale.api.PluginContext -import ktale.api.config.ConfigCodec -import ktale.api.config.ConfigKey -import ktale.api.config.ConfigMigration -import java.time.Duration - -class FakeServerEndToEndConfigSchedulerTest : FunSpec({ - test("plugin can load config and schedule deterministic work via FakeServer") { - val server = FakeServer() - - val key = object : ConfigKey { - override val id: String = "demo.yml" - override val version: Int = 1 - override val codec: ConfigCodec = object : ConfigCodec { - override fun decode(text: String): TestCfg { - // Extremely small, format-agnostic "codec" for tests: - // expects a single line: message: - val line = text.lines().firstOrNull().orEmpty() - val prefix = "message:" - val v = line.substringAfter(prefix, "").trim() - return TestCfg(message = if (v.isBlank()) "hello" else v) - } - - override fun encode(value: TestCfg): String = "message: ${value.message}" - } - override fun defaultValue(): TestCfg = TestCfg(message = "hello") - override val migrations: List = emptyList() - } - - val plugin = object : KtalePlugin { - override fun onLoad(context: PluginContext) = Unit - - override fun onEnable(context: PluginContext) { - val cfg = context.configs.load(key) - var ran = 0 - context.scheduler.runSyncDelayed(Duration.ofMillis(10), Runnable { - ran++ - context.services.register(Int::class.java, ran, replace = true) - context.logger.info("Ran scheduled task: ${cfg.message}") - }) - - // deterministically run the scheduled task - server.platform.schedulerHooks.advanceBy(Duration.ofMillis(10), runDueAfterAdvance = true) - context.services.require(Int::class.java) shouldBe 1 - } - - override fun onDisable(context: PluginContext) = Unit - } - - server.runPlugin("demo", plugin) - } -}) - -private data class TestCfg(val message: String) - - diff --git a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerSmokeTest.kt b/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerSmokeTest.kt deleted file mode 100644 index 56b51a1..0000000 --- a/ktale-platform-fake/src/test/kotlin/ktale/platform/fake/FakeServerSmokeTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package ktale.platform.fake - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import ktale.api.KtalePlugin -import ktale.api.PluginContext -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandResult - -class FakeServerSmokeTest : FunSpec({ - test("fake server can run plugin lifecycle and dispatch commands") { - val server = FakeServer() - - val plugin = object : KtalePlugin { - override fun onLoad(context: PluginContext) = Unit - - override fun onEnable(context: PluginContext) { - context.commands.register( - object : CommandDefinition { - override val name: String = "ping" - override val aliases: Set = emptySet() - override val description: String? = null - override val permission = null - override fun execute(context: CommandContext): CommandResult = CommandResult.Success - } - ) - } - - override fun onDisable(context: PluginContext) = Unit - } - - server.runPlugin("test", plugin) { ctx -> - val sender = FakePlayer("Alice") - val res = server.platform.commandBridge.dispatchInbound( - object : CommandContext { - override val sender = sender - override val label = "ping" - override val args = emptyList() - } - ) - res shouldBe CommandResult.Success - } - } -}) - - diff --git a/ktale-platform-hytale/build.gradle.kts b/ktale-platform-hytale/build.gradle.kts deleted file mode 100644 index d0ffbb2..0000000 --- a/ktale-platform-hytale/build.gradle.kts +++ /dev/null @@ -1,8 +0,0 @@ -description = "KTale Hytale platform adapter placeholder (intentionally incomplete)." - -dependencies { - implementation(project(":ktale-api")) - implementation(project(":ktale-platform")) -} - - diff --git a/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/CommandRoutingHypothesis.kt b/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/CommandRoutingHypothesis.kt deleted file mode 100644 index 1a33fc6..0000000 --- a/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/CommandRoutingHypothesis.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPECULATION RATIONALE: - * We do not know how Hytale will represent commands (string-based, structured, chat-driven, etc.). - * This sketch exists to reason about what the platform adapter might need to translate. - * - * CONFIDENCE: LOW - * - * This file lives in `ktale.experimental.hypothesis` and is OPTIONAL/REMOVABLE. - * Core modules must not depend on it. - */ -package ktale.experimental.hypothesis - -/** - * Hypothetical shapes of inbound command input a host might provide. - */ -public sealed interface CommandRoutingHypothesis { - /** Host provides raw text after a leading slash (or similar). */ - public data class RawLine(val line: String) : CommandRoutingHypothesis - - /** Host provides a pre-tokenized representation. */ - public data class Tokens(val label: String, val args: List) : CommandRoutingHypothesis - - /** Host provides a structured argument model. */ - public data object Structured : CommandRoutingHypothesis -} - - diff --git a/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ServerLifecycleHypothesis.kt b/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ServerLifecycleHypothesis.kt deleted file mode 100644 index ca53644..0000000 --- a/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ServerLifecycleHypothesis.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPECULATION RATIONALE: - * Hytale server lifecycle contracts are unknown. Many server runtimes have phases roughly like - * "load -> enable -> disable", but we do not know: - * - whether plugins are isolated per world/realm - * - whether reloads exist - * - whether enable is synchronous or async - * - * CONFIDENCE: MEDIUM - * - * This file lives in `ktale.experimental.hypothesis` and is OPTIONAL/REMOVABLE. - * Core modules must not depend on it. - */ -package ktale.experimental.hypothesis - -/** - * Hypothetical lifecycle phases a host runtime might expose. - * - * ## Non-guarantee - * These are NOT promises about Hytale. They are a vocabulary to discuss potential mapping. - */ -public enum class ServerLifecycleHypothesis { - DISCOVERY, - LOAD, - ENABLE, - DISABLE, - SHUTDOWN, -} - - diff --git a/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ThreadingHypothesis.kt b/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ThreadingHypothesis.kt deleted file mode 100644 index 21b938d..0000000 --- a/ktale-platform-hytale/src/main/kotlin/ktale/experimental/hypothesis/ThreadingHypothesis.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPECULATION RATIONALE: - * We do not know Hytale's server threading model. Possible models include: - * - single-threaded "main loop" with background workers - * - region/world partitioned threads - * - fully async actor-style model - * - * CONFIDENCE: LOW - * - * This file lives in `ktale.experimental.hypothesis` and is OPTIONAL/REMOVABLE. - * Core modules must not depend on it. - */ -package ktale.experimental.hypothesis - -/** - * Hypothetical execution contexts that a server might provide. - * - * KTale's `sync`/`async` terminology is intentionally abstract; this enum is only a speculative mapping aid. - */ -public enum class ThreadingHypothesis { - MAIN_THREAD, - WORKER_POOL, - REGION_THREAD, - ASYNC_RUNTIME, -} - - diff --git a/ktale-platform-hytale/src/main/kotlin/ktale/platform/hytale/HytalePlatformPlaceholder.kt b/ktale-platform-hytale/src/main/kotlin/ktale/platform/hytale/HytalePlatformPlaceholder.kt deleted file mode 100644 index 2458e73..0000000 --- a/ktale-platform-hytale/src/main/kotlin/ktale/platform/hytale/HytalePlatformPlaceholder.kt +++ /dev/null @@ -1,82 +0,0 @@ -package ktale.platform.hytale - -import ktale.api.logging.KtaleLogger -import ktale.api.logging.LogLevel -import ktale.platform.Platform -import ktale.platform.PlatformClock -import ktale.platform.PlatformCommandBridge -import ktale.platform.PlatformLoggerFactory -import ktale.platform.PlatformSchedulerHooks -import java.time.Duration - -/** - * Placeholder platform adapter for Hytale. - * - * ## Important: intentionally incomplete - * The Hytale server software and API do not exist yet (for us), so this module must not - * pretend we can integrate. This class exists only to: - * - provide a compile-time location for a future adapter - * - make uncertainty explicit through TODOs - * - * ## Rules - * - No assumptions about real APIs - * - No concrete integration logic - * - Stubs must fail loudly if used - */ -public class HytalePlatformPlaceholder : Platform { - override val platformId: String = "hytale-placeholder" - - override val clock: PlatformClock = object : PlatformClock { - override fun nowEpochMillis(): Long = error("TODO(Hytale): Provide real time source from host runtime") - override fun monotonicNanos(): Long = error("TODO(Hytale): Provide monotonic clock from host runtime") - } - - override val loggers: PlatformLoggerFactory = object : PlatformLoggerFactory { - override fun logger(name: String): KtaleLogger = object : KtaleLogger { - override fun log( - level: LogLevel, - message: String, - throwable: Throwable?, - context: Map, - ) { - error("TODO(Hytale): Provide real logging backend. Tried to log [$level] $name: $message") - } - } - } - - override val scheduler: PlatformSchedulerHooks = object : PlatformSchedulerHooks { - override fun runSync(task: Runnable) = error("TODO(Hytale): Wire sync scheduling to host") - override fun runAsync(task: Runnable) = error("TODO(Hytale): Wire async scheduling to host") - override fun runSyncDelayed(delay: Duration, task: Runnable) = - error("TODO(Hytale): Wire delayed sync scheduling to host (delay=$delay)") - - override fun runAsyncDelayed(delay: Duration, task: Runnable) = - error("TODO(Hytale): Wire delayed async scheduling to host (delay=$delay)") - - override fun runSyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable) = - error("TODO(Hytale): Wire repeating sync scheduling to host (initialDelay=$initialDelay interval=$interval)") - - override fun runAsyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable) = - error("TODO(Hytale): Wire repeating async scheduling to host (initialDelay=$initialDelay interval=$interval)") - } - - override val commands: PlatformCommandBridge = object : PlatformCommandBridge { - override fun onRegister(definition: ktale.api.commands.CommandDefinition) { - error("TODO(Hytale): Register command with host (name=${definition.name})") - } - - override fun onUnregister(name: String) { - error("TODO(Hytale): Unregister command with host (name=$name)") - } - - override fun bind(registry: ktale.api.commands.CommandRegistry) { - error("TODO(Hytale): Bind inbound command execution to registry") - } - - override fun dispatchInbound(context: ktale.api.commands.CommandContext): ktale.api.commands.CommandResult { - error("TODO(Hytale): Dispatch inbound command from host into KTale") - } - } -} - - diff --git a/ktale-platform/build.gradle.kts b/ktale-platform/build.gradle.kts deleted file mode 100644 index 717d9c6..0000000 --- a/ktale-platform/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -description = "KTale platform boundary (no game logic)." - -dependencies { - implementation(project(":ktale-api")) -} - - diff --git a/ktale-platform/src/main/kotlin/ktale/platform/Platform.kt b/ktale-platform/src/main/kotlin/ktale/platform/Platform.kt deleted file mode 100644 index a1af13e..0000000 --- a/ktale-platform/src/main/kotlin/ktale/platform/Platform.kt +++ /dev/null @@ -1,47 +0,0 @@ -package ktale.platform - -/** - * Platform boundary for KTale. - * - * ## Design note (architectural rule) - * This module defines *only* the boundary. It contains: - * - no game logic - * - no assumptions about Hytale APIs - * - no entity/world modeling - * - * ## Unknown host model (explicit uncertainty) - * KTale deliberately does not commit (yet) to whether it will be used as: - * - a plugin/mod SDK hosted by an official server runtime, or - * - a bundled/custom server software distribution that embeds KTale as its core. - * - * Both models can be represented by an implementation of this boundary without changing `ktale-api`. - * - * Core implementations may depend on this boundary, but the boundary must remain portable - * across unknown future server APIs. - */ -public interface Platform { - /** Stable identifier for diagnostics (e.g. "fake", "hytale", "custom"). */ - public val platformId: String - - /** Platform-provided time source. */ - public val clock: PlatformClock - - /** Platform-provided logging backend. */ - public val loggers: PlatformLoggerFactory - - /** - * Scheduler hooks for sync/async execution. - * - * Platforms define what "sync" and "async" mean. - */ - public val scheduler: PlatformSchedulerHooks - - /** - * Command IO bridge. - * - * This is the platform boundary for command registration and inbound command execution. - */ - public val commands: PlatformCommandBridge -} - - diff --git a/ktale-platform/src/main/kotlin/ktale/platform/PlatformClock.kt b/ktale-platform/src/main/kotlin/ktale/platform/PlatformClock.kt deleted file mode 100644 index 2fc6510..0000000 --- a/ktale-platform/src/main/kotlin/ktale/platform/PlatformClock.kt +++ /dev/null @@ -1,24 +0,0 @@ -package ktale.platform - -/** - * Platform-provided time source. - * - * ## Design note - * KTale avoids tying itself to any specific time API (tick counters, wall clocks, etc.). - * This contract provides both: - * - a wall-ish clock ([nowEpochMillis]) for timestamps/logging - * - a monotonic clock ([monotonicNanos]) for scheduling/drift-safe measurements - */ -public interface PlatformClock { - /** Current wall-ish time in epoch milliseconds. */ - public fun nowEpochMillis(): Long - - /** - * Current monotonic time in nanoseconds. - * - * Values have no meaning as absolute timestamps; only differences are meaningful. - */ - public fun monotonicNanos(): Long -} - - diff --git a/ktale-platform/src/main/kotlin/ktale/platform/PlatformCommandBridge.kt b/ktale-platform/src/main/kotlin/ktale/platform/PlatformCommandBridge.kt deleted file mode 100644 index 7f920af..0000000 --- a/ktale-platform/src/main/kotlin/ktale/platform/PlatformCommandBridge.kt +++ /dev/null @@ -1,47 +0,0 @@ -package ktale.platform - -import ktale.api.commands.CommandContext -import ktale.api.commands.CommandDefinition -import ktale.api.commands.CommandRegistry -import ktale.api.commands.CommandResult - -/** - * Platform boundary for command IO. - * - * ## Responsibilities - * - Let core register/unregister commands with the host (if the host supports it). - * - Allow the platform adapter to connect inbound command input to a [CommandRegistry]. - * - * ## Anti-goal - * This does not define a parsing engine. Platforms decide how input is tokenized and how - * help/completions are implemented (if at all). - */ -public interface PlatformCommandBridge { - /** - * Called by core when a command is registered. - * - * Platforms may use this to expose the command to the host so that input routes into KTale. - */ - public fun onRegister(definition: CommandDefinition) - - /** - * Called by core when a command is unregistered. - */ - public fun onUnregister(name: String) - - /** - * Binds inbound command execution to a registry. - * - * Platform adapters should call this once they have a [registry] instance to dispatch into. - */ - public fun bind(registry: CommandRegistry) - - /** - * Convenience method for platform adapters to dispatch an inbound command to the bound registry. - * - * Implementations may throw if [bind] has not been called. - */ - public fun dispatchInbound(context: CommandContext): CommandResult -} - - diff --git a/ktale-platform/src/main/kotlin/ktale/platform/PlatformLoggerFactory.kt b/ktale-platform/src/main/kotlin/ktale/platform/PlatformLoggerFactory.kt deleted file mode 100644 index 5744ddf..0000000 --- a/ktale-platform/src/main/kotlin/ktale/platform/PlatformLoggerFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ktale.platform - -import ktale.api.logging.KtaleLogger - -/** - * Factory for platform-backed loggers. - * - * Platforms may route logs to consoles, files, structured telemetry, or remote sinks. - */ -public interface PlatformLoggerFactory { - /** - * Creates a logger scoped to [name]. - * - * Conventionally, platforms should scope by plugin id (e.g. "myplugin") and/or component. - */ - public fun logger(name: String): KtaleLogger -} - - diff --git a/ktale-platform/src/main/kotlin/ktale/platform/PlatformSchedulerHooks.kt b/ktale-platform/src/main/kotlin/ktale/platform/PlatformSchedulerHooks.kt deleted file mode 100644 index dc42e38..0000000 --- a/ktale-platform/src/main/kotlin/ktale/platform/PlatformSchedulerHooks.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ktale.platform - -import ktale.api.scheduler.TaskHandle -import java.time.Duration - -/** - * Platform scheduler hooks. - * - * ## Design note - * This is a low-level hook surface used by `ktale-core` and adapters to map KTale scheduling concepts - * onto a host runtime. - * - * Implementations should be deterministic where possible and make [TaskHandle.cancel] idempotent. - */ -public interface PlatformSchedulerHooks { - public fun runSync(task: Runnable): TaskHandle - public fun runAsync(task: Runnable): TaskHandle - - public fun runSyncDelayed(delay: Duration, task: Runnable): TaskHandle - public fun runAsyncDelayed(delay: Duration, task: Runnable): TaskHandle - - public fun runSyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable): TaskHandle - public fun runAsyncRepeating(initialDelay: Duration, interval: Duration, task: Runnable): TaskHandle -} - - diff --git a/ktale-runtime-deps/build.gradle.kts b/ktale-runtime-deps/build.gradle.kts deleted file mode 100644 index 8edc21d..0000000 --- a/ktale-runtime-deps/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -description = "KTale runtime dependency manifest + resolver (for standalone host runtimes)." - -dependencies { - // Maven Resolver (Aether) - used for downloading plugin dependencies at runtime. - api("org.apache.maven.resolver:maven-resolver-impl:1.9.22") - api("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.22") - api("org.apache.maven.resolver:maven-resolver-transport-http:1.9.22") - api("org.apache.maven.resolver:maven-resolver-util:1.9.22") -} - - diff --git a/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifest.kt b/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifest.kt deleted file mode 100644 index 6ed359e..0000000 --- a/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ktale.runtime.deps - -/** - * A runtime dependency manifest. - * - * ## Purpose - * This exists for the "KTale as a bundled/standalone server" host model: - * plugins can remain unshaded (clean jars) while the host downloads dependencies on demand. - * - * ## Non-goal - * This does not prescribe *how* classpaths are modified. - * A host runtime can use the resolved jar files to build isolated classloaders. - */ -public data class DependencyManifest( - /** Maven coordinates in `group:artifact:version` form. */ - public val coordinates: List, - /** Optional additional repositories (id + url). */ - public val repositories: List, -) { - public data class Repository( - public val id: String, - public val url: String, - /** - * Optional env var name for username. - * - * IMPORTANT: actual secrets must not be stored in jars; only env var names are recorded. - */ - public val usernameEnv: String? = null, - /** Optional env var name for password/token. */ - public val passwordEnv: String? = null, - ) -} - - diff --git a/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifestReader.kt b/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifestReader.kt deleted file mode 100644 index 6b91b9b..0000000 --- a/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/DependencyManifestReader.kt +++ /dev/null @@ -1,53 +0,0 @@ -package ktale.runtime.deps - -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader -import java.nio.charset.StandardCharsets - -/** - * Reads `.dependencies` and `.repositories` resources from a jar/plugin classloader. - * - * ## File format - * - `.dependencies`: one Maven coordinate per line (`group:artifact:version`), `#` comments allowed. - * - `.repositories`: `id url` per line, `#` comments allowed. - */ -public object DependencyManifestReader { - public fun fromResources( - classLoader: ClassLoader, - dependenciesResource: String = ".dependencies", - repositoriesResource: String = ".repositories", - ): DependencyManifest { - val coords = readLines(classLoader.getResourceAsStream(dependenciesResource)) - val repos = readLines(classLoader.getResourceAsStream(repositoriesResource)) - .mapNotNull { parseRepoLine(it) } - return DependencyManifest(coords, repos) - } - - private fun readLines(stream: InputStream?): List { - if (stream == null) return emptyList() - stream.use { - val r = BufferedReader(InputStreamReader(it, StandardCharsets.UTF_8)) - return r.lineSequence() - .map { line -> line.trim() } - .filter { line -> line.isNotEmpty() && !line.startsWith("#") } - .toList() - } - } - - private fun parseRepoLine(line: String): DependencyManifest.Repository? { - // Supported formats: - // - "id url" - // - "id url USER_ENV PASS_ENV" - val parts = line.split(Regex("\\s+")) - if (parts.size < 2) return null - val id = parts[0].trim() - val url = parts[1].trim() - if (id.isEmpty() || url.isEmpty()) return null - val userEnv = parts.getOrNull(2)?.trim()?.takeIf { it.isNotEmpty() } - val passEnv = parts.getOrNull(3)?.trim()?.takeIf { it.isNotEmpty() } - return DependencyManifest.Repository(id, url, userEnv, passEnv) - } -} - - diff --git a/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/MavenDependencyResolver.kt b/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/MavenDependencyResolver.kt deleted file mode 100644 index a16d6fe..0000000 --- a/ktale-runtime-deps/src/main/kotlin/ktale/runtime/deps/MavenDependencyResolver.kt +++ /dev/null @@ -1,99 +0,0 @@ -package ktale.runtime.deps - -import org.eclipse.aether.DefaultRepositorySystemSession -import org.eclipse.aether.RepositorySystem -import org.eclipse.aether.RepositorySystemSession -import org.eclipse.aether.artifact.DefaultArtifact -import org.eclipse.aether.collection.CollectRequest -import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory -import org.eclipse.aether.graph.Dependency -import org.eclipse.aether.impl.DefaultServiceLocator -import org.eclipse.aether.repository.LocalRepository -import org.eclipse.aether.repository.RemoteRepository -import org.eclipse.aether.resolution.DependencyRequest -import org.eclipse.aether.spi.connector.RepositoryConnectorFactory -import org.eclipse.aether.transport.http.HttpTransporterFactory -import org.eclipse.aether.spi.connector.transport.TransporterFactory -import org.eclipse.aether.util.repository.AuthenticationBuilder -import java.nio.file.Path - -/** - * Resolves Maven coordinates to local jar files using Maven Resolver (Aether). - * - * ## Intended usage - * This is for a standalone KTale host runtime that builds plugin classloaders from resolved jars. - * - * ## Non-goal - * This class does not modify classpaths. It only downloads and returns file paths. - */ -public class MavenDependencyResolver( - cacheDir: Path, - repositories: List = listOf( - RemoteRepository.Builder("modlabs-mirror", "default", "https://nexus.modlabs.cc/repository/maven-mirrors/").build() - ), -) { - private val repoSystem: RepositorySystem = newRepoSystem() - private val session: RepositorySystemSession = newSession(repoSystem, cacheDir) - private val repos: List = repositories - - /** - * Resolves all [coordinates] (and their transitive runtime dependencies) to local jar paths. - * - * @param coordinates Maven coords in `group:artifact:version` - */ - public fun resolve(coordinates: List): List { - val results = mutableListOf() - for (coord in coordinates) { - val rootDep = Dependency(DefaultArtifact(coord), "runtime") - val collect = CollectRequest().apply { - root = rootDep - repositories = repos - } - val request = DependencyRequest(collect, null) - val resolved = repoSystem.resolveDependencies(session, request) - for (artifactResult in resolved.artifactResults) { - val file = artifactResult.artifact.file ?: continue - results.add(file.toPath()) - } - } - return results.distinct() - } - - public companion object { - /** - * Builds a [RemoteRepository], optionally attaching authentication from environment variables. - * - * @param usernameEnv env var name for username, if any - * @param passwordEnv env var name for password/token, if any - */ - public fun repo( - id: String, - url: String, - usernameEnv: String? = null, - passwordEnv: String? = null, - ): RemoteRepository { - val builder = RemoteRepository.Builder(id, "default", url) - val user = usernameEnv?.let { System.getenv(it) }?.takeIf { it.isNotBlank() } - val pass = passwordEnv?.let { System.getenv(it) }?.takeIf { it.isNotBlank() } - if (user != null && pass != null) { - builder.setAuthentication(AuthenticationBuilder().addUsername(user).addPassword(pass).build()) - } - return builder.build() - } - - private fun newRepoSystem(): RepositorySystem { - val locator = DefaultServiceLocator() - locator.addService(RepositoryConnectorFactory::class.java, BasicRepositoryConnectorFactory::class.java) - locator.addService(TransporterFactory::class.java, HttpTransporterFactory::class.java) - return locator.getService(RepositorySystem::class.java) - } - - private fun newSession(system: RepositorySystem, cacheDir: Path): DefaultRepositorySystemSession { - val session = DefaultRepositorySystemSession() - session.localRepositoryManager = system.newLocalRepositoryManager(session, LocalRepository(cacheDir.toFile())) - return session - } - } -} - - diff --git a/ktale-runtime-deps/src/test/kotlin/ktale/runtime/deps/DependencyManifestReaderTest.kt b/ktale-runtime-deps/src/test/kotlin/ktale/runtime/deps/DependencyManifestReaderTest.kt deleted file mode 100644 index d35663b..0000000 --- a/ktale-runtime-deps/src/test/kotlin/ktale/runtime/deps/DependencyManifestReaderTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package ktale.runtime.deps - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import java.io.ByteArrayInputStream -import java.net.URL -import java.net.URLConnection -import java.net.URLStreamHandler -import java.util.Collections - -class DependencyManifestReaderTest : FunSpec({ - test("reads dependencies and repositories resources") { - val loader = MapResourceClassLoader( - mapOf( - ".dependencies" to """ - # comment - cc.modlabs:ktale-api:1.0.0 - - org.example:lib:2.0.0 - """.trimIndent(), - ".repositories" to """ - # comment - modlabs https://nexus.modlabs.cc/repository/maven-mirrors/ - priv https://repo.example.com/maven/ USER_ENV PASS_ENV - """.trimIndent(), - ) - ) - - val manifest = DependencyManifestReader.fromResources(loader) - manifest.coordinates shouldBe listOf("cc.modlabs:ktale-api:1.0.0", "org.example:lib:2.0.0") - manifest.repositories shouldBe listOf( - DependencyManifest.Repository("modlabs", "https://nexus.modlabs.cc/repository/maven-mirrors/"), - DependencyManifest.Repository("priv", "https://repo.example.com/maven/", "USER_ENV", "PASS_ENV"), - ) - } -}) - -private class MapResourceClassLoader( - private val resources: Map, -) : ClassLoader(null) { - override fun getResourceAsStream(name: String): java.io.InputStream? { - val v = resources[name] ?: return null - return ByteArrayInputStream(v.toByteArray(Charsets.UTF_8)) - } -} - - diff --git a/ktale-runtime-host/build.gradle.kts b/ktale-runtime-host/build.gradle.kts deleted file mode 100644 index 1af58e0..0000000 --- a/ktale-runtime-host/build.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -description = "Standalone KTale plugin host (loads plugin jars + resolves deps, builds isolated classloaders)." - -dependencies { - implementation(project(":ktale-api")) - implementation(project(":ktale-platform")) - implementation(project(":ktale-core")) - implementation(project(":ktale-runtime-deps")) -} - - diff --git a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/JarPluginDescriptorReader.kt b/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/JarPluginDescriptorReader.kt deleted file mode 100644 index fed031d..0000000 --- a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/JarPluginDescriptorReader.kt +++ /dev/null @@ -1,39 +0,0 @@ -package ktale.runtime.host - -import java.io.InputStream -import java.nio.file.Path -import java.util.Properties -import java.util.jar.JarFile - -/** - * Reads `ktale-plugin.properties` from a plugin jar. - * - * Expected keys: - * - `id` (required) - * - `main` (required) fully qualified class name implementing `ktale.api.KtalePlugin` - */ -public object JarPluginDescriptorReader { - public const val DEFAULT_RESOURCE: String = "ktale-plugin.properties" - - public fun read(jarPath: Path, resourceName: String = DEFAULT_RESOURCE): PluginDescriptor { - JarFile(jarPath.toFile()).use { jar -> - val entry = jar.getJarEntry(resourceName) - ?: error("Missing $resourceName in plugin jar: $jarPath") - jar.getInputStream(entry).use { stream -> - return parse(stream) - } - } - } - - public fun parse(input: InputStream): PluginDescriptor { - val props = Properties() - props.load(input) - val id = props.getProperty("id")?.trim().orEmpty() - val main = props.getProperty("main")?.trim().orEmpty() - require(id.isNotBlank()) { "ktale-plugin.properties missing required key: id" } - require(main.isNotBlank()) { "ktale-plugin.properties missing required key: main" } - return PluginDescriptor(id, main) - } -} - - diff --git a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/PluginDescriptor.kt b/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/PluginDescriptor.kt deleted file mode 100644 index 83aa4a5..0000000 --- a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/PluginDescriptor.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ktale.runtime.host - -/** - * Minimal standalone-host plugin descriptor. - * - * ## Design note (explicitly host-specific) - * This is NOT part of `ktale-api` because it assumes a particular packaging model: - * a plugin jar carries a properties file naming its entrypoint class. - */ -public data class PluginDescriptor( - public val id: String, - public val mainClass: String, -) - - diff --git a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginClassLoaderFactory.kt b/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginClassLoaderFactory.kt deleted file mode 100644 index e8cb438..0000000 --- a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginClassLoaderFactory.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ktale.runtime.host - -import java.net.URL -import java.net.URLClassLoader -import java.nio.file.Path - -/** - * Builds an isolated plugin classloader for a standalone host. - * - * ## Design note - * - Parent is the host classloader (provides KTale + server runtime types). - * - URLs include plugin jar + resolved dependency jars. - * - Classloader strategy (parent-first vs child-first) is a host decision; we start parent-first. - */ -public object StandalonePluginClassLoaderFactory { - public fun create(pluginJar: Path, dependencyJars: List, parent: ClassLoader): URLClassLoader { - val urls: Array = (listOf(pluginJar) + dependencyJars) - .distinct() - .map { it.toUri().toURL() } - .toTypedArray() - return URLClassLoader(urls, parent) - } -} - - diff --git a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginHost.kt b/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginHost.kt deleted file mode 100644 index 4df7452..0000000 --- a/ktale-runtime-host/src/main/kotlin/ktale/runtime/host/StandalonePluginHost.kt +++ /dev/null @@ -1,109 +0,0 @@ -package ktale.runtime.host - -import ktale.api.KtalePlugin -import ktale.core.autoregister.AutoRegistrar -import ktale.core.config.FileConfigTextStore -import ktale.core.runtime.CoreRuntime -import ktale.platform.Platform -import ktale.runtime.deps.DependencyManifestReader -import ktale.runtime.deps.MavenDependencyResolver -import java.net.URLClassLoader -import java.nio.file.Path -import java.util.concurrent.ConcurrentHashMap - -/** - * Standalone KTale plugin host. - * - * ## Explicit assumptions (host-specific) - * - Plugins are packaged as jars that contain: - * - `ktale-plugin.properties` with `id` + `main`, and optionally - * - `.dependencies` / `.repositories` for runtime resolution. - * - The host builds an isolated classloader per plugin. - * - * This is intentionally *not* part of `ktale-api` because it assumes a packaging model. - */ -public class StandalonePluginHost( - private val platform: Platform, - private val cacheDir: Path, - private val pluginDataDir: Path, -) { - private val loaded = ConcurrentHashMap() - - public fun load(pluginJar: Path): LoadedPlugin { - val descriptor = JarPluginDescriptorReader.read(pluginJar) - - if (loaded.containsKey(descriptor.id)) { - error("Plugin already loaded: ${descriptor.id}") - } - - val manifest = DependencyManifestReader.fromResources( - classLoader = jarOnlyClassLoader(pluginJar), - ) - - val repos = buildRepos(manifest.repositories) - val resolver = MavenDependencyResolver(cacheDir, repos) - val depJars = resolver.resolve(manifest.coordinates) - - val cl = StandalonePluginClassLoaderFactory.create(pluginJar, depJars, parent = javaClass.classLoader) - val plugin = instantiate(descriptor, cl) - - val runtime = CoreRuntime(platform, FileConfigTextStore(pluginDataDir.resolve(descriptor.id).resolve("config"))) - val ctx = runtime.pluginContext(descriptor.id) - - val loadedPlugin = LoadedPlugin(descriptor, plugin, cl, ctx, descriptorJarPath = pluginJar) - loaded[descriptor.id] = loadedPlugin - return loadedPlugin - } - - public fun enable(id: String) { - val p = loaded[id] ?: error("Plugin not loaded: $id") - p.instance.onLoad(p.context) - // Plug-and-play: discover + register commands/listeners before enabling. - AutoRegistrar.registerAllFromJar(p.descriptorJarPath, p.classLoader, p.context) - p.instance.onEnable(p.context) - } - - public fun disable(id: String) { - val p = loaded[id] ?: return - try { - p.instance.onDisable(p.context) - } finally { - p.classLoader.close() - loaded.remove(id) - } - } - - private fun instantiate(descriptor: PluginDescriptor, cl: ClassLoader): KtalePlugin { - val clazz = Class.forName(descriptor.mainClass, true, cl) - require(KtalePlugin::class.java.isAssignableFrom(clazz)) { - "Plugin main class does not implement KtalePlugin: ${descriptor.mainClass}" - } - val ctor = clazz.getDeclaredConstructor() - ctor.isAccessible = true - @Suppress("UNCHECKED_CAST") - return ctor.newInstance() as KtalePlugin - } - - private fun jarOnlyClassLoader(pluginJar: Path): ClassLoader = - URLClassLoader(arrayOf(pluginJar.toUri().toURL()), null) - - private fun buildRepos(extra: List): List { - val base = mutableListOf( - MavenDependencyResolver.repo("modlabs-mirror", "https://nexus.modlabs.cc/repository/maven-mirrors/") - ) - extra.forEach { r -> - base += MavenDependencyResolver.repo(r.id, r.url, r.usernameEnv, r.passwordEnv) - } - return base - } - - public data class LoadedPlugin( - public val descriptor: PluginDescriptor, - public val instance: KtalePlugin, - public val classLoader: URLClassLoader, - public val context: ktale.api.PluginContext, - internal val descriptorJarPath: Path, - ) -} - - diff --git a/ktale-runtime-host/src/test/kotlin/ktale/runtime/host/JarPluginDescriptorReaderTest.kt b/ktale-runtime-host/src/test/kotlin/ktale/runtime/host/JarPluginDescriptorReaderTest.kt deleted file mode 100644 index 0e181a9..0000000 --- a/ktale-runtime-host/src/test/kotlin/ktale/runtime/host/JarPluginDescriptorReaderTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package ktale.runtime.host - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import java.nio.file.Files -import java.util.jar.JarEntry -import java.util.jar.JarOutputStream - -class JarPluginDescriptorReaderTest : FunSpec({ - test("reads ktale-plugin.properties from jar") { - val tmp = Files.createTempFile("ktale-plugin", ".jar") - JarOutputStream(Files.newOutputStream(tmp)).use { jar -> - jar.putNextEntry(JarEntry("ktale-plugin.properties")) - jar.write("id=test\nmain=example.Main\n".toByteArray(Charsets.UTF_8)) - jar.closeEntry() - } - - val desc = JarPluginDescriptorReader.read(tmp) - desc.id shouldBe "test" - desc.mainClass shouldBe "example.Main" - } -}) - - diff --git a/settings.gradle.kts b/settings.gradle.kts index dc2757e..b84553f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,15 +1,4 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } -rootProject.name = "ktale" - -include( - "ktale-api", - "ktale-core", - "ktale-platform", - "ktale-platform-fake", - "ktale-platform-hytale", - "ktale-runtime-deps", - "ktale-gradle-plugin", - "ktale-runtime-host", -) \ No newline at end of file +rootProject.name = "ktale" \ No newline at end of file From 8e486b2b48a4e6df0dc86c7960ba2c3ceb8a4078 Mon Sep 17 00:00:00 2001 From: Liam Sage Date: Wed, 14 Jan 2026 10:08:21 +0100 Subject: [PATCH 2/3] Migrate CI to GitHub Actions and add text utilities Replaces Woodpecker CI with GitHub Actions workflows for build and documentation. Adds PlayerExtensions and MessageBuilder for MiniMessage-like text formatting and sending in Hytale plugins. Updates Gradle build for publishing, Dokka, and JVM target handling. Adds initial human-written docs and cleans up toolchain settings. --- .github/workflows/build.yml | 25 ++ .github/workflows/docs.yml | 8 +- .woodpecker/build.yml | 19 - build.gradle.kts | 71 +++- docs/index.mdx | 10 + gradle.properties | 7 +- settings.gradle.kts | 3 - .../cc/modlabs/ktale/ext/PlayerExtensions.kt | 36 ++ .../cc/modlabs/ktale/text/MessageBuilder.kt | 334 ++++++++++++++++++ 9 files changed, 480 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .woodpecker/build.yml create mode 100644 docs/index.mdx create mode 100644 src/main/kotlin/cc/modlabs/ktale/ext/PlayerExtensions.kt create mode 100644 src/main/kotlin/cc/modlabs/ktale/text/MessageBuilder.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3e71f3b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,25 @@ +name: Build +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "temurin" + cache: "gradle" + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build --no-daemon --stacktrace + diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4632856..6894026 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,6 +26,7 @@ jobs: with: distribution: temurin java-version: "21" + cache: gradle - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 @@ -33,13 +34,13 @@ jobs: - name: Make Gradle wrapper executable run: chmod +x ./gradlew - - name: Build Dokka (multi-module) - run: ./gradlew dokkaHtmlMultiModule --no-daemon + - name: Build Dokka (single-module) + run: ./gradlew dokkaHtml --no-daemon - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: - path: build/dokka/htmlMultiModule + path: build/dokka/html deploy: needs: build @@ -52,4 +53,3 @@ jobs: id: deployment uses: actions/deploy-pages@v4 - diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml deleted file mode 100644 index 2f15741..0000000 --- a/.woodpecker/build.yml +++ /dev/null @@ -1,19 +0,0 @@ -kind: pipeline -type: docker -name: build - -when: - event: - - pull_request - branch: - - main - -steps: - - name: build - image: eclipse-temurin:21-jdk - commands: - - chmod +x ./gradlew - - ./gradlew build --stacktrace --no-daemon - - - diff --git a/build.gradle.kts b/build.gradle.kts index 45a7b22..afce8b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,8 @@ plugins { val kotlin_version: String by System.getProperties() kotlin("jvm").version(kotlin_version) `java-library` + `maven-publish` + id("org.jetbrains.dokka") version "2.0.0" } group = "cc.modlabs" @@ -27,18 +29,79 @@ tasks.test { } kotlin { - jvmToolchain(25) + val requestedJvmTargetStr: String = (System.getProperty("ktale_jvm_target") ?: "25").trim() + + // Hytale plugins are expected to run on JVM 25. + // Kotlin may lag behind in classfile targets; clamp to the max supported by Kotlin (currently 24). + val effectiveJvmTargetStr: String = try { + org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(requestedJvmTargetStr) + requestedJvmTargetStr + } catch (_: Throwable) { + logger.warn("Requested ktale_jvm_target=$requestedJvmTargetStr but Kotlin doesn't support it; using 24 for Kotlin bytecode.") + "24" + } + + // Use JDK 21 to *compile*, but we can still emit newer Kotlin bytecode targets when supported. + // (This matches your requirement: JDK 21, target JVM 25 — with Kotlin clamped until it supports 25.) + jvmToolchain(21) compilerOptions { // Kotlin's supported JVM targets lag behind Java toolchains. Use string target for maximum compatibility. // If your Kotlin version supports 25, this will work; otherwise set systemProp.ktale_jvm_target (e.g. 21). - val target: String = (System.getProperty("ktale_jvm_target") ?: "25").trim() - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(target)) + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(effectiveJvmTargetStr)) } } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(25)) + // JDK used for compilation. + languageVersion.set(JavaLanguageVersion.of(21)) } withSourcesJar() +} + +publishing { + repositories { + mavenLocal() + val user = System.getenv("NEXUS_USER") + val pass = System.getenv("NEXUS_PASS") + if (!user.isNullOrBlank() && !pass.isNullOrBlank()) { + maven { + name = "ModLabs" + url = uri("https://nexus.modlabs.cc/repository/maven-public/") + credentials { + username = user + password = pass + } + } + } + } + + publications { + create("maven") { + from(components["java"]) + pom { + name.set("KTale") + description.set("Kotlin extensions + utilities for Hytale Server plugin development.") + url.set("https://github.com/ModLabsCC/ktale") + licenses { + license { + name.set("GPL-3.0") + url.set("https://github.com/ModLabsCC/ktale/blob/main/LICENSE") + } + } + developers { + developer { + id.set("ModLabsCC") + name.set("ModLabsCC") + email.set("contact@modlabs.cc") + } + } + scm { + connection.set("scm:git:git://github.com/ModLabsCC/ktale.git") + developerConnection.set("scm:git:git@github.com:ModLabsCC/ktale.git") + url.set("https://github.com/ModLabsCC/ktale") + } + } + } + } } \ No newline at end of file diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 0000000..21c8178 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,10 @@ +--- +title: KTale +--- + +KTale is a lightweight Kotlin-first helper library for building **Hytale Server plugins**. + +This `/docs` folder is for human-written guides (MDX/Markdown). + +API reference is generated from KDoc via **Dokka** and published to **GitHub Pages** by `.github/workflows/docs.yml`. + diff --git a/gradle.properties b/gradle.properties index b243a32..f087dc3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,11 @@ kotlin.code.style=official org.gradle.configuration-cache=true +kotlin.jvm.target.validation.mode=ignore # Required by build.gradle.kts (mirrors your plugin build convention). # You can override with: ./gradlew -Dkotlin_version=... systemProp.kotlin_version=2.2.21 -# Kotlin may not support JVM target 25 yet, even if you compile with JDK 25. -# Set to 25 when your Kotlin version supports it; otherwise keep at 21. -systemProp.ktale_jvm_target=21 \ No newline at end of file +# Hytale runs on JVM 25. Kotlin cannot emit classfile 25 yet (max is currently 24), +# so the build will clamp 25 -> 24 for Kotlin bytecode until Kotlin adds 25 support. +systemProp.ktale_jvm_target=25 \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b84553f..e7972c6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1 @@ -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" -} rootProject.name = "ktale" \ No newline at end of file diff --git a/src/main/kotlin/cc/modlabs/ktale/ext/PlayerExtensions.kt b/src/main/kotlin/cc/modlabs/ktale/ext/PlayerExtensions.kt new file mode 100644 index 0000000..c53872e --- /dev/null +++ b/src/main/kotlin/cc/modlabs/ktale/ext/PlayerExtensions.kt @@ -0,0 +1,36 @@ +package cc.modlabs.ktale.ext + +import cc.modlabs.ktale.text.MessageBuilder +import com.hypixel.hytale.server.core.Message +import com.hypixel.hytale.server.core.entity.entities.Player + +/** + * Sends a pre-built [Message] to this player. + */ +public fun Player.send(message: Message) { + this.sendMessage(message) +} + +/** + * Sends a MiniMessage-like formatted string (via [MessageBuilder]) to this player. + * + * Example: + * + * ```kotlin + * player.send("Hello World") + * ``` + */ +public fun Player.send(text: String) { + this.send(MessageBuilder.fromMiniMessage(text)) +} + +/** Sends raw text to this player (no tag parsing). */ +public fun Player.sendRaw(text: String) { + this.send(Message.raw(text)) +} + +/** Explicit MiniMessage-like send (same as [send]). */ +public fun Player.sendMini(text: String) { + this.send(MessageBuilder.fromMiniMessage(text)) +} + diff --git a/src/main/kotlin/cc/modlabs/ktale/text/MessageBuilder.kt b/src/main/kotlin/cc/modlabs/ktale/text/MessageBuilder.kt new file mode 100644 index 0000000..0deffa4 --- /dev/null +++ b/src/main/kotlin/cc/modlabs/ktale/text/MessageBuilder.kt @@ -0,0 +1,334 @@ +package cc.modlabs.ktale.text + +import com.hypixel.hytale.server.core.Message +import java.awt.Color +import java.util.regex.Pattern + +/** + * Builder for creating Hytale [Message] objects from a MiniMessage-like format. + * + * Supports tags like ``, ``, `<#RRGGBB>`, ``, etc. + * + * Example: + * + * ```kotlin + * val msg = MessageBuilder.fromMiniMessage("Hello World!") + * player.sendMessage(msg) + * ``` + */ +public class MessageBuilder { + public companion object { + // Pattern to match MiniMessage tags: or or + private val TAG_PATTERN: Pattern = Pattern.compile("<([/]?)([^>]+)>") + + // Named colors mapping + private val NAMED_COLORS: Map = mapOf( + "black" to "#000000", + "dark_blue" to "#0000AA", + "dark_green" to "#00AA00", + "dark_aqua" to "#00AAAA", + "dark_red" to "#AA0000", + "dark_purple" to "#AA00AA", + "gold" to "#FFAA00", + "gray" to "#AAAAAA", + "grey" to "#AAAAAA", + "dark_gray" to "#555555", + "dark_grey" to "#555555", + "blue" to "#5555FF", + "green" to "#55FF55", + "aqua" to "#55FFFF", + "red" to "#FF5555", + "light_purple" to "#FF55FF", + "yellow" to "#FFFF55", + "white" to "#FFFFFF", + "reset" to "#FFFFFF", + ) + + private data class GradientInfo(val startColor: String, val endColor: String) + private data class MessageNode(val message: Message, val tagName: String, val gradientInfo: GradientInfo? = null) + + /** + * Parses a MiniMessage-like string and converts it to a Hytale [Message]. + * + * @param miniMessage The MiniMessage formatted string + * @return A Hytale [Message] + */ + public fun fromMiniMessage(miniMessage: String): Message { + if (!miniMessage.contains('<')) { + // No tags, return simple raw message + return Message.raw(miniMessage) + } + + val root = Message.empty() + val stack = mutableListOf() + stack.add(MessageNode(root, "", null)) + + var lastIndex = 0 + val matcher = TAG_PATTERN.matcher(miniMessage) + + while (matcher.find()) { + val start = matcher.start() + val end = matcher.end() + val isClosing = matcher.group(1) == "/" + val tagContent = matcher.group(2) + val tagName = parseTagName(tagContent) + + // Add text before the tag + if (start > lastIndex) { + val text = miniMessage.substring(lastIndex, start) + if (text.isNotEmpty()) { + val currentNode = stack.lastOrNull() + val gradientInfo = findGradientInfo(stack) + if (gradientInfo != null) { + applyGradientText(currentNode?.message ?: root, text, gradientInfo) + } else { + currentNode?.message?.insert(text) + } + } + } + + if (isClosing) { + // Closing tag - pop from stack until we find matching tag + for (i in stack.size - 1 downTo 1) { + if (stack[i].tagName == tagName) { + while (stack.size > i) { + stack.removeAt(stack.size - 1) + } + break + } + } + } else { + // Opening tag - create new message and apply formatting + val message = Message.empty() + val gradientInfo = applyTag(message, tagContent, tagName) + stack.lastOrNull()?.message?.insert(message) + stack.add(MessageNode(message, tagName, gradientInfo)) + } + + lastIndex = end + } + + // Add remaining text + if (lastIndex < miniMessage.length) { + val text = miniMessage.substring(lastIndex) + if (text.isNotEmpty()) { + val currentNode = stack.lastOrNull() + val gradientInfo = findGradientInfo(stack) + if (gradientInfo != null) { + applyGradientText(currentNode?.message ?: root, text, gradientInfo) + } else { + currentNode?.message?.insert(text) + } + } + } + + // If root has children, join them, otherwise return root + val children = root.children + return when { + children.isEmpty() -> root + children.size == 1 -> children[0] + else -> Message.join(*children.toTypedArray()) + } + } + + /** + * Applies a tag to a [Message]. Returns [GradientInfo] if this is a gradient tag, otherwise null. + */ + private fun applyTag(message: Message, tagContent: String, tagName: String): GradientInfo? { + val parts = tagContent.split(':') + val baseTagName = parts[0].lowercase() + val tagValue = if (parts.size > 1) parts[1] else null + + when (baseTagName) { + "gradient" -> { + if (parts.size >= 3) { + val startColor = parseColor(parts[1]) ?: return null + val endColor = parseColor(parts[2]) ?: return null + return GradientInfo(startColor, endColor) + } + return null + } + "bold", "b" -> { + message.bold(true) + return null + } + "italic", "i" -> { + message.italic(true) + return null + } + "underlined", "u" -> return null + "strikethrough", "s" -> return null + "obfuscated", "obf" -> return null + "monospace" -> { + message.monospace(true) + return null + } + "color" -> { + if (tagValue != null) { + val color = parseColor(tagValue) + if (color != null) message.color(color) + } + return null + } + "click" -> return null + "hover" -> return null + "link" -> { + if (tagValue != null) message.link(tagValue) + return null + } + else -> { + val colorHex = NAMED_COLORS[baseTagName] + if (colorHex != null) { + message.color(colorHex) + return null + } else if (baseTagName.startsWith("#")) { + if (isValidHex(baseTagName)) message.color(baseTagName) + return null + } + return null + } + } + } + + private fun findGradientInfo(stack: List): GradientInfo? { + for (i in stack.size - 1 downTo 0) { + stack[i].gradientInfo?.let { return it } + } + return null + } + + private fun applyGradientText(parentMessage: Message, text: String, gradientInfo: GradientInfo) { + if (text.isEmpty()) return + + val startColor = hexToRgb(gradientInfo.startColor) ?: return + val endColor = hexToRgb(gradientInfo.endColor) ?: return + + val chars = text.toCharArray() + val charCount = chars.size + if (charCount == 0) return + + for (i in chars.indices) { + val ratio = if (charCount > 1) i.toDouble() / (charCount - 1) else 0.0 + val interpolatedColor = interpolateColor(startColor, endColor, ratio) + val hexColor = rgbToHex(interpolatedColor) + + val charMessage = Message.raw(chars[i].toString()).color(hexColor) + parentMessage.insert(charMessage) + } + } + + private fun hexToRgb(hex: String): Triple? { + val cleanHex = hex.removePrefix("#") + if (cleanHex.length != 6) return null + + return try { + val r = cleanHex.substring(0, 2).toInt(16) + val g = cleanHex.substring(2, 4).toInt(16) + val b = cleanHex.substring(4, 6).toInt(16) + Triple(r, g, b) + } catch (_: NumberFormatException) { + null + } + } + + private fun rgbToHex(rgb: Triple): String = + String.format("#%02X%02X%02X", rgb.first, rgb.second, rgb.third) + + private fun interpolateColor( + start: Triple, + end: Triple, + ratio: Double, + ): Triple { + val r = (start.first + (end.first - start.first) * ratio).toInt().coerceIn(0, 255) + val g = (start.second + (end.second - start.second) * ratio).toInt().coerceIn(0, 255) + val b = (start.third + (end.third - start.third) * ratio).toInt().coerceIn(0, 255) + return Triple(r, g, b) + } + + private fun parseColor(colorValue: String): String? { + val trimmed = colorValue.trim() + + if (trimmed.startsWith("#")) { + return if (isValidHex(trimmed)) trimmed else null + } + + val namedColor = NAMED_COLORS[trimmed.lowercase()] + if (namedColor != null) return namedColor + + if (trimmed.length == 6 && isValidHex("#$trimmed")) { + return "#$trimmed" + } + + return null + } + + private fun isValidHex(hex: String): Boolean = + hex.matches(Regex("#[0-9A-Fa-f]{6}")) + + private fun parseTagName(tagContent: String): String = + tagContent.split(':')[0].lowercase() + + public fun builder(): MessageBuilder = MessageBuilder() + } + + private var message: Message = Message.empty() + + /** Adds raw text to the message. */ + public fun text(text: String): MessageBuilder { + message.insert(text) + return this + } + + /** Adds a MiniMessage-like formatted text. */ + public fun miniMessage(text: String): MessageBuilder { + val parsed = fromMiniMessage(text) + message.insert(parsed) + return this + } + + /** Sets the color. */ + public fun color(color: String): MessageBuilder { + message.color(color) + return this + } + + /** Sets the color. */ + public fun color(color: Color): MessageBuilder { + message.color(color) + return this + } + + /** Sets bold formatting. */ + public fun bold(bold: Boolean = true): MessageBuilder { + message.bold(bold) + return this + } + + /** Sets italic formatting. */ + public fun italic(italic: Boolean = true): MessageBuilder { + message.italic(italic) + return this + } + + /** Sets monospace formatting. */ + public fun monospace(monospace: Boolean = true): MessageBuilder { + message.monospace(monospace) + return this + } + + /** Sets a link. */ + public fun link(url: String): MessageBuilder { + message.link(url) + return this + } + + /** Inserts another message. */ + public fun insert(other: Message): MessageBuilder { + message.insert(other) + return this + } + + /** Builds the final [Message]. */ + public fun build(): Message = message +} + From 150933bc0ed21c98b0663c39aed622562e13edd0 Mon Sep 17 00:00:00 2001 From: Liam Sage Date: Wed, 14 Jan 2026 10:11:37 +0100 Subject: [PATCH 3/3] Add message formatting guide to documentation Introduces a new 'Message formatting' guide in the docs, detailing how to use MiniMessage-like tags with KTale, including quick start examples, raw vs formatted messages, message building, and supported tags. Also updates the index to link to the new guide. --- docs/index.mdx | 2 ++ docs/message-formatting.mdx | 63 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 docs/message-formatting.mdx diff --git a/docs/index.mdx b/docs/index.mdx index 21c8178..a2cd6ed 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -8,3 +8,5 @@ This `/docs` folder is for human-written guides (MDX/Markdown). API reference is generated from KDoc via **Dokka** and published to **GitHub Pages** by `.github/workflows/docs.yml`. +### Guides +- [Message formatting](./message-formatting.mdx) diff --git a/docs/message-formatting.mdx b/docs/message-formatting.mdx new file mode 100644 index 0000000..fe2c40e --- /dev/null +++ b/docs/message-formatting.mdx @@ -0,0 +1,63 @@ +--- +title: Message formatting +--- + +KTale provides a small MiniMessage-like formatting layer on top of Hytale’s `com.hypixel.hytale.server.core.Message`. + +### Quick start (recommended) +Use the `Player.send(String)` extension. It parses MiniMessage-like tags and sends the resulting `Message`. + +```kotlin +import cc.modlabs.ktale.ext.send + +player.send("Hello World") +player.send("Gradient text") +player.send("<#FFAA00>Hex colors") +``` + +### Raw vs formatted +- **Raw** (no parsing): + +```kotlin +import cc.modlabs.ktale.ext.sendRaw + +player.sendRaw("This will not parse tags") +``` + +- **Formatted** (MiniMessage-like): + +```kotlin +import cc.modlabs.ktale.ext.sendMini + +player.sendMini("Tags are parsed here") +``` + +### Building messages directly +If you want a `Message` object (e.g. to insert into other messages), use `MessageBuilder`: + +```kotlin +import cc.modlabs.ktale.text.MessageBuilder + +val msg = MessageBuilder.fromMiniMessage("Warning: Danger") +player.sendMessage(msg) +``` + +Or the fluent builder API: + +```kotlin +import cc.modlabs.ktale.text.MessageBuilder + +val msg = MessageBuilder.builder() + .miniMessage("Welcome") + .text(" to the server!") + .build() +``` + +### Supported tags (current) +- Colors: ``, ``, etc. + hex colors like `<#RRGGBB>` +- Formatting: ``, ``, `` +- Gradients: `...` (color names or hex) +- Links: `text` (maps to `Message.link(...)`) + +Anything else is currently ignored (by design) to keep this lightweight and aligned with the Hytale `Message` API. +