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), чтобы не хранить и не копировать весь реестр.
Основные типы среды выполнения
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 не загружена.
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()— программные аналоги полей YMLvoxelize:/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 имеют базовые сущности и проходят обе проверки.
Общие события взаимодействия:
ModeledEntityLeftClickEventModeledEntityRightClickEventModeledEntityHitboxContactEventModeledEntityHitByProjectileEvent
Все четыре события 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. Некорректный кандидат на загрузку может привести к отказу от перезагрузки с сохранением действующей среды.