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.luafile 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
idmust befmm:pet/followed by its name, sopet_behaviors/mine/slow_drift.luadeclaresid = '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 filenameabout 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 reloadrereads 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'srevisionafter 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.
- Copy it under a new name, for example
pet_behaviors/mine/slow_drift.luafromdrift.lua. - Change its
idtofmm:pet/slow_drift. - Rename its memories, behaviors and sensors so they start with the new name:
drift_tobecomesslow_drift_to, inmemoriesand in everyc.memorycall, and the behavior'sid = 'fmm:pet/drift'becomesfmm: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. - List it in the pet file in place of the original:
- slow_drift, or with settings,- slow_drift: { speed: 0.03 }. - Run
/fmm reloadand 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 }.
| Field | Notes |
|---|---|
id | Required. fmm:pet/ and the file name |
revision | Required. Any positive whole number |
settings | Optional. 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 |
memories | Optional. 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 |
dependencies | Optional. 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 |
behaviors | Optional. ai.behavior { ... } entries, which move or turn the pet |
sensors | Optional. 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
| Name | What it is |
|---|---|
c.pet | The pet, read-only. See c.pet |
c.settings | This 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.memory | get(name), set(name, value), forget(name) and contains(name) for the names in memories. Each pet has its own |
c.tick | The current game tick, for timers |
c.perception, c.actuator, c.entity | MagmaCore'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.petandc.pet.speciesare read-only. Assigning to them raises an error; keep per-pet values inc.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_ownerandc.pet:stoprefresh them. A move made another way, such asc.entity:teleport, shows on the next tick.speedis the exception and is always read fresh. - Each read of a location gives a new table, which the module may change.
Properties
| Name | Type | Value |
|---|---|---|
wild | bool | true for a wild pet, false for an owned one |
staying | bool | Told to stay |
settled | bool | Not staying, and wild or within follow.distance of its owner |
baby | bool | A baby |
migrating | bool | Flying a night migration route |
model | string | Its variant's model. A baby wearing babyModel still reads its variant |
position | point | Where its body is |
home | point/nil | Where a wild pet lives; nil for an owned pet |
anchor | point/nil | What 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_anchor | point/nil | The spot it was told to stay at; nil when not staying |
owner_location | point/nil | nil for a wild pet, or when the owner is offline, dead or in another world |
owner_distance | number | Blocks to the owner; math.huge when there is none in this world |
night, day | bool | day only in daylight in a normal world; dusk, dawn, the Nether and the End count as night |
raining | bool | Rain falls on the pet, checked at most every 10 ticks |
speed | number | Its body's speed now, in blocks per tick |
route_cruise | number | Its flock's cruise speed on a migration, otherwise 0 |
species | table | The pet file's numbers, below |
c.pet.species holds:
| Field | Pet file key |
|---|---|
id | species |
speed | follow.speed |
hover_height | float.hoverHeight |
hover_variance | float.hoverVariance |
roam_radius | follow.roamRadius |
follow_distance | follow.distance |
leash_distance | follow.leashDistance |
teleport_distance | follow.teleportDistance |
scale | scale |
baby_scale | babyScale |
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.
| Method | Returns | What it does |
|---|---|---|
steer_to(point, cruise) | bool | One 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() | nil | Stops moving. Needs move |
teleport_near_owner(radius) | bool | Puts 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.
| Method | Returns | What it does |
|---|---|---|
hover_point(center, radius) | point/nil | A point in its hover band within radius |
rest_point(center, radius) | point/nil | A 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/nil | A 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/nil | A swimming spot between the two depths under the surface (0 to 16) |
ground_below(point, depth) | number/nil | The 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/nil | back 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) | number | Blocks from its body |
route_target() | point/nil | Where its migration route wants it this tick. nil when it is not migrating or the route has ended |
find_perch(height, radius) | point/nil | height 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/nil | The 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) | bool | Whether a fire found earlier is still lit and still fits |
Effects
These work from any callback; sensors are the usual place.
| Method | Returns | What it does |
|---|---|---|
set_glow(light, lantern_bone, animation) | nil | Lights 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() | nil | Back to world light, ending that animation |
scale_toward(target, span, ticks, elapsed) | nil | Eases 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) | number | Stings 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) | number | Stings hostile mobs within reach blocks (0 to 4) of the model's hitbox, never players or pets, and returns how many |
drizzle(depth, chance) | bool | When 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) | bool | Gives 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) | bool | Plays 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.moveandai.controls.lookfor thec.petmovement calls, and only one behavior holds a control at a time. While it is stopped, MagmaCore asks itscan_start. When that returnstrueand the controls are free, it takes them and runsstart, thentickevery tick whilecan_continuereturnstrue(withoutcan_continue,can_startdecides), thenstop. See Lifecycle. - The lower
prioritynumber goes first. A behavior takes the controls from a running one with a higher number, whosestopthen runs. The example's12keeps following and staying ahead of it and puts it ahead of drifting and hovering, but also ahead ofcampfire, so a pet listing both never visits a fire at night. A number above15would let the fire win. - A behavior only competes for the controls it lists.
idle_looktakes onlylook, so it runs while no behavior holds the look; every shipped moving behavior takeslooktoo, so moving always wins over it. - A pet file cannot change a priority. To reorder, copy the module and change the number.
probe_intervalis optional, in ticks: while the behavior is stopped,can_startis 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 to1and counts as200above that. A running behavior still runs every tick.- A sensor never moves the pet. It runs
senseon the first tick the pet's body ticks, then everyintervalticks on the pet's own phase, whatever the behaviors are doing. Glow, swelling, stings, drizzle and owner effects are sensors.
The shipped modules use:
| Module | Priority | probe_interval | Sensor interval |
|---|---|---|---|
migrate | 1 | 10 | |
stay | 5 | 4 | |
follow_owner | 10 | 4 | |
campfire | 15 | 10 | |
day_perch | 18 | 10 | |
rain_bloom | 19 | 10 | 4 (swelling) |
drift, wander, swim | 20 | 10 | |
hover | 30 | 4 | |
idle_look | 30 | 10 | |
night_glow | 40 | ||
sting | 4 (players), 10 (mobs) | ||
crop_drizzle | 20 | ||
owner_effect | 20 |
Rules
- Call
steer_toandstoponly from a behavior'sstart,tickorstop, in a behavior that lists the controls they need. - Keep per-pet state in
c.memory, declared undermemories, 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:
| Limit | Value |
|---|---|
| One callback | 1.5 ms |
| One pet's callbacks in a tick | 3 ms, at most 32 callbacks |
| All pets' callbacks in a tick | 10 ms |
| CPU time of one callback, hard limit | 20 ms |
| Lua instructions of one callback, hard limit | 20,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 asfmm: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.petcall fails inside FreeMinecraftModels. The cause is logged once per species and call, with its stack trace, and the callback fails withc.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.