A journey is an order to travel through waypoints in turn; the last one is the destination. It is carried out on the server, so it goes on when the player looks at another entity, logs out, or orders it from the companion app. A player’s click on the map, a saved route sent from the app and a creature’s own AI all start the same thing.

Who orders itEntityHow
A player, in the gamethe master, any bestiaMoveEntityCMSG with waypoints
A player, in the companion appa bestia in the worldPUT .../bestias/{id}/journey over the bus
A creature’s AIany NPCthe TravelTo behaviour tree leaf

The code is the travel slice in zone-server, between casting and ai.

The two tiers, and what a traveller knows

A route is planned on two tiers, the split navigation already makes:

  • Coarse: the macro nav graph of roads, bridges, gates and open country, generated by the nav_graph stage. A* over it gives a course: the nodes to reach and the edge to each.
  • Detailed: the local A* over the walk data of the ground as it is now, 32 tiles at a time. Player edits change it at once.

Between them, RegionGrid splits the world into 32 m cells that are either dry or water. Bridge decks count as dry. It lays the stretches off the graph round water: from the traveller onto the course, and from the course to the waypoint.

The graph’s nodes stand kilometres apart, so the nearest node to either end can lie far behind it. A short walk past a town could be led into the town centre and back out. So RoutePlanner measures the course, legs included. When it is more than a quarter longer than the straight line, and the ends are within 8 km, the planner also asks the grid for a way straight there. It takes that way when the course is more than a quarter longer. A road a little longer still wins, because the grid knows water but not slopes. The straight way is never taken over a bridge the knower believes is down, since the grid takes every deck for ground.

The coarse planner does not read whether an edge is truly blocked. It reads what the traveller’s knower believes:

  • The knower of a master and of all its bestias is the master. What any of them sees is known to all.
  • The knower of an NPC is the NPC itself.

An edge nobody saw is believed open, as generated. So a bestia sent across a bridge that fell where nobody of its master’s saw it walks to that bridge. It turns back when the bridge comes into sight, and only then plans the way round. This is RouteKnowledge.

A traveller learns in two ways:

HowWhenBelieved for
By seeing (RouteSense)a road, bridge or gate comes within the 3×3 chunks around it, or changes while it is nearknowledge-ttl, 21 d
By failingit makes no headway on an edge for stall-after, 10 s - a wall across a mountain passfailure-ttl, 1 d

Seeing an edge standing again clears the belief. Only blockages are kept, and a master’s are stored (route_blockage), so they outlast a restart. Learning anything new makes every journey of that knower plan again from where it stands.

Vicinity: what an entity notices

Vicinity is the general “what is near me” layer, in world/vicinity, for anything later that needs an NPC to sense its surroundings. An entity with a Vicinity covers the chunk columns within its radius. VicinitySystem moves the cover only when the entity crosses a chunk border, and tells each VicinitySense which columns just came near. VicinityIndex answers the other way round, who is near a column, so a sense can push a change to exactly those entities. A standing entity costs one lookup per tick.

Every master and bestia has a Vicinity from the moment it spawns, so a broken bridge a player walks past by hand is known to the master’s bestias as well.

The simulation around every bestia

A bestia away from any player used to stand in a frozen world: walk data existed only for chunks sent to a client, and dens and wildlife only woke near the entity a player drives. So it could not path, and nothing could find it.

  • ChunkResidency refcounts which chunks are simulated. All clients together are one holder; walk data and ground marks exist for the held chunks. Props stay scoped to what clients see.
  • Every master and player bestia is a SimulationAnchor. AnchorResidencySystem holds the same view volume around it that a client is offered, so it can path. Dens and wildlife wake around anchors, and AI near one runs at full rate. Townsfolk stay where clients look, because they need props.
  • Hostility: wild creatures now see owned bestias as enemies, so they find a bestia out on its own. Owned bestias still do not hunt each other.
  • A lone anchor holds about 180 chunks, some 2 MB of walk data. simulation-anchors.max-holding-anchors (512) caps how many hold ground: the driven entity first, then travellers, then resting bestias.

The journey engine

TravelSystem runs before MoveSystem and carries each journey one step further:

  1. A dead traveller ends the journey (DIED).
  2. Reaching the current waypoint moves on to the next, or ends the journey as arrived.
  3. Without a plan, or after the knower learned something, RoutePlanner plans the course. With no known way the journey ends (NO_ROUTE).
  4. LegRefiner turns the next stretch into steps. It searches again when the leg runs short and every repath-interval (2 s) on the way, so a wall built in front of the traveller is walked round. A leg that already ends on its aim is left alone; MoveSystem refuses a blocked step anyway, and the leg is then searched again.
  5. ProgressWatchdog watches the distance to the aim. Without headway it first blames the edge ahead (learning by failing). After give-up-after (30 s) the journey ends (STUCK): walled in, or in a dead end.

Searches come from their own budget (travel.pathfinds-per-tick), so travel cannot starve the AI’s. The journeys that waited longest go first.

Player orders suspend the AI. A bestia on a journey its player ordered keeps walking, attacked or not. When the AI takes it back - after a journey, a switch to another entity or a logout - its home becomes where it stands, so it does not walk back to where it used to live.

AI orders hold a lease. TravelTo renews it each time it is ticked. When the plan moves on, the leaf is no longer ticked, the lease lapses, and the journey is dropped without a word.

Messages

MessageDirectionWhat
MoveEntityinAny owned entity, waypoints (none stops it), append, the client’s first leg, a route’s name and id
JourneyComponentoutOwner only, at any distance: waypoints, the next index, name and the way ahead in coarse points
JourneyEndedoutTo the owner: arrived, or NO_ROUTE, STUCK, DIED, and where
SavedRoutes (+4)bothThe master’s routes, kept, renamed and forgotten by name

MoveEntity replaced MoveActiveEntity. Naming the entity grants nothing new: selecting another owned entity and moving it was always open to a client. The server owns every step. The client still walks its own click at once, and its first steps are walked as sent when each one is valid, so the prediction is not corrected a round trip later.

A bestia’s journey is kept in its row (player_bestia.journey), so it goes on after a restart.

Saved routes

A saved route is a named list of waypoints a master keeps, at most 32, in map metres. With no name given it is named from the places it starts and ends in (“Ironhill → Ashford”); a blank rename gives that name back. A route saved from the map starts where the entity stands, so it can be sent back the other way. To send one, the game sends its waypoints itself with its name and notebook key, so the tick never reads the database; the companion app names only the key, and the zone reads the route.

Routes are kept in the master’s notebook, the one store for what a player keeps - routes, map marks, map preferences, and later settings shared between the two apps. NotebookService numbers every write to a collection and tells both apps of it after the commit: the game by NotebookChangedSMSG on its socket, the companion by a NotebookChanged event on its live stream. Each app applies the next number, ignores what it has seen, and lists the collection again when the number jumps, so a route saved in one app appears in the other at once. See Using the Map.

Open problems

  • The AI profile does not decide yet how a travelling bestia reacts to an attack. It keeps walking.
  • A bestia not in the world after a restart cannot be ordered until its master is selected again.
  • The anchor cap is a guess. It needs a load measurement with a few hundred parked bestias.

Last updated 09 Nov 2026, 23:57 +0100 . history