Java API
ChaEngine 的稳定入口位于 com.github.ginirohikocha.engine.api。业务插件应依赖这些 API 类,不要直接调用 manager 或 network 包中的内部实现。
所有会读取 Bukkit 实体、修改物品或发送数据的方法都应在服务端主线程调用。返回成功只表示服务端完成校验并提交本次操作;涉及客户端显示时,还要求目标玩家在线、安装兼容 ChaEngineMod、完成连接并拥有对应资源。
模型目录 API
ModelCatalog 用于明确选择模型类别:ModelCatalog.ENTITY 表示实体模型,ModelCatalog.BLOCK 表示方块模型。即使两类模型使用了相同 ID,查询结果也不会混在一起。
| 方法 | 返回值 | 说明 |
|---|---|---|
ChaEngineModelCatalogAPI.getModelIds(ModelCatalog catalog) | Set<String> | 返回所选类别当前已加载模型 ID 的当前快照;结果按自然排序排列且不可修改 |
ChaEngineModelCatalogAPI.hasModelId(ModelCatalog catalog, String id) | boolean | 在指定类别中精确查询模型 ID;空白 ID 返回 false |
传入 null 作为 catalog 时,两个方法都会抛出 IllegalArgumentException。getModelIds 返回的是与后续配置变化分离的快照;执行 /chaengine reload 后,应重新调用该方法取得新目录,不要长期保存旧结果。
下面的示例会在插件启用时读取两类模型目录,并检查一个实体模型是否存在:
package com.example.modelcatalog;
import com.github.ginirohikocha.engine.api.catalog.ChaEngineModelCatalogAPI;
import com.github.ginirohikocha.engine.api.catalog.ModelCatalog;
import java.util.Set;
import org.bukkit.plugin.java.JavaPlugin;
public final class ModelCatalogExamplePlugin extends JavaPlugin {
@Override
public void onEnable() {
Set<String> entityModelIds =
ChaEngineModelCatalogAPI.getModelIds(ModelCatalog.ENTITY);
Set<String> blockModelIds =
ChaEngineModelCatalogAPI.getModelIds(ModelCatalog.BLOCK);
getLogger().info("实体模型:" + entityModelIds);
getLogger().info("方块模型:" + blockModelIds);
boolean hasGuardian = ChaEngineModelCatalogAPI.hasModelId(
ModelCatalog.ENTITY, "crystal_guardian");
getLogger().info("crystal_guardian 已加载:" + hasGuardian);
}
}与 ChaUI 3D 场景配合
ChaUI 3D 场景直接使用模型类别和模型 ID 选择模型:ModelCatalog.ENTITY 表示实体模型,ModelCatalog.BLOCK 表示方块模型,两者都与模型 ID 一起用于页面配置。不需要创建伪实体,也不需要依赖实体名称或 NBT 间接选中模型。
ChaUI 场景模型、工作室光照、实时阴影、材质和动画控制当前支持六个 ChaEngineMod 客户端模块:Forge 1.16.5、Forge 1.20.1,以及 NeoForge 1.21.1、1.21.4、1.21.8、1.21.11。页面端还需要安装同版本的 ChaUIMod;不同版本或加载器的文件不能混用。
模型首次成功渲染后,客户端会自动取得整个模型的点击范围。点击范围不读取配置中的 collision,也不需要页面作者手工填写点击盒;模型尚未成功显示时不会响应模型点击。
第一版只支持整个模型点击,不支持按骨骼或 part 分别点击。需要多个独立操作入口时,请放置多个模型节点,或使用普通 ChaUI 组件承载对应操作。
客户端模型接入由 ChaUI 与 ChaEngineMod 在内部完成,不是 Bukkit 实体动画 API。插件开发者只需通过公开的模型目录 API 检查类别和模型 ID,不要通过反射调用客户端模型接入或渲染能力。
ChaEngineBlockAPI
| 方法 | 返回值 | 说明 |
|---|---|---|
hasModelId(String id) | boolean | 检查已加载方块模型 ID |
giveModelBlock(Player player, String id) | boolean | 玩家在线且 ID 有效时创建并发放载体物品 |
setCarrierBlockModelAtLocation(String id, String locationString) | SetModelResult | 修改 world,x,y,z 指向的载体方块自身数据 |
playCarrierBlockAnimationAtLocation(String locationString, String animationName) | AnimationResult | 播放指定位置模型动画 |
stopCarrierBlockAnimationAtLocation(String locationString) | AnimationResult | 停止指定位置模型动画 |
isCarrierBlockModel(Block block, String id) | boolean | 检查方块自身保存的模型 ID |
isCarrierItemModel(ItemStack itemStack, String id) | boolean | 检查方块载体物品的模型 ID |
getCarrierBlockModelId(Block block) | String 或 null | 读取方块自身模型 ID |
getCarrierItemModelId(ItemStack itemStack) | String 或 null | 读取方块载体物品模型 ID |
SetModelResult 包含 SUCCESS、INVALID_MODEL_ID、INVALID_LOCATION、NOT_CARRIER_BLOCK、COMMAND_FAILED。AnimationResult 包含 SUCCESS、INVALID_LOCATION、INVALID_ANIMATION、NOT_CARRIER_BLOCK。
import com.github.ginirohikocha.engine.api.block.ChaEngineBlockAPI;
import com.github.ginirohikocha.engine.manager.block.BlockModelManager;
BlockModelManager.SetModelResult result =
ChaEngineBlockAPI.setCarrierBlockModelAtLocation(
"altar", "world,120,64,-30");
if (result != BlockModelManager.SetModelResult.SUCCESS) {
getLogger().warning("设置方块模型失败:" + result);
}ChaEngineItemAPI
| 方法 | 返回值 | 拒绝规则 |
|---|---|---|
hasItemId(String id) | boolean | 空或未知 ID 返回 false |
giveCarrierItem(Player player, String id, int amount) | boolean | 玩家无效或 ID 未加载时返回 false;数量最小为 1 |
createCarrierItem(String id, int amount) | ItemStack 或 null | ID 无效时返回 null;数量最小为 1 |
getCarrierItemModelId(ItemStack itemStack) | String 或 null | 非 ChaEngine 物品返回 null |
setCarrierItemScale(ItemStack itemStack, double multiplier) | boolean | 写入单件物品缩放倍率;只接受大于 0 的有限数 |
getCarrierItemScale(ItemStack itemStack) | Double 或 null | 读取单件物品倍率;未设置或数据无效时返回 null |
clearCarrierItemScale(ItemStack itemStack) | boolean | 删除单件物品倍率并恢复默认 1.0 |
import com.github.ginirohikocha.engine.api.item.ChaEngineItemAPI;
import org.bukkit.inventory.ItemStack;
ItemStack reward = ChaEngineItemAPI.createCarrierItem("crystal_sword", 1);
if (reward != null) {
ChaEngineItemAPI.setCarrierItemScale(reward, 1.25D);
player.getInventory().addItem(reward);
}动态倍率保存在物品自身数据中,同时作用于三维物品模型和平面 item-texture。调用方必须把修改后的 ItemStack 放回实际背包、容器或持久化位置。
ChaEngineEntityAPI
实体模型与动画
| 方法 | 返回值 | 说明 |
|---|---|---|
setCarrierEntityModel(Entity entity, String id) | SetModelResult | 默认临时模式 |
setCarrierEntityModel(Entity entity, String id, SetMode mode) | SetModelResult | TEMPORARY 或 PERMANENT;永久模式写入实体自身数据 |
clearCarrierEntityModel(Entity entity) | SetModelResult | 清除强制模型 |
setCarrierEntityModel(EntityTarget target, String id) | SetModelResult | 按目标定位实体并验证配置 ID |
setCarrierEntityModel(EntityTarget target, String id, boolean validateConfig) | SetModelResult | 仅可信内部数据才应关闭配置验证 |
clearCarrierEntityModel(EntityTarget target) | SetModelResult | 清除目标实体强制模型 |
playCarrierEntityAnimation(Entity entity, String animationName) | AnimationResult | 播放特殊动画 |
stopCarrierEntityAnimation(Entity entity) | AnimationResult | 恢复客户端自动动画 |
playCarrierEntityOverlayAnimation(Entity entity, String animationName) | AnimationResult | 在基础动画上叠加并重新开始覆盖动画 |
stopCarrierEntityOverlayAnimation(Entity entity) | AnimationResult | 停止覆盖层,保留基础动画 |
SetModelResult 包含 SUCCESS、INVALID_ENTITY、INVALID_MODEL_ID、COMMAND_FAILED;AnimationResult 包含 SUCCESS、INVALID_ENTITY、INVALID_ANIMATION。
跨封装边界定位实体时,可使用 WorldEntityTarget.of(String dimensionId, UUID entityUuid)。维度 ID 不能为空或包含 |,UUID 不能为空;目标实体必须仍在对应世界中可解析。
import com.github.ginirohikocha.engine.api.entity.ChaEngineEntityAPI;
import com.github.ginirohikocha.engine.manager.entity.EntityModelManager;
import org.bukkit.entity.Entity;
EntityModelManager.SetModelResult result =
ChaEngineEntityAPI.setCarrierEntityModel(
entity, "crystal_guardian", EntityModelManager.SetMode.PERMANENT);自定义荧光描边
| 方法 | 返回值 | 说明 |
|---|---|---|
setCarrierEntityGlow(LivingEntity entity, String color) | GlowResult | 为有效生物设置严格 #RRGGBB 自定义描边颜色 |
clearCarrierEntityGlow(LivingEntity entity) | GlowResult | 清除该生物的自定义颜色;未设置时同样成功 |
setCarrierEntityGlow(EntityTarget target, String color) | GlowResult | 定位目标并确认它仍是有效生物后设置颜色 |
clearCarrierEntityGlow(EntityTarget target) | GlowResult | 定位目标并清除自定义颜色 |
GlowResult 包含 SUCCESS、INVALID_ENTITY、INVALID_COLOR、COMMAND_FAILED。颜色必须是带 # 的六位十六进制 RGB,例如 #33CCFF;重复设置会替换旧颜色。EntityTarget 找不到实体或解析到非生物实体时返回 INVALID_ENTITY。
import com.github.ginirohikocha.engine.api.entity.ChaEngineEntityAPI;
import com.github.ginirohikocha.engine.manager.entity.EntityModelManager;
import org.bukkit.entity.LivingEntity;
public void applyQuestGlow(LivingEntity entity) {
EntityModelManager.GlowResult result =
ChaEngineEntityAPI.setCarrierEntityGlow(entity, "#33CCFF");
if (result != EntityModelManager.GlowResult.SUCCESS) {
getLogger().warning("设置实体荧光失败:" + result);
}
}该 API 不调用实体的 setGlowing,也不写入实体 NBT 或 PDC,因此不会覆盖其他插件维护的原版发光状态。颜色对所有收到该实体状态的玩家一致;清除后恢复客户端原版发光和队伍颜色逻辑。状态不持久化,实体卸载、切换维度、插件重载或服务器重启时都会清理。
玩家与实体时装
| 方法 | 返回值 | 说明 |
|---|---|---|
addCarrierEquip(Player player, String id) | EquipResult | 添加进程期保留的玩家时装 |
addTemporaryCarrierEquip(Player player, String id) | EquipResult | 添加临时时装 |
removeCarrierEquip(Player player, String id) | EquipResult | 按配置 ID 移除其部位时装 |
clearCarrierEquips(Player player) | EquipResult | 清空玩家全部时装 |
addCarrierEquip(EntityTarget target, String id, String part) | EquipResult | 给已定位实体添加指定部位时装并验证 ID |
addCarrierEquip(EntityTarget target, String id, String part, boolean validateConfig) | EquipResult | 可控制是否验证配置 ID |
removeCarrierEquip(EntityTarget target, String part) | EquipResult | 清除该部位对应的已知时装 |
EquipResult 包含 SUCCESS、INVALID_PLAYER、INVALID_EQUIP_ID。玩家版本要求玩家在线;实体目标版本要求目标可解析。玩家“保留”状态不写数据库,服务器重启后需由业务插件恢复。
import com.github.ginirohikocha.engine.api.entity.ChaEngineEntityAPI;
import com.github.ginirohikocha.engine.manager.entity.EquipModelManager;
EquipModelManager.EquipResult result =
ChaEngineEntityAPI.addCarrierEquip(player, "winter_hat");ChaUI 时装预览的四个方法也位于此类,生命周期和返回值见 ChaUI 时装模型预览。
ChaEngineCameraAPI 与 CameraStatusEvent
调用相机前先用 isSupported(Player) 检查当前连接是否已经声明相机能力。不要按 Minecraft 版本硬编码支持状态。
| 方法 | 返回值 | 说明 |
|---|---|---|
isSupported(Player player) | boolean | 当前在线玩家是否已完成相机能力准备 |
setPerspective(Player player, CameraPerspective perspective, boolean locked) | CameraRequestResult | 切换第一人称、背面第三人称或正面第三人称,并可锁定玩家主动切换 |
applyPreset(Player player, String presetId) | CameraRequestResult | 应用 camera/presets/ 下的相机预设 |
playScene(Player player, String sceneId) | CameraRequestResult | 播放 camera/scenes/ 下的场景镜头 |
stop(Player player, UUID sessionId) | CameraRequestResult | 只停止指定会话 |
interrupt(Player player, UUID sessionId) | CameraRequestResult | 独立打断指定场景并等待 interrupt 衔接;不会降级为 stop |
reset(Player player) | CameraRequestResult | 清空该玩家全部远程相机会话并恢复原状态 |
CameraPerspective 包含 FIRST_PERSON、THIRD_BACK、THIRD_FRONT;“第二人称”使用 THIRD_FRONT,不要在 API 中自造第二套枚举名。
CameraRequestResult.status() 可能返回 SENT、UNSUPPORTED_CLIENT、INVALID_ARGUMENT、PRESET_NOT_FOUND、SCENE_NOT_FOUND、PAYLOAD_TOO_LARGE、SEND_FAILED、UNKNOWN_SESSION。成功创建会话时用 sessionId() 保存 UUID,后续只停止自己的会话。
import com.github.ginirohikocha.engine.api.camera.CameraRequestResult;
import com.github.ginirohikocha.engine.api.camera.ChaEngineCameraAPI;
CameraRequestResult result =
ChaEngineCameraAPI.playScene(player, "intro");
if (result.sent()) {
activeScenes.put(player.getUniqueId(), result.sessionId());
} else {
getLogger().warning("场景镜头发送失败:" + result.status());
}SENT 只表示请求已发送。客户端确认后触发 CameraStatusEvent,可通过 player()、sessionId()、revision()、kind()、status()、reason() 读取结果。kind() 为 CONTROL 或 SCENE;status() 为 STARTED、FINISHED、ABORTED、INTERRUPTED、REJECTED 或 UNSUPPORTED。
正常结束、管理员停止、玩家跳过和异常中止是不同状态。业务插件应分别处理 FINISHED、ABORTED 和 INTERRUPTED,并在玩家退出或自身停用时清理保存的会话 ID。配置与使用说明见相机系统、相机预设和场景镜头。
ChaEngineKeyBindAPI 与事件
| 方法 | 返回值 | 说明 |
|---|---|---|
registerKeyBind(Plugin plugin, KeyBindConfig config) | boolean | 校验并以插件名登记;成功后广播完整快照 |
unregisterKeyBind(Plugin plugin, String id) | boolean | 只有同一所有者可注销 |
clearPluginKeyBinds(Plugin plugin) | int | 返回清除数量 |
hasKeyBind(String id) | boolean | 检查当前注册表 |
getKeyBind(String id) | KeyBindConfig 或 null | 返回配置快照 |
completeKeyBindIds(String current) | List<String> | 返回按前缀过滤并排序的 ID |
注册要求 Plugin、id、name、defaultKey 有效。布尔返回值表示服务端注册表是否改变,不表示每个客户端都已显示按键。客户端上报已注册 ID 后,服务端先触发 KeyBindPressEvent 或 KeyBindReleaseEvent,再执行对应动作;两个事件都通过 getPlayer() 和 getId() 读取数据,且不可取消。
import com.github.ginirohikocha.engine.api.key.ChaEngineKeyBindAPI;
import com.github.ginirohikocha.engine.api.key.event.KeyBindPressEvent;
import com.github.ginirohikocha.engine.entity.config.KeyBindConfig;
import org.bukkit.event.EventHandler;
KeyBindConfig key = new KeyBindConfig();
key.setId("myplugin_dash");
key.setName("冲刺");
key.setDefaultKey("SHIFT+R");
key.setCategory("MyPlugin");
ChaEngineKeyBindAPI.registerKeyBind(this, key);
@EventHandler
public void onDash(KeyBindPressEvent event) {
if (event.getId().equals("myplugin_dash")) {
// 在服务端主线程处理按下事件
}
}插件停用前调用 clearPluginKeyBinds(plugin),不要留下已失去所有者的运行期定义。
ChaEngineParticleAPI
粒子程序按四种范围提供同构方法:
| 范围 | 启动 / 更新 | 停止 / 全部停止 |
|---|---|---|
| 全局 | startGlobal(players, groupId, ttlTicks, scripts) / updateGlobal(...) | stopGlobal(players, groupId) / stopAllGlobal(players) |
| 维度 | startDimension(players, world, groupId, origin, ttlTicks, scripts) / updateDimension(...) | stopDimension(players, world, groupId) / stopAllDimension(players, world) |
| 实体 | startEntity(players, entity, groupId, ttlTicks, scripts) / updateEntity(...) | stopEntity(players, entity, groupId) / stopAllEntity(players, entity) |
| 方块 | startBlock(players, location, groupId, ttlTicks, scripts) / updateBlock(...) | stopBlock(players, location, groupId) / stopAllBlock(players, location) |
另有单玩家便捷重载 startGlobal(Player, String, int, List<String>) 与 stopGlobal(Player, String)。所有方法返回 boolean:至少向一个在线接收者提交数据时为 true。它不证明客户端资源、维度、距离或脚本解析成功。TTL 限制为 1..36000 tick,非正数归一为 20;最多发送 64 行,每行去空白后最多 1024 字符。
import com.github.ginirohikocha.engine.api.particle.ChaEngineParticleAPI;
import java.util.List;
boolean sent = ChaEngineParticleAPI.startEntity(
viewers, boss, "myplugin_boss_aura", 20 * 15,
List.of("ring minecraft:end_rod radius 1.2 points 20 spin 8 every 1"));完整 DSL、范围和客户端预算见粒子程序。
BedrockParticleAPI 与 BedrockParticleOptions
| 方法 | 返回值 | 说明 |
|---|---|---|
playOnce(players, location, effectId, parameters) | boolean | 生成随机实例 ID,使用位置朝向播放一次 |
start(players, location, instanceId, effectId, ttlTicks, options) | boolean | 启动或替换具名实例 |
stop(players, instanceId) | boolean | 停止单个实例 |
stopAll(players) | boolean | 清除接收客户端上的全部基岩版粒子实例 |
BedrockParticleOptions 是不可变对象。用 defaults() 创建,再链式调用 withRotation(float,float,float)、withSeed(long)、withParameter(String,double);读取方法为 getYaw()、getPitch()、getRoll()、getSeed()、getParameters()。
实例 ID 与效果 ID 必须是命名空间 ID。参数名只接受小写字母、数字、下划线和点,最多 64 UTF-8 字节;最多 32 个参数,旋转和参数值必须为有限数。API 对空接收者、空参数或非法 ID 返回 false;构建选项时的非法参数由 with... 方法抛出 IllegalArgumentException。
import com.github.ginirohikocha.engine.api.particle.BedrockParticleAPI;
import com.github.ginirohikocha.engine.api.particle.BedrockParticleOptions;
BedrockParticleOptions options = BedrockParticleOptions.defaults()
.withSeed(42L)
.withRotation(location.getYaw(), location.getPitch(), 0.0F)
.withParameter("speed", 0.8D);
boolean sent = BedrockParticleAPI.start(
viewers, location, "myplugin:portal_01", "myplugin:portal", 200, options);资源定义和表达式语法见基岩版粒子。
实体动画事件
EntityModelAnimationStartEvent 在服务端接受有效的特殊动画播放操作后触发,提供 getEntity() 与 getAnimationName()。EntityModelAnimationEndEvent 在特殊动画停止时触发,提供 getEntity() 与可空的 getAnimationName();无法确认结束名称时为 null。二者均为同步 Bukkit 事件,不可取消。
import com.github.ginirohikocha.engine.api.entity.event.EntityModelAnimationEndEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public final class AnimationListener implements Listener {
@EventHandler
public void onAnimationEnd(EntityModelAnimationEndEvent event) {
String name = event.getAnimationName();
getLogger().info("实体动画结束:" + (name == null ? "未知" : name));
}
}动作与攻击判定
通用人形动作工作室可以为攻击片段制作多个判定箱。第三方插件通过以下 API 提交攻击输入、停止动作,或接管箱体命中效果。
入口类为 com.github.ginirohikocha.engine.api.action.ChaEngineActionAPI;所有方法只能在服务器主线程调用。
| 方法 | 返回值 | 用途 |
|---|---|---|
getControllers() | Set<String> | 内置规则与当前自定义工程规则名称的只读快照 |
requestAttack(Player) | boolean | 按玩家当前主手物品提交一次攻击输入;需要客户端准备完成,且已设置自定义模型、穿戴时装或开启原版玩家动作 |
stop(Player) | void | 清除当前连段、缓冲输入和攻击判定,并同步停止动作 |
getActiveAttack(Player) | ActionAttack 或 null | 当前攻击段的只读信息 |
runEffect(Player, Runnable) | void | 执行插件自己的攻击效果;用于事件外或延迟的玩家伤害,避免被识别为重复原版左键伤害 |
requestAttack 返回 true 表示已提交到有效规则。输入仍可能处于衔接缓冲区、早于允许时刻,或被事件取消,不能将返回值当作已命中目标。接受新一段时会触发 ActionAttackEvent,包括缓冲输入随后触发的攻击段。
已发布通用默认规则时,主手物品先匹配武器专用规则,未匹配再使用默认攻击;空手也可使用默认挥拳。动画所在的分类不参与规则选择。分类整理、普通动画播放与攻击输入是不同操作,第三方插件需要实际攻击时仍调用 requestAttack。
标准十部位玩家模型无需保存绑定就能使用默认动作;已有自定义绑定优先。模型实际骨骼是否满足自动适配条件由客户端加载模型后判断,提交成功不代表模型一定能显示该动作。内置规则不开启判定箱攻击,制作自定义判定箱仍需发布工程。
事件位于 com.github.ginirohikocha.engine.api.action.event:
| 事件 | 可读取的数据 | 取消或接管 |
|---|---|---|
ActionAttackEvent | getAttack() | setCancelled(true) 阻止这一段动画及判定箱启动 |
ActionHitEvent | getAttack()、getHitbox()、getTargets() | 取消或 setHandled(true) 均跳过本批目标的默认普攻;exclude(target) 仅跳过某一个目标 |
ActionAttack 提供 getPlayer()、唯一段 ID getId()、规则名 getController()、片段名 getClip()、从 1 开始的连段序号 getStep() 和 isHitboxCombat()。没有启用判定箱攻击时,起手事件只控制动作,不会撤销已经发生的原版伤害。
ActionHitbox 提供 getName()、片段内采样秒数 getTime()、世界坐标中心 getCenter()、完整边长 getSize()、八个世界坐标角点 getCorners()。几何返回值均为副本;目标列表只读,修改这些数据不会改写动作工程。一次事件仅包含该箱体在本次采样新增命中的目标;同一目标在这一攻击段后续的箱体或帧中不会重复触发事件。
下面是完整的监听示例:潜行时禁止判定箱攻击;其他情况下接管“横斩范围”,给目标造成 6 点伤害。damage 仍经过 Bukkit 伤害事件,其他保护插件可以取消它。
package example;
import com.github.ginirohikocha.engine.api.action.event.ActionAttackEvent;
import com.github.ginirohikocha.engine.api.action.event.ActionHitEvent;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
public final class ActionEffectsPlugin extends JavaPlugin implements Listener {
@Override
public void onEnable() {
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler(ignoreCancelled = true)
public void onAttack(ActionAttackEvent event) {
if (event.getAttack().isHitboxCombat()
&& event.getAttack().getPlayer().isSneaking()) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onHit(ActionHitEvent event) {
if (event.isHandled() || !event.getHitbox().getName().equals("横斩范围")) {
return;
}
event.setHandled(true);
Player attacker = event.getAttack().getPlayer();
for (LivingEntity target : event.getTargets()) {
if (!event.isExcluded(target)) {
target.damage(6.0, attacker);
}
}
}
}配套 plugin.yml:
name: ActionEffects
version: 1.0.0
main: example.ActionEffectsPlugin
api-version: '1.16'
depend: [ChaEngine]不取消、未标记已处理且未排除的目标会执行一次原生近战攻击,包括攻击冷却、附魔和其他插件的伤害监听。取消或接管的目标也会计入本段已处理集合。事件内可以直接处理自定义伤害;需要延后处理时先标记 setHandled(true),保存攻击 ID,在服务器主线程核对玩家与目标仍有效,再用 ChaEngineActionAPI.runEffect(attacker, () -> target.damage(6.0, attacker)) 执行。该方法只放行 ChaEngine 的重复左键防护,不跳过其他插件的伤害事件。
直接生成模型残影
ChaEngineActionAPI.spawnAfterimage 可在技能执行时记录玩家当前模型的姿态和位置,不需要指定动画或在工作室添加特效。一次调用即可生成整批残影,不要再用服务器定时任务逐道调用。
所有调用必须在服务器主线程;插件依赖仍为 depend: [ChaEngine]。例如在已有技能处理方法中调用下面的 cast:
import com.github.ginirohikocha.engine.api.action.ActionAfterimageResult;
import com.github.ginirohikocha.engine.api.action.ChaEngineActionAPI;
import org.bukkit.entity.Player;
public final class AfterimageSkill {
public static ActionAfterimageResult cast(Player player) {
// 8 道冷青色残影,间隔 0.08 秒;每道在生成后 0.6 秒内淡出。
return ChaEngineActionAPI.spawnAfterimage(player, "#80BDEFFF", 0.6, 8, 0.08);
}
}| 方法 | 含义 |
|---|---|
spawnAfterimage(player, color, lifetimeSeconds) | 生成 1 道残影 |
spawnAfterimage(player, color, lifetimeSeconds, count) | 指定数量,间隔默认 0.08 秒 |
spawnAfterimage(player, color, lifetimeSeconds, count, intervalSeconds) | 一次提交完整批次 |
color 接受 #RRGGBB(默认 40% 不透明度)或 #AARRGGBB(前两位指定不透明度,不能为 00)。存留时间为 0.03~10 秒,数量为 1~32,间隔为 0.02~1 秒。非有限数字、越界值和无效颜色会抛出 IllegalArgumentException;异步调用抛出 IllegalStateException,请先调度回服务器主线程。
返回 ActionAfterimageResult:
| 结果 | 含义 |
|---|---|
SUBMITTED | 至少向一个附近就绪客户端提交成功,不表示已经绘制 |
PLAYER_OFFLINE | 玩家为空或已离线 |
TARGET_NOT_ACTIVE | 玩家没有已应用的实体或时装模型 |
NO_READY_VIEWERS | 没有可接收并成功提交的附近客户端,可能未同步、不可见或发送失败 |
优先选择当前实体模型,没有时选择当前全身时装。没有显式全身绑定时,按当前首个时装提交,客户端仍须将它识别为隐藏原人物的全身模型才会捕获。此能力不要求人形标准骨骼;记录的是实际绘制姿态,不打断主动动画和独立分组,也不影响伤害、移动或视角。支持范围与原有模型残影相同:模型本体,不包括原版皮肤、局部时装、第一人称手部或手持物。观察者须位于同世界 128 格内,且已完成同步、能够看到目标玩家。
每个客户端从首次成功捕获开始,按同一批次的固定时间点采样;只传时间长度,不依赖服务器和客户端系统时间一致。错过采样点时跳过,不会补出一串相同姿态,也不会把误差累积到下一次。不同客户端的网络延迟和帧率仍可能导致开始时刻、实际数量不同。目标须在画面中实际绘制;3 秒内未开始捕获的请求会失效。每个模型最多同时保留 8 批请求,复杂模型或大量同时采样会受性能限制。
重载动作库、资源变化、断线及切换世界会清理残影;模型切换后不补采样旧模型。残影是一次性显示效果,不保存到动画库。请同时更新并重启服务端和客户端。无需写插件时可使用残影指令。
分组动画播放
所有主动播放入口均读取动画的“允许移动”和“允许转动视角”两个独立开关。有限动画结束或停止后移除该动画的限制,并行组仍按各自设置生效;此 API 本身不提供技能位移。持续效果的条件只控制自动播放,显式 API 调用可独立开始或停止该组;持续效果管理说明加入、移出与起始动画。
ChaEngineActionAPI 可单独播放人物动作、光环或悬浮物。一个组同时执行一个片段,切换或停止该组不重置其他组;character 是固定人物组,独立组 ID 来自动作工程。所有调用都在服务器主线程。
职业技能只需让当前人物做一个动作时,可以直接调用:
ActionPlaybackResult result = ChaEngineActionAPI.playAnimation(player, "后翻滚");
if (result != ActionPlaybackResult.SUBMITTED) {
player.sendMessage("动画未提交:" + result.name());
}
// 需要中途结束或停止循环时,再调用:
// ChaEngineActionAPI.stopAnimation(player);动画名来自 getAnimations(),无需添加进人物分组。简化方法自动选择当前实体模型;没有实体模型时优先已配置的整身时装,否则选择当前首个时装。没有实体 / 时装模型返回 TARGET_NOT_ACTIVE,不会替玩家穿戴模型;只需要原版皮肤默认动作时使用服务端分段四肢开关。多个时装需要精确控制时,使用下面显式目标的方法。
模型目标使用 ActionModelTarget,显式区分 Catalog.ENTITY 和 Catalog.EQUIP,模型 ID 填服务端配置中的 ID。玩家必须已设置对应实体模型或穿戴对应时装。
分组设为“所有同名骨骼模型”后,getGroups(target) 也会返回适用于该目标的通用组。播放与停止仍填写玩家当前使用的实际模型 ID,例如同一法阵组可分别用于魔法少女和魔法少年;停止其中一个目标不会停止其他模型。通用组缺失骨骼时跳过对应轨道,其他匹配骨骼继续播放,不会仅因某个模型没有法阵而报 MISSING_MAPPING。
| 方法 | 结果 |
|---|---|
getAnimations() | 当前动画 ID 的只读集合 |
getGroups(target) | 目标的独立组 ID,加固定 character 组 |
playAnimation(player, animation) | 在玩家当前实体 / 时装上播放人物动作,返回提交结果 |
stopAnimation(player) | 停止当前模型上的主动人物动画,不停止装饰组 |
playAnimation(player, target, group, animation) | 提交对应组的播放,返回 ActionPlaybackResult |
stopAnimation(player, target, group) | 停止对应组,返回结果;独立组在模型重新出现前不会自动重新启动 |
getPlayingAnimation(player, target, group) | 服务器当前片段 ID;已停止或单次结束返回空字符串 |
getPlaybackResult(player, target, group) | 查询该次播放的提交、客户端应用或失败状态 |
工作室导入并保存的片段使用服务器动画库 ID。旧目录迁移的标准文件仍保留相对文件名加 :: 和原始动画名,例如 剑类职业普攻.animation.json::剑客_attack1;新增动画请通过工作室导入。先用 getAnimations() 获取实际 ID,不要由分类名猜测。独立组只能播放其 clips 列表中的动画;人物组可直接播放库中的动画。每段动画选择的骨架形式会同时用于工作室与实际人物播放。
SUBMITTED 表示服务器接受并提交,CLIENT_APPLIED 表示玩家自己的客户端已经在渲染该角色时成功应用姿态;不代表所有观察者都显示完成。角色暂未绘制时可能一直处于提交状态。NOT_READY、TARGET_NOT_ACTIVE、UNKNOWN_GROUP、UNKNOWN_ANIMATION、UNSUPPORTED_ANIMATION、NOT_IN_GROUP 分别表示尚未就绪、模型未启用、组或动画不存在、数据不支持、动画不属于该组。MISSING_MAPPING、GROUP_CONFLICT、INVALID_POSE 是客户端应用失败原因,STOPPED 表示没有当前播放。
对于通用组和直接播放的人物动画,CLIENT_APPLIED 也包含缺失骨骼已按规则跳过的情况,并不保证出现可见变化。重名、范围冲突和无效姿态仍会返回相应失败状态。简化停止方法返回 STOPPED 表示停止请求已提交;它不是客户端已显示停止的确认。
这些播放方法只改变显示,不触发攻击判定或伤害。 需要职业普攻时,发布武器与判定规则并调用 requestAttack;取消命中或接管伤害仍使用上面的攻击事件。
下面插件使用示例工程的光环组。输入 /aurapreview 重播光环,/aurapreview stop 停止光环,/aurapreview status 查询状态;人物动作与悬浮物继续运行。
package example;
import com.github.ginirohikocha.engine.api.action.ActionModelTarget;
import com.github.ginirohikocha.engine.api.action.ActionPlaybackResult;
import com.github.ginirohikocha.engine.api.action.ChaEngineActionAPI;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public final class AuraPreviewPlugin extends JavaPlugin {
private final ActionModelTarget target =
new ActionModelTarget(ActionModelTarget.Catalog.EQUIP, "magical_girl");
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player)) {
sender.sendMessage("请在游戏中执行。");
return true;
}
Player player = (Player) sender;
String operation = args.length == 0 ? "play" : args[0];
ActionPlaybackResult result;
if (operation.equalsIgnoreCase("stop")) {
result = ChaEngineActionAPI.stopAnimation(player, target, "aura");
} else if (operation.equalsIgnoreCase("status")) {
result = ChaEngineActionAPI.getPlaybackResult(player, target, "aura");
} else {
result = ChaEngineActionAPI.playAnimation(player, target, "aura", "aura_spin");
}
player.sendMessage("光环播放状态:" + result.name());
return true;
}
}配套完整 plugin.yml:
name: AuraPreview
version: 1.0.0
main: example.AuraPreviewPlugin
api-version: '1.16'
depend: [ChaEngine]
commands:
aurapreview:
description: 播放、停止或查询示例光环动画
usage: /aurapreview [play|stop|status]
permission: aurapreview.use
permissions:
aurapreview.use:
default: op调用检查表
- 在插件依赖中声明 ChaEngine,并等待它启用后再注册动态能力。
- 只在服务端主线程读写 Bukkit 对象或调用发送类 API。
- 显式处理枚举和布尔返回值,不把“已发送”解释为“已渲染”。
- 玩家退出、页面关闭或业务对象销毁时,停止粒子、相机会话并清理临时预览。
- 服务端重载后重新核对业务保存的模型、时装与按键 ID 是否仍存在。
猹件开发组