Skip to main content

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 models
  • LocationAPI — register dungeon detectors and protection providers (feeds Lua em.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

  • ModeledEntity
  • StaticEntity
  • DynamicEntity
  • PropEntity

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 nameplate
  • ModeledEntity#setDisplayNameVisible(boolean) -- same requirement
  • ModeledEntity#setLeftClickCallback(...)
  • ModeledEntity#setRightClickCallback(...)
  • ModeledEntity#setHitboxContactCallback(...)
  • ModeledEntity#setModeledEntityHitByProjectileCallback(...)
  • ModeledEntity#playAnimation(String, boolean blend, boolean loop) -- returns false when the name matches neither a built-in state nor an animation in the model. blend queues rather than cross-fades; loop only applies to custom animations. See Animations
  • ModeledEntity#stopCurrentAnimations()
  • ModeledEntity#hasAnimation(String)
  • ModeledEntity#damage(double) / damage(Entity damager, double) / damage(Entity damager) / damage(Projectile) -- routed through the entity's DamageableComponent
  • ModeledEntity#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, or null
  • ModeledEntity#showUnderlyingEntity(Player) / hideUnderlyingEntity(Player) -- per-player visibility of the backing vanilla entity
  • ModeledEntity#getEntityID() -- returns the model ID string
  • ModeledEntity#getModelInstanceId() -- per-instance UUID, stable for the life of the model
  • ModeledEntity#isRemoved() / isDying() -- lifecycle flags
  • ModeledEntity#getLocation() -- returns the current Location
  • ModeledEntity#getSpawnLocation() -- returns the Location the model was created at
  • ModeledEntity#getWorld() -- returns the World
  • ModeledEntity#getSkeleton() / getSkeletonBlueprint() / getMountPointManager() -- runtime structure access
  • ModeledEntity#getInteractionComponent() / getHitboxComponent() / getDamageableComponent() / getAnimationComponent() -- the component objects behind the convenience methods above
  • ModeledEntity.getLoadedModeledEntities() -- static live set of every loaded modeled entity
  • ModeledEntity#getViewers() -- returns the HashSet<UUID> of players who can see the entity
  • ModeledEntity#getNametagBones() -- returns List<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 animation
  • ModeledEntity#remove() -- immediately removes the entity and all bones
  • ModeledEntity#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. Pass null to clear.
  • ModeledEntity#setViewDistanceOverride(int) / getEffectiveViewDistance() -- overrides DefaultConfig.maxModelViewDistance for a single entity. Pass -1 to revert to the plugin-wide default.
  • DynamicEntity#setSyncMovement(boolean)
  • DynamicEntity#isDamagesOnContact() / setDamagesOnContact(boolean) -- controls whether the entity damages players via hitbox contact
  • DynamicEntity.isDynamicEntity(Entity) / DynamicEntity.getDynamicEntity(Entity) -- static lookups from a Bukkit entity
  • DynamicEntity#getBodyLocation() -- body-oriented location, distinct from getLocation()
  • 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 stand
  • PropEntity.hasLoadedPropOnSameBlock(String entityID, Location) -- the duplicate check spawnPropEntity runs internally; call it first if you want to branch instead of null-checking
  • PropEntity.respawnPropEntityFromArmorStand(String entityID, ArmorStand) -- rebuilds a prop around an armor stand that survived a chunk reload
  • PropEntity.getPropEntities() -- live map of loaded props keyed by armor-stand UUID
  • PropEntity#setPersistent(boolean) -- toggles persistence on the backing armor stand
  • PropEntity#setCustomDataString(NamespacedKey, String) / getCustomDataString(NamespacedKey) -- read/write your own PDC values on the prop. This is the same store the Lua set_persistent_data / get_persistent_data helpers use, under the fmm_lua_<key> namespace
  • PropEntity#remove() / remove(boolean showRealBlocks) -- removes the model but leaves the persistent entry
  • PropEntity#permanentlyRemove() -- removes the model and its persistent entry
  • PropEntity#setVoxelizeConfig(boolean voxelize, boolean solidify) / applySolidify() -- the runtime equivalents of the voxelize: / solidify: YML fields
  • PropEntity#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:

  • ModeledEntityLeftClickEvent
  • ModeledEntityRightClickEvent
  • ModeledEntityHitboxContactEvent
  • ModeledEntityHitByProjectileEvent

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 + 1 distinct 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 like 01_em_flame_sword into Flame 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) -- returns true if a .json display model exists for this model
  • getRegisteredModels() -- returns an immutable Set<String> of all model IDs that have display models registered
  • shutdown() -- 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, or null.
  • applyScriptedItemData(ItemStack, String): stamps fmm_item_id and, when present, the registered display model. Returns false for 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) -- returns false if the model ID is not loaded
  • undisguise(Player) -- returns true if a disguise was removed
  • isDisguised(Player) -- quick boolean check
  • getDisguiseModelID(Player) -- returns the active model ID or null
  • getDisguisedPlayers() -- 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 returning true flags the location as "in dungeon"
  • registerProtectionProvider(String providerName, Predicate<Location> predicate) — any registered predicate returning true flags 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:

ProvidercanBuild behavior
Built-in WorldGuard adapterHonors WorldGuard's own bypass, then defers to WorldGuard's testBuild — so region members and owners can build normally
Built-in GriefPrevention adapterNo claim at the location means allowed; inside a claim it defers to GriefPrevention's own build permission for that player
Provider registered via LocationAPI.registerProtectionProviderFalls 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 LocationOwnership are 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

HookTrigger
on_spawnProp is spawned into the world
on_game_tickEvery tick while the prop is alive
on_zone_enterA player enters a script-created watched zone
on_zone_leaveA player leaves a script-created watched zone
on_destroyProp is removed
on_left_clickPlayer left-clicks the prop
on_right_clickPlayer right-clicks the prop
on_projectile_hitReserved: 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 name
  • current_location -- the prop's current location
  • play_animation(name, blend, loop) -- plays the named animation (blend and loop default to true)
  • stop_animation() -- stops all current animations
  • hurt_visual() -- plays the hurt (red flash) visual on the prop
  • pickup() -- queues removing the prop and dropping its placement item
  • mount(player) -- queues a mount attempt; a true return means the player and mount manager were valid, not that a seat was ultimately assigned
  • dismount(player) -- queues a dismount check; a true return means the player and mount manager were valid
  • get_passengers() -- returns a list of players currently riding the prop
  • spawn_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, and on_zone_leave
  • cancel(), uncancel(), is_cancelled when the underlying hook is cancellable
  • player -- the player who triggered the event

context.world:

  • spawn_entity(entity_type, x, y, z) -- spawns a vanilla entity, or returns nil if the entity type is invalid
  • set_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 cooldown
  • global_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; type is the uppercase Bukkit material name
  • consume_held_item() -- removes one of the held item
  • has_item(material) -- checks if the player has an item
  • send_message(text) -- sends a chat message to the player
  • game_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 LocationQueryRegistry and WorldFolderResolver.
  • 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 feed LocationAPI and 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.