Skip to main content

Pet Behavior Modules

A pet behavior module is a Lua file that tells pets what to do: follow their owner, drift, circle a campfire, sting. Each pet species file lists the modules its pets run and gives them settings. This page is for creature authors who want to change a module or write a new one. To use and tune the shipped modules, see Pets.

Modules use the same Lua dialect as EliteMobs behavior programs: ai.module, ai.behavior, ai.sensor, ai.controls, typed memories, and the c.memory, c.perception, c.actuator and c.entity context. That page covers the shared rules. This one covers what pets add: the module files, c.pet, c.settings, and the limits pets run under.

Files​

  • Modules live in plugins/FreeMinecraftModels/pet_behaviors/, in that folder or any subfolder. Every .lua file there is a module.
  • A module's name is its file name without .lua, in lower case. It may use only letters, digits, ., _ and -. Pet files list modules by this name.
  • A module's id must be fmm:pet/ followed by its name, so pet_behaviors/mine/slow_drift.lua declares id = 'fmm:pet/slow_drift'. A module whose id does not match its file name is skipped with a warning.
  • When two files share a name, the one whose path sorts first loads, and the console warns Duplicate content filename about the other. A byte-identical copy is skipped silently.
  • A file or folder that links outside pet_behaviors/ is skipped.
  • FreeMinecraftModels writes each of its 15 shipped modules when no file of that name exists anywhere in the folder. A deleted one comes back on the next load with the current shipped version. A file already there is never overwritten, edited or not, even after you move it into a subfolder, so it keeps your changes and also misses later updates to the shipped module.
  • /fmm reload rereads every module and pet file, puts the pets away and brings them back on the new modules. Their memories start empty. You do not need to raise a module's revision after editing it.

Start from a copy​

The shipped modules are the best examples: drift.lua moves a flyer, wander.lua walks, night_glow.lua is a single sensor and sting.lua has two. Start from the one closest to what you want.

  1. Copy it under a new name, for example pet_behaviors/mine/slow_drift.lua from drift.lua.
  2. Change its id to fmm:pet/slow_drift.
  3. Rename its memories, behaviors and sensors so they start with the new name: drift_to becomes slow_drift_to, in memories and in every c.memory call, and the behavior's id = 'fmm:pet/drift' becomes fmm:pet/slow_drift. Two modules on one creature cannot share a memory name, a behavior id or a sensor id; the second one listed is left out with a warning. Renaming lets the copy run beside the original.
  4. List it in the pet file in place of the original: - slow_drift, or with settings, - slow_drift: { speed: 0.03 }.
  5. Run /fmm reload and check the console.

Editing a shipped module in place works too, but it changes every species that lists it.

A module file​

This module is small but complete. At night the pet settles on one spot near its anchor instead of drifting, and every 10 seconds from the moment it picks that spot, it plays an animation.

-- At night: settles on one spot near its anchor (its owner, or home when wild) and plays an animation now and then.
return ai.module {
id = 'fmm:pet/night_rest', revision = 1,
-- radius: blocks around the anchor. speed: blocks per tick for a flyer, at species speed 1.
settings = { radius = 3, speed = 0.04, animation = 'interact' },
memories = { night_rest_to = 'position', night_rest_retry = 'integer' },
behaviors = {
ai.behavior {
id = 'fmm:pet/night_rest', priority = 12, probe_interval = 10,
controls = { ai.controls.move, ai.controls.look },
can_start = function(c)
return c.pet.night and not c.pet.staying and c.pet.anchor ~= nil
and c.tick >= (c.memory:get('night_rest_retry') or 0)
end,
can_continue = function(c)
return c.pet.night and not c.pet.staying and c.memory:contains('night_rest_to')
end,
start = function(c)
local anchor = c.pet.anchor
local to = anchor and c.pet:rest_point(anchor, c.settings.radius)
if to ~= nil then
c.memory:set('night_rest_to', to)
else
-- Nothing fits here: look again in 5 seconds instead of every tick.
c.memory:set('night_rest_retry', c.tick + 100)
end
end,
tick = function(c)
local to = c.memory:get('night_rest_to')
if to ~= nil then c.pet:steer_to(to, c.settings.speed * c.pet.species.speed) end
end,
stop = function(c)
c.memory:forget('night_rest_to')
c.pet:stop()
end
}
},
sensors = {
ai.sensor {
id = 'fmm:pet/night_rest_animation', interval = 200,
sense = function(c)
if c.memory:contains('night_rest_to') then c.pet:play_animation(c.settings.animation) end
end
}
}
}

A pet file then lists it as - night_rest, or - night_rest: { radius: 5, animation: idle }.

FieldNotes
idRequired. fmm:pet/ and the file name
revisionRequired. Any positive whole number
settingsOptional. The defaults pet files can change. Each default's type is the type a pet file must give: a number, true/false, text, or a list of text. See Setting values
memoriesOptional. What the module remembers for each pet, by name and type: integer, number, boolean, string, uuid or position. Start each name with the module's name. See Memories
dependenciesOptional. Ids of other modules in pet_behaviors/ this one needs. They run on every creature that lists this one, at their default settings unless the pet file lists them too
behaviorsOptional. ai.behavior { ... } entries, which move or turn the pet
sensorsOptional. ai.sensor { ... } entries, which never move the pet

Code above return, such as local functions and constants, is allowed; wander.lua and idle_look.lua have some.

What a callback gets​

NameWhat it is
c.petThe pet, read-only. See c.pet
c.settingsThis module's settings: its defaults with the pet file's and the variant's values on top, as in c.settings.radius. Each pet has its own copy, so writing to it changes only that pet
c.memoryget(name), set(name, value), forget(name) and contains(name) for the names in memories. Each pet has its own
c.tickThe current game tick, for timers
c.perception, c.actuator, c.entityMagmaCore's Mind context, as in EliteMobs behavior programs. c.entity is the hidden body, not the model. See Mind program context. idle_look.lua uses c.perception:nearest_player(range) and c.actuator:look_at(point)

c.pet​

A point is a table with x, y and z, and optionally world, like c.pet.position. A point without world is in the pet's world. A point in another world counts as nothing: methods answer nil, false or 0, and distance_to answers math.huge. The points c.pet returns are always in the pet's world, and one that would be elsewhere reads as nil.

  • Methods are called with a colon: c.pet:distance_to(point).
  • Number arguments must be finite. Radii, depths, heights, amplifiers, durations and the other ranges are capped as listed below. No method loads a chunk.
  • c.pet and c.pet.species are read-only. Assigning to them raises an error; keep per-pet values in c.memory.
  • The properties and route_target() are a snapshot: each is read once per pet tick and stays the same through every callback of that tick. c.pet:teleport_near_owner and c.pet:stop refresh them. A move made another way, such as c.entity:teleport, shows on the next tick. speed is the exception and is always read fresh.
  • Each read of a location gives a new table, which the module may change.

Properties​

NameTypeValue
wildbooltrue for a wild pet, false for an owned one
stayingboolTold to stay
settledboolNot staying, and wild or within follow.distance of its owner
babyboolA baby
migratingboolFlying a night migration route
modelstringIts variant's model. A baby wearing babyModel still reads its variant
positionpointWhere its body is
homepoint/nilWhere a wild pet lives; nil for an owned pet
anchorpoint/nilWhat it roams around: its owner, or for a wild pet its home, or where it is when it has none. nil when the owner is offline or in another world
stay_anchorpoint/nilThe spot it was told to stay at; nil when not staying
owner_locationpoint/nilnil for a wild pet, or when the owner is offline, dead or in another world
owner_distancenumberBlocks to the owner; math.huge when there is none in this world
night, dayboolday only in daylight in a normal world; dusk, dawn, the Nether and the End count as night
rainingboolRain falls on the pet, checked at most every 10 ticks
speednumberIts body's speed now, in blocks per tick
route_cruisenumberIts flock's cruise speed on a migration, otherwise 0
speciestableThe pet file's numbers, below

c.pet.species holds:

FieldPet file key
idspecies
speedfollow.speed
hover_heightfloat.hoverHeight
hover_variancefloat.hoverVariance
roam_radiusfollow.roamRadius
follow_distancefollow.distance
leash_distancefollow.leashDistance
teleport_distancefollow.teleportDistance
scalescale
baby_scalebabyScale

Movement​

Call these only from a behavior's start, tick or stop, in a behavior whose controls list what they need. From can_start or a sensor, steer_to and stop raise an error that says where they belong.

MethodReturnsWhat it does
steer_to(point, cruise)boolOne tick toward the point, so call it every tick. Needs move and look. A flyer flies at cruise blocks per tick (0 to 4), slowing as it arrives. A walker or swimmer paths there natively at 4 × cruise times its body type's movement speed, never under a quarter of it; a cruise of 0 stops it and returns false. false when it cannot go there
stop()nilStops moving. Needs move
teleport_near_owner(radius)boolPuts the body beside its owner, within radius blocks (up to 64), in its own medium; a swimmer looks for water within radius or 6 blocks, whichever is larger. A walker or swimmer lands at least a block from the owner. false when the owner is away or in another world, nothing fits, or the spot is not at least a block nearer the owner than the pet is. It needs no control, but call it from a behavior that holds move, as follow_owner does, so it does not pull the pet away from another behavior

Spots and distances​

These work from any callback. The ones that return a point return nil when nothing fits or the chunk is not loaded. Radii go up to 64.

MethodReturnsWhat it does
hover_point(center, radius)point/nilA point in its hover band within radius
rest_point(center, radius)point/nilA spot to rest in its own medium: the hover band for a flyer, standing ground for a walker or a stationary body, water for a swimmer (at center's height when center is in water, otherwise 0.5 to 2 blocks under the surface), and ground or else water for an amphibious body
ground_point(center, radius)point/nilA standing spot, from about 5 blocks below to 4 above center. nil over liquid or a long drop
water_point(center, radius, min_depth, max_depth)point/nilA swimming spot between the two depths under the surface (0 to 16)
ground_below(point, depth)number/nilThe height of the ground's top within depth blocks down (1 to 64). A water or lava surface counts as ground unless the species is AQUATIC. With no ground in reach, the point's height minus depth
trail_point(back, height)point/nilback blocks behind where the owner faces, height above their feet for a FLYING species and at their feet otherwise. nil when the owner is away or in another world
distance_to(point)numberBlocks from its body
route_target()point/nilWhere its migration route wants it this tick. nil when it is not migrating or the route has ended
find_perch(height, radius)point/nilheight blocks (0 to 8) above the nearest leaves under it, up to radius blocks (0 to 4) to the side, or above the ground right below it. An owned pet only gets a perch within follow.distance of its owner
nearest_campfire(radius, keep_ticks, blocks, soul_variants, height)point/nilThe block of the nearest lit fire it may use within radius (1 to 32). blocks lists the fire kinds it takes, CAMPFIRE, SOUL_CAMPFIRE or both, and a list naming neither takes both; soul_variants lists the models that take only soul campfires, and every other model then takes only plain ones. An owned pet only takes a fire whose spot height above it is within follow.distance minus 1 of its owner. Each chunk's search is kept for keep_ticks (20 to 1200) and shared by every pet
campfire_usable(fire, blocks, soul_variants, height)boolWhether a fire found earlier is still lit and still fits

Effects​

These work from any callback; sensors are the usual place.

MethodReturnsWhat it does
set_glow(light, lantern_bone, animation)nilLights the model's display bones at light (0 to 15), for Java players. A model with a bone named lantern_bone also loops animation. The last two are optional
clear_glow()nilBack to world light, ending that animation
scale_toward(target, span, ticks, elapsed)nilEases the body's size toward target times its base size. A full change from 1 to span takes ticks (at least 2); elapsed is the ticks since the last call. Multipliers 0.1 to 16
sting_contacts(on, effect, duration, amplifier, damage, cooldown)numberStings the players whose touch reached a wild pet's model in the last second, never one in Creative or Spectator mode, and returns how many. It clears the touches even when on is false. Owned pets record no touches
sting_enemies(reach, effect, duration, amplifier, damage, cooldown)numberStings hostile mobs within reach blocks (0 to 4) of the model's hitbox, never players or pets, and returns how many
drizzle(depth, chance)boolWhen the pet hovers still over a crop within depth blocks (1 to 8), water particles and a chance (0 to 1) that the crop grows a stage. true when it is over a crop
owner_effect(effect, duration, amplifier, range)boolGives the owner the potion effect for duration ticks (1 to 6000) when they are within range blocks, with no particles. An effect the owner already has that is at least as strong and lasts at least as long is left alone. false when the owner is away, in another world or out of range, or the effect does not exist
play_animation(name)boolPlays one of the model's animations once. false when the model does not have it

For both stings, effect is a potion effect name or NONE, duration is in ticks (1 to 6000), amplifier 0 is level I (up to 255), damage is in half hearts, and cooldown is the ticks (at least 1) before this pet can sting the same victim again. A sting shows sparks and plays a bee sting sound, and an adult plays its attack animation. A potion effect name that does not exist is warned about once per species and name, and applies nothing.

Behaviors and sensors​

  • A behavior moves or turns the pet. It lists the controls it needs, ai.controls.move and ai.controls.look for the c.pet movement calls, and only one behavior holds a control at a time. While it is stopped, MagmaCore asks its can_start. When that returns true and the controls are free, it takes them and runs start, then tick every tick while can_continue returns true (without can_continue, can_start decides), then stop. See Lifecycle.
  • The lower priority number goes first. A behavior takes the controls from a running one with a higher number, whose stop then runs. The example's 12 keeps following and staying ahead of it and puts it ahead of drifting and hovering, but also ahead of campfire, so a pet listing both never visits a fire at night. A number above 15 would let the fire win.
  • A behavior only competes for the controls it lists. idle_look takes only look, so it runs while no behavior holds the look; every shipped moving behavior takes look too, so moving always wins over it.
  • A pet file cannot change a priority. To reorder, copy the module and change the number.
  • probe_interval is optional, in ticks: while the behavior is stopped, can_start is asked only every that many ticks, on each pet's own phase. It saves server time when many pets are out, at the cost of the behavior starting up to that many ticks late. It defaults to 1 and counts as 200 above that. A running behavior still runs every tick.
  • A sensor never moves the pet. It runs sense on the first tick the pet's body ticks, then every interval ticks on the pet's own phase, whatever the behaviors are doing. Glow, swelling, stings, drizzle and owner effects are sensors.

The shipped modules use:

ModulePriorityprobe_intervalSensor interval
migrate110
stay54
follow_owner104
campfire1510
day_perch1810
rain_bloom19104 (swelling)
drift, wander, swim2010
hover304
idle_look3010
night_glow40
sting4 (players), 10 (mobs)
crop_drizzle20
owner_effect20

Rules​

  • Call steer_to and stop only from a behavior's start, tick or stop, in a behavior that lists the controls they need.
  • Keep per-pet state in c.memory, declared under memories, with names that start with the module's name.
  • Do not write to c.pet; assigning to it raises an error.
  • Keep callbacks short. Every pet runs under these fixed limits, which modules and pet files cannot change:
LimitValue
One callback1.5 ms
One pet's callbacks in a tick3 ms, at most 32 callbacks
All pets' callbacks in a tick10 ms
CPU time of one callback, hard limit20 ms
Lua instructions of one callback, hard limit20,000

When a pet reaches one of the first three limits, its remaining callbacks wait for the next tick. A callback that passes a hard limit is stopped and fails like an error. See Budgets.

When something is wrong​

Nothing here stops the server or the other creatures. Search the console for pet behavior, pet_behaviors and .pet.yml to find these warnings.

  • A module does not load. A module that does not compile, whose id does not match its file name, or whose file name has other characters is skipped with a warning naming the file, and the line for a Lua error. Every other module still loads.
  • A pet file names a module that is not there. The warning names the pet file and suggests the closest module name when one is a letter or two off. The creature loads without it. When the module exists but failed to load, the warning points to the warning about its file.
  • A setting is wrong. A setting the module does not have, or a value of the wrong type, is reported with the pet file's name, and the default stays. See Setting values.
  • Two modules clash. When two modules on one creature declare the same memory name, behavior id or sensor id, the creature keeps the first in the list and runs without the other, with a warning.
  • A callback fails while pets run. It is logged once per program, callback and kind of failure until the next reload. Programs are named fmm:pet/creature/<species>/<variant>, and callbacks by their behavior or sensor id, such as fmm:pet/drift.tick. A failed behavior is stopped, or not started, and tried again from the next tick; while it keeps failing, behaviors with higher priority numbers that move or turn the pet may not run. A failed sensor runs again at its next interval.
  • A c.pet call fails inside FreeMinecraftModels. The cause is logged once per species and call, with its stack trace, and the callback fails with c.pet:<method> failed; the server log has the cause.
  • A potion effect name does not exist. It is warned about once per species and name, and the call applies no effect.

After a fix, run /fmm reload.