FreeMinecraftModels API and Developer Guide
FreeMinecraftModels is both a standalone plugin and an API surface for other plugins.
Maven Repository
<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
<dependency>
<groupId>com.magmaguy</groupId>
<artifactId>FreeMinecraftModels</artifactId>
<version>LATEST.VERSION.HERE</version>
<scope>provided</scope>
</dependency>
Use it as compileOnly/provided. Do not shade the plugin into your own jar.
Core Entry Points
ModeledEntityManager.modelExists(String)ModeledEntityManager.reload()ModeledEntityManager.getAllEntities()ModeledEntityManager.getDynamicEntities()ModeledEntityManager.propEntities()DisguiseAPI— disguise / undisguise players as loaded modelsLocationAPI— register dungeon detectors and protection providers (feeds Luaem.location.*predicates)ScriptedItemAPI- stamp third-party ItemStacks with FMM authored-item identity and display data
ModeledEntityManager.getAllEntities(), getDynamicEntities(), and propEntities() return point-in-time copies. Mutating the returned set or map does not modify FMM's live registries. Use modelExists(String) for an existence check instead of retaining and repeatedly copying a full registry.
Core Runtime Types
ModeledEntityStaticEntityDynamicEntityPropEntity
Creating Entities
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);
All creation paths return null if the requested model ID is not loaded.
PropEntity.spawnPropEntity additionally returns null when a prop of that same model is already loaded on the target block — the duplicate is refused rather than stacked, and a [FMM Props] Prevented duplicate prop spawn ... warning is logged. Always null-check its return value; a non-null return is the only confirmation the prop was actually created.
createWithInvisibility is a variant that applies an invisibility potion instead of hiding the entity from clients. This keeps the entity tracked client-side, which is needed for vehicle steering (used internally by /fmm mount).
Useful Runtime Methods
ModeledEntity#setDisplayName(String)- sets the model-owned nameplateModeledEntity#setDisplayNameVisible(boolean)-- same requirementModeledEntity#setLeftClickCallback(...)ModeledEntity#setRightClickCallback(...)ModeledEntity#setHitboxContactCallback(...)ModeledEntity#setModeledEntityHitByProjectileCallback(...)ModeledEntity#playAnimation(String, boolean blend, boolean loop)-- returnsfalsewhen the name matches neither a built-in state nor an animation in the model.blendqueues rather than cross-fades;looponly applies to custom animations. See AnimationsModeledEntity#stopCurrentAnimations()ModeledEntity#hasAnimation(String)ModeledEntity#damage(double)/damage(Entity damager, double)/damage(Entity damager)/damage(Projectile)-- routed through the entity'sDamageableComponentModeledEntity#attack(LivingEntity)/attack(LivingEntity, double damage)ModeledEntity#teleport(Location, boolean teleportUnderlyingEntity)ModeledEntity#setUnderlyingEntity(Entity)/ModeledEntity.getModeledEntity(Entity)-- static reverse lookup from a Bukkit entity to the model attached to it, ornullModeledEntity#showUnderlyingEntity(Player)/hideUnderlyingEntity(Player)-- per-player visibility of the backing vanilla entityModeledEntity#getEntityID()-- returns the model ID stringModeledEntity#getModelInstanceId()-- per-instanceUUID, stable for the life of the modelModeledEntity#isRemoved()/isDying()-- lifecycle flagsModeledEntity#getLocation()-- returns the currentLocationModeledEntity#getSpawnLocation()-- returns theLocationthe model was created atModeledEntity#getWorld()-- returns theWorldModeledEntity#getSkeleton()/getSkeletonBlueprint()/getMountPointManager()-- runtime structure accessModeledEntity#getInteractionComponent()/getHitboxComponent()/getDamageableComponent()/getAnimationComponent()-- the component objects behind the convenience methods aboveModeledEntity.getLoadedModeledEntities()-- static live set of every loaded modeled entityModeledEntity#getViewers()-- returns theHashSet<UUID>of players who can see the entityModeledEntity#getNametagBones()-- returnsList<Bone>of nametag bones (useful for placing additional text)ModeledEntity#getScaleModifier()/setScaleModifier(double)ModeledEntity#removeWithDeathAnimation()-- removes with the death animation (if one exists)ModeledEntity#removeWithMinimizedAnimation()-- removes with a scaling-down animationModeledEntity#remove()-- immediately removes the entity and all bonesModeledEntity#setTintColor(Color)/getTintColor()-- applies a persistent tint via the leather-armor dye channel. Damage flashes briefly override the tint and then fade back to it. Passnullto clear.ModeledEntity#setViewDistanceOverride(int)/getEffectiveViewDistance()-- overridesDefaultConfig.maxModelViewDistancefor a single entity. Pass-1to revert to the plugin-wide default.DynamicEntity#setSyncMovement(boolean)DynamicEntity#isDamagesOnContact()/setDamagesOnContact(boolean)-- controls whether the entity damages players via hitbox contactDynamicEntity.isDynamicEntity(Entity)/DynamicEntity.getDynamicEntity(Entity)-- static lookups from a Bukkit entityDynamicEntity#getBodyLocation()-- body-oriented location, distinct fromgetLocation()Bone#getBoneLocation()
Nameplates And Look Targets
ModeledEntity now exposes setDisplayNameLines(List<String>), setDisplayNameScale(float), setDisplayNameLineGap(double), setDisplayNameVisible(boolean), and getNameplateLocation(). A tag bone is optional; the fallback anchor is above the scaled hitbox.
DynamicEntity.setLookTarget(Location) gives the skeleton a copied visual look target without rotating or moving the backing entity. Pass null to release it.
PropEntity specifics
PropEntity.isPropEntity(ArmorStand)/PropEntity.getPropEntityID(ArmorStand)-- identify a prop from its backing armor standPropEntity.hasLoadedPropOnSameBlock(String entityID, Location)-- the duplicate checkspawnPropEntityruns internally; call it first if you want to branch instead of null-checkingPropEntity.respawnPropEntityFromArmorStand(String entityID, ArmorStand)-- rebuilds a prop around an armor stand that survived a chunk reloadPropEntity.getPropEntities()-- live map of loaded props keyed by armor-stand UUIDPropEntity#setPersistent(boolean)-- toggles persistence on the backing armor standPropEntity#setCustomDataString(NamespacedKey, String)/getCustomDataString(NamespacedKey)-- read/write your own PDC values on the prop. This is the same store the Luaset_persistent_data/get_persistent_datahelpers use, under thefmm_lua_<key>namespacePropEntity#remove()/remove(boolean showRealBlocks)-- removes the model but leaves the persistent entryPropEntity#permanentlyRemove()-- removes the model and its persistent entryPropEntity#setVoxelizeConfig(boolean voxelize, boolean solidify)/applySolidify()-- the runtime equivalents of thevoxelize:/solidify:YML fieldsPropEntity#showFakePropBlocksToPlayer(Player)/showRealBlocksToPlayer(Player)and the...ToAllPlayers()variants -- control the packet-only barrier blocks that give a solidified prop client-side collision
Magic weapon integrations
Use MagicWeaponAPI.isOperational() before depending on the magic service, isWeapon(String) to check an authored ID, and applyWeaponData(ItemStack, String) to apply its weapon identity and display without replacing the host's name, lore, level, or enchantments. Service registration alone is not proof that it is operational.
An optional MagicAttackResolver can reject casts, filter/rank targets, capture launch-time combat facts, and resolve damage. It must use the supplied one-shot MagicDamageApplication, not damage the target directly. FMM owns input, projectile travel, collision, and damage application.
MagicProjectileTravelEvent is a synchronous, cancellable swept-segment collision query, including an impact-time check. Cancellation absorbs the projectile without damage or an explosion. A segment may be checked more than once, so listeners must not treat it as a damage notification.
Event Surface
Right-click inputs first fire ModeledEntityInteractEvent against the real backing entity. It inherits PlayerInteractEntityEvent handlers so ordinary entity-protection listeners can cancel it. Cancellation prevents the modeled right-click event and callback. This check is distinct from permission to place or break blocks.
Generic interaction events:
ModeledEntityLeftClickEventModeledEntityRightClickEventModeledEntityHitboxContactEventModeledEntityHitByProjectileEvent
All four are cancellable Bukkit events; FMM's built-in detection paths dispatch them on the primary server thread. Cancelling one prevents FMM's matching callback/default behavior. Hitbox-contact scans run every two server ticks; click events still have a short per-player deduplication window so the packet-interaction and OBB-raytrace paths cannot fire the same action twice.
Lifecycle events:
FmmReloadedEvent-- fires after FMM finishes its initialization sequence on startup and after every/fmm reload. Always fires on the main server thread.
The public interaction surface is intentionally model-type-neutral. Older StaticEntity*Event, DynamicEntity*Event, and PropEntity*Event variants, along with ResourcePackGenerationEvent, are no longer part of the current API. Use the four generic modeled-entity events above and FmmReloadedEvent instead.
Consumer plugins that hold long-lived DynamicEntity or PropEntity references (EliteMobs, BetterStructures, etc.) must handle this event by re-creating their model attachments on the surviving underlying entities. Without this, those entities go invisible after a reload because FMM tore down the display entities during onDisable while the consumer's reference is now stale.
@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 And OBB Projectile Detection
FMM uses OBB (oriented bounding box) hit detection for projectiles against modeled entities. When a projectile intersects a modeled entity's OBB hitbox, FMM fires a ModeledEntityHitByProjectileEvent. This is a standard cancellable Bukkit event.
Key details:
- Detection sweeps the projectile's whole movement segment from the previous tick, so fast arrows cannot tunnel through a thin model between samples
- Candidates on that segment are resolved nearest-first, independently of model-registry iteration order
- A non-piercing projectile is routed to one modeled target. A Piercing arrow can hit
pierce level + 1distinct modeled targets, in travel order, without double-firing when both the OBB and vanilla backing hitboxes observe it - The default dynamic-entity handler forwards the impact to the underlying living entity as a real projectile damage event, using the projectile's impact-time velocity. This preserves projectile cause, combat-plugin handling, and shooter attribution
- Cancelling the event prevents FMM's callback/default damage behavior, but the collision still spends that projectile's modeled-target budget. A cancelled non-piercing hit therefore still consumes the projectile
@EventHandler
public void onProjectileHitModel(ModeledEntityHitByProjectileEvent event) {
ModeledEntity target = event.getModeledEntity();
Projectile projectile = event.getProjectile();
// Cancel to prevent damage
event.setCancelled(true);
}
Item & Model Utilities
ModelItemFactory
Factory class for creating model-related ItemStacks programmatically.
// 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)-- creates a placement item for props. On 1.21.4+, automatically applies display model rendering if a display JSON exists.createCustomItem(String itemId, PropScriptConfigFields config)-- creates a custom item with name, lore, enchantments, and display model from the unified config.formatModelName(String modelId)-- utility that converts a model ID like01_em_flame_swordintoFlame Sword.
DisplayModelRegistry
Simple registry that tracks which models have a display JSON available.
// Check if a model has a display model JSON registered
boolean has3D = DisplayModelRegistry.hasDisplayModel("magic_sword");
register(String modelId)-- registers a model ID (called internally during reload)hasDisplayModel(String modelId)-- returnstrueif a.jsondisplay model exists for this modelgetRegisteredModels()-- returns an immutableSet<String>of all model IDs that have display models registeredshutdown()-- clears all registrations
ItemScriptManager
Despite its retained class name, this is now an authored-item and magic-weapon catalog, not an equipped-item script runtime. getItemDefinitions() exposes registered item definitions; getWeaponCatalog() exposes validated weapon definitions. The former active-script, equip-update, and player-script removal APIs are gone.
ScriptedItemAPI
isValidItemId(String): checks for the exact authored item ID (configuration filename without.yml).getItemConfig(String): returns the definition, ornull.applyScriptedItemData(ItemStack, String): stampsfmm_item_idand, when present, the registered display model. Returnsfalsefor an invalid item ID or missing item metadata.
This method does not change the host item's name, lore, or enchantments, and does not attach Lua behavior. Apply enchantments separately through MagmaCore. Bow and crossbow draw states belong to the base display-model definition; do not append _idle to the authored item ID.
ModelItemAPI
Use ModelItemAPI.applyDisplayModel(ItemStack, String) when the host needs only an FMM display model. It checks the display-model registry and does not stamp authored-item identity. This is distinct from choosing an FMM weapon or item definition.
DisguiseAPI
Public entry point for the player-disguise feature. Third-party plugins should call this class rather than the internal DisguiseManager so internal refactors stay safe.
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)-- returnsfalseif the model ID is not loadedundisguise(Player)-- returnstrueif a disguise was removedisDisguised(Player)-- quick boolean checkgetDisguiseModelID(Player)-- returns the active model ID ornullgetDisguisedPlayers()-- unmodifiable snapshot of disguised players
Disguised players are made invisible to others and stay that way until undisguised — milk buckets, beacon effect clears, and similar interactions do not break the invisibility.
LocationAPI
Public API for plugins to contribute dungeon detection and region-protection checks. The registered predicates feed FMM's Lua em.location.is_in_dungeon and em.location.is_protected checks (used by premade scripts like pickupable.lua and storage_double.lua).
Plugins pass a plain Predicate<Location> so no shaded FMM types cross plugin classloaders.
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)— any registered predicate returningtrueflags the location as "in dungeon"registerProtectionProvider(String providerName, Predicate<Location> predicate)— any registered predicate returningtrueflags the location as protected
Operators can verify registration with /fmm location, which reports the live provider count and tests both predicates against their current location.
Protection providers and prop placement
The same protection providers also drive the preventPropPlacementInProtectedRegions prop-placement check, but that check calls canBuild(player, location) rather than the location-only isProtected(location). The distinction matters:
| Provider | canBuild behavior |
|---|---|
| Built-in WorldGuard adapter | Honors WorldGuard's own bypass, then defers to WorldGuard's testBuild — so region members and owners can build normally |
| Built-in GriefPrevention adapter | No claim at the location means allowed; inside a claim it defers to GriefPrevention's own build permission for that player |
Provider registered via LocationAPI.registerProtectionProvider | Falls back to !isProtected(location) — location-only, blocks everyone in a location your predicate calls protected |
That fallback is intentional and conservative: a Predicate<Location> cannot express player-specific permission, so FMM will not invent one. If you want genuinely player-aware behavior for your own region system, implement MagmaCore's RegionProtectionProvider directly and override canBuild, then register it with LocationQueryRegistry.registerProtectionProvider instead of going through the predicate convenience wrapper.
Two more behaviors worth knowing:
- Adapter failures fail closed. If a provider throws during a build query, the placement is refused and a warning naming the provider is logged. It never silently falls through to "allowed".
- Every registered provider must agree. The first provider to say no wins; ownership providers registered through
LocationOwnershipare still location-only and block everyone.
PropScriptConfigFields
Parses the model-adjacent YAML used for props and authored items. Prop scripts: lists bind independent Lua scripts. A material: definition instead registers a held item and cannot contain a nonempty scripts: list.
See model configuration for separate prop and held-item examples, and magic weapons for the weapon schema.
Lua Scripting
FreeMinecraftModels supports Lua scripts for both props and custom items through the MagmaCore 2.0 scripting engine. Script files are placed in plugins/FreeMinecraftModels/scripts/ and are bound to models via a sibling YML config next to the model file. The script file on disk must end in .lua; config entries may include the extension or omit it.
Props bind every script listed in scripts: as independent instances. Custom items currently bind only the first valid script in the list for each player/item pair.
Prop Script Hooks
| Hook | Trigger |
|---|---|
on_spawn | Prop is spawned into the world |
on_game_tick | Every tick while the prop is alive |
on_zone_enter | A player enters a script-created watched zone |
on_zone_leave | A player leaves a script-created watched zone |
on_destroy | Prop is removed |
on_left_click | Player left-clicks the prop |
on_right_click | Player right-clicks the prop |
on_projectile_hit | Reserved: accepted by validation, but not dispatched to prop scripts in the current runtime |
Item Script Hooks
The equipped-item Lua hooks and context.item runtime are retired. Use enchantment definitions for item effects. Prop hooks below are still supported.
Prop Script Context Table
Prop scripts receive a context table. Here is a summary of the key APIs -- see Lua Prop API for full details.
context.prop:
model_id-- the blueprint model namecurrent_location-- the prop's current locationplay_animation(name, blend, loop)-- plays the named animation (blend and loop default totrue)stop_animation()-- stops all current animationshurt_visual()-- plays the hurt (red flash) visual on the proppickup()-- queues removing the prop and dropping its placement itemmount(player)-- queues a mount attempt; atruereturn means the player and mount manager were valid, not that a seat was ultimately assigneddismount(player)-- queues a dismount check; atruereturn means the player and mount manager were validget_passengers()-- returns a list of players currently riding the propspawn_elitemobs_boss(filename, x, y, z)-- spawns an EliteMobs boss at absolute coordinates in the prop's current world
context.event:
- Available in prop
on_left_click,on_right_click,on_zone_enter, andon_zone_leave cancel(),uncancel(),is_cancelledwhen the underlying hook is cancellableplayer-- the player who triggered the event
context.world:
spawn_entity(entity_type, x, y, z)-- spawns a vanilla entity, or returnsnilif the entity type is invalidset_block_at(x, y, z, material)-- queues a block change if the material is valid; unloaded chunks are skipped- Plus particles, sounds, block queries, lightning, and nearby-entity lookups
context.cooldowns:
check_local(key, ticks)-- checks and starts a per-script cooldownglobal_ready()/set_global(ticks)-- shared cooldown for the prop or player owner
Player objects (from context.player or context.event.player):
get_held_item()-- returns the item the player is holding;typeis the uppercase Bukkit material nameconsume_held_item()-- removes one of the held itemhas_item(material)-- checks if the player has an itemsend_message(text)-- sends a chat message to the playergame_mode-- the player's current game mode
Prop Script Example
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
}
Item Script Example
The former equipped-item example is no longer supported. See magic weapons and enchantment definitions for current item authoring.
Notes
- FreeMinecraftModels is an installed-plugin dependency, not an embeddable library.
- If your plugin needs freshly imported models, call
ModeledEntityManager.reload()instead of trying to rebuild FreeMinecraftModels state yourself. - All plugins in the Nightbreak ecosystem now depend on MagmaCore 2.2.0-SNAPSHOT, which includes the shared Lua scripting engine used by FreeMinecraftModels prop scripts and EliteMobs Lua powers, plus the shared
LocationQueryRegistryandWorldFolderResolver. - FreeMinecraftModels declares WorldGuard, WorldEdit, GriefPrevention, Vault, floodgate, and Geyser-Spigot as
softdepend. None are required to start the plugin, but they unlock specific features: WorldGuard/WorldEdit/GriefPrevention feedLocationAPIand the player-aware prop-placement check, Vault enables the furniture shop, and floodgate/Geyser-Spigot enable the per-model Bedrock backend. ModeledEntityManager.reload()performs a full plugin reload cycle (onDisable/onLoad/onEnable). Call it on the main server thread.