A third pathfinder in this atlas, and a different one each time: the Random Map Generator's own Dijkstra pass measures zone distance at generation time, off the live map entirely; Battle System's hex movement is a different coordinate system in lib/battle/. This one is the live search a hero's move actually runs on — a Fibonacci-heap priority queue and a five-rule chain, hardcoded in order, that the server, an AI, and even a turn-order check all share and reconfigure differently.
Not a tile. A tile times a layer — and the whole map's worth of them exist at once, reused search after search.
CPathsInfo::nodes is a 4-dimensional array, [layer][z][x][y] — every tile gets one CGPathNode per EPathfindingLayer (land, sail, air, water-walk), because a hero standing on a coastal tile in a boat and a hero standing on that same tile on foot are, for pathing purposes, different places. A node carries its coord and layer, a running cost in fractional turns, moveRemains, a theNodeBefore parent pointer for walking the path back once the search ends, and an EPathAccessibility (ACCESSIBLE / VISITABLE / GUARDED / BLOCKVIS / FLYABLE / BLOCKED).
Nodes aren't reallocated between searches — they're reset by a generation counter. CPathsInfo::beginSearch() just increments currentGeneration; a node whose stamp doesn't match the current one reads back as untouched (isCurrent()), the same lazy-invalidation trick this atlas's Bonus System doc found at a larger scale — here it's one counter over one hero's own grid, not a DAG-wide cache.
Pop the cheapest unvisited node. For each reachable layer, for each neighbour, run five rules in a fixed order. First one to block it wins.
SingleHeroPathfinderConfig::buildRuleSet() lists them, and CPathfinder::calculatePaths() breaks the loop the instant any one of them sets destination.blocked. It's the same shape this atlas keeps finding — RMG's modifier graph, Battle's damage-calculator chain, Spells' effect list — except this chain is fixed C++, not config or Lua: nothing here is moddable by editing JSON.PathfinderOptions is the knob. The same search runs a genuinely different query depending on who's asking.
the player's own query, unrestricted
Guards block, water needs a boat, flight needs the spell — every default in PathfinderOptions's constructor is the ordinary H3 rule set a human move request runs under.
The New Turn's own finding, closed here
TurnOrderProcessor::playersInContact() sets exactly two fields on the same SingleHeroPathfinderConfig: options.ignoreGuards = true; options.turnLimit = 1; — one turn's worth of reach, guards irrelevant, run once per hero on both sides, then just checks whether the two reachable-tile sets overlap.
for the AI specifically
allowLayerTransitioningAfterBattle's own comment: “For AI. AI pathfinder needs to ignore this rule as it simulates battles on the way” — the ordinary engine assumes a layer change can't happen mid-fight; Nullkiller's own config sets this true because its planning already routes through a hypothetical battle that isn't really stopping the hero at all.
AI/Nullkiller2/Pathfinding — a second config, same engine
Nullkiller doesn't fork this engine, it extends it: AIPathfinderConfig : PathfinderConfig and AINodeStorage : INodeStorage plug straight into the identical CPathfinder loop and five-rule chain above. Its own useDimensionDoor flag turns a Dimension Door cast into an explicit teleport edge in the search graph rather than a movement layer — a spell effect folded into the pathfinding graph itself, for planning purposes only.
One more knob worth naming on its own: oneTurnSpecialLayersLimit — flying and water-walking stop extending a path once movement runs out mid-air or mid-water, rather than letting the search assume a hero can simply wait there until next turn. The option's own comment calls this “default H3 mechanics” that a mod might choose to disable.