Long-Distance Travel
How masters, bestias and NPCs travel far on the server - journeys, what a traveller knows of the routes, the simulation around every bestia, and how a journey ends - and how the game client and the companion app order one.
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 it | Entity | How |
|---|---|---|
| A player, in the game | the master, any bestia | MoveEntityCMSG with waypoints |
| A player, in the companion app | a bestia in the world | PUT .../bestias/{id}/journey over the bus |
| A creature’s AI | any NPC | the 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_graphstage. 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:
| How | When | Believed for |
|---|---|---|
By seeing (RouteSense) | a road, bridge or gate comes within the 3×3 chunks around it, or changes while it is near | knowledge-ttl, 21 d |
| By failing | it makes no headway on an edge for stall-after, 10 s - a wall across a mountain pass | failure-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.
ChunkResidencyrefcounts 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.AnchorResidencySystemholds 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:
- A dead traveller ends the journey (
DIED). - Reaching the current waypoint moves on to the next, or ends the journey as arrived.
- Without a plan, or after the knower learned something,
RoutePlannerplans the course. With no known way the journey ends (NO_ROUTE). LegRefinerturns the next stretch into steps. It searches again when the leg runs short and everyrepath-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;MoveSystemrefuses a blocked step anyway, and the leg is then searched again.ProgressWatchdogwatches the distance to the aim. Without headway it first blames the edge ahead (learning by failing). Aftergive-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
| Message | Direction | What |
|---|---|---|
MoveEntity | in | Any owned entity, waypoints (none stops it), append, the client’s first leg, a route’s name and id |
JourneyComponent | out | Owner only, at any distance: waypoints, the next index, name and the way ahead in coarse points |
JourneyEnded | out | To the owner: arrived, or NO_ROUTE, STUCK, DIED, and where |
SavedRoutes (+4) | both | The 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 .