Skip to content
On this page

API 方法表

ChaUI 服务端 API 供其他 Bukkit 插件控制已经打开的页面、槽位和实体组件,也可以监听玩家在 ChaUI 页面中的交互。

公开类位于以下包中:

java
import com.github.ginirohikocha.chaui.api.ChaUIAPI;
import com.github.ginirohikocha.chaui.api.ChaUIEntityAPI;
import com.github.ginirohikocha.chaui.api.ChaUISlotAPI;
import com.github.ginirohikocha.chaui.api.GlobalScriptExecutionInfo;
import com.github.ginirohikocha.chaui.api.PageRegistrationResult;
import com.github.ginirohikocha.chaui.api.PersistentWorldPage;
import com.github.ginirohikocha.chaui.api.WorldMoveType;
import com.github.ginirohikocha.chaui.api.event.ChaUIClientReadyEvent;
import com.github.ginirohikocha.chaui.api.event.ChaUIPacketEvent;
import com.github.ginirohikocha.chaui.api.event.ChaUISlotClickEvent;
import java.util.List;

身份字段速查

字段含义作用范围
pageId页面配置的 id标识页面种类。按 pageId 下发的状态会应用到该玩家当前打开的同 ID 页面;同一页面存在多个世界实例时会一起收到状态
sessionId页面每次打开时生成的会话 ID精确标识一次打开会话,关闭并重新打开后会变化
elementId / componentIdelements 中组件的 id标识页面中的一个组件;两个名称表达同一种组件身份
mode + binditem_slot.itemSlot.mode + bind标识玩家的一份槽位状态;不同页面使用相同组合时共享,同 bind 的不同模式彼此隔离
instanceId打开世界页面时指定的实例 ID在同一玩家当前打开的全部世界页面中全局唯一;关闭世界实例时不需要 pageId

不要混用

槽位 API 使用 mode + bind,不接收页面或 sessionId;普通页面状态修改通常使用 pageId + elementId,世界实例关闭只使用 instanceId

ChaUIAPI

用于打开、关闭和修改页面运行时状态。状态修改只影响当前客户端页面会话,不会改写服务端的页面 yml。

方法参数返回值说明
setVariable(Player player, String pageId, String name, Object value)玩家、页面 ID、变量名、变量值void修改一个 vars 变量。变量名和变量值必须符合页面变量规则
setVariables(Player player, String pageId, Map<String, Object> variables)玩家、页面 ID、变量映射void一次修改多个 vars 变量
setComponentState(Player player, String pageId, String elementId, String property, Object value)玩家、页面 ID、组件 ID、属性路径、值void修改一个允许动态更新的组件属性,例如 visibletext.value
setComponentStates(Player player, String pageId, String elementId, Map<String, Object> properties)玩家、页面 ID、组件 ID、属性映射void一次修改同一组件的多个属性
runScript(Player player, String pageId, String script)玩家、页面 ID、ChaUI Scriptvoid在玩家当前页面会话中执行客户端脚本
executeGlobalMethod(Player player, String methodId, Object... arguments)在线玩家或 null、通用方法 ID、顺序参数String启动一项独立服务端通用方法并返回根执行 ID;必须在 Bukkit 主线程调用
stopGlobalScript(String executionId)根执行 IDboolean停止仍活动的通用脚本及其子方法和计时任务;找到并停止时返回 true
activeGlobalScripts()只读 List<GlobalScriptExecutionInfo>返回当前仍活动的通用脚本根执行快照,包含执行 ID、方法、来源和目标玩家信息
playSound(Player player, String pageId, String soundPath)玩家、页面 ID、ChaUI 素材目录下的 .ogg 相对路径void在玩家客户端播放本地 ChaUI 音效;非法路径会抛出 IllegalArgumentException
resetRememberedComponentPositions(Player player)玩家void重置该玩家在当前服务器下记住的全部组件位置;只接受在线且客户端已就绪的玩家
registerPage(Plugin sourcePlugin, String resourcePath)来源插件、Jar 内页面资源路径PageRegistrationResult不覆盖地注册并立即加载第三方页面
registerPage(Plugin sourcePlugin, YamlConfiguration configuration)来源插件、完整页面配置PageRegistrationResult直接从 Bukkit YamlConfiguration 注册并立即加载页面
elementType(String pageId, String elementId)页面 ID、组件 IDStringnull返回当前已加载页面中指定组件的类型;参数为空、页面未加载或组件不存在时返回 null,不会暴露内部页面定义对象
open(Player player, String pageId)玩家、页面 IDvoid从 ChaUI 已加载的页面中查找并打开普通页面或 HUD。同一玩家的相同页面会先完整关闭旧会话再打开新会话
openSubPage(Player player, String pageId)玩家、子页面 IDvoid在玩家当前普通 GUI 或容器替换 GUI 上方打开一层 screen 子页面
replaceSubPage(Player player, String pageId)玩家、目标子页面 IDCompletableFuture<String>关闭根页面上方的旧子页面链,再把目标页面直接挂到根页面;完成后返回新子页面的 sessionId。快速连续调用时最后一次请求生效
closeSubPage(Player player)玩家void只关闭当前 GUI 栈最上层子页面
openWorld(Player player, String pageId, String instanceId)玩家、世界页面 ID、实例 IDvoid使用页面 display.world.x/y/z/yaw/pitch 的默认值打开世界页面
openWorld(Player player, String pageId, String instanceId, double x, double y, double z, String yaw)玩家、世界页面 ID、实例 ID、世界坐标、朝向void兼容重载;使用调用方坐标与 yaw,并把 pitch 设为 0
openWorld(Player player, String pageId, String instanceId, double x, double y, double z, String yaw, double pitch)玩家、世界页面 ID、实例 ID、世界坐标、水平朝向、倾角void使用调用方给出的完整位置和朝向。yaw 可填写有限数字字符串或 follow_playerpitch 必须是有限数字
moveWorld(Player player, String instanceId, double x, double y, double z)玩家、世界实例 ID、目标坐标void立即把已经打开的世界页面实例移动到目标坐标,不关闭或重新打开页面
moveWorld(Player player, String instanceId, double x, double y, double z, long durationMillis, WorldMoveType type)玩家、世界实例 ID、目标坐标、毫秒时长、插值类型void在指定时间内移动现有实例;支持 LINEAREASE_IN_OUT
close(Player player, String pageId)玩家、页面 IDvoid关闭该玩家当前打开的普通页面或 HUD,并执行完整关闭生命周期
closeWorld(Player player, String instanceId)玩家、世界实例 IDvoid在该玩家的全部世界页面中查找并关闭指定实例,不需要页面 ID
addPersistentWorldPage(String worldName, String pageId, String instanceId)世界名、世界页面 ID、全服实例 IDPersistentWorldPage使用页面默认世界坐标、yaw 与 pitch 添加并持久化全服常驻实例
addPersistentWorldPage(String worldName, String pageId, String instanceId, double x, double y, double z, String yaw)世界名、页面 ID、实例 ID、坐标、朝向PersistentWorldPage兼容重载;使用显式位置与 yaw,并把 pitch 设为 0
addPersistentWorldPage(String worldName, String pageId, String instanceId, double x, double y, double z, String yaw, double pitch)世界名、页面 ID、实例 ID、坐标、水平朝向、倾角PersistentWorldPage使用显式位置和完整朝向添加并持久化全服常驻实例
removePersistentWorldPage(String instanceId)全服常驻实例 IDboolean删除持久记录并关闭在线玩家对应实例;不存在时返回 false
persistentWorldPages()只读 List<PersistentWorldPage>按注册顺序返回常驻实例快照

服务端通用脚本示例

java
String executionId = ChaUIAPI.executeGlobalMethod(
        player, "notify_player", "欢迎回来");

List<GlobalScriptExecutionInfo> active = ChaUIAPI.activeGlobalScripts();
boolean stopped = ChaUIAPI.stopGlobalScript(executionId);

executeGlobalMethodstopGlobalScript 只能在 Bukkit 主线程调用。传入玩家时必须在线;已安装并启用 PlaceholderAPI 时,字符串参数会在执行开始时为该玩家解析一次,占位结果不会在后续 timer / interval 中实时刷新。没有 PlaceholderAPI 时保留原始 %...% 文本。参数数量和 Java 类型必须与 global-scripts.yml 的声明严格一致,不会把字符串数字、字符串布尔值或其他对象隐式转换。

GlobalScriptExecutionInfo 是不可变记录,字段依次为 executionIdmethodIdsourcesourceIdplayerIdplayerNamesource 可能是 apicommandpagecron;页面的 sourceId 是调用页面 ID,Cron 的 sourceId 是 Cron ID,控制台执行的玩家字段为 null。返回列表只是一刻的只读快照,已经完成或已停止的执行不会保留在其中。

同步完成且没有活动计时任务的方法也会返回执行 ID,但随后停止会得到 false。传入 null 只适合不依赖玩家、玩家 PAPI、玩家命令或页面打开动作的方法。完整教程见服务端通用脚本与 Cron

registerPage 只能在 Bukkit 主线程调用,成功返回时页面已经可以立即打开,不需要再执行 reload。完整资源布置、三种返回结果和异常处理请阅读页面注册 API

第三方插件需要校验已注册页面或服务器已有同 ID 页面中的组件类型时,应调用 elementTypePageCache、页面定义和组件定义属于 ChaUI 内部实现,不是混淆稳定的公开 API。

调用方只需要传页面 ID,不需要读取或构造 ChaUI 内部的页面定义对象。页面 ID 非法、页面不存在、打开方式与页面显示模式不匹配,或世界页面缺少 display.world 时,打开方法会抛出 IllegalArgumentException。客户端尚未发送 client_ready 时,打开操作会安全忽略;需要在进服时立即打开业务页面的插件应监听 ChaUIClientReadyEvent

常驻世界页面 API 管理的是全服注册表,不接收 Player。添加与删除只能在 Bukkit 主线程调用;添加时目标世界必须已经加载,页面必须是有效世界页面,实例 ID 在整张常驻列表中必须唯一。磁盘保存失败不会提交内存状态,也不会提前向玩家打开或关闭实例。注册记录保存在 plugins/ChaUI/persistent-world-pages.yml,玩家进服就绪或进入匹配世界时自动打开,离开世界时自动关闭。

打开与关闭示例

java
// 打开普通页面或 HUD
ChaUIAPI.open(player, "welcome");

// 使用页面 yml 中配置的世界坐标和朝向
ChaUIAPI.openWorld(player, "world_welcome", "主城欢迎牌");

// 临时覆盖世界坐标,并让竖直页面朝向跟随玩家
ChaUIAPI.openWorld(player, "world_welcome", "副本入口", 120.5, 65.0, -30.5, "follow_player");

// 把地图正面朝上横放;固定 yaw 可保持地图方向不随玩家旋转
ChaUIAPI.openWorld(player, "ground_map", "主城地图", 120.5, 64.01, -30.5, "0", 90.0D);

// 立即移动现有实例,不触发 close/open 生命周期
ChaUIAPI.moveWorld(player, "副本入口", 125.0, 66.0, -28.0);

// 在 800 毫秒内平滑移动;新移动会立即取代尚未完成的旧移动
ChaUIAPI.moveWorld(player, "副本入口", 130.0, 66.0, -25.0,
        800L, WorldMoveType.EASE_IN_OUT);

// 普通页面和 HUD 按页面 ID 关闭
ChaUIAPI.close(player, "welcome");

// 把当前内容子页面替换为商店页,并在真正打开后发送初始变量
ChaUIAPI.replaceSubPage(player, "barn_shop").thenAccept(sessionId ->
        ChaUIAPI.setVariables(player, "barn_shop", Map.of("quantity", 1))
);

// 重置该玩家在当前服务器下记住的全部组件位置
ChaUIAPI.resetRememberedComponentPositions(player);

// 世界页面按全局唯一的实例 ID 关闭,不填写页面 ID
ChaUIAPI.closeWorld(player, "主城欢迎牌");

replaceSubPage 适合顶部 Tab 和同级内容区。它会等待旧页面完成关闭与临时槽收尾,不需要调用方添加 tick 延迟。请不要用“先 closeSubPage、再立即 openSubPage”模拟替换。

instanceId 是玩家当前全部世界页面中的实例名称,可以使用中文或纯数字。重复使用同一个实例 ID 打开页面时,旧世界实例会先完整关闭,再由新实例替换。

moveWorld 只修改现有世界实例的运行时坐标,不改写页面 yml,也不会重新初始化布局、素材、离屏纹理或页面生命周期。任意新移动请求都会终止旧移动,并从客户端当前实际显示位置开始;LINEAR 是匀速移动,EASE_IN_OUT 会在起点与终点处平滑减速。

resetRememberedComponentPositions 需要在 Bukkit 主线程调用。目标玩家必须在线并已经完成 ChaUI 客户端就绪握手;否则会明确拒绝,不会排队到下次登录。重置只影响当前服务器身份下该玩家的本地位置记忆,不会修改页面 yml,也不会影响其他玩家或其他服务器。

页面状态示例

java
ChaUIAPI.setVariable(player, "welcome", "progress", 0.75);
ChaUIAPI.setComponentState(player, "welcome", "tip", "visible", true);
ChaUIAPI.setComponentState(player, "welcome", "dialog", "pointerEvents", "block");
ChaUIAPI.setComponentState(player, "welcome", "dialog", "scale", 1.05);
ChaUIAPI.setComponentState(player, "welcome", "dialog", "opacity", 0.9);
ChaUIAPI.runScript(player, "welcome", "vars.ready = true");

scaleopacity 也可以通过 runScript 绑定公式,例如 component("dialog").opacity = formula("vars.fade")。API 传入的属性和值仍会按页面字段规则校验;布局组件不能设置缩放或透明度,实体不能设置根级透明度。

世界页面状态

setVariablesetComponentStaterunScript 等状态接口按 pageId 匹配页面。如果同一玩家打开了同一页面的多个世界实例,这些实例会一起应用该状态。

ChaUISlotAPI

槽位状态由服务端裁决。槽位身份是玩家 UUID 与 mode + bindId,不绑定页面或会话。操作 mapped 槽位会读写玩家的真实映射槽;操作 persistenttemporary 槽位会读写玩家级独立槽位系统。

方法参数返回值说明
getSlotItem(Player player, String mode, String bindId)玩家、模式、槽位绑定 IDItemStacknull读取一组可合法放入原版背包的物品;逻辑数量超过原版一组时只返回一组
getSlotPrototype(Player player, String mode, String bindId)玩家、模式、槽位绑定 IDItemStacknull读取数量固定为 1 的完整物品原型,适合序列化和持久化
getSlotAmount(Player player, String mode, String bindId)玩家、模式、槽位绑定 IDint读取槽位的完整逻辑数量,例如 999
getSlotMaxStack(Player player, String mode, String bindId)玩家、模式、槽位绑定 IDint读取当前实际数量上限;玩家没有打开任何 ChaUI 运行时页面时返回 0
setSlotItem(Player player, String mode, String bindId, ItemStack item)玩家、模式、槽位绑定 ID、物品void写入槽位并同步所有已打开的相同 mode + bindId;传入 null 可清空
setSlotItem(Player player, String mode, String bindId, ItemStack prototype, int amount)玩家、模式、槽位绑定 ID、物品原型、逻辑数量void使用物品原型和独立逻辑数量写入大容量槽;原型会按数量 1 保存
clearSlotItem(Player player, String mode, String bindId)玩家、模式、槽位绑定 IDvoid清空指定槽位并同步相关页面
refreshSlot(Player player, String mode, String bindId)玩家、模式、槽位绑定 IDvoid向所有已打开的相同 mode + bindId 组件刷新权威状态
refreshSlots(Player player, String mode)玩家、模式void刷新玩家当前打开页面中引用的该模式全部槽位
setItemDisplay(Player player, String pageId, String elementId, ItemStack item)玩家、页面 ID、item_display 组件 ID、展示物品void用一次原子状态补丁同步完整物品快照、物品 ID 和数量;空气、数量不大于 0 或 null 都按清空处理
clearItemDisplay(Player player, String pageId, String elementId)玩家、页面 ID、item_display 组件 IDvoid用一次原子状态补丁同时清空完整物品快照、物品 ID 和数量

修改读取结果

需要改变槽位物品时,请调用 setSlotItem。不要只修改 getSlotItem 返回的对象并期待页面自动同步。

大容量槽不能把 999 直接写入 Bukkit ItemStack.amount。Minecraft 只允许 ItemStack 使用当前版本认可的原版数量范围;超出后,物品序列化、保存或发包都可能失败。请把物品与数量分开处理:

java
ItemStack prototype = new ItemStack(Material.DIAMOND, 1);

// 页面中的 itemSlot.maxStack 必须允许 999。
ChaUISlotAPI.setSlotItem(player, "persistent", "roi_loot_000", prototype, 999);

// 持久化时分别保存原型和逻辑数量。
ItemStack savedPrototype = ChaUISlotAPI.getSlotPrototype(
        player, "persistent", "roi_loot_000");
int savedAmount = ChaUISlotAPI.getSlotAmount(
        player, "persistent", "roi_loot_000");

恢复数据时仍调用五参数 setSlotItem,不要执行 savedPrototype.setAmount(savedAmount)getSlotItem 适合把一组物品交给原版背包;读取全部库存数量必须使用 getSlotAmount

player 不能为空;mode 只允许 persistenttemporarymappedbindId 必须是安全绑定 ID,且 mapped 只能使用明确支持的 backpack_* 映射。persistent 的读取、写入、清空和刷新都要求 bindId 已在 slot.yml 注册,中文 ID 同样支持。非法参数或未注册持久槽会抛出 IllegalArgumentException。参数校验通过后,如果玩家没有打开任何 ChaUI 运行时页面,物品读取返回 null,数量与上限读取返回 0,写入、清空和刷新安全忽略。

只要玩家至少打开了一个运行时页面,persistenttemporary 就可以在当前页面尚无对应组件时提前写入。ChaUI 会从已加载页面目录中查找相同 mode + bindIdmaxStack 契约,因此目标槽不必恰好出现在当前页面。之后任意页面出现相同槽位,会立即取得预存物品。预存临时槽位默认使用安全的 return_to_player 收尾策略。

API 调用不会执行组件的 itemSlot.check;该字段只是客户端点击发包前的便捷条件。setSlotItem 属于可信服务端写入,可以绕过注册项的 acceptWhen,但不能绕过 persistent ID 注册。插件业务仍需自行完成权限和物品安全判断。注册文件与接收条件见槽位注册表

ChaUIEntityAPI

用于查询或修改 entity 组件。状态设置接口按 pageId 下发,因此同一页面的多个世界实例会同时更新。

方法参数返回值说明
entityUuid(Player player, String pageId, String elementId)玩家、页面 ID、实体组件 IDUUIDnull查询普通页面或 HUD 当前会话中实体组件所显示来源实体的 UUID。页面未打开、组件不存在、组件类型不是 entity、来源实体无法解析或参数为空时返回 null;世界页面实例当前不能通过此方法查询
setDisplayName(Player player, String pageId, String elementId, String displayName)玩家、页面 ID、实体组件 ID、显示名void修改 entity.displayName
setSource(Player player, String pageId, String elementId, String source)玩家、页面 ID、实体组件 ID、来源void修改 entity.source,可用值为 selfuuidentity_idvirtual
setEntityType(Player player, String pageId, String elementId, String entityType)玩家、页面 ID、实体组件 ID、实体类型 IDvoid修改虚拟实体的 entity.entityType,例如 minecraft:zombie
setYaw(Player player, String pageId, String elementId, Object yaw)玩家、页面 ID、实体组件 ID、有限数值void修改实体整体 yaw;无法转换为有限数值时抛出 IllegalArgumentException
setPitch(Player player, String pageId, String elementId, Object pitch)玩家、页面 ID、实体组件 ID、有限数值void修改实体整体 pitch;无法转换为有限数值时抛出 IllegalArgumentException
setRoll(Player player, String pageId, String elementId, Object roll)玩家、页面 ID、实体组件 ID、有限数值void修改实体整体 roll;无法转换为有限数值时抛出 IllegalArgumentException
setOrthographic(Player player, String pageId, String elementId, boolean orthographic)玩家、页面 ID、实体组件 ID、是否正交投影void修改 entity.orthographicfalse 为默认 30 度透视投影,true 为正交投影
setTrackMouse(Player player, String pageId, String elementId, boolean trackMouse)玩家、页面 ID、实体组件 ID、是否追踪鼠标void修改 entity.trackMouse

entityUuid 应在页面会话建立后调用,其返回值由实体来源决定:

entity.sourceUUID 来源
self目标玩家的真实 UUID
uuidentity.uuid 指定实体的 UUID
entity_id目标玩家当前世界中对应网络实体 ID 的真实 UUID
virtual合法的 entity.virtualUuid;未配置或无效时按页面 ID、会话 ID 和组件 ID 生成确定性虚拟 UUID

ChaUIClientReadyEvent

客户端确认本地玩家、世界和 Play 发包通道均已可用后,会向服务端发送一次 client_ready。服务端验证协议版本并完成本次连接的幂等登记后,先触发 ChaUIClientReadyEvent,随后才统一同步页面定义、槽位会话、原版容器替换注册表、常驻 HUD 和当前世界匹配的常驻世界页面。

该事件不可取消,同一玩家同一次在线连接只触发一次;玩家退出并重新进入后会再次触发。

方法返回值说明
getPlayer()Player已完成 ChaUI 客户端就绪握手的玩家
getProtocolVersion()int客户端已通过验证的 ChaUI 协议版本

客户端就绪示例

java
public final class ChaUIReadyListener implements Listener {
    @EventHandler
    public void onClientReady(ChaUIClientReadyEvent event) {
        ChaUIAPI.open(event.getPlayer(), "welcome");
    }
}

不要再用 PlayerJoinEvent + runTaskLater 猜测客户端加载时间。这个事件就是第三方插件在玩家进服后调用 ChaUI 打开、槽位和状态 API 的统一起点。

ChaUISlotClickEvent

玩家点击 item_slot 后,ChaUI 会先校验会话和槽位版本、计算默认交换结果,再触发该事件。监听器可以取消操作,也可以修改最终的光标物品和槽位物品。

事件实现了 Bukkit Cancellable

方法返回值或参数可修改说明
getPlayer()Player触发操作的玩家
getSessionId()String被点击页面的会话 ID
getPageId()String页面 ID
getComponentId()String被点击的槽位组件 ID
getBind()String槽位绑定 ID
getAction()String槽位动作名称,当前点击流程为 click
getSlotDefinition()ItemSlotDefinition当前槽位配置
getCursorItemBefore()ItemStacknull操作前的服务端光标物品,返回副本
getSlotItemBefore()ItemStacknull操作前的槽位物品,返回副本
getCursorItemAfter()ItemStacknull通过 setter默认计算后的光标物品,返回副本
setCursorItemAfter(ItemStack item)void覆盖操作完成后写回的光标物品
getSlotItemAfter()ItemStacknull通过 setter默认计算后的槽位物品,返回副本
setSlotItemAfter(ItemStack item)void覆盖操作完成后写回的槽位物品
isCancelled()boolean通过 setter查询事件是否已取消
setCancelled(boolean cancelled)void取消后不写回本次操作结果

槽位事件示例

java
public final class RewardSlotListener implements Listener {
    @EventHandler
    public void onSlotClick(ChaUISlotClickEvent event) {
        if (!"reward_input".equals(event.getBind())) {
            return;
        }

        ItemStack result = event.getSlotItemAfter();
        if (result != null && result.getAmount() > 16) {
            event.setCancelled(true);
        }
    }
}

ChaUIPacketEvent

真实运行时脚本执行 packet(...)发包(...) 后,服务端完成会话与页面校验,再触发该事件。该动作可以来自页面事件、组件全部事件、自定义方法、计时器回调或 API 下发脚本,不限于点击和悬浮。编辑器编辑状态与隔离预览不会发包。ChaUI 不会替业务插件处理 data 内容。

该事件不可取消,全部字段只读。

方法返回值说明
getPlayer()Player触发发包的玩家
getActionId()String本次已授权脚本动作的稳定 ID,可用于业务去重或追踪
getSessionId()String触发动作的页面会话 ID
getPageId()String页面 ID
getElementId()String触发脚本的组件 ID;页面级事件固定为空字符串
getTrigger()String原始组件事件或页面事件;方法和计时器继承创建它们的上下文,缺少明确事件来源时为 script
getPacketId()String页面脚本填写的业务包 ID
getData()只读 List<String>页面脚本传入的数据列表。复杂内容可由页面作者放入某一项 JSON 字符串,再由业务插件解析

自定义包事件示例

java
public final class ChaUIPacketListener implements Listener {
    @EventHandler
    public void onPacket(ChaUIPacketEvent event) {
        if (!"treasure.claim".equals(event.getPacketId())) {
            return;
        }

        Player player = event.getPlayer();
        List<String> data = event.getData();
        // 在这里校验业务状态,再执行奖励逻辑。
    }
}

注册事件监听器

监听器仍按标准 Bukkit 方式注册:

java
@Override
public void onEnable() {
    getServer().getPluginManager().registerEvents(new RewardSlotListener(), this);
    getServer().getPluginManager().registerEvents(new ChaUIPacketListener(), this);
    getServer().getPluginManager().registerEvents(new ChaUIReadyListener(), this);
}

服务端始终权威

不要根据客户端发来的字符串直接发放物品、执行命令或修改经济数据。业务插件仍需根据玩家、页面、会话和自身数据再次判断操作是否合法。