API 概览
Fand 插件只依赖 fand-api。fand-api 是面向插件作者的稳定编译期 API,运行时实现由 Fand Server 提供。
IMPORTANT
不要根据 fand-api 源码里的 Java default 方法体、兜底返回值或占位异常判断运行时行为。它们主要用于源码/二进制兼容,让插件可以在 API 演进时继续编译;真实行为由当前 Fand Server 运行时、插件作用域包装层或已注册 provider 提供。
建议优先从 PluginContext 获取服务。它代表“当前插件”的生命周期作用域,命令、监听器、任务、服务 provider、GUI、BossBar、TabList 等资源都可以跟随插件禁用自动清理。只有在需要全服视图时,再使用 Fand.server()。
为什么这样设计
Fand API 有意采用和 Bukkit/Paper 不完全相同的风格。它更像现代 Java record/接口风格的领域 API:读取状态时优先使用 player.location()、entity.uniqueId()、world.key() 这类属性式方法;会修改状态、发送数据包或改变生命周期的操作才使用动词,例如 teleport(...)、setVelocity(...)、register(...)、close()。
这样设计主要为了几件事:
- 更少模板代码:
location()比getLocation()更接近 Java record accessor,也和PluginDescriptor.id()、MapView.id()等 API 保持一致。 - 区分读取和动作:无前缀方法通常表示读取一个值或句柄,
set*/ 动词方法表示有副作用的操作。 - 编译期 API 与运行时解耦:插件只依赖
fand-api;具体行为由当前 Fand Server runtime 提供。API 里的兼容默认方法不是功能说明。 - 插件作用域优先:
context.xxx()返回的服务通常会帮插件追踪注册资源,降低卸载后残留命令、任务、BossBar、TabList 行等问题。 - 明确线程边界:异步阶段适合做 I/O 和计算;需要按 tick 顺序应用结果时,用 scheduler 切回服务端线程。
从 Paper 迁移时,不要机械寻找 getXxx() 对应物。先判断自己是在读取状态、注册资源、修改世界,还是发送给某个 viewer 的表现效果,再选择对应的 Fand 服务。
两个核心入口
| 入口 | 作用 | 使用场景 |
|---|---|---|
PluginContext | 插件作用域服务集合 | 注册命令、事件、任务、权限、GUI、数据包、跨插件服务 |
Fand.server() | 全局服务端视图 | 查询在线玩家、世界、性能、全局注册表、广播消息 |
public final class ExamplePlugin implements Plugin {
@Override
public void onEnable(PluginContext context) {
context.logger().info("{} enabled", context.descriptor().id());
context.commands();
context.events();
context.scheduler();
}
}import io.fand.api.item.ItemKey;
Fand.server().players();
Fand.server().worlds();
Fand.server().performance();
Fand.server().itemType(ItemKey.DIAMOND.key());API 分层
Fand API 可以按开发任务分成几层:
| 分层 | 代表 API | 说明 |
|---|---|---|
| 插件基础 | plugin、lifecycle、config、storage | 插件加载、配置、数据目录和持久化 |
| 交互入口 | command、event、scheduler、permission | 玩家输入、监听服务端行为、异步/主线程任务、权限控制 |
| 玩家体验 | text、placeholder、bossbar、tablist、scoreboard、gui、map | 文本、占位符、界面、BossBar、玩家列表、记分板和地图渲染 |
| 世界与实体 | world、entity、player、block、item、component、inventory、tag | 世界、实体、玩家、方块、物品、组件、背包/容器和原版标签查询 |
| 内容扩展 | customitem、customblock、recipe、loot、advancement、enchantment、datapack、structure | 自定义内容、配方、战利品表、进度、附魔、data pack 文件、结构模板和生成相关能力 |
| 生态互通 | service、integration、messaging、region | 跨插件 provider、外部资源策略、插件消息和区域保护 |
| 底层表现 | packet、component、registry、performance、gamerule、nbs | 网络数据包、组件、注册表、性能快照、自定义规则和 NBS 解析 |
PluginContext 服务矩阵
| 服务 | 入口 | 典型用途 |
|---|---|---|
| 日志 | context.logger() | 使用插件 id 命名的 SLF4J logger |
| 描述文件 | context.descriptor() | 读取 id、version、mainClass、apiVersion、加载关系、元数据和权限声明 |
| 生命周期事件 | context.events() | 注册玩家、实体、世界、插件和服务端事件监听器 |
| 命令 | context.commands() | Builder 命令、注解命令、补全、可见命令查询 |
| 调度器 | context.scheduler() | 主线程、异步、延迟、周期、tick-based 任务 |
| 权限 | context.permissions() | 权限节点、权限树、附件、组、prefix/suffix/meta、上下文查询 |
| 配置 | context.config() | 插件默认 config.yml,支持重载和保存 |
| 配置加载器 | context.configurations() | YAML、JSON、TOML、properties 等通用配置文件 |
| 存储 | context.storage() | 插件作用域 JSON/KV 持久化数据 |
| 服务注册 | context.services() | 注册和发现经济、聊天、权限桥、区域保护等 Java provider |
| 区域 | context.regions() | 区域定义、flag 注册、优先级排序、解析 trace |
| 数据包 | context.packets() | 拦截、构造、发送、custom payload、fake block/entity |
| 占位符 | context.placeholders() | 注册和解析 %namespace_value% 风格占位符 |
| MiniMessage | context.miniMessages() | Adventure MiniMessage 与 Fand 占位符整合 |
| GUI | context.guis() | 容器菜单、槽位点击处理器、关闭回调 |
| 记分板 | context.scoreboard() | objective、display slot、team、nameplate |
| BossBar | context.bossBars() | 创建和更新 BossBar,按插件生命周期清理 |
| TabList | context.tabLists() | per-viewer 玩家列表显示、隐藏、排序和条目管理 |
| 地图 | context.maps() | 地图 renderer、cursor、玩家相关渲染 |
| 插件消息 | context.pluginMessaging() | 标准 plugin message channel |
| 自定义物品 | context.customItems() | 注册自定义物品类型、模板物品和基础物品绑定 |
| 自定义方块 | context.customBlocks() | 注册自定义方块类型、监听器和物品绑定 |
| 配方 | context.recipes() | 注册、查询和移除插件命名空间下的配方 |
| 战利品表 | context.lootTables() | 查询、生成和替换插件命名空间下的战利品表 |
| 进度 | context.advancements() | 注册和查询插件命名空间下的进度 |
| 附魔 | context.enchantments() | 注册和查询插件命名空间下的附魔 |
| 数据包文件树 | context.dataPacks() | 插件作用域 vanilla data pack 文件树 |
| 结构 | context.structures() | 模板保存、导入、导出、放置和定位 |
| 游戏规则 | context.gameRules() | 插件命名空间自定义 game rule |
| 模拟玩家 | context.simulatedPlayers() | 服务端侧模拟玩家 |
| 外部集成 | context.integrations() | SQL、Redis、MQ 等外部资源策略描述 |
全局 Server 视图
Server 是 Adventure ForwardingAudience,会把消息转发给当前在线玩家。它适合做全局查询和广播,但插件自己的注册行为仍建议走 PluginContext。
| 能力 | 入口 |
|---|---|
| 服务端信息 | brand()、version()、minecraftVersion()、phase() |
| 玩家 | players()、player(UUID)、player(String)、playerAccess() |
| 世界 | worlds()、world(Key)、defaultWorld()、createWorld(...)、unloadWorld(...) |
| 注册表查询 | blockType(...)、itemType(...)、entityType(...)、blockTags()、itemTags() |
| 全局服务 | events()、commands()、permissions()、scheduler()、scoreboard()、packets() |
| 性能 | performance()、currentTick() |
| 广播 | sendMessage(...)、broadcast(...) |
最佳实践
- 生命周期相关注册放在
onEnable,释放外部资源放在onDisable。 - 优先使用
context.xxx(),除非明确需要全服查询或全局广播。 - 事件监听器运行在触发事件的线程上,需要修改世界、实体、背包或容器时跳回主线程。
- 异步任务不要直接操作主线程状态;用
context.scheduler().runMain(...)回到服务端线程。 ServiceRegistry适合做生态互通,不适合替代普通 Java 依赖注入。- 权限节点、命令、配置 key 建议统一使用插件 id 作为前缀。
常见坑
- 看到
fand-api接口里的 default 返回值或占位异常,就误以为运行时没有实现;真实行为由 Fand Server 运行时提供。 - 从 Paper 迁移时机械寻找
getXxx()方法,忽略了 Fand 的属性式 accessor。 - 把所有服务都从
Fand.server()获取,导致插件作用域清理失效或资源归属不清。 - 在异步任务或异步事件里直接操作未封装的世界、实体、背包或容器对象。
- 把
ServiceRegistry当作插件内部对象容器,注册 DAO、配置对象或线程池。 - 没有在 descriptor 或
PermissionService注册公开权限节点,管理工具无法发现默认策略。
综合示例:最小但完整的插件骨架
下面的例子展示一个插件入口如何组合 descriptor、配置、权限、命令、事件和调度器。更复杂的 GUI、区域、packet、scoreboard 等能力可以按同样方式从 PluginContext 拆到独立组件。
package com.example;
import io.fand.api.event.player.PlayerJoinEvent;
import io.fand.api.permission.PermissionDefault;
import io.fand.api.permission.PermissionDescriptor;
import io.fand.api.plugin.Plugin;
import io.fand.api.plugin.PluginContext;
import net.kyori.adventure.text.Component;
public final class ExamplePlugin implements Plugin {
@Override
public void onEnable(PluginContext context) {
context.logger().info("{} {}", context.descriptor().id(), context.descriptor().version());
var enabled = context.config().getBoolean("welcome.enabled", true);
var message = context.config().getString("welcome.message", "Welcome, {player}");
context.permissions().register(new PermissionDescriptor(
"example.reload",
PermissionDefault.OPERATOR));
context.commands().register("example", command -> command
.permission("example.reload")
.literal("reload", reload -> reload.executes(reloadCommand -> {
context.reloadConfig();
reloadCommand.sender().sendMessage(Component.text("Example config reloaded"));
})));
context.events().subscribe(PlayerJoinEvent.class, event -> {
if (enabled) {
event.player().sendMessage(Component.text(
message.replace("{player}", event.player().name())));
}
});
}
}依赖配置
官方 Gradle 插件版本是 0.1.2,它会自动配置 API 依赖,并处理 fand-plugin.json。新插件工程建议优先用这种写法。
plugins {
id("io.fand.plugin") version "0.1.2"
}如果你只是写普通 Java/Gradle/Maven 项目,可以直接依赖 fand-api。下面分别是 Gradle Kotlin DSL、Gradle Groovy DSL 和 Maven POM 的写法。
Gradle Kotlin DSL
repositories {
maven("https://repo.fandmc.cn/repository/maven-public/")
}
dependencies {
compileOnly("io.fand:fand-api:0.1.2")
}Gradle Groovy DSL
repositories {
maven {
url = uri("https://repo.fandmc.cn/repository/maven-public/")
}
}
dependencies {
compileOnly "io.fand:fand-api:0.1.2"
}Maven POM
Maven 项目建议写固定版本,避免每次构建时解析到不同 API。下面用 0.1.2 举例。
<repositories>
<repository>
<id>fandmc</id>
<url>https://repo.fandmc.cn/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.fand</groupId>
<artifactId>fand-api</artifactId>
<version>0.1.2</version>
<scope>provided</scope>
</dependency>
</dependencies>