ai
Server
modkit.ai spawns AI creatures, counts them and gives them a brain of your own. Every spawn creates a pawn and a matching AI controller, lets the controller possess the pawn, fills its stats and registers it with the game's AI world spawner.
The first spawn of a class may load the blueprint from disk on the game thread. That can cause one short hitch. Later spawns of the same class are fast.
The first half of this page covers spawning and counting. The second half, from Brains on, covers custom AI. The custom AI guide walks through it step by step.
modkit.ai.species
modkit.ai.species()
Returns: array of the 39 keys that spawn accepts, all lower case. Dinosaurs such as "tyrannosaurus", "carnotaurus", "psittacosauruscoastal", animals such as "boar", "deer", "goat", "rabbit", "chicken", "crab", "bullfrog", "lizard", "seaturtle", and the fish schools "rainbowfish", "coalacanth", "muskelunge", "longear".
print(table.concat(modkit.ai.species(), ", "))
modkit.ai.spawn
modkit.ai.spawn(species, x, y, z [, options])
| Parameter | Type | Description |
|---|---|---|
| species | string | key from modkit.ai.species(). Case and punctuation are ignored. |
| x, y, z | number | position. The pawn spawns 5 m above z and drops. |
| options.count | integer | 1 to 25, default 1. More than one are placed in rows of five, 3 m apart. Animals from one call form a pack. |
| options.growth | number | 0.01 to 1. Default 1, or stats.growth of the brain when options.brain is set. |
| options.yaw | number | facing in degrees |
| options.behavior | string | "default", "passive" (always flees) or "aggressive" (never flees, starts in the aggressive state). Has no effect on an animal with a brain. |
| options.brain | string | name of a brain. The animal runs on that brain from its first moment. |
| options.home | table | home point for the brain, { x, y, z } or { x = , y = , z = }. Default is the spawn position. |
Returns: array of integer handles, or nil and a reason. Reasons are "unknown AI species", "unknown brain", "AI class could not be loaded", "too many script spawned AI alive (limit 500)", "spawn failed".
modkit.commands.add("boars", function(player)
local x, y, z = player:pos()
local ids, why = modkit.ai.spawn("boar", x + 800, y, z, { count = 3, behavior = "passive" })
player:message(ids and (#ids .. " boars spawned") or why)
end, true)
modkit.ai.despawn
modkit.ai.despawn(handle)
Returns: true when the creature was still there and got destroyed. Fires aiDespawned with the reason "script".
modkit.ai.list, count
modkit.ai.list() returns the creatures with a handle that still exist, as tables with id, species, alive, x, y, z. These are the creatures your scripts and spawners put into the world, plus every wild animal a brain took over. A dead creature stays in the list until the game removes its body.
modkit.ai.count() returns the length of that list.
for _, a in ipairs(modkit.ai.list()) do print(a.id, a.species, a.alive) end
modkit.ai.census
modkit.ai.census()
Counts every AI controlled character in the world, including the game's own AI.
Returns: a table species -> count and the total as second value.
This walks all game objects. Call it on demand, not in a timer that runs every second.
local counts, total = modkit.ai.census()
print("AI in the world: " .. total)
modkit.ai.wipe
modkit.ai.wipe()
Destroys every AI controlled character in the world, the game's own AI included. The game spawns new AI afterwards as usual.
Returns: number of destroyed creatures. Walks all game objects.
Brains
A brain describes how an animal senses, moves, picks targets, fights, flees, behaves in a group and when it disappears. An animal with a brain keeps its body, its pathfinding and its attacks from the game. The game's own decision making for that animal is switched off and the brain decides instead. Taking the brain away gives the animal back to the game.
There are three ways to put a brain to work.
| Way | What it does |
|---|---|
modkit.ai.spawn with options.brain, or modkit.ai.setBrain |
one animal, from a script |
| a spawner | keeps a number of animals with a brain alive around a point and refills after deaths |
| wild AI | gives every animal of one species that the game spawns a brain |
Brains come from two places. The Server Tool writes data\ai\brains.json and data\ai\spawns.json under the server root. Scripts define brains with modkit.ai.defineBrain. A brain from a script replaces a file brain of the same name while the server runs. The file stays as it is. The plugin looks at both files every 5 seconds and loads them again when they changed.
Brain names and spawner ids have 1 to 32 characters from a-z, 0-9 and _.
Units
Distances are Unreal units, 100 units are 1 m. Times are seconds. Shares and chances go from 0 to 1. Angles are degrees. Species names are compared without regard to case and punctuation.
Brain fields
Every field is optional. A missing field has its default.
| Field | Type | Default | Description |
|---|---|---|---|
label |
string | the name | display name in the Server Tool |
preset |
string | "custom" |
a marker for the Server Tool: "predator", "prey", "guard", "passive" or "custom". No effect on behaviour. |
senses
| Field | Default | Range | Description |
|---|---|---|---|
sightRange |
6000 | 0 to 200000 | how far the animal sees targets. An animal looks for targets while a player is within 150 m of it, so a player is spotted at 15000 at most. |
sightAngle |
360 | 1 to 360 | view cone in degrees, 360 sees all around |
hearingRange |
9000 | 0 to 200000 | range for calls of its own species and for the noise of a fight |
memorySeconds |
12 | 0 to 3600 | how long a target out of sight is remembered |
lineOfSight |
false | check the line of sight to a player with a trace before spotting. Runs only while a player is within 60 m. |
movement
| Field | Default | Range | Description |
|---|---|---|---|
roamRadius |
4000 | 100 to 500000 | roaming area around the home point |
roamPauseMin, roamPauseMax |
2, 6 | 0 to 3600 | pause between two walks |
roamGait, chaseGait, fleeGait |
"walk", "sprint", "sprint" |
"walk", "trot" or "sprint" |
|
leash |
8000 | 0 to 1000000 | how far beyond the roaming area a chase goes. 0 means no leash. |
homeMode |
"spawn" |
"spawn" keeps the home point where the animal started. "wander" moves the home point along with the animal. |
targets
| Field | Default | Description |
|---|---|---|
players |
true | players are possible targets |
ai |
false | other animals with a brain are possible targets |
onlySpecies |
{} |
when filled, only these species |
ignoreSpecies |
{} |
never these species |
ignoreSameSpecies |
true | never its own species |
minGrowth, maxGrowth |
0, 1 | growth of the target, 0 to 1 |
ignorePermission |
"" |
players with this acl permission are never a target |
temperament
| Field | Default | Range | Description |
|---|---|---|---|
aggression |
0 | 0 to 1 | chance to go after a target it sees. 0 never attacks first, 1 always does. The animal decides once per target and keeps the decision for memorySeconds. |
retaliate |
true | hits back when it is hit. With false a hit animal fights back with the chance from aggression and flees otherwise. |
|
fightToDeath |
false | never flees | |
fleeHealth |
0.25 | 0 to 1 | flees below this share of its health |
fleeFromBigger |
true | flees from a bigger player | |
fearSizeRatio |
1.25 | 0.1 to 100 | a player counts as bigger when the body is this many times as wide as the animal's |
fleeFromEveryone |
false | prey animal. Flees from every player it sees and from the noise of a fight nearby. | |
fleeSeconds |
8 | 1 to 3600 | shortest length of a flight |
activeAt |
"any" |
"any", "day" (06:00 to 18:00) or "night". Outside that time the animal rests at its home point. |
attack
| Field | Default | Range | Description |
|---|---|---|---|
rangeFromBody |
true | reach is the body radius times bodyRangeMultiplier plus biteLength |
|
bodyRangeMultiplier |
2.5 | 0.1 to 100 | |
biteLength |
250 | 0 to 10000 | |
range |
400 | 10 to 100000 | fixed reach, used when rangeFromBody is false |
cooldown |
1.0 | 0.1 to 600 | seconds between two attacks |
autoAbilities |
true | with an empty abilities list the brain picks the attacks of the species by itself |
|
abilities |
{} |
up to 32 | your own attacks, see below |
An entry of attack.abilities is { name = "bite", weight = 1, minRange = 0, maxRange = 0, cooldown = 0 }.
| Field | Default | Description |
|---|---|---|
name |
required | a piece of the ability's class name, case is ignored. modkit.ai.abilities(id) lists the full names. |
weight |
1 | 0 to 1000. Share in the random pick. 0 is never picked. |
minRange, maxRange |
0, 0 | distance window to the target. maxRange 0 means the normal reach. |
cooldown |
0 | seconds until this ability may fire again. 0 means attack.cooldown. |
Before each attack the brain collects every ability whose distance window fits and whose cooldown is over, then picks one at random by weight.
pack
| Field | Default | Range | Description |
|---|---|---|---|
size |
1 | 1 to 25 | animals per group when a spawner spawns |
shareTarget |
true | roaming pack mates take over the target of a member | |
answerCalls |
true | answers calls of its own species | |
callRange |
6000 | 0 to 200000 | how far a call is answered. Also the distance within which animals of the same brain and species count as one pack when they were not spawned together. |
callChance |
0.5 | 0 to 1 | chance to answer one call |
idle is a list of up to 16 entries { ability = "roar", minSeconds = 60, maxSeconds = 240, onlyAlone = false, whenBusy = true }. The animal uses the ability at a random moment between minSeconds and maxSeconds, again and again. onlyAlone waits until no player is in sight range. whenBusy = false limits it to the roam state. Defaults are 60, 240, false and true.
stats
| Field | Default | Description |
|---|---|---|
infiniteStamina |
true | stamina is topped up while a player is within 150 m |
growth |
1.0 | 0.01 to 1, growth of animals spawned with this brain |
despawn
| Field | Default | Range | Description |
|---|---|---|---|
afterSeconds |
0 | 0 to 604800 | lifetime. 0 means for ever. |
noPlayerWithin |
0 | 0 to 1000000 | removes the animal when no player was inside this radius for noPlayerSeconds. 0 turns it off. |
noPlayerSeconds |
120 | 1 to 604800 |
States
An animal with a brain is in one of six states. The names are part of the API.
| State | Meaning |
|---|---|
roam |
walks to random points in its roaming area, with pauses |
chase |
runs at its target and attacks in reach |
flee |
runs away from a threat |
return |
the leash ended a chase, or the animal is outside its area. It trots home. |
rest |
outside activeAt. It walks home and stands still. |
scripted |
a command from a script is running |
How often a brain thinks
The distance to the nearest player sets the pace.
| Nearest player | Think interval | Notes |
|---|---|---|
| up to 60 m | 0.2 s | |
| up to 150 m | 1 s | |
| up to 400 m | 5 s | no spotting of targets, no think callback |
| further | 20 s | no new paths either |
In chase the interval is 0.1 s. In flee and scripted it is 0.5 s at most. The distance is checked once per second, so an animal far away picks up speed within a second when a player comes close.
All brains together get 2 ms per server tick. What does not fit waits for the next tick. At most 500 animals can have a brain at the same time. Beyond that setBrain returns false, spawners with a brain stop filling up and further wild animals stay with the game's AI.
modkit.ai.defineBrain
modkit.ai.defineBrain(name, definition)
| Parameter | Type | Description |
|---|---|---|
| name | string | 1 to 32 characters from a-z, 0-9 and _ |
| definition | table | the brain fields, plus the callbacks |
Returns: true. An unknown field, a wrong type, a value outside its range or a bad name raises a Lua error that names the field.
Defining a name again replaces the brain. Animals that already run on it use the new values from their next think step.
modkit.ai.defineBrain("night_stalker", {
label = "Night stalker",
preset = "predator",
senses = { sightRange = 9000, sightAngle = 200 },
movement = { roamRadius = 12000, leash = 20000 },
targets = { minGrowth = 0.3 },
temperament = { aggression = 0.8, fleeFromBigger = false, activeAt = "night" },
attack = { abilities = { { name = "bite", weight = 3 }, { name = "claw", weight = 1 } } },
pack = { size = 2 },
})
modkit.ai.getBrain, brains
modkit.ai.getBrain(name) returns a table with every brain field filled in, defaults included, plus name and source ("lua" or "file"). It returns nil for an unknown name. Callbacks are not part of the table.
modkit.ai.brains() returns the names of all brains, sorted.
for _, name in ipairs(modkit.ai.brains()) do
local brain = modkit.ai.getBrain(name)
print(name, brain.source, brain.temperament.aggression)
end
modkit.ai.removeBrain
modkit.ai.removeBrain(name)
Removes a brain that a script defined. When brains.json holds a brain of the same name, that one takes over and the animals keep running. When only the file has the name, the file brain is dropped until the file is loaded the next time.
Animals left without a brain go back to the game's AI. Wild animals among them get a new brain when their species still has one assigned.
Returns: true when a brain of that name existed.
modkit.ai.reload
modkit.ai.reload()
Loads brains.json and spawns.json again right away. A file with broken JSON is skipped with a log line and the brains and spawners from before stay. A brain in the file with an unknown field or a bad value is loaded with the default for that field, and one log line names the problem.
Spawners that are no longer in the file lose their animals, with aiDespawned and the reason "spawner_removed".
Returns: true.
modkit.ai.setBrain
modkit.ai.setBrain(id, name)
| Parameter | Type | Description |
|---|---|---|
| id | integer | handle from spawn, list or an AI event |
| name | string or nil | brain name. nil takes the brain away and gives the animal back to the game's AI. |
Returns: true on success. false for an unknown handle, an unknown brain, or when 500 animals already have a brain.
The home point is the place where the animal stands at that moment. Change it with setHome.
local ids = modkit.ai.spawn("deer", x, y, z, { count = 4 })
for _, id in ipairs(ids or {}) do modkit.ai.setBrain(id, "shy_prey") end
modkit.ai.info
modkit.ai.info(id)
Returns: a table, or nil when the handle is unknown.
| Field | Type | Description |
|---|---|---|
id |
integer | the handle |
species |
string | |
brain |
string | brain name |
state |
string | one of the states |
target |
table or nil | { kind = "player", steamId = "..." } or { kind = "ai", id = 12 } |
health, maxHealth |
number | absolute values |
x, y, z |
number | position from the last think step |
home |
table | { x, y, z } as named fields |
lod |
integer | 0 to 3, the row of the think interval table |
spawner |
string | id of the spawner that made the animal. Missing for other animals. |
An animal without a brain gives a short table with id, species, health, maxHealth, x, y, z. Test info.brain to tell the two apart.
Targets in commands
Wherever a function takes a target, three forms work: a player table, a Steam ID as string, or the handle of another animal as number.
modkit.ai.moveTo
modkit.ai.moveTo(id, x, y, z [, gait])
Sends the animal to a point. The state is scripted until it arrives or 120 seconds passed. After that the brain decides again. gait is "walk", "trot" or "sprint", default is the brain's roamGait.
Returns: true, or false when the animal has no brain.
modkit.ai.follow
modkit.ai.follow(id, target [, distance])
The animal follows the target and keeps distance (default 600). It walks when it is close, trots when it falls behind and sprints when it is far behind. The state stays scripted until release, another command, or until the target is dead or gone.
Returns: true, or false when the animal has no brain or the target does not exist.
modkit.ai.attack
modkit.ai.attack(id, target)
Forces chase on this target, whatever the brain's targets and aggression say. A forced chase ignores the leash, memorySeconds and fleeHealth. It ends when the target is dead or gone. hold, moveTo, follow and flee end it earlier.
Returns: true, or false when the animal has no brain or the target is dead or does not exist.
modkit.ai.flee
modkit.ai.flee(id, target [, seconds])
modkit.ai.flee(id, x, y, z [, seconds])
The animal runs away from a target or from a point. seconds defaults to the brain's fleeSeconds.
Returns: true, or false when the animal has no brain or the target does not exist.
modkit.ai.hold
modkit.ai.hold(id [, seconds])
The animal stops and stands still, in the state scripted. Without seconds it holds until release or another command.
Returns: true, or false when the animal has no brain.
modkit.ai.release
modkit.ai.release(id)
Ends scripted. The brain decides again.
Returns: true, or false when the animal has no brain or is not in scripted.
modkit.commands.add("heel", function(player)
for _, id in ipairs(myPets[player.steamId] or {}) do modkit.ai.follow(id, player, 800) end
end)
modkit.commands.add("stay", function(player)
for _, id in ipairs(myPets[player.steamId] or {}) do modkit.ai.hold(id) end
end)
modkit.commands.add("free", function(player)
for _, id in ipairs(myPets[player.steamId] or {}) do modkit.ai.release(id) end
end)
modkit.ai.setHome
modkit.ai.setHome(id, x, y, z [, roamRadius])
Moves the home point of one animal. roamRadius replaces the brain's movement.roamRadius for this animal.
Returns: true, or false when the animal has no brain.
modkit.ai.abilities, useAbility
modkit.ai.abilities(id) returns the class names of the abilities the species has, for example "GA_Bite_C". The list is empty for an unknown handle.
modkit.ai.useAbility(id, name) lets the animal use an ability right now. name is a piece of the class name, case is ignored. It returns true when the ability was found and started. Both functions work for every animal with a handle, with or without a brain.
modkit.commands.add("abilities", function(player, id)
player:message(table.concat(modkit.ai.abilities(tonumber(id) or 0), ", "))
end, true)
Brain callbacks
Four optional functions in the definition table let a script take part in the decisions. They exist only for brains from defineBrain.
| Field | Called | Return value |
|---|---|---|
think(agent, senses) |
at every think step while a player is within 150 m, before the built-in decision | nil lets the built-in logic decide. A table decides, see below. |
pickAttack(agent, target, distance, abilities) |
before every attack. abilities holds the names that fit the distance right now. |
an ability name, or nil for the weighted pick |
onStateChange(agent, from, to) |
after every change of state | none |
onDamaged(agent, attacker) |
when the animal was hit, before it reacts. attacker has the form of info.target and can be nil. |
none |
agent is the table from modkit.ai.info.
senses has these fields. Both lists are sorted by distance and hold 16 entries at most.
| Field | Description |
|---|---|
players |
list of { steamId, species, growth, distance, visible } inside sightRange. visible is true when the player is inside the view cone. |
ai |
list of { id, species, distance }, the animals with a brain inside sightRange |
threat |
the current target, otherwise the last attacker, in the form of info.target, or nil |
timeOfDay |
hour of the day, 0 to 24 |
health |
share of health, 0 to 1 |
Decisions that think can return.
| Table | Effect |
|---|---|
{ state = "chase", target = t } |
chase t, like modkit.ai.attack |
{ state = "flee", from = t } |
flee from a target or from a point { x, y, z } |
{ state = "moveTo", x = , y = , z = , gait = } |
walk to a point, like modkit.ai.moveTo |
{ state = "hold" } |
stand still. Lasts 3 seconds, return it again to keep holding. |
{ state = "roam" } |
go back to roaming |
A callback runs inside the server tick. Keep it short, and do not start HTTP requests or storage writes from think.
An error in a callback is logged with the brain name. After 5 errors that callback is switched off for this brain, and the animals keep running on the built-in logic. Defining the brain again turns it back on.
modkit.ai.defineBrain("gate_guard", {
preset = "guard",
movement = { roamRadius = 1500, leash = 3000 },
temperament = { aggression = 0, fightToDeath = true },
think = function(agent, senses)
for _, p in ipairs(senses.players) do
local player = modkit.players.get(p.steamId)
if player and not modkit.acl.allowed(player, "gate.pass") and p.distance < 2500 then
return { state = "chase", target = p.steamId }
end
end
end,
pickAttack = function(agent, target, distance, abilities)
if agent.health < agent.maxHealth * 0.5 then return abilities[1] end
end,
onStateChange = function(agent, from, to)
if to == "chase" then modkit.log.info("guard " .. agent.id .. " attacks") end
end,
})
modkit.ai.addSpawner, removeSpawner, spawners
modkit.ai.addSpawner(definition) adds a spawner for as long as the server runs. It is not written to spawns.json. A spawner with the same id as one from the file replaces it while the server runs. An unknown field, a bad value or a species that is not in modkit.ai.species() raises a Lua error.
| Field | Default | Range | Description |
|---|---|---|---|
id |
required | 1 to 32 characters from a-z, 0-9 and _ |
|
label |
the id | display name | |
species |
required | key from modkit.ai.species() |
|
brain |
"" |
brain name. Empty leaves the animals to the game's AI. | |
x, y, z |
required | centre | |
radius |
3000 | 0 to 500000 | spawn points are spread inside this radius |
count |
1 | 0 to 100 | how many animals the spawner keeps alive |
respawnSeconds |
300 | 1 to 604800 | wait after a death or despawn before the refill |
growth |
from the brain | 0.01 to 1 | growth of the animals. Replaces stats.growth of the brain. |
activeAt |
"any" |
"any", "day" or "night". Outside that time the spawner does not spawn. |
|
playersWithin |
0 | 0 to 1000000 | spawns only while a player is inside this radius. 0 spawns always. |
enabled |
true |
A spawner fills up in groups of pack.size of its brain, one group per second. Each group spawns at a random point inside radius. That point is the home of the group, and its animals are one pack.
modkit.ai.removeSpawner(id) removes a spawner that a script added and despawns its animals. When spawns.json has a spawner of the same id, that one takes over and the animals stay. It returns true when the spawner existed.
modkit.ai.spawners() returns all spawners as tables with the fields above, plus alive (animals alive right now) and source ("lua" or "file").
modkit.events.add("serverStarted", function()
modkit.ai.addSpawner({
id = "lake_boars", species = "boar", brain = "shy_prey",
x = -180000, y = 52000, z = 1500, radius = 6000,
count = 6, respawnSeconds = 600, playersWithin = 60000,
})
end)
modkit.ai.setSpeciesBrain, speciesBrains
modkit.ai.setSpeciesBrain(species, name) gives every animal of that species that the game itself spawns the brain name. An assignment from a script wins over the wild entry of the same species in spawns.json. nil as name ends the assignment from a script. The entry from the file applies again when there is one. Without one, the animals of that species that were taken over go back to the game's AI.
Returns: true. false for an unknown brain, and for nil when no script had assigned that species.
The plugin looks for new wild animals every 10 seconds. A wild animal that got a brain has a handle from then on and shows up in modkit.ai.list.
modkit.ai.speciesBrains() returns a table species -> brain name with the assignments from scripts and from the file.
modkit.ai.setSpeciesBrain("deer", "shy_prey")
AI events
| Event | Arguments | When |
|---|---|---|
aiSpawned |
id |
a script or a spawner put an animal into the world |
aiStateChanged |
id, from, to |
an animal with a brain changed its state |
aiTargetChanged |
id, target |
an animal with a brain has a new target, or none (nil) |
aiDamaged |
id, attacker |
an animal with a brain was hit. attacker can be nil. |
aiDied |
id, killer |
an animal with a brain died. killer is the last attacker of the 15 seconds before, or nil. |
aiDespawned |
id, reason |
an animal with a handle is gone |
target, attacker and killer have the form of info.target.
Reasons of aiDespawned.
| Reason | Meaning |
|---|---|
"script" |
modkit.ai.despawn |
"timeout" |
despawn.afterSeconds of the brain |
"no_players" |
despawn.noPlayerWithin of the brain |
"spawner_removed" |
the spawner was removed |
"gone" |
the game removed the animal |
modkit.events.add("aiDied", function(id, killer)
if killer and killer.kind == "player" then
local player = modkit.players.get(killer.steamId)
if player then player:notify("You brought down a guarded animal.") end
end
end)
Clients can not send these names.