API FreeMinecraftModels и руководство разработчика
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. Не шейдите плагин в свой 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);
Все пути создания возвращают null, если запрашиваемый ID модели не загружен.
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>костей nametag (полезно для размещения дополнительного текста)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 снёс display-сущности вonDisable, а ссылка потребителя теперь устарела.@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 (ориентированный bounding box) для определения попаданий снарядов по моделированным сущностям. Когда снаряд пересекает 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-ов, связанных с моделями.
// Создать предмет размещения реквизита (использует PDC-ключ "model_id")
ItemStack placementItem = ModelItemFactory.createModelItem("lamp_post", Material.STICK);
// Создать пользовательский предмет из конфига (использует PDC-ключ "fmm_item_id")
PropScriptConfigFields config = ItemScriptManager.getItemDefinitions().get("magic_sword");
ItemStack customItem = ModelItemFactory.createCustomItem("magic_sword", config);
createModelItem(String modelId, Material material)-- создаёт предмет размещения для реквизита. На 1.21.4+ автоматически применяет рендеринг display-модели, если существует display JSON.createCustomItem(String itemId, PropScriptConfigFields config)-- создаёт пользовательский предмет с именем, lore, зачарованиями и display-моделью из унифицированного конфига.formatModelName(String modelId)-- утилита, которая преобразует ID модели вроде01_em_flame_swordвFlame Sword.
DisplayModelRegistry
Простой реестр, который отслеживает, у каких моделей доступен display JSON.
// Проверить, зарегистрирован ли у модели display JSON
boolean has3D = DisplayModelRegistry.hasDisplayModel("magic_sword");
register(String modelId)-- регистрирует ID модели (вызывается внутренне при перезагрузке)hasDisplayModel(String modelId)-- возвращаетtrue, если для этой модели существует.jsondisplay-моделиgetRegisteredModels()-- возвращает неизменяемыйSet<String>всех ID моделей с зарегистрированными display-моделямиshutdown()-- очищает все регистрации
ItemScriptManager
Управляет жизненным циклом Lua-скриптов для пользовательских предметов на каждого игрока (модели с заданным material: в YML-конфиге).
// Получить все зарегистрированные определения пользовательских предметов
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/unequipremovePlayer(Player player)-- завершает все скрипты для игрока (вызывайте при выходе)getItemDefinitions()-- возвращает мапу ID предмета вPropScriptConfigFields
ScriptedItemAPI
Публичный API для внешних плагинов, чтобы интегрироваться с системой скриптуемых предметов FMM. Это позволяет другим плагинам помечать собственные ItemStack-и данными скриптуемых предметов FMM (PDC-тег + модель предмета), чтобы для этих предметов срабатывали Lua-хуки FMM, без того, чтобы FMM перезаписывал имя предмета, lore или зачарования.
// Проверить, существует ли определение скриптуемого предмета
boolean exists = ScriptedItemAPI.isValidItemId("flame_blade");
// Применить данные скриптуемого предмета FMM к существующему ItemStack
// Это устанавливает:
// - PDC-тег fmm_item_id (чтобы система скриптов FMM распознавала предмет)
// - Модель предмета (1.21.4+) из реестра display-моделей FMM
// НЕ изменяет имя, lore, зачарования или другие свойства предмета.
boolean success = ScriptedItemAPI.applyScriptedItemData(itemStack, "flame_blade");
// Получить конфиг скриптуемого предмета
PropScriptConfigFields config = ScriptedItemAPI.getItemConfig("flame_blade");
isValidItemId(String itemId)-- возвращаетtrue, если ID предмета зарегистрирован в определениях предметов FMMapplyScriptedItemData(ItemStack itemStack, String itemId)-- проставляет PDC-тег и модель предмета на существующий ItemStack. Возвращаетtrueпри успехе,false, если ID предмета невалиден или у ItemStack нет meta. Замечание про луки/арбалеты: если у указанногоitemIdнет display-модели, но уitemId + "_idle"она есть (то есть у предмета есть модели состояний лука/арбалета), метод автоматически использует модель_idleв качестве display-моделиgetItemConfig(String itemId)-- возвращаетPropScriptConfigFieldsдля данного ID предмета, либоnull, если не найдено
EliteMobs использует этот API внутренне через поле конфигурации scriptedItem. Когда пользовательский предмет 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)-- возвращаетfalse, если ID модели не загруженundisguise(Player)-- возвращаетtrue, если маскировка была снятаisDisguised(Player)-- быстрая булева проверкаgetDisguiseModelID(Player)-- возвращает активный ID модели илиnullgetDisguisedPlayers()-- неизменяемый снимок замаскированных игроков
Замаскированные игроки делаются невидимыми для других и остаются такими до снятия маскировки — ведро молока, очистка эффектов маяком и подобные действия не нарушают невидимость.
LocationAPI
Публичный API для плагинов, чтобы вносить вклад в обнаружение подземелий и проверки защиты регионов. Зарегистрированные предикаты питают проверки em.location.is_in_dungeon и em.location.is_protected в Lua-скриптах FMM (используются предзаготовленными скриптами вроде pickupable.lua и storage_double.lua).
Плагины передают обычный Predicate<Location>, поэтому шейженные типы FMM не пересекают classloader-ы плагинов.
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 поддерживает Lua-скрипты как для реквизита, так и для пользовательских предметов через движок скриптов MagmaCore 2.0. Файлы скриптов размещаются в 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 | По реквизиту попадает снаряд |
Хуки скриптов предметов
Пользовательские предметы (модели с заданным 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 относительно реквизита
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, который содержит общий движок Lua-скриптов, используемый и скриптами реквизита FreeMinecraftModels, и Lua-силами EliteMobs, плюс общие
LocationQueryRegistryиWorldFolderResolver. - FreeMinecraftModels объявляет WorldGuard, WorldEdit, GriefPrevention и Vault как
softdepend. Ни один из них не требуется для запуска плагина, но они открывают конкретные функции: WorldGuard/WorldEdit/GriefPrevention питаютLocationAPI, а Vault включает магазин мебели.