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

API FreeMinecraftModels и руководство разработчика

Сгенерированный справочник Java позволяет искать классы, методы и сигнатуры. Для каждого модуля указаны версия исходного кода и коммит.

FreeMinecraftModels работает как самостоятельный плагин и предоставляет API другим плагинам.

Общие настройки зависимостей и среды выполнения описаны в указателе Java API. В примере используется опубликованная версия 2.12.9; текущая версия исходного кода — 2.12.9. Выбирайте зависимость под установленную на сервере версию плагина.

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>2.12.9</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 — добавление идентификатора предмета FMM и данных отображения в сторонние ItemStack.
  • ItemConstructionSnapshot — снимок данных предметов и моделей отображения в серверном потоке для создания предметов в другом потоке.

ModeledEntityManager.getAllEntities(), getDynamicEntities() и propEntities() возвращают копии на момент вызова. Изменение этих множеств и карт не меняет рабочие реестры FMM. Для проверки существования модели используйте modelExists(String), чтобы не хранить и не копировать весь реестр.

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

  • 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 не загружена.

PropEntity.spawnPropEntity также возвращает null, если на целевом блоке уже загружен реквизит той же модели. Дубликат не создаётся, а в журнал записывается предупреждение [FMM Props] Prevented duplicate prop spawn .... Всегда проверяйте результат на null: только непустой результат подтверждает создание реквизита.

createWithInvisibility применяет эффект невидимости вместо скрытия сущности от клиентов. Клиент продолжает отслеживать сущность, что необходимо для управления транспортом (используется внутри /fmm mount) и позволяет display-костям модели ехать на сущности на клиентах Java; см. displayBonesRideTheirEntity.

Полезные методы среды выполнения​

  • ModeledEntity#setDisplayName(String) — задаёт имя на табличке модели.
  • ModeledEntity#setDisplayNameVisible(boolean) — управляет видимостью таблички.
  • ModeledEntity#setLeftClickCallback(...)
  • ModeledEntity#setRightClickCallback(...)
  • ModeledEntity#setHitboxContactCallback(...)
  • ModeledEntity#setModeledEntityHitByProjectileCallback(...)
  • ModeledEntity#playAnimation(String, boolean blend, boolean loop) — возвращает false, если имя не совпадает ни со встроенным состоянием, ни с анимацией модели, а также после запроса death. Для пользовательской анимации blend = true проигрывает её на слое действий поверх idle или walk, а false позволяет ей занять всю позу; loop действует только на пользовательские анимации. См. смешивание анимаций.
  • ModeledEntity#stopCurrentAnimations() — завершает пользовательские анимации и атаки и сразу переводит модель в idle; после начала смерти ничего не делает.
  • ModeledEntity#hasAnimation(String)
  • ModeledEntity#damage(double) / damage(Entity damager, double) / damage(Entity damager) / damage(Projectile) — передают урон через DamageableComponent сущности.
  • ModeledEntity#attack(LivingEntity) / attack(LivingEntity, double damage)
  • ModeledEntity#teleport(Location, boolean teleportUnderlyingEntity)
  • ModeledEntity#setUnderlyingEntity(Entity) / ModeledEntity.getModeledEntity(Entity) — привязка базовой сущности и статический поиск модели по Bukkit-сущности; поиск возвращает null, если модели нет.
  • ModeledEntity#showUnderlyingEntity(Player) / hideUnderlyingEntity(Player) — видимость базовой ванильной сущности для конкретного игрока.
  • ModeledEntity#getEntityID() — строка с ID модели.
  • ModeledEntity#getModelInstanceId() — UUID экземпляра, постоянный в течение жизни модели.
  • ModeledEntity#isRemoved() / isDying() — признаки состояния жизненного цикла.
  • ModeledEntity#getLocation() — текущий Location.
  • ModeledEntity#getSpawnLocation() — Location, в котором модель была создана.
  • ModeledEntity#getWorld() — мир World.
  • ModeledEntity#getSkeleton() / getSkeletonBlueprint() / getMountPointManager() — доступ к структурам модели.
  • ModeledEntity#getInteractionComponent() / getHitboxComponent() / getDamageableComponent() / getAnimationComponent() — компоненты, через которые работают перечисленные выше методы.
  • ModeledEntity.getLoadedModeledEntities() — статическое рабочее множество всех загруженных сущностей с моделями.
  • 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) — управление уроном игрокам при касании хитбокса.
  • DynamicEntity.isDynamicEntity(Entity) / DynamicEntity.getDynamicEntity(Entity) — статические проверки по Bukkit-сущности.
  • DynamicEntity#getBodyLocation() — позиция сущности с поворотом по yaw модели. При включённом weightedTurning этот yaw следует за yaw тела моба с инерцией поворота, поэтому может отставать от моба, пока модель поворачивается. getLocation() возвращает ту же позицию, пока включена синхронизация движения.
  • Bone#getBoneLocation()

Свет, наклон, центрирование и клики​

  • ModeledEntity#setBrightnessOverride(int blockLight, int skyLight) — освещает каждую кость отображения фиксированным уровнем, каждый от 0 до 15, вместо света в месте кости. 15, 15 — полная яркость. Зрители получают изменение со следующим обновлением костей. Только для костей отображения Java: кости Bedrock и стойки для брони сохраняют свет мира
  • ModeledEntity#clearBrightnessOverride() / hasBrightnessOverride() / getBrightnessOverrideBlockLight() / getBrightnessOverrideSkyLight() — снимают или читают фиксированный свет. Геттеры возвращают -1 без переопределения
  • DynamicEntity#setTravelOrientation(TravelOrientation) / getTravelOrientation() — наклоняет модель по ходу движения. TravelOrientation — это record из Style (UPRIGHT, TOP_FIRST, FRONT_FIRST), startSpeed, fullSpeed, maxTilt и turnSpeed; по умолчанию TravelOrientation.UPRIGHT, а null восстанавливает его. См. Ориентация при движении
  • ModeledEntity#setBodyTilt(Quaterniondc) / getBodyTilt() — наклоняет всю модель поворотом в мировых координатах вокруг середины её кубов, после направления и анимаций. null ставит её вертикально. Ориентация при движении, отличная от UPRIGHT, задаёт это на каждом тике; у вертикальной модели заданный вами наклон сохраняется
  • ModeledEntity#setCenteredOnBody(boolean) / isCenteredOnBody() / getBodyOffset() — рисует модель так, что середина её кубов совпадает с серединой её сущности. См. Центрирование на теле
  • DynamicEntity#setClickSurface(boolean) / isClickSurface() — при true клиенты получают невидимую сущность Interaction поверх модели, поэтому игроки кликают по той модели, которую видят, а не по собственному боксу тела. Она принимает размер кости hitbox модели или собственных границ модели, если кости нет, и применяется в течение двух тиков. По умолчанию выключено, потому что бокс, покрывающий длинную или повёрнутую модель, ловит клики и в её пустых углах
  • DynamicEntity#isSteerableMount() / hasSteeringRider() / seatSteeringRider(Player) — модель на осёдланной лошади является управляемым маунтом. seatSteeringRider сажает игрока на саму лошадь как управляющего всадника, если на ней никто не едет, и возвращает, едет ли он теперь на ней. Правый клик по такой модели делает это по умолчанию
  • DynamicEntity#liftSaddleTo(Bone) — масштабирует моба так, чтобы его седло оказалось на указанной кости сиденья в позе покоя модели, а сама модель сохраняла размер. /fmm mount использует его с первой костью m_ модели. Ничего не делает, если сервер не может сообщить высоту седла

Таблички и направление взгляда​

ModeledEntity предоставляет setDisplayNameLines(List<String>), setDisplayNameScale(float), setDisplayNameLineGap(double), setDisplayNameVisible(boolean) и getNameplateLocation(). Кость таблички необязательна. Без неё табличка находится на четверть блока (четыре пикселя) выше самого высокого куба модели в покое, с учётом масштаба сущности.

Многострочные имена:

  • setDisplayName("Top line\nBottom line") и setDisplayNameLines(List<String>) принимают строки в порядке чтения, сверху вниз. Нижняя строка остаётся на якоре, а дополнительные строки идут вверх. Строки задаются явными переводами строки; автоматического переноса текста нет.
  • Пустое имя скрывает табличку. setDisplayNameVisible(false) скрывает каждую строку и остаётся в силе при смене текста.
  • setDisplayNameScale(float) масштабирует текст независимо от размера модели. setDisplayNameLineGap(double) задаёт чистый промежуток между строками в блоках при масштабе 1, по умолчанию 0.1. Строка имеет высоту 0.225 блока при масштабе 1, поэтому последовательные строки по умолчанию расположены на расстоянии 0.325 блока друг от друга.
  • Если у модели несколько костей tag_, табличку закрепляет первая по алфавиту, включая её движение и анимацию.
  • Игроки Java видят текстовые отображения, которые масштабируют и глифы, и интервалы. Игроки Bedrock видят подписи на отдельных стойках для брони в режиме маркера, которые масштабировать нельзя, поэтому их интервалы остаются как при масштабе 1. Чтобы выбрать подписи для Bedrock, на бэкенд-сервере нужны Floodgate или Geyser, определяющие игроков Bedrock; конфигурация только с прокси без пересылки идентификации не может надёжно их выбрать.
  • Дальность просмотра, невидимость, удаление и перезагрузка следуют за моделью. Неизменённые текст и позиции пакеты не отправляют.

О том, как EliteMobs располагает имя и роль NPC, использующего модель FreeMinecraftModels, см. Создание NPC.

DynamicEntity.setLookTarget(Location) задаёт скелету визуальную цель взгляда, не поворачивая и не перемещая базовую сущность. Переданная позиция копируется. Для снятия цели передайте null.

Методы PropEntity​

  • PropEntity.isPropEntity(ArmorStand) / PropEntity.getPropEntityID(ArmorStand) — определение реквизита по его стойке для брони.
  • PropEntity.hasLoadedPropOnSameBlock(String entityID, Location) — проверка дубликата, которую выполняет spawnPropEntity. Её можно вызвать заранее для выбора дальнейшего действия.
  • PropEntity.respawnPropEntityFromArmorStand(String entityID, ArmorStand) — восстановление реквизита вокруг стойки, сохранившейся после перезагрузки чанка.
  • PropEntity.getPropEntities() — снимок загруженного реквизита с UUID стоек в качестве ключей; его изменение не меняет действующий реестр.
  • PropEntity#setPersistent(boolean) — переключение сохранения базовой стойки.
  • PropEntity#setCustomDataString(NamespacedKey, String) / getCustomDataString(NamespacedKey) — чтение и запись собственных значений PDC. Lua-функции set_persistent_data / get_persistent_data используют это же хранилище с именами fmm_lua_<key>.
  • PropEntity#remove() / remove(boolean showRealBlocks) — удаление модели с сохранением постоянной записи.
  • PropEntity#permanentlyRemove() — удаление модели и постоянной записи.
  • PropEntity#setVoxelizeConfig(boolean voxelize, boolean solidify) / applySolidify() — программные аналоги полей YML voxelize: / solidify:.
  • PropEntity#showFakePropBlocksToPlayer(Player) / showRealBlocksToPlayer(Player) и варианты ...ToAllPlayers() — управление блоками-барьерами, отправляемыми только в пакетах. Они обеспечивают столкновения с твёрдым реквизитом на стороне клиента.

Интеграция магического оружия​

Перед использованием магического сервиса проверяйте MagicWeaponAPI.isOperational(). isWeapon(String) проверяет ID определения оружия, а applyWeaponData(ItemStack, String) применяет его идентификатор и отображение, сохраняя исходные имя, описание, уровень и зачарования предмета. Одна лишь регистрация сервиса не подтверждает его работоспособность.

Необязательный MagicAttackResolver может отклонять заклинания, отбирать и упорядочивать цели, сохранять боевые данные на момент запуска и рассчитывать урон. Для нанесения урона он обязан использовать переданный одноразовый MagicDamageApplication, а не повреждать цель напрямую. Ввод, полёт снаряда, столкновения и применение урона остаются за FMM.

MagicProjectileTravelEvent — синхронный отменяемый запрос столкновений на всём отрезке движения, включая проверку в момент попадания. Отмена поглощает снаряд без урона и взрыва. Один отрезок может проверяться несколько раз, поэтому обработчик не должен считать это событие уведомлением о нанесённом уроне.

События​

Правый клик сначала вызывает ModeledEntityInteractEvent для реальной базовой сущности. Он наследует обработчики PlayerInteractEntityEvent, поэтому обычные плагины защиты сущностей могут его отменить. Отмена предотвращает событие правого клика по модели и его обратный вызов. Эта проверка не связана с правом размещать или ломать блоки.

Как сочетаются проверки правого клика:

  • Взаимодействия с блоками и с моделями решаются отдельно. Отмена PlayerInteractEvent сохраняет действие с блоком или предметом отменённым, но правый клик всё равно может выбрать модель через проверки досягаемости и перекрытия блоками в FMM. Отмена левого клика работает как прежде.
  • ModeledEntityInteractEvent использует основную руку. FMM вызывает ModeledEntityRightClickEvent только после завершения этой обработки, а отменённый ModeledEntityRightClickEvent останавливает обратный вызов модели, поведение реквизита и посадку по умолчанию.
  • Событие-мост — это проверка прав, а не ещё один клик. Собственные роутеры ввода, магического оружия и реквизита в FMM его игнорируют. Плагин, выполняющий действия по нативным событиям взаимодействия с сущностью, может пропускать ModeledEntityInteractEvent, чтобы не действовать дважды; плагины защиты должны продолжать его обрабатывать.
  • Модель без базовой сущности Bukkit не может вызвать событие сущности Bukkit. Она по-прежнему вызывает отменяемое ModeledEntityRightClickEvent. Постоянные реквизиты и NPC EliteMobs имеют базовые сущности и проходят обе проверки.

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

  • ModeledEntityLeftClickEvent
  • ModeledEntityRightClickEvent
  • ModeledEntityHitboxContactEvent
  • ModeledEntityHitByProjectileEvent

Все четыре события Bukkit отменяемы. Встроенные механизмы FMM вызывают их в основном потоке сервера. Отмена предотвращает соответствующий обратный вызов или стандартное действие FMM. Контакты с хитбоксом проверяются каждые два тика. Для кликов действует короткий интервал подавления повторов на игрока, чтобы пакетное взаимодействие и OBB-трассировка не вызвали одно действие дважды.

Событие жизненного цикла:

  • FmmReloadedEvent — планируется в основном потоке через 40 тиков после успешной полной инициализации, включая запуск и полную перезагрузку. Ограниченная перезагрузка импортированного контента также вызывает его при успехе. Отклонение запроса на этапе подготовки сохраняет действующую среду и не отправляет событие завершения перезагрузки.

Публичные события взаимодействия не зависят от типа модели. Старые варианты StaticEntity*Event, DynamicEntity*Event, PropEntity*Event и ResourcePackGenerationEvent больше не входят в API. Используйте четыре общих события выше и FmmReloadedEvent.

Плагины, долго хранящие ссылки на DynamicEntity или PropEntity (например, EliteMobs и BetterStructures), должны обрабатывать это событие и заново прикреплять модели к сохранившимся базовым сущностям. Иначе после перезагрузки сущности станут невидимыми: FMM удаляет сущности отображения в 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 — ориентированным ограничивающим параллелепипедам моделей. При пересечении снарядом такого хитбокса вызывается стандартное отменяемое событие Bukkit ModeledEntityHitByProjectileEvent.

Детали обработки:

  • Проверяется отрезок между отслеживаемыми позициями для обнаружения пересечений тонких моделей; если предыдущей позиции нет, используется проверка перекрытия.
  • Цели на отрезке обрабатываются от ближней к дальней, независимо от порядка в реестре моделей.
  • Обычный снаряд направляется только в одну цель с моделью. Стрела с Piercing может поразить pierce level + 1 разных целей в порядке полёта. Если попадание обнаружили и OBB, и ванильный хитбокс, оно не учитывается дважды.
  • Стандартный обработчик динамической сущности передаёт попадание базовой живой сущности как настоящий урон от снаряда, используя скорость в момент удара. Сохраняются тип урона, обработка боевыми плагинами и сведения о стрелке.
  • Отмена предотвращает обратный вызов и стандартный урон FMM, но столкновение всё равно расходует лимит целей снаряда. Поэтому отменённое попадание обычного снаряда также поглощает его.
@EventHandler
public void onProjectileHitModel(ModeledEntityHitByProjectileEvent event) {
ModeledEntity target = event.getEntity();
Projectile projectile = event.getProjectile();
// Cancel to prevent damage
event.setCancelled(true);
}

Утилиты для предметов и моделей​

ModelItemFactory​

Фабрика для программного создания ItemStack, связанных с моделями.

// Create a prop placement item (uses "model_id" PDC key)
ItemStack placementItem = ModelItemFactory.createModelItem("lamp_post", Material.STICK);

// Create a custom item from config (uses "fmm_item_id" PDC key)
PropScriptConfigFields config = ItemScriptManager.getItemDefinitions().get("magic_sword");
ItemStack customItem = ModelItemFactory.createCustomItem("magic_sword", config);
  • createModelItem(String modelId, Material material) — создаёт предмет для размещения реквизита. На 1.21.4+ автоматически применяет модель отображения, если существует соответствующий JSON.
  • createCustomItem(String itemId, PropScriptConfigFields config) — создаёт предмет с именем, описанием, зачарованиями и моделью отображения из общей конфигурации.
  • formatModelName(String modelId) — преобразует ID вроде 01_em_flame_sword в Flame Sword.

DisplayModelRegistry​

Реестр моделей, для которых доступен JSON отображения.

// Check if a model has a display model JSON registered
boolean has3D = DisplayModelRegistry.hasDisplayModel("magic_sword");
  • register(String modelId) — регистрирует ID; вызывается внутри плагина при перезагрузке.
  • hasDisplayModel(String modelId) — проверяет регистрацию модели отображения; некорректный .json не регистрируется лишь потому, что файл существует.
  • getRegisteredModels() — неизменяемый Set<String> всех ID с зарегистрированными моделями отображения.
  • shutdown() — очищает реестр.

ItemScriptManager​

Несмотря на сохранённое имя класса, теперь это каталог определений предметов и магического оружия, а не среда выполнения скриптов экипировки. getItemDefinitions() предоставляет зарегистрированные определения предметов, getWeaponCatalog() — проверенные определения оружия. Старые API активных скриптов, обновления экипировки и удаления скриптов игрока убраны.

ScriptedItemAPI​

  • isValidItemId(String) — проверяет точный ID определения предмета: имя конфигурационного файла без .yml.
  • getItemConfig(String) — возвращает определение или null.
  • applyScriptedItemData(ItemStack, String) — записывает fmm_item_id и применяет зарегистрированную модель отображения, если она есть. Возвращает false при неверном ID или отсутствии метаданных у предмета.

Метод не меняет имя, описание и зачарования исходного предмета и не добавляет Lua-поведение. Зачарования применяйте отдельно через MagmaCore. Состояния натяжения лука и арбалета входят в базовое определение модели отображения; не добавляйте _idle к ID предмета.

ModelItemAPI​

Если нужна только модель отображения FMM, используйте ModelItemAPI.applyDisplayModel(ItemStack, String). Метод проверяет реестр моделей отображения, не записывая идентификатор определения предмета. Это отдельная операция от выбора определения оружия или предмета FMM.

ItemConstructionSnapshot​

Для плагинов, создающих предметы вне серверного потока. Вызовите ItemConstructionSnapshot.capture() в серверном потоке: он копирует текущие определения авторских предметов, модели отображения и виды магического оружия. Затем рабочий поток может вызвать item(id) или weapon(id) и применить возвращённый ItemData к ItemStack через apply(ItemStack). Этот метод записывает fmm_item_id для авторского предмета и модель отображения, если она зарегистрирована. weapon(id) возвращает null, если ID не является магическим оружием или магический сервис не работал в момент снимка. Перед выдачей созданных предметов вызовите requireCurrent() в серверном потоке: он выбрасывает ConcurrentModificationException, если определения FMM изменились после снимка, например после перезагрузки.

DisguiseAPI​

Публичная точка входа для маскировки игроков. Сторонним плагинам следует использовать её вместо внутреннего DisguiseManager, чтобы изменения внутренней реализации не затрагивали интеграцию.

import com.magmaguy.freeminecraftmodels.api.DisguiseAPI;

// Disguise a player as a loaded model. Replaces any existing disguise cleanly.
boolean ok = DisguiseAPI.disguise(player, "dragon");

// Undisguise (returns true if a disguise was removed).
DisguiseAPI.undisguise(player);

// Query state.
boolean disguised = DisguiseAPI.isDisguised(player);
String modelID = DisguiseAPI.getDisguiseModelID(player); // null if not disguised

// Snapshot of all currently disguised players.
Collection<Player> all = DisguiseAPI.getDisguisedPlayers();
  • disguise(Player, String modelID) — снимает прежнюю маскировку до попытки замены; неизвестный ID возвращает false после её снятия. Передавайте подключённого игрока и точный загруженный ID; API не нормализует ID и не проверяет права команды.
  • undisguise(Player) — возвращает true, если маскировка снята.
  • isDisguised(Player) — быстрая проверка наличия маскировки.
  • getDisguiseModelID(Player) — текущий ID модели или null.
  • getDisguisedPlayers() — неизменяемая копия списка замаскированных игроков.

FMM сочетает стандартную видимость игрока, эффект зелья невидимости и подмену отображения экипировки. Снятый эффект восстанавливается только в том случае, если его добавила эта маскировка; ранее существовавший эффект не контролируется и не восстанавливается. В отличие от API, команда проверяет модель до замены. Ограничения жизненного цикла и отображения см. в Маскировках игроков.

LocationAPI​

Публичный API регистрации проверок подземелий и защиты регионов. Предикаты используются в Lua-функциях FMM em.location.is_in_dungeon и em.location.is_protected, например в готовых скриптах pickupable.lua и storage_double.lua.

Плагины передают обычный Predicate<Location>, поэтому внутренние типы, включённые в FMM, не передаются между загрузчиками классов разных плагинов.

import com.magmaguy.freeminecraftmodels.api.LocationAPI;

// On your plugin's enable, after WorldGuard/EliteMobs/etc. are available.
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.
  • unregisterDungeonLocator(String providerName) и unregisterProtectionProvider(String providerName) удаляют предикат, зарегистрированный под этим именем. Вызывайте их в onDisable своего плагина, чтобы FMM перестал обращаться к отключённому экземпляру.

Повторная регистрация под тем же именем провайдера заменяет прежний предикат, поэтому плагин, регистрирующийся при каждом включении, не накапливает копии. Имя провайдера null регистрирует предикат, который нельзя ни заменить, ни удалить.

Операторы могут проверить регистрацию командой /fmm location: она показывает текущее число провайдеров и проверяет оба предиката в позиции игрока.

Провайдеры защиты и размещение реквизита​

Те же провайдеры участвуют в preventPropPlacementInProtectedRegions, но эта проверка вызывает canBuild(player, location), а не isProtected(location). Различия:

ПровайдерПоведение canBuild
Встроенный адаптер WorldGuardУчитывает обход защиты WorldGuard и вызывает testBuild, позволяя участникам и владельцам региона строить
Встроенный адаптер GriefPreventionРазрешает строительство вне привата; внутри проверяет права конкретного игрока средствами GriefPrevention
Зарегистрированный через LocationAPI.registerProtectionProviderИспользует !isProtected(location): проверяет только позицию и запрещает всем размещение там, где предикат считает её защищённой

Predicate<Location> не может описывать права конкретного игрока. Для такой проверки в собственной системе регионов реализуйте RegionProtectionProvider из MagmaCore, переопределите canBuild и зарегистрируйте провайдер через LocationQueryRegistry.registerProtectionProvider.

Также учитывайте:

  • Ошибка приводит к отказу. Если провайдер выбросил исключение при проверке строительства, размещение запрещается, а в журнале появляется предупреждение с именем провайдера.
  • Разрешение должны дать все провайдеры. Первого отказа достаточно. Провайдеры владения, зарегистрированные через LocationOwnership, по-прежнему проверяют только позицию и запрещают размещение всем.

Размещение предметом сначала проверяет соседнюю ячейку. Вокселизированный реквизит также проверяет занимаемую область после поворота, используя округлённые размеры хитбокса с минимумом в один блок; объём пакетных барьеров рассчитывается иначе. Прямые вызовы PropEntity.spawnPropEntity не выполняют проверки размещения предметом игрока. Перед предоставлением размещения через другой плагин прочитайте Конфигурацию.

PropScriptConfigFields​

Читает YAML рядом с моделью для определений реквизита и предметов. Список scripts: реквизита привязывает независимые Lua-скрипты. Поле material: вместо этого регистрирует предмет для руки; непустой список scripts: для него недопустим.

Отдельные примеры приведены в настройке моделей, а формат оружия — в разделе магического оружия.

Lua-скрипты​

FMM поддерживает Lua-скрипты реквизита через движок MagmaCore. Файлы находятся в plugins/FreeMinecraftModels/scripts/ и привязываются через YML рядом с моделью. Файл на диске должен оканчиваться на .lua; в конфигурации расширение можно указать или опустить.

Реквизит привязывает все скрипты из scripts: как независимые экземпляры. Эффекты предметов в руке работают через отдельную среду провайдеров зачарований; непустой список scripts: у предмета запрещён.

Хуки скриптов реквизита​

ХукКогда вызывается
on_spawnРеквизит появляется в мире
on_game_tickКаждый тик, пока существует реквизит
on_zone_enterИгрок входит в отслеживаемую зону, созданную скриптом
on_zone_leaveИгрок покидает эту зону
on_destroyРеквизит удаляется окончательно (не при выгрузке чанка и не при перезагрузке)
on_left_clickИгрок нажимает левую кнопку на реквизите
on_right_clickИгрок нажимает правую кнопку на реквизите
on_projectile_hitСнаряд попадает в реквизит; context.projectile — снаряд, а context.event.player — стрелок

Хуки скриптов предметов​

Lua-хуки экипированных предметов и их context.item удалены. Для эффектов предметов используйте определения зачарований. Хуки реквизита по-прежнему поддерживаются.

Таблица контекста скрипта реквизита​

Скрипты реквизита получают таблицу context. Ниже перечислены основные возможности; полный справочник находится в Lua API реквизита.

context.prop:

  • model_id — имя модели в определении.
  • current_location — текущая позиция реквизита.
  • play_animation(name, blend, loop) — запрашивает анимацию; blend и loop по умолчанию равны true, что накладывает зацикленную анимацию поверх idle реквизита; запрос применяется на следующем тике модели.
  • stop_animation() — завершает пользовательские анимации и возвращает idle, если она есть. Не прерывает конечное состояние смерти.
  • hurt_visual() — показывает красную вспышку урона.
  • pickup() — ставит в очередь удаление реквизита и выпадение предмета для его размещения.
  • mount(player) — ставит в очередь попытку посадки. true подтверждает допустимость игрока и менеджера посадки, но не выделение места.
  • dismount(player) — ставит в очередь проверку высадки. true подтверждает допустимость игрока и менеджера посадки.
  • get_passengers() — список игроков, сидящих на реквизите.
  • spawn_elitemobs_boss(filename, x, y, z) — создаёт босса EliteMobs по абсолютным координатам в текущем мире реквизита.

context.event:

  • Доступна в хуках реквизита on_left_click, on_right_click, on_zone_enter, on_zone_leave и on_projectile_hit.
  • cancel(), uncancel() и is_cancelled доступны, если базовое событие допускает отмену.
  • player — игрок, вызвавший событие; в on_projectile_hit — живая сущность, выпустившая снаряд.

context.world:

  • spawn_entity(entity_type, x, y, z) — создаёт ванильную сущность; возвращает nil при неверном типе.
  • set_block_at(x, y, z, material) — ставит изменение блока в очередь, если материал допустим. Незагруженные чанки пропускаются.
  • Также доступны частицы, звуки, запросы блоков, молнии и поиск ближайших сущностей.

context.cooldowns:

  • check_local(key, ticks) — проверяет и запускает перезарядку конкретного скрипта.
  • global_ready() / set_global(ticks) — общая перезарядка реквизита.

Объекты игрока из context.player или context.event.player:

  • get_held_item() — возвращает предмет в руке; type содержит имя материала Bukkit в верхнем регистре.
  • is_holding_elitemobs_item(filename) проверяет тег имени файла EliteMobs у предмета в основной руке, например shroomwhispers_shears.yml. Отображаемые имена и переводы не влияют на проверку. Возвращает false для пустой руки, другого предмета или недоступной интеграции с EliteMobs.
  • consume_held_item() — планирует удаление одной единицы из основной руки в её состоянии на момент выполнения задачи; не возвращает результат оплаты.
  • has_item(material) — проверяет наличие предмета у игрока.
  • send_message(text) — отправляет сообщение в чат.
  • game_mode — текущий игровой режим.

Определяйте квестовые предметы EliteMobs по имени файла:

if not player:is_holding_elitemobs_item("shroomwhispers_shears.yml") then
return
end

Этот метод предоставляет интеграция FMM с EliteMobs. Переименование обычного предмета не добавляет ему нужный тег имени файла.

Пример скрипта реквизита​

return {
api_version = 1,

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

on_right_click = function(context)
if context.cooldowns:check_local("activate", 40) then
context.prop:play_animation("activate", false, false)
end
end
}

Пример скрипта предмета​

Старый пример экипированного предмета больше не поддерживается. Для создания предметов в текущей системе см. магическое оружие и определения зачарований.

Сборка из исходного кода​

Исходный код находится на GitHub. FreeMinecraftModels — это проект Maven для Java 21:

mvn -DskipTests package
  • Jar плагина записывается в target/FreeMinecraftModels.jar.
  • MagmaCore, bStats и другие встроенные библиотеки включаются (shaded) на фазе package. MagmaCore перемещён в com.magmaguy.freeminecraftmodels.magmacore, поэтому серверу не нужна отдельная установка MagmaCore.
  • Maven разрешает MagmaCore 2.2.0-SNAPSHOT из репозитория MagmaGuy. Если исходному коду нужны изменения MagmaCore, которые там ещё не опубликованы, сначала соберите MagmaCore и опубликуйте его в локальный репозиторий Maven. -Dmagmacore.version=<version> собирает проект с другой версией MagmaCore.
  • Когда задана переменная окружения MC_DIST_DIR, сборка также копирует jar в эту папку.

Импортированные модели, текстуры, шрифты и пакеты контента сохраняют лицензии своих авторов. Генерация из них ресурс-пака не меняет эти права.

Примечания​

  • FreeMinecraftModels подключается как установленный плагин, а не встраиваемая библиотека.
  • Для использования новых импортированных моделей вызывайте ModeledEntityManager.reload(), не пытаясь самостоятельно восстановить состояние FMM.
  • Текущий код FMM зависит от MagmaCore 2.2.0-SNAPSHOT, предоставляющего общий Lua-движок, LocationQueryRegistry и WorldFolderResolver.
  • FreeMinecraftModels объявляет WorldGuard, WorldEdit, GriefPrevention, Vault, floodgate и Geyser-Spigot как softdepend. WorldGuard и GriefPrevention предоставляют встроенные адаптеры защиты с учётом игрока; WorldEdit — зависимость WorldGuard, а не независимый адаптер FMM. Магазин требует Vault, провайдера экономики и включённой конфигурации магазина. Отображение Bedrock также требует совместимого пака и подходящей интеграции.
  • ModeledEntityManager.reload() запрашивает ту же асинхронную подготовку и полный жизненный цикл, что и /fmm reload, не ожидая завершения. Вызывайте его из основного потока и подтверждайте результат уведомлением успешного жизненного цикла, а не возвратом void. Некорректный кандидат на загрузку может привести к отказу от перезагрузки с сохранением действующей среды.