AstraTemplate
一个采用 Kotlin 编写的生产级 Minecraft 插件/模组模板。提供模块化、生命周期驱动的架构,可从单一共享代码库在 Paper、Forge 和 NeoForge 上运行。
基于此模板构建的插件
项目结构
AstraTemplate/
├── instances/
│ ├── bukkit/ ← Paper 入口点 + 平台连接
│ ├── forge/ ← Forge 入口点 + 平台连接
│ └── neoforge/ ← NeoForge 入口点 + 平台连接
└── modules/
├── api/
│ ├── local/ ← 数据库 (Exposed ORM,与平台无关)
│ └── remote/ ← REST 客户端 (Ktor,与平台无关)
├── core/ ← 配置、翻译、协程作用域
├── build-konfig/ ← 编译时常量 (id, version 等)
├── feature-command/ ← 所有命令 (与平台无关!)
├── feature-gui/
│ ├── api/ ← GUI 接口 (Router, GuiModule)
│ └── bukkit/ ← Bukkit 箱子 GUI 实现
└── feature-event/
├── bukkit/ ← Bukkit 事件监听器
├── forge/ ← Forge 事件监听器
└── neoforge/ ← NeoForge 事件监听器
每个 instances/<platform> 通过 ShadowJar 构建胖 jar,且是唯一知晓特定平台的地方。modules/ 中的所有内容要么完全与平台无关,要么具有明确命名的平台变体。
模块
modules/core — 配置、翻译、协程作用域
这是其他所有模块依赖的基础。提供:
- 配置 —
PluginConfiguration是一个写入config.yml的@Serializable数据类。通过StateFlowKrate在执行/atempreload时重新加载。 - 翻译 —
PluginTranslation以相同方式使用translation.yml。每个字符串都有默认值,因此即使没有文件存在,插件也能开箱即用。 - 协程作用域 —
ioScope、mainScope和unconfinedScope由KotlinDispatchers支持(对Dispatchers.IO/ 主线程等的平台提供的抽象)。所有作用域均在onDisable中取消。
modules/api/local — 通过 Exposed ORM 访问本地数据库
通过 Jetbrains Exposed ORM 访问本地数据库。LocalDao 接口公开了针对 UserTable 和 UserRatingTable 进行 CRUD 操作的挂起函数。底层数据库连接是从配置流中反应式派生的,因此从 H2 切换到 MySQL 只需一行配置更改并重新加载即可。
支持的驱动程序(在 libs.versions.toml 中配置):H2、SQLite、MySQL、MariaDB。
modules/api/remote — 通过 Ktor 构建的 REST API 客户端
使用 Ktor 构建的 REST API 客户端。演示了从外部 HTTP 端点(Rick & Morty API)获取数据。RickMortyApi 接口返回 Result<T> — 错误永远不会被抛出,总是显式返回。
modules/build-konfig — 编译时常量
通过 BuildConfig Gradle 插件生成编译时常量(id、version 等)。可从任何需要在运行时引用插件身份而无需硬编码字符串的模块中导入。
modules/feature-command — 跨平台命令(无平台导入)
所有命令集中在一处,无平台导入。使用来自 AstraLibs 的 Brigadier DSL 来定义命令,这些命令在 Paper、Forge 和 NeoForge 上编译和运行的方式完全相同。特定平台的 MultiplatformCommand 适配器在 RootModule 层级注入。
modules/feature-gui — 箱子 GUI(Bukkit,其他平台为存根)
分为 api(Router 接口 + GuiModule)和 bukkit(实现)。Bukkit 实现提供了一个由 StateFlow 驱动的分页箱子库存 — 每当底层数据发生变化时,GUI 会自动重新渲染。在 Forge/NeoForge 上,StubGuiModule 满足该接口,以便共享的命令模块可以在不引入 Bukkit 的情况下编译。
modules/feature-event — 特定平台的事件监听器
特定平台的事件监听器,每个平台一个子模块。Bukkit 变体监听 BlockPlaceEvent;Forge 和 NeoForge 变体监听服务器刻(tick)。每个子模块都公开一个 Lifecycle,以便 RootModule 可以干净地注册和注销监听器。
架构
生命周期树
每个模块都公开一个包含三个回调的 Lifecycle:onEnable、onDisable、onReload。插件入口点创建一个 RootModule,链接所有子生命周期,并将任务委托给它们:
// instances/bukkit — AstraTemplate.kt
class AstraTemplate : LifecyclePlugin() {
private val rootModule = RootModule(this)
override fun onEnable() = rootModule.lifecycle.onEnable()
override fun onDisable() = rootModule.lifecycle.onDisable()
override fun onReload() = rootModule.lifecycle.onReload()
}
// instances/bukkit — RootModule.kt
class RootModule(plugin: AstraTemplate) {
val coreModule = CoreModule(plugin.dataFolder, DefaultBukkitDispatchers(plugin))
val apiLocalModule = ApiLocalModule(coreModule.configKrate.cachedStateFlow, coreModule.ioScope)
val apiRemoteModule = ApiRemoteModule()
val eventModule = EventModule(coreModule, plugin)
val guiModule = BukkitGuiModule(coreModule, apiLocalModule)
val commandModule = CommandModule(coreModule, apiRemoteModule, guiModule, ...)
val lifecycle = Lifecycle.Lambda(
onEnable = { listOf(coreModule, eventModule, apiLocalModule, commandModule).forEach(Lifecycle::onEnable) },
onDisable = { /* 同上列表,反转顺序 */ },
onReload = { /* 同上列表 */ }
)
}
这使得插件支持运行时热重载 — /atempreload 以相反顺序遍历相同的链并重新启用它,从而即时拾取任何配置或翻译更改。
graph TD
Plugin --> RootModule
RootModule --> CoreModule
RootModule --> ApiLocalModule
RootModule --> ApiRemoteModule
RootModule --> EventModule
RootModule --> CommandModule
EventModule --> TemplateEvent
EventModule --> BetterAnotherEvent
依赖注入
没有使用 DI 框架。每个模块都是一个普通类,其构造函数接收它所依赖的其他模块接口。RootModule 是组合根,以正确的顺序实例化所有内容,在必须延迟初始化的地方使用 lazy {}。
// 传递整个模块接口,而不是从中提取的单个服务
val commandModule = CommandModule(
coreModule = coreModule,
guiModule = guiModule,
apiRemoteModule = apiRemoteModule,
...
)
这保持了耦合的显式性,并避免了因缺少绑定而导致的隐藏运行时故障。
跨平台命令
命令位于 modules/feature-command — 这是一个零平台依赖的普通 Kotlin 模块。它们使用来自 AstraLibs 的 Brigadier DSL,该 DSL 抽象了 Paper 和 Forge 的原生 Brigadier 适配器。
// 在 Paper、Forge 和 NeoForge 上无需任何更改即可工作
command("rickandmorty") {
literal("random") {
runs { ctx ->
scope.launch(dispatchers.IO) {
rmApi.getRandomCharacter(Random.nextInt(0, 100))
.onSuccess { ctx.getSender().sendMessage(...) }
.onFailure { ctx.getSender().sendMessage(...) }
}
}
}
literal("specific") {
argument("number", IntegerArgumentType.integer()) { numberArg ->
runs { ctx -> send(ctx.getSender(), ctx.requireArgument(numberArg)) }
}
}
}
在每个平台上,RootModule 提供一个由正确适配器支持的 MultiplatformCommand(PaperMultiplatformCommands、MinecraftMultiplatformCommands)。共享的命令代码永远不需要更改。
可用命令
| 命令 | 描述 |
|---|---|
/add <player> <material> [amount] | 向玩家背包添加物品 |
/translation | 显示当前翻译值(重载后有用) |
/adamage <player> <amount> | 对玩家造成伤害 |
/atempgui | 打开示例分页 GUI |
/rickandmorty random | 通过 REST 获取随机的 Rick & Morty 角色 |
/rickandmorty specific <id> | 通过 ID 获取特定角色 |
/atempreload | 重载配置、翻译和数据库连接 |
配置
配置和翻译是普通的 @Serializable 数据类,通过 kaml 序列化为 YAML。行内文档注释直接渲染到生成的 YAML 文件中:
@Serializable
data class PluginConfiguration(
@YamlComment("config1 的第一行描述", "config1 的第二行描述")
@SerialName("config_1")
val config1: String = "NONE",
@SerialName("database")
val database: DatabaseConfiguration = DatabaseConfiguration.H2("db")
)
配置和翻译均存储为 StateFlowKrate / CachedKrate。任何读取它们的模块在重载后总能看到最新值 — 无需手动传播。
本地数据库
modules/api/local 使用 Jetbrains Exposed 作为 ORM。数据库连接是从配置流中反应式派生的 — 当配置被重载并带有新的数据库 URL 时,连接会自动替换:
private val databaseFlow = configFlow
.map { it.database }
.distinctUntilChanged()
.flatMapLatest { configuration -> configuration.connectAsFlow() }
.onEach { db ->
transaction(db) { SchemaUtils.create(UserRatingTable, UserTable) }
}
.shareIn(ioScope, SharingStarted.Eagerly, 1)
支持的驱动程序(在 libs.versions.toml 中交换):H2、SQLite、MySQL、MariaDB。
远程 API
modules/api/remote 展示了如何使用 Ktor 调用外部 REST 端点。接口非常精简:
interface RickMortyApi {
suspend fun getRandomCharacter(id: Int): Result<RMResponse>
}
错误作为 Result<T> 返回 — 从不抛出 — 因此调用者可以显式处理故障。
GUI (Bukkit)
GUI 层位于 modules/feature-gui/api 中定义的 Router 接口之后。Bukkit 实现提供了一个带有 Kotlin StateFlow 反应式状态的分页箱子库存:
SampleGuiComponent拥有状态(Loading/Items/Users)SampleGUI观察状态并在每次发射时重新渲染- 导航(下一页/上一页、更改模式、添加用户、返回/关闭)由专用按钮对象处理
在 Forge/NeoForge 上,StubGuiModule 满足 GuiModule 接口,以便共享的 CommandModule 可以在没有 Bukkit 依赖的情况下编译。
构建
# Paper 插件
./gradlew :instances:bukkit:shadowJar
# Forge 模组
./gradlew :instances:forge:shadowJar
# NeoForge 模组
./gradlew :instances:neoforge:shadowJar
# 运行所有测试
./gradlew allTests
输出的 jar 文件位于每个实例的 build/libs/ 目录中,并可由 FTP Gradle 插件选择性地复制到远程服务器(在 libs.versions.toml 中配置目标)。
测试服务器 (Docker)
项目根目录下的 docker-compose.yml 使用 itzg/minecraft-server 启动本地测试服务器。
在运行之前,请手动编辑 docker-compose.yml,取消注释目标平台(Forge、NeoForge 或 Paper)的块,并注释掉其他块。每个块设置 TYPE、VERSION 和特定平台的版本变量,以及其下方匹配的 volumes 条目。
docker compose up
选择礼物
使用社区积分,为喜欢的作品送上一份支持
打赏记录
感谢每一位送出礼物的社区成员


