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 脚本化物品元数据
核心运行时类型
ModeledEntityStaticEntityDynamicEntityPropEntity
创建实体
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()—— 返回当前LocationModeledEntity#getWorld()—— 返回WorldModeledEntity#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()
事件接口
通用交互事件:
ModeledEntityLeftClickEventModeledEntityRightClickEventModeledEntityHitboxContactEventModeledEntityHitByProjectileEvent
StaticEntity、DynamicEntity 与 PropEntity 也存在对应的类型化变体。
生命周期事件:
-
ResourcePackGenerationEvent—— FMM 生成或重新生成资源包完成时触发(启动时与执行/fmm reload时)。监听此事件可以对生成的资源包进行后处理。 -
FmmReloadedEvent—— FMM 在启动时完成初始化序列以及每次执行/fmm reload之后触发。始终在主服务器线程上触发。持有长期
DynamicEntity或PropEntity引用的消费者插件(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)—— 若该模型存在.jsondisplay 模型则返回truegetRegisteredModels()—— 返回所有已注册 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 的物品定义中注册则返回trueapplyScriptedItemData(ItemStack itemStack, String itemId)—— 在现有 ItemStack 上盖印 PDC 标签和物品模型。成功返回true,物品 ID 无效或 ItemStack 无 meta 时返回false。关于弓/弩的说明: 如果给定的itemId没有 display 模型,但itemId + "_idle"有(即该物品具有弓/弩状态模型),该方法会自动使用_idle模型作为 display 模型getItemConfig(String itemId)—— 返回给定物品 ID 的PropScriptConfigFields,若不存在则返回null
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 未加载则返回falseundisguise(Player)—— 若有伪装被移除则返回trueisDisguised(Player)—— 快速布尔检查getDisguiseModelID(Player)—— 返回活动的模型 ID 或nullgetDisguisedPlayers()—— 不可修改的伪装玩家快照
伪装的玩家对他人保持不可见,直到解除伪装 —— 牛奶桶、信标效果清除以及类似交互均不会破坏该不可见性。
LocationAPI
供插件贡献地下城检测和区域保护检查的公共 API。注册的谓词会喂给 FMM 中的 Lua em.location.is_in_dungeon 和 em.location.is_protected 检查(被 pickupable.lua 和 storage_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_click、on_right_click和on_projectile_hit中可用 cancel()、uncancel()、is_cancelledplayer—— 触发该事件的玩家
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 脚本引擎,以及共用的
LocationQueryRegistry和WorldFolderResolver。 - FreeMinecraftModels 将 WorldGuard、WorldEdit、GriefPrevention 和 Vault 声明为
softdepend。它们都不是启动插件所必需的,但它们存在时会解锁特定功能:WorldGuard/WorldEdit/GriefPrevention 会喂给LocationAPI,Vault 启用家具商店。