Suite

Capability 开发教程

自定义 Capability、注册与查找、跨模块通信的完整开发教程——原理、内置能力表、提供方/使用方示例、EventBus。

Capability 开发教程

Capability 是 Suite 各模块之间协作的标准方式。宿主在 ModuleRegistry 中维护一张能力注册表:模块启动时注册自己提供的接口,其他模块按 Java 接口类型查找并调用——全程无需 import 对方实现类。

为什么用 Capability

方式问题
直接 import 其他模块的 ServiceClassLoader 隔离、循环依赖、版本耦合
Bukkit 自定义事件适合广播,不适合「调用对方业务方法」
Capability只依赖 axs-api 中的接口,松耦合、可 softdepends

架构一览

sequenceDiagram
    participant Title as Title 模块
    participant Registry as ModuleRegistry
    participant Event as EventPacket 模块
 
    Title->>Registry: registerCapability(TitleGrantable, impl)
    Note over Registry: capabilities 表: TitleGrantable → impl
    Event->>Registry: getCapability(TitleGrantable.class)
    Registry-->>Event: TitleGrantable 实例
    Event->>Title: giveTitle(playerId, titleId, ...)

数据流:

提供方 startService()
    └── context.registerCapability(接口.class, 实现)

使用方 startService()
    └── capability = context.getCapability(接口.class)  // 可能为 null

宿主 onDisable(提供方)
    └── removeCapabilities(moduleId)  // 自动清理

三种 Capability 形态

1. 单例 Capability(最常见)

一个接口类型全局只有一个实现。后注册会覆盖先注册。

context.registerCapability(TitleGrantable.class, titleService);

适用于:TitleGrantableMailDispatchableMapNavigable 等。

2. 多实例 Capability

PlayerDataPurgeableDatabaseMigratable 允许多个模块各注册一个实例。宿主把它们放入独立列表,供 /axs purge/axs migrate 统一调度。

context.registerCapability(PlayerDataPurgeable.class, new PlayerDataPurgeable() {
    @Override public String moduleId() { return "mymodule"; }
    @Override public int purgePlayerData(UUID uuid) { return repo.deletePlayer(uuid); }
});

3. 全局 EventBus(内置)

宿主在初始化时注册唯一的 EventBusCapability,任何模块可发布/订阅主题,适合 1 对多广播。

EventBusCapability bus = context.getCapability(EventBusCapability.class);
bus.publish("axs.market.listing_created", player, Map.of("item", "diamond"));

SignalDispatchable(面向 EventPacket 规则引擎的 1 对 1 触发)互补。

加载顺序与 softdepends

module.yml 中:

  • depends:硬依赖,保证提供方先于使用方 onEnable
  • softdepends:软依赖,提供方可能不存在——使用方必须判空
# 使用方 module.yml
id: mymodule
depends: []
softdepends: [title, mail, eventpacket]

即使写了 depends: [title],仍建议用 Supplier 延迟查找,以应对 reload 顺序边缘情况。

提供方:如何暴露能力

步骤 1 — 定义接口

接口放在 axs-api(官方能力)或你自己的 api 子包(第三方能力),供他人 compileOnly

package com.example.mymodule.api;
 
import org.bukkit.entity.Player;
 
public interface ShopDiscountable {
    double getDiscount(Player player);
}

步骤 2 — 实现并在 startService 注册

@Override
protected void startService() {
    shopService = new ShopService(this, config);
    shopService.start();
 
    registerCapability(ShopDiscountable.class, player ->
        shopService.resolveDiscount(player));
}

步骤 3 — 官方模块示例(Title)

registerCapability(TitleGrantable.class,
    (playerId, titleId, duration, source) -> {
        var spec = TitleDurationParser.parse(duration);
        if (spec.isEmpty()) return false;
        return service.giveTitle(playerId, titleId, spec.get(), source).success();
    });
 
registerCapability(TitleConfigQueryable.class, titleId -> {
    TitleDefinition def = config.title(titleId);
    return def == null ? null
        : new TitleConfigQueryable.TitleInfo(def.displayName(), def.qualityName(), def.description());
});

使用方:如何调用其他模块

基本用法

TitleGrantable title = getCapability(TitleGrantable.class);
if (title != null) {
    title.giveTitle(player.getUniqueId(), "legend", "7d", "MyModule");
}

推荐:Supplier 延迟查找

避免在 onEnable 瞬间因加载顺序得到 null

private Supplier<TitleGrantable> titleGrantable;
private Supplier<MailDispatchable> mailDispatchable;
 
@Override
protected void startService() {
    titleGrantable = () -> getCapability(TitleGrantable.class);
    mailDispatchable = () -> getCapability(MailDispatchable.class);
    dispatchService = new DispatchService(titleGrantable, mailDispatchable);
    dispatchService.start();
}
 
// 业务代码中
public void onQuestComplete(Player player) {
    TitleGrantable title = titleGrantable.get();
    if (title != null) {
        title.giveTitle(player.getUniqueId(), "hero", "permanent", "Quest");
    }
    MailDispatchable mail = mailDispatchable.get();
    if (mail != null) {
        mail.dispatchPreset(new MailPresetRequest("quest_reward", player.getName(), "Quest", null));
    }
}

外部 Bukkit 插件接入:AxsCapabilities

上文 getCapability() 仅供 AXS 模块内部使用。面向外部 Bukkit 插件,Suite 提供静态门面 AxsCapabilitiesaxs-apixuanmo.arcartxsuite.api 包),配合 @PublicCapability 注解做可见性过滤——只有标注该注解的接口才能被外部获取,其余一律返回 null

当前公开的 Capability

接口提供方用途
EventBusCapability宿主主题 pub/sub
SecondaryPasswordAccess宿主二级密码访问
AxsMailServicemail邮件发送/查询/领取/删除(全异步)
AxsMarketServicemarket市场 UI/挂单/搜索/计数
MapNavigablemap地图第三方插件注册路径点推送、导航回调、导航互斥
QuestGpsNavigablequestgps任务推送/接取/追踪/门禁检查
BattlePassAccessbattlepass战令经验/等级/档位/任务进度
FishingAccessfishing钓鱼小游戏/图鉴/经验/疲劳
RegionQueryableregions保护区域查询
BossTrackerQueryableentitytracker活跃 Boss 会话查询
LotteryAccesslottery抽奖积分/兑换/开箱
LoginViewQueryableloginview认证状态/打开登录界面
PropAccessibleprop道具列表/应用到主手
AnnouncerBroadcastable / SubtitlePlayableannouncer公告广播 / 播放字幕组
RgbRenderablergb渐变文本渲染
TitleGrantable / TitleConfigQueryabletitle发放称号 / 称号元数据
ChatCardSendable / ChatMutablechat聊天卡片 / 禁言管理
TabRefreshabletab刷新 Tab 列表
CombatEffectTriggerablecombateffect触发战斗特效
SignalDispatchableeventpacket触发规则引擎信号
MenuOpenablemenu打开配置菜单
AfkRewardDispatchableafkreward挂机状态/原地挂机
WarehouseAutoDepositablewarehouse仓库自动存入
ExtraBackpackAccessextrabackpack扩展背包访问
PickupNotifiable / PickupInterceptorpickup拾取通知 / 拾取拦截(分模式注册)
QQBotNotifiable / QQBotBroadcastable / QqBindCapableqqbotQQ 群事件/推送/绑定查询
EssentialsQueryableessentialsAFK/隐身/禁言/昵称查询
OnlineRewardsQueryableonlinerewards签到/在线时长查询与操作
InteractionStateconversation对话交互状态
TooltipDataCapabletooltip物品提示数据

完整方法签名见 Capability API 参考

接入步骤

  1. 插件 plugin.yml 声明 depend: [ArcartXSuite](或 softdepend
  2. 构建时 compileOnly 依赖 axs-api
  3. 调用 AxsCapabilities.get(接口.class),结果判 null
// 外部插件 onEnable / 使用前
AxsMailService mail = AxsCapabilities.get(AxsMailService.class);
if (mail != null) {
    mail.getUnreadCount(player.getUniqueId()).thenAccept(count -> {
        // 异步结果,勿在主线程 join()
    });
}
  • AxsCapabilities.get(Class) — 类型未公开、未注册或本体未就绪时返回 null
  • AxsCapabilities.has(Class) — 判断指定公开 capability 是否可用
  • 模块热卸载后对应 capability 自动摘除,下次 get() 返回 null——建议每次使用时现取,不要长期缓存

EventBus 使用

// 发布方(Market 模块)
EventBusCapability bus = getCapability(EventBusCapability.class);
if (bus != null) {
    bus.publish("axs.market.listing_created", player, Map.of("price", "1000", "item", "钻石剑"));
}
 
// 订阅方(QQBot 模块)
EventBusCapability bus = getCapability(EventBusCapability.class);
if (bus != null) {
    String subId = bus.subscribe("axs.market.*", event -> {
        // event.topic()、event.player()、event.typedPayload()
        qqBot.sendToAllGroups("[交易] " + event.player().getName() + " 购买了物品");
    });
}

声明发布主题

若模块通过 EventBus 发布事件,应在 publishedTopics() 中声明,以便其他模块在启动时通过 hasPublisher(topic) 检测:

@Override
protected List<String> publishedTopics() {
    return List.of("axs.market.listing_created", "axs.market.auction_purchased");
}

基类会在 startService() 之前自动将这些主题注册到 EventBus。

内置 Capability 能力图

Capability 接口提供模块公开典型使用方用途
TitleGrantabletitleeventpacket, battlepass发放称号
TitleConfigQueryabletitletab, chat查询称号元数据
MailDispatchablemaileventpacket, onlinerewards按预设发邮件(内部)
AxsMailServicemail外部插件邮件完整服务
AxsMarketServicemarket外部插件市场 UI/挂单/搜索/计数
SubtitlePlayableannouncereventpacket播放字幕组
AnnouncerBroadcastableannouncer外部插件即时/排队公告广播
ChatCardSendablechateventpacket发送聊天卡片
ChatMutablechatessentials禁言/解禁
QuestGpsNavigablequestgpseventpacket, 外部插件任务导航
MapNavigablemapquestgps, 外部插件地图外部导航点
TabRefreshabletabtitle, chat刷新 Tab 列表
CombatEffectTriggerablecombateffecteventpacket, prop触发战斗特效
SignalDispatchableeventpacketonlinerewards, afkreward触发规则引擎信号
BattlePassAccessbattlepass外部插件战令经验/等级/档位
FishingAccessfishing外部插件钓鱼状态/经验/疲劳
RegionQueryableregions外部插件区域信息查询
BossTrackerQueryableentitytracker外部插件活跃 Boss 会话查询
LotteryAccesslottery外部插件抽奖积分/兑换/开箱
LoginViewQueryableloginview外部插件认证状态/打开登录界面
PropAccessibleprop外部插件道具列表/应用到主手
RgbRenderablergb外部插件渐变文本渲染
OnlineRewardsQueryableonlinerewards外部插件签到/在线时长
QQBotBroadcastableqqboteventpacket, mail推送到 QQ 群
QQBotNotifiableqqbot第三方监听群消息/进退群
QqBindCapableqqbotloginviewQQ 绑定查询
WarehouseAutoDepositablewarehousepickup拾取自动入库
ExtraBackpackAccessextrabackpack外部插件扩展背包访问
PickupNotifiable / PickupInterceptorpickupwarehouse拾取通知/拦截(分模式)
EssentialsQueryableessentialstab, chatAFK/隐身/禁言/昵称
MenuOpenablemenuessentials, 第三方打开配置菜单
AfkRewardDispatchableafkrewardessentials挂机状态/原地挂机
InteractionStateconversationpickup, prop对话中让出共享按键
EventBusCapability宿主market, qqbot, 第三方/外部插件主题 pub/sub
SecondaryPasswordAccess宿主外部插件二级密码访问
TooltipDataCapabletooltip外部插件物品提示数据
PlayerDataPurgeable多模块宿主 /axs purge清除玩家数据
DatabaseMigratable多模块宿主 /axs migrate跨库迁移
PolygonSelectionCapability宿主/regionsfishing 等多边形选区管理
ModuleAdminCapability宿主调试工具模块管理(@Internal

宿主命令与 Capability

部分 Capability 由宿主命令统一调用,模块只需注册即可接入:

命令使用的 Capability
/axs purge <玩家> <模块|all>PlayerDataPurgeable
/axs migrate <模块> <方向>DatabaseMigratable

实现 PlayerDataPurgeablemoduleId() 必须与本模块 id 一致。

第三方模块:完整示例

模块 A(提供方) — 声望系统:

// api/ReputationQuery.java
public interface ReputationQuery {
    int getReputation(UUID playerId);
}
 
// ReputationModule.java
@Override
protected void startService() {
    service = new ReputationService(this);
    service.start();
    registerCapability(ReputationQuery.class, service);
}

模块 B(使用方) — 商店折扣:

# shop/module.yml
softdepends: [reputation]
@Override
protected void startService() {
    Supplier<ReputationQuery> rep = () -> getCapability(ReputationQuery.class);
    shopService = new ShopService(this, rep);
}
 
// ShopService 内
double discount = 1.0;
ReputationQuery rep = reputationSupplier.get();
if (rep != null && rep.getReputation(player.getUniqueId()) >= 1000) {
    discount = 0.9;
}

B 的 Jar 不需要依赖 A 的实现类,只需 compileOnly A 发布的 api 接口 Jar。

生命周期与清理

事件行为
模块 onEnablestartService()registerCapability
模块 onDisable宿主自动 removeCapabilities(moduleId)
/axs unload同上,并关闭 ClassLoader

不要在 onDisable 里留悬挂引用

使用方应把 Capability 引用置空;提供方无需手动 unregister(宿主会处理)。若你在静态字段缓存了 Capability,unload 后可能持有已失效对象。

最佳实践 checklist

  • 接口放在 api 包,仅含业务方法,不暴露内部 Service
  • 使用方对 getCapability 结果始终判 null
  • 可选依赖写 softdepends,必选依赖写 depends
  • 使用 Supplier 延迟查找
  • 单接口单一职责;不要做一个「万能 Capability」
  • 为对外 Capability 编写 Javadoc 与 wiki 说明
  • 持久化模块实现 PlayerDataPurgeable / DatabaseMigratable 以接入宿主运维命令