跳到主要内容

FreeMinecraftModels API 与开发者指南

FreeMinecraftModels 既是独立插件,也为其他插件提供 API 接口。

Maven 仓库

<repository>
<id>magmaguy-repo-releases</id>
<name>MagmaGuy's Repository</name>
<url>https://repo.magmaguy.com/releases</url>
</repository>
<repository>
<id>magmaguy-repo-snapshots</id>
<name>MagmaGuy's Snapshot Repository</name>
<url>https://repo.magmaguy.com/snapshots</url>
</repository>

依赖

<dependency>
<groupId>com.magmaguy</groupId>
<artifactId>FreeMinecraftModels</artifactId>
<version>LATEST.VERSION.HERE</version>
<scope>provided</scope>
</dependency>

请使用 compileOnly/provided 范围。不要将插件着色(shade)进你自己的 jar。

核心入口

  • ModeledEntityManager.modelExists(String)
  • ModeledEntityManager.reload()
  • ModeledEntityManager.getAllEntities()
  • ModeledEntityManager.getDynamicEntities()
  • ModeledEntityManager.propEntities()
  • DisguiseAPI —— 将玩家伪装/解除伪装为已加载的模型
  • LocationAPI —— 注册地下城探测器和保护区提供者(喂给 Lua 中的 em.location.* 谓词)
  • ScriptedItemAPI —— 为第三方 ItemStack 标记上 FMM 脚本化物品元数据

核心运行时类型

  • ModeledEntity
  • StaticEntity
  • DynamicEntity
  • PropEntity

创建实体

StaticEntity preview = StaticEntity.create("example_model", location);
DynamicEntity mobModel = DynamicEntity.create("example_model", livingEntity);
DynamicEntity mount = DynamicEntity.createWithInvisibility("example_model", livingEntity);
PropEntity prop = PropEntity.spawnPropEntity("example_model", location);

如果请求的模型 ID 未加载,所有创建路径都会返回 null

createWithInvisibility 是一个变体,会应用隐身药水效果而不是从客户端隐藏该实体。这样实体在客户端仍受追踪,对于载具转向是必需的(被 /fmm mount 内部使用)。

实用的运行时方法

  • ModeledEntity#setDisplayName(String)
  • ModeledEntity#setDisplayNameVisible(boolean)
  • ModeledEntity#setLeftClickCallback(...)
  • ModeledEntity#setRightClickCallback(...)
  • ModeledEntity#setHitboxContactCallback(...)
  • ModeledEntity#setModeledEntityHitByProjectileCallback(...)
  • ModeledEntity#playAnimation(String, boolean, boolean)
  • ModeledEntity#stopCurrentAnimations()
  • ModeledEntity#hasAnimation(String)
  • ModeledEntity#getEntityID() —— 返回模型 ID 字符串
  • ModeledEntity#getLocation() —— 返回当前 Location
  • ModeledEntity#getWorld() —— 返回 World
  • ModeledEntity#getViewers() —— 返回能看到该实体的玩家 HashSet<UUID>
  • ModeledEntity#getNametagBones() —— 返回名牌骨骼的 List<Bone>(可用于附加额外文字)
  • ModeledEntity#getScaleModifier() / setScaleModifier(double)
  • ModeledEntity#removeWithDeathAnimation() —— 以死亡动画移除(若存在)
  • ModeledEntity#removeWithMinimizedAnimation() —— 以缩小动画移除
  • ModeledEntity#remove() —— 立刻移除实体及其所有骨骼
  • ModeledEntity#setTintColor(Color) / getTintColor() —— 通过皮革盔甲染色通道应用持久的着色。受伤闪烁会暂时覆盖该着色,之后再淡回。传入 null 可清除。
  • ModeledEntity#setViewDistanceOverride(int) / getEffectiveViewDistance() —— 为单个实体覆盖 DefaultConfig.maxModelViewDistance。传入 -1 恢复为插件全局默认值。
  • DynamicEntity#setSyncMovement(boolean)
  • DynamicEntity#isDamagesOnContact() / setDamagesOnContact(boolean) —— 控制实体是否通过碰撞箱接触对玩家造成伤害
  • Bone#getBoneLocation()

事件接口

通用交互事件:

  • ModeledEntityLeftClickEvent
  • ModeledEntityRightClickEvent
  • ModeledEntityHitboxContactEvent
  • ModeledEntityHitByProjectileEvent

StaticEntityDynamicEntityPropEntity 也存在对应的类型化变体。

生命周期事件:

  • ResourcePackGenerationEvent —— FMM 生成或重新生成资源包完成时触发(启动时与执行 /fmm reload 时)。监听此事件可以对生成的资源包进行后处理。

  • FmmReloadedEvent —— FMM 在启动时完成初始化序列以及每次执行 /fmm reload 之后触发。始终在主服务器线程上触发。

    持有长期 DynamicEntityPropEntity 引用的消费者插件(EliteMobs、BetterStructures 等)必须处理此事件,在仍存活的底层实体上重新创建其模型附着。否则那些实体会在重载后变为不可见,因为 FMM 在 onDisable 期间已经销毁了 display 实体,而消费者的引用已经失效。

    @EventHandler
    public void onFmmReloaded(FmmReloadedEvent event) {
    for (MyTrackedEntity tracked : myEntities) {
    if (tracked.bukkitEntity != null && tracked.bukkitEntity.isValid()) {
    tracked.fmmModel = DynamicEntity.create("my_model", tracked.bukkitEntity);
    }
    }
    }

ModeledEntityHitByProjectileEvent 与 OBB 抛射物检测

FMM 对针对模型化实体的抛射物使用 OBB(定向包围盒)命中检测。当抛射物与模型化实体的 OBB 碰撞箱相交时,FMM 会触发 ModeledEntityHitByProjectileEvent。这是一个标准的可取消 Bukkit 事件。

关键细节:

  • 箭矢伤害的计算会考虑 POWER 附魔加成
  • 尊重 PIERCING 附魔 —— 箭矢可以穿过并击中更多目标
  • 取消该事件可阻止伤害并完全屏蔽这次命中
@EventHandler
public void onProjectileHitModel(ModeledEntityHitByProjectileEvent event) {
ModeledEntity target = event.getModeledEntity();
Projectile projectile = event.getProjectile();
// 取消以阻止伤害
event.setCancelled(true);
}

物品与模型工具

ModelItemFactory

用于以编程方式创建模型相关 ItemStack 的工厂类。

// 创建道具放置物品(使用 "model_id" PDC 键)
ItemStack placementItem = ModelItemFactory.createModelItem("lamp_post", Material.STICK);

// 从配置创建自定义物品(使用 "fmm_item_id" PDC 键)
PropScriptConfigFields config = ItemScriptManager.getItemDefinitions().get("magic_sword");
ItemStack customItem = ModelItemFactory.createCustomItem("magic_sword", config);
  • createModelItem(String modelId, Material material) —— 创建道具放置物品。在 1.21.4+ 上,如果存在 display JSON 会自动应用 display 模型渲染。
  • createCustomItem(String itemId, PropScriptConfigFields config) —— 从统一配置创建带有名称、Lore、附魔和 display 模型的自定义物品。
  • formatModelName(String modelId) —— 工具方法,将 01_em_flame_sword 之类的模型 ID 转换为 Flame Sword

DisplayModelRegistry

简单的注册表,用于追踪哪些模型拥有可用的 display JSON。

// 检查某个模型是否已注册 display 模型 JSON
boolean has3D = DisplayModelRegistry.hasDisplayModel("magic_sword");
  • register(String modelId) —— 注册一个模型 ID(重载时由内部调用)
  • hasDisplayModel(String modelId) —— 若该模型存在 .json display 模型则返回 true
  • getRegisteredModels() —— 返回所有已注册 display 模型 ID 的不可变 Set<String>
  • shutdown() —— 清除所有注册

ItemScriptManager

管理自定义物品(在 YML 配置中设置了 material: 的模型)每个玩家级别的 Lua 脚本生命周期。

// 获取所有已注册的自定义物品定义
Map<String, PropScriptConfigFields> items = ItemScriptManager.getItemDefinitions();

// 获取某玩家 + 物品的活动 Lua 脚本实例
ScriptInstance instance = ItemScriptManager.getActiveScript(playerUUID, "magic_sword");

// 获取某玩家所有活动脚本
Map<String, ScriptInstance> scripts = ItemScriptManager.getActiveScripts(playerUUID);
  • scanForCustomItems(File modelsFolder) —— 扫描模型 YML 配置中的自定义物品
  • updateEquippedScripts(Player player) —— 比对已装备物品与运行中的脚本,触发 equip/unequip 钩子
  • removePlayer(Player player) —— 关闭某玩家的所有脚本(退出时调用)
  • getItemDefinitions() —— 返回从物品 ID 到 PropScriptConfigFields 的映射

ScriptedItemAPI

供外部插件与 FMM 脚本化物品系统集成的公共 API。这让其他插件能够为自己的 ItemStack 添加 FMM 脚本化物品数据(PDC 标签 + 物品模型),从而让 FMM 的 Lua 脚本钩子对这些物品生效,而无需 FMM 覆盖物品的名称、Lore 或附魔。

// 检查脚本化物品定义是否存在
boolean exists = ScriptedItemAPI.isValidItemId("flame_blade");

// 将 FMM 脚本化物品数据应用到现有 ItemStack
// 这会设置:
// - fmm_item_id PDC 标签(使 FMM 的脚本系统识别该物品)
// - 物品模型(1.21.4+),来自 FMM 的 display 模型注册表
// 不会修改名称、Lore、附魔或任何其他物品属性。
boolean success = ScriptedItemAPI.applyScriptedItemData(itemStack, "flame_blade");

// 获取脚本化物品的配置
PropScriptConfigFields config = ScriptedItemAPI.getItemConfig("flame_blade");
  • isValidItemId(String itemId) —— 若物品 ID 已在 FMM 的物品定义中注册则返回 true
  • applyScriptedItemData(ItemStack itemStack, String itemId) —— 在现有 ItemStack 上盖印 PDC 标签和物品模型。成功返回 true,物品 ID 无效或 ItemStack 无 meta 时返回 false关于弓/弩的说明: 如果给定的 itemId 没有 display 模型,但 itemId + "_idle" 有(即该物品具有弓/弩状态模型),该方法会自动使用 _idle 模型作为 display 模型
  • getItemConfig(String itemId) —— 返回给定物品 ID 的 PropScriptConfigFields,若不存在则返回 null
EliteMobs 集成

EliteMobs 通过 scriptedItem 配置字段在内部使用此 API。当一个 EliteMobs 自定义物品设置 scriptedItem: flame_blade 时,EliteMobs 会正常构建其物品(名称、Lore、附魔、等级),然后调用 ScriptedItemAPI.applyScriptedItemData() 在其上添加 FMM 的模型与脚本行为。

DisguiseAPI

玩家伪装功能的公共入口。第三方插件应调用此类,而不是内部的 DisguiseManager,以便在内部重构时保持安全。

import com.magmaguy.freeminecraftmodels.api.DisguiseAPI;

// 将玩家伪装为已加载的模型。会干净地替换已有的伪装。
boolean ok = DisguiseAPI.disguise(player, "dragon");

// 解除伪装(若有伪装被移除则返回 true)。
DisguiseAPI.undisguise(player);

// 查询状态。
boolean disguised = DisguiseAPI.isDisguised(player);
String modelID = DisguiseAPI.getDisguiseModelID(player); // 未伪装时为 null

// 当前所有伪装玩家的快照。
Collection<Player> all = DisguiseAPI.getDisguisedPlayers();
  • disguise(Player, String modelID) —— 若模型 ID 未加载则返回 false
  • undisguise(Player) —— 若有伪装被移除则返回 true
  • isDisguised(Player) —— 快速布尔检查
  • getDisguiseModelID(Player) —— 返回活动的模型 ID 或 null
  • getDisguisedPlayers() —— 不可修改的伪装玩家快照

伪装的玩家对他人保持不可见,直到解除伪装 —— 牛奶桶、信标效果清除以及类似交互均不会破坏该不可见性。

LocationAPI

供插件贡献地下城检测和区域保护检查的公共 API。注册的谓词会喂给 FMM 中的 Lua em.location.is_in_dungeonem.location.is_protected 检查(被 pickupable.luastorage_double.lua 等预制脚本使用)。

插件传入的是简单的 Predicate<Location>,因此没有被着色的 FMM 类型跨插件类加载器。

import com.magmaguy.freeminecraftmodels.api.LocationAPI;

// 在你的插件 enable 时,于 WorldGuard/EliteMobs/等可用之后调用。
LocationAPI.registerDungeonLocator("EliteMobs",
location -> EliteMobs.isInsideDungeon(location));

LocationAPI.registerProtectionProvider("WorldGuard",
location -> WorldGuardBridge.isProtected(location));
  • registerDungeonLocator(String providerName, Predicate<Location> predicate) —— 任一注册的谓词返回 true 即将该位置标记为"在地下城中"
  • registerProtectionProvider(String providerName, Predicate<Location> predicate) —— 任一注册的谓词返回 true 即将该位置标记为受保护

运维人员可以通过 /fmm location 来验证注册情况,该命令会报告活动的提供者数量并针对当前位置测试两个谓词。

PropScriptConfigFields

模型 YML 配置文件的统一配置类。被道具脚本和自定义物品共用。

# 示例:torch_01.yml
isEnabled: true
scripts:
- torch_glow.lua
material: STICK # 若设置,模型将成为自定义物品
name: "&eMagic Torch" # 自定义显示名称(可选)
lore: # 自定义 Lore 行(可选)
- "&7Glows in the dark"
enchantments: # 附魔(可选,格式:NAME,LEVEL)
- "FIRE_ASPECT,1"

关键方法:isCustomItem()getParsedMaterial()getParsedEnchantments()getScripts()

Lua 脚本

FreeMinecraftModels 通过 MagmaCore 2.0 脚本引擎为道具自定义物品提供 Lua 脚本支持。脚本文件放置在 plugins/FreeMinecraftModels/scripts/ 中,并通过模型文件同目录下的 YML 配置绑定到模型上。

道具脚本钩子

钩子触发时机
on_spawn道具被生成到世界中
on_game_tick道具存活期间的每一刻
on_zone_enter玩家进入道具区域
on_zone_leave玩家离开道具区域
on_destroy道具被移除
on_left_click玩家左键点击道具
on_right_click玩家右键点击道具
on_projectile_hit抛射物击中道具

物品脚本钩子

自定义物品(在 YML 中设置了 material: 的模型)支持 22 个 Lua 钩子:

钩子触发时机
on_equip物品进入受追踪的装备栏位
on_unequip物品离开受追踪的装备栏位
on_game_tick物品装备期间的每一刻
on_attack_entity玩家手持物品攻击实体
on_kill_entity玩家手持物品击杀实体
on_take_damage玩家装备物品时受到伤害
on_shield_block玩家使用盾牌格挡
on_shoot_bow玩家射出弓
on_projectile_hit玩家发射的抛射物命中目标
on_projectile_launch玩家发射抛射物
on_right_click玩家手持物品时右键点击
on_left_click玩家手持物品时左键点击
on_shift_right_click玩家手持物品时 Shift + 右键
on_shift_left_click玩家手持物品时 Shift + 左键
on_interact_entity玩家手持物品右键点击实体
on_swap_hands玩家在主副手之间切换物品
on_drop玩家丢弃物品
on_break_block玩家手持物品时破坏方块
on_consume玩家食用物品
on_item_damage物品受到耐久度损耗
on_fish玩家使用钓鱼竿
on_death玩家在装备物品时死亡

物品脚本会收到 context.item(包含物品 ID 与玩家信息)而不是 context.prop

道具脚本上下文表

道具脚本会收到一个 context 表。下面是关键 API 的简要说明 —— 完整内容请参阅 Lua 道具 API

context.prop

  • model_id —— 蓝图模型名称
  • current_location —— 道具的当前位置
  • play_animation(name, blend, loop) —— 播放指定动画(blend 与 loop 默认为 true
  • stop_animation() —— 停止所有当前动画
  • hurt_visual() —— 在道具上播放受伤(红色闪烁)视觉效果
  • pickup() —— 移除道具并掉落其放置物品
  • mount(player) —— 让玩家骑乘道具
  • dismount(player) —— 让玩家从道具下来
  • get_passengers() —— 返回当前骑乘该道具的玩家列表
  • spawn_elitemobs_boss(filename, x, y, z) —— 在相对于道具的位置生成 EliteMobs Boss

context.event

  • on_left_clickon_right_clickon_projectile_hit 中可用
  • cancel()uncancel()is_cancelled
  • player —— 触发该事件的玩家

context.world

  • spawn_entity(entity_type, location) —— 生成原版实体
  • set_block_at(location, material) —— 在世界中设置方块
  • 此外还有粒子、声音、方块查询、闪电以及附近实体查找

玩家对象(来自 context.event.player):

  • get_held_item() —— 返回玩家手持的物品
  • consume_held_item() —— 消耗一个手持物品
  • has_item(material) —— 检查玩家是否拥有某物品
  • send_message(text) —— 向玩家发送聊天消息
  • game_mode —— 玩家当前的游戏模式

道具脚本示例

function on_spawn(context)
context.prop.play_animation("idle", true, true)
end

function on_right_click(context)
context.prop.play_animation("activate", false, false)
end

物品脚本示例

function on_equip(context)
context.event.player.send_message("&6You equipped the Flame Blade!")
end

function on_attack_entity(context)
-- 命中时的火焰效果
context.event.player.send_message("&cBurn!")
end

function on_unequip(context)
context.event.player.send_message("&7Flame Blade sheathed.")
end

说明

  • FreeMinecraftModels 是作为已安装插件的依赖,而不是可嵌入的库。
  • 如果你的插件需要刚导入的模型,请调用 ModeledEntityManager.reload(),而不要尝试自行重建 FreeMinecraftModels 的状态。
  • Nightbreak 生态中的所有插件目前都依赖于 MagmaCore 2.2.0-SNAPSHOT,它包含 FreeMinecraftModels 道具脚本和 EliteMobs Lua 能力所共用的 Lua 脚本引擎,以及共用的 LocationQueryRegistryWorldFolderResolver
  • FreeMinecraftModels 将 WorldGuard、WorldEdit、GriefPrevention 和 Vault 声明为 softdepend。它们都不是启动插件所必需的,但它们存在时会解锁特定功能:WorldGuard/WorldEdit/GriefPrevention 会喂给 LocationAPI,Vault 启用家具商店。