Skip to content

API 概览

Fand 插件只依赖 fand-apifand-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()全局服务端视图查询在线玩家、世界、性能、全局注册表、广播消息
java
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();
    }
}
java
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说明
插件基础pluginlifecycleconfigstorage插件加载、配置、数据目录和持久化
交互入口commandeventschedulerpermission玩家输入、监听服务端行为、异步/主线程任务、权限控制
玩家体验textplaceholderbossbartablistscoreboardguimap文本、占位符、界面、BossBar、玩家列表、记分板和地图渲染
世界与实体worldentityplayerblockitemcomponentinventorytag世界、实体、玩家、方块、物品、组件、背包/容器和原版标签查询
内容扩展customitemcustomblockrecipelootadvancementenchantmentdatapackstructure自定义内容、配方、战利品表、进度、附魔、data pack 文件、结构模板和生成相关能力
生态互通serviceintegrationmessagingregion跨插件 provider、外部资源策略、插件消息和区域保护
底层表现packetcomponentregistryperformancegamerulenbs网络数据包、组件、注册表、性能快照、自定义规则和 NBS 解析

PluginContext 服务矩阵

服务入口典型用途
日志context.logger()使用插件 id 命名的 SLF4J logger
描述文件context.descriptor()读取 idversionmainClassapiVersion、加载关系、元数据和权限声明
生命周期事件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% 风格占位符
MiniMessagecontext.miniMessages()Adventure MiniMessage 与 Fand 占位符整合
GUIcontext.guis()容器菜单、槽位点击处理器、关闭回调
记分板context.scoreboard()objective、display slot、team、nameplate
BossBarcontext.bossBars()创建和更新 BossBar,按插件生命周期清理
TabListcontext.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 拆到独立组件。

java
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。新插件工程建议优先用这种写法。

kotlin
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

kotlin
repositories {
    maven("https://repo.fandmc.cn/repository/maven-public/")
}

dependencies {
    compileOnly("io.fand:fand-api:0.1.2")
}

Gradle Groovy DSL

groovy
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 举例。

xml
<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>