Перейти к основному содержимому

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

Основные типы среды выполнения

  • 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);

Все пути создания возвращают 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() -- возвращает текущий Location
  • ModeledEntity#getWorld() -- возвращает World
  • ModeledEntity#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()

События

Общие события взаимодействия:

  • ModeledEntityLeftClickEvent
  • ModeledEntityRightClickEvent
  • ModeledEntityHitboxContactEvent
  • ModeledEntityHitByProjectileEvent

Также существуют типизированные варианты для 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, если для этой модели существует .json display-модели
  • 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/unequip
  • removePlayer(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 предмета зарегистрирован в определениях предметов FMM
  • applyScriptedItemData(ItemStack itemStack, String itemId) -- проставляет PDC-тег и модель предмета на существующий ItemStack. Возвращает true при успехе, false, если ID предмета невалиден или у ItemStack нет meta. Замечание про луки/арбалеты: если у указанного itemId нет display-модели, но у itemId + "_idle" она есть (то есть у предмета есть модели состояний лука/арбалета), метод автоматически использует модель _idle в качестве display-модели
  • getItemConfig(String itemId) -- возвращает PropScriptConfigFields для данного ID предмета, либо null, если не найдено
Интеграция с EliteMobs

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 модели или null
  • getDisguisedPlayers() -- неизменяемый снимок замаскированных игроков

Замаскированные игроки делаются невидимыми для других и остаются такими до снятия маскировки — ведро молока, очистка эффектов маяком и подобные действия не нарушают невидимость.

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_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, который содержит общий движок Lua-скриптов, используемый и скриптами реквизита FreeMinecraftModels, и Lua-силами EliteMobs, плюс общие LocationQueryRegistry и WorldFolderResolver.
  • FreeMinecraftModels объявляет WorldGuard, WorldEdit, GriefPrevention и Vault как softdepend. Ни один из них не требуется для запуска плагина, но они открывают конкретные функции: WorldGuard/WorldEdit/GriefPrevention питают LocationAPI, а Vault включает магазин мебели.