模块化架构
ArcartX-Suite 使用 宿主 + 模块 Jar 架构。宿主提供核心基础设施,各功能模块可以打包为独立 Jar 放入 modules/ 目录按需加载。
开源 SDK 仓库
公开 API 与宿主参考实现见 ArcartXSuite-Core。下述目录结构与该仓库一致(axs-api/、proxy/ 等)。
项目结构
ArcartX-Suite/
├── ArcartX-Suite-api/ # 模块 API 接口层(AXSModule, ModuleContext 等)
├── ArcartX-Suite-core/ # 宿主核心(ShadowJar 输出)
├── modules/
│ ├── announcer/ # Announcer 播报 + Subtitle 字幕
│ ├── entitytracker/ # EntityTracker 实体追踪 + 目标 HUD
│ ├── essentials/ # Essentials 基础工具 (传送/管控/砍树/InvActions)
│ ├── chat/ # Chat 频道聊天
│ ├── conversation/ # Conversation 对话桥
│ ├── eventpacket/ # EventPacket 事件引擎
│ ├── combateffect/ # CombatEffect 战斗特效 + 伤害飘字
│ ├── loginview/ # LoginView 登录界面
│ ├── mail/ # Mail 邮箱
│ ├── map/ # Map 世界地图
│ ├── onlinerewards/ # OnlineRewards 在线奖励
│ ├── pickup/ # Pickup 拾取提示
│ ├── prop/ # Prop 快捷道具
│ ├── questgps/ # QuestGPS 任务导航
│ ├── regions/ # Regions 区域保护 + 世界规则
│ ├── rgb/ # RGB 渐变色文本
│ ├── tab/ # Tab 在线列表
│ ├── title/ # Title 称号
│ ├── warehouse/ # Warehouse 仓库银行
│ ├── market/ # Market 全球市场
│ ├── qqbot/ # QQBot QQ群服互联
│ ├── battlepass/ # BattlePass 战令系统
│ ├── fishing/ # Fishing 钓鱼系统
│ ├── lottery/ # Lottery 抽奖系统
│ ├── afkreward/ # AfkReward 挂机奖励
│ └── menu/ # Menu 通用菜单系统核心组件
| 组件 | 包路径 | 说明 |
|---|---|---|
AXSModule | ArcartX-Suite-api | 模块生命周期接口:onEnable / onDisable / onReload / isReady |
ModuleContext | ArcartX-Suite-api | 宿主暴露给模块的上下文:plugin 实例、Logger、各种 Bridge |
ModuleDescriptor | ArcartX-Suite-api | 模块元数据:id / name / version / depends |
ModuleCommandHandler | ArcartX-Suite-api | 可选命令处理接口,实现后自动注册 /axs <moduleId> 子命令 |
ModuleRegistry | ArcartX-Suite-core | 模块扫描 / 加载 / 启用 / 禁用 / 重载 |
ModuleClassLoader | ArcartX-Suite-core | 模块隔离 ClassLoader,每个模块 Jar 独立加载 |
DefaultModuleContext | ArcartX-Suite-core | ModuleContext 的默认实现 |
启动流程
onEnable()
├── 初始化反射桥 (packetBridge, clientBridge, itemStackBridge …)
├── 创建 ModuleRegistry
├── scanAvailableModuleIds()
│ └── 预扫描 modules/ 目录,收集所有外部模块 Jar 的 id
├── 对每个内置模块:
│ externalModuleIds.contains(id) → 跳过(交给 ModuleRegistry)
│ 否则 → 执行宿主内置加载(走内置 Service 初始化)
├── printModuleStatusSummary()
├── moduleRegistry.loadAll()
│ └── 按拓扑排序加载所有外部模块 Jar
└── 加载完成关键设计:外部模块 Jar 已完全独立化,通过自建 Service 运行;宿主仅对无外部 Jar 的模块执行内置加载,防止双重初始化。
重载流程
/axs reload all
对每个模块判断加载来源:
- 外部 Jar 已加载 →
moduleRegistry.reloadModule(id)→ 触发模块onReload() - 内置加载 → 执行宿主内置重载逻辑(调用模块
onReload())
/axs reload <模块名>
单模块重载遵循同样逻辑,通过 isExternalModule() 判断走外部还是内置路径。
热加载 / 热卸载
不同于 reload(onDisable + onEnable 在同一个 ClassLoader 内复位),/axs load|unload 提供真正的运行时插拔:
/axs load <模块名>
- 检查模块未加载(已加载则拒绝,提示走 reload)。
- 扫描
modules/目录寻找 id 匹配的 jar。 - 进入与启动期相同的
loadAndEnable(DiscoveredModule)流程:- 检查外部插件依赖 / ArcartX-Suite 模块依赖(depends)
- 创建独立
ModuleClassLoader(URLClassLoader 子类) - 实例化
AXSModule主类 - 构建
DefaultModuleContext - 注册模块的
ModuleConfigSpec(用于配置诊断) - 调用
instance.onEnable(context)
- 失败时调用
cleanupFailedModule(id)回滚(onDisable + 关闭 ClassLoader + 从 modules 表移除)。
/axs unload <模块名>
- 反向依赖检查:遍历所有已启用模块的
descriptor.depends(),若存在依赖该模块的 dependent,则拒绝卸载并提示 dependents 列表。 - 执行
disableModule(loaded):commandHandlers.remove(id)— 取消/axs <id>子命令instance.onDisable()— 模块自清理(停止 Service、注销事件、关闭数据库)- 标记
loaded.setEnabled(false)
removePacketHandlers(id)— 移除该模块注册的ClientPacketHandler。modules.remove(id)— 从注册表移除。closeClassLoader(loaded)— 调用URLClassLoader.close()释放 jar 文件句柄。
已知约束
- Capability 清理:当前 capability 表不跟踪 owner,模块需在
onDisable中自行清理 capabilities(否则旧引用会持有死对象)。ModuleRegistry.removeCapabilities(id)保留作为接口契约位。 - 依赖图变化:
unload不会自动 disable dependents,要求管理员按依赖顺序手动 unload。
UI 注册与更新
每个有 UI 的模块在 reload 时严格执行以下步骤:
| 步骤 | 操作 |
|---|---|
| 1 | onDisable() 停止旧 Service,基类自动注销所有已注册 UI |
| 2 | onEnable() 加载新配置 → 根据 uiResourceMappings() 导出 UI YAML 文件 |
| 3 | startService() 中调用 registerModuleUi() → 内部使用 packetBridge.registerOrReloadUi() 重新读取文件并注册/热重载 |
| 4 | 创建并启动新 Service |
registerModuleUi() 是 AbstractAXSModule 提供的统一封装,内部调用 registerOrReloadUi(),每次都会重新读取磁盘上的 UI 文件,确保手动修改的 YAML 在 /axs reload 后立即生效。onDisable() 时基类会自动 unregisterUi,不再存在 UI 残留问题。
替代旧 API
旧代码中使用的 context.prepareUiBinding() 在 reload 时不会重新读取文件,已废弃。新模块或重构时请改用 registerModuleUi()。
模块实现模式
独立模式
模块自建 Service,完全不依赖宿主业务逻辑。适合逻辑简单或已完全解耦的模块。
public final class RgbModule implements AXSModule {
private ModuleContext context;
private RgbService service;
@Override
public ModuleDescriptor descriptor() {
return ModuleDescriptor.builder("rgb")
.name("RGB").version("1.2.0-beta")
.mainClass(getClass().getName()).build();
}
@Override
public boolean onEnable(ModuleContext context) throws Exception {
this.context = context;
service = new RgbService(context);
service.start();
return true;
}
@Override
public void onDisable() {
if (service != null) { service.shutdown(); service = null; }
}
@Override
public void onReload() throws Exception {
onDisable();
if (context != null) onEnable(context);
}
}模块 Jar 描述文件
每个模块 Jar 在 resources/ 中必须包含 module.yml:
id: mymodule # 唯一标识,与 config.yml 中的键对应
name: MyModule # 显示名称
version: 1.2.0-beta
main: com.example.MyModule # AXSModule 实现类全限定名
api-version: 1.0
depends: [] # 强依赖的其他模块 id
softdepends: [] # 软依赖的其他模块 id
external-depends: [] # 强依赖的外部 Bukkit 插件名
external-softdepends: []ModuleContext API
完整参考见 ModuleContext 上下文。
跨模块协作请阅读 Capability 详解;第三方模块开发见 开发者指南。
| 方法 | 返回类型 | 稳定性 | 说明 |
|---|---|---|---|
plugin() | JavaPlugin | — | 宿主插件实例 |
logger() | Logger | — | 模块专用 Logger |
dataFolder() | File | — | 模块私有数据目录(plugins/ArcartX-Suite/data/<moduleId>/) |
pluginDataFolder() | File | — | 宿主插件数据目录(plugins/ArcartX-Suite/) |
packetBridge() | PacketBridgeAPI | @Stable | ArcartX UI/Packet 桥接 |
clientBridge() | ClientBridgeAPI | @Stable | ArcartX 客户端桥接 |
itemStackBridge() | ItemBridgeAPI | @Stable | ItemStack → JSON |
packetGuard() | PacketGuardAPI | @Stable | 客户端包守卫(频率限制) |
accountTypeService() | AccountTypeService | @Stable | 统一账号识别服务(微软正版 / LittleSkin / 离线) |
itemSourceRegistry() | ItemSourceRegistry | @Stable | 全局物品来源注册表 |
itemMatcher() | ItemMatcherAPI | @Stable | 全局物品匹配器 |
currencyManager() | CurrencyBridgeAPI | @Stable | 全局货币管理器 |
attributeBridge() | AttributeBridgeRegistry | @Stable | 全局属性桥接注册表 |
registerCapability() | void | @Stable | 注册跨模块能力 |
getCapability() | T | @Stable | 查找跨模块能力 |
hasPlugin(String) | boolean | — | 检查外部 Bukkit 插件 |
QuestGPS × Chemdah
- overlay 根键 = Chemdah
Template.getId()(裸 ID,如gps_main_newcomer) - 分类:
category.source二选一 —chemdah仅 meta.type,overlay仅 overlaycategory - UI 发包:
categories/quests/tasks/rewardsMap(Title 式entryKey模板列表) - 详见 QuestGPS 模块文档