Pathfinding - ilikegoodfood/CommunityLib GitHub Wiki

Pathfinding

The Community Library completely replaces the vanilla pathfinding algorithm, both for general paths, and for trade routes.

The vanilla pathfinding solution uses breadth-first search for paths for everything excpet trade routes, and a heuristic-less A* pathfinding implementation for trade routes. Both pathfinding algorithms are entirely hard-coded, prevennting any custom movment constraints or path costs from being introduced by mods, or for mods to exempt certain agents from such constraints.

The Community Library makes use a similar heuristic-less A* pathfinding implementation for all pathfinding requirements. It uses hooks to gather delegates from dependant mods which can be used to define the pathfinding behaviour. It also offers a two-pass solution, allowing mods to define constraints that should be met if possible, but can be ignored if not.

Table of Contents

General Pathfinding

The getPathTo functions:

// --- DLC --- //
// getPathTo Location
Location[] getPathTo(Location locA, Location locB, Unit u = null, bool safeMove = false)

// getPathTo SocialGroup
Location[] getPathTo(Location loc, SocialGroup sg, Unit u, bool safeMove = false)
Location[] getPathTo(Location loc, SocialGroup sg, Unit u, int targetMapLayer, bool safeMove = false)
Location[] getPathTo(Location loc, SocialGroup sg, Unit u, List<int> targetMapLayers, bool safeMove = false)

// getPathTo Delegate
Location[] getPathTo(Location loc, Func<Location[], Location, Unit, List<int>, bool> destinationValidityDelegate, Unit u, List<int> targetMapLayers, bool safeMove)

// --- Base Game --- //
// getPathTo Location
Location[] getPathTo(Location locA, Location locB, Unit u = null, bool safeMove = false)

// getPathTo SocialGroup
Location[] getPathTo(Location loc, SocialGroup sg, Unit u, bool safeMove = false)

// getPathTo Delegate
Location[] getPathTo(Location loc, Func<Location[], Location, Unit, bool> destinationValidityDelegate, Unit u, bool safeMove)

While the getPathTo Location override has an exact endpoint, the other overrides could have many valid destinations. To offer further control over these, one or more target map layers can be provided. If they are, the destination will have to be on one of those map layers to be considered valid.

A dependent mod can call the Community Library's pathfinding function like so:

Location[] pathTo = Pathfinding.getPathTo(/*...enter args here...*/);

The getPathTo Delegate override is ideal for sending a unit to nearest of something, such as sending a deep one to the nearest ocean, or cave spiders back to the wilderness after they chase prey into a civilised area. It performs both the search for the nearest possible destination, and gathers the path at the same time.

NOTE: While the Community Library is enabled, all calls to Map.getPathTo are automatically intercepted and redirected to the Community Library's Pathfiding system. Thus this system will handle tasks for non-dependent mods, as well as vanilla.

Path Costs

The A* Pathfinding algorithm is designed to find the path with the lowest total path cost to the destination, or destinations that it can reach. It will always find the cheapest path to the cheapest-to-reach destination.

When writing Pathfinding Delegates, it is important for the values returned by the delegates to be preportionate to one another. The costs used by the Community Library are used below:

Invalid Path - 10,000
Base Cost - 10 (all locations cost this by default before the delegates)
Weak Avoidance - 10
Strong Avoidance - 30
10% chance of a free movement point - -0.5
100% chance of a free movement point - -5 (if two different things give this effect at the same time, the step cost for that step will be 0, or less. This is not a problem)

NOTE: Unlike the vanilla pathfinding system, which will never check a location after it has been found invalid, the Community Library's pathfnding algorithm will check an invalid location from all possible connected locations. This means that you can make a location impossible to access from certain directions or terrain types, and the pathfinding algortithm will still find it, as long as a single valid path exists.

NOTE 2: These path costs also apply to the trade route pathfinding section below.

Delegates

In order to actually change how the pathfinding is conducted, you need to make use of the onPopulatingPathfindingDelegates hook described below. The hook is called by the Community Library's pathfinding algorithm whenever it is called, and can be used to assign pathfinding delegates to the pathfinding algorithm, or remove delagates fom it.

These delegates are functions with a very specfic format that they must follow.

Path-Cost Delegates

Their signitures must match the following return and parameter types and order perfectly:

// DLC
double delegate_YourDelegateName(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
// Base-Game
double delegate_YourDelegateName(Location[] currentPath, Location location, Unit u)

They receive the path up to this point as a Location array (Location[] currentPath), the location that is being tested to see if it is a valid next step in the path (Location location), the unit that the check is being conducted for (Unit u) which may be null, and, if you are developing for the DLC branch, the list of map layers that the destination may be on and the origin is on (List targetMapLayers). It returns the cost of the path, as a double.

If any delegate returns a path cost of, or the total cost of the path is ever 10,000 or greater, the location is considered inaccessable for that path.

Here is an example delegate that mimics the behaviour of MoveType.AQUAPHIBIOUS:

public static double delegate_AQUAPHIBIOUS(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (location.isOcean || location.isCoastal)
    {
        return 0.0;
    }
    return 10000.0;
}

Each delegate should be a simple, self contained unit that only concerns itself with one soecific constraint that you wish to add to the pathfidning system. That way, other mods can remove delegates without worrying about loosing multiple conststarints at a time.

When using the onPopulatingPathfindingDelegates hook, all you need to do is add a reference to the delegate, to the pathfindingDelegates list that is passed into the hook, like so:

// DLC
public virtual void onPopulatingPathfindingDelegates(Location loc, Unit u, List<int> expectedMapLayers, List<Func<Location[], Location, Unit, List<int>, double>> pathfindingDelegates)
{
    if (u != null && u.moveType == Unit.MoveType.AQUAPHIBIOUS)
    {
        pathfindingDelegates.Add(CommunityLib.Pathfinding.delegate_AQUAPHIBIOUS);
    }
}

// Base-Game
public virtual void onPopulatingPathfindingDelegates(Location loc, Unit u, List<Func<Location[], Location, Unit, double>> pathfindingDelegates)
{
    if (u != null && u.moveType == Unit.MoveType.AQUAPHIBIOUS)
    {
        pathfindingDelegates.Add(CommunityLib.Pathfinding.delegate_AQUAPHIBIOUS);
    }
}

The Community Library comes with some delegates already made, both to fulfill the move type and safe move requirements, and some just for fun. These are detailed below.

NOTE: Before any mod's onPopulatingPathfindingDelegates hook is called, the pathfinding system will have already added any required delegates for the unit's moveType (aquaphibious and desert only), and for safeMove.

NOTE 2: Since this hook is called on each mod in the order that they are loaded, you will only be able to detect and remove delegates from mods that load before your mod. This may be changed at a later date, through the addition of a second hook, if there is demand.

Destination Validity Delegates

The getPathTo Delegate override requires a destination validity delegate to be passed in as a parameter. This delegate is what the pathfinding algorithm uses to determine if the location it is checking is a valid destination.

Its signitures must match the following return and parameter types and order perfectly:

// DLC
bool delegate_Valid_YourDelegateName(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
// Base-Game
bool delegate__Valid_YourDelegateName(Location[] currentPath, Location location, Unit u)

They receive the path up to this point as a Location array (Location[] currentPath), the location that is being tested to see if it is a valid destination (Location location), the unit that the check is being conducted for (Unit u) which may be null, and, if you are developing for the DLC branch, the list of map layers that the destination may be on and the origin is on (List targetMapLayers). It returns if the location is a valid destination, as a bool.

If any delegate returns false, the destination is considered invalid for that path.

Here is the delegate that handles ensuring the destination is on the target map layers, if any were provided:

public static bool delegate_VALID_LAYERBOUND(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    return targetMapLayers == null || targetMapLayers.Count == 0 || targetMapLayers.Contains(location.hex.z);
}

NOTE: If any target map layers were passed into the function, the Layerbound delegate is automatically added to the destination validity delegates for that path. Your delegate does not, therefore, need to handle map layers.

Hooks

The Community Library provides two hooks for general pathfinding. (see hooks, pathfinding.

onPopulatingPathfindingDelegates

The second set of hooks is called when the Community Library's pathfinding function has been called.

// DLC
void onPopulatingPathfindingDelegates(Location loc, Unit u, List<int> expectedMapLayers, List<Func<Location[], Location, Unit, List<int>, double>> pathfindingDelegates)
// Base-Game
void onPopulatingPathfindingDelegates(Location loc, Unit u, List<Func<Location[], Location, Unit, double>> pathfindingDelegates)

It recieves the location the path is from (loc), the unit that is seeking the path (u) which may be null, the list of map layers that the destination should be on (List expectedMapLayers) if you are developing for the DLC branch, and the list of pathfinding delegates that have already been assigned to the path (pathfindingDelegates), including the unit's movement type and safemove requirements.

In order to modify how the path is calculated, add one or more new pathfinding delegates to the pathfindingDelegates variable.

onPathfinding_AllowSecondPass

The third set of hooks is called if the pathfinding algorithm failed to find a valid path on the first pass. This could be due to overly strict pathfinding constraints introduced by a mod, or due to layer contstraints in the DLC.

// DLC
bool onPathfinding_AllowSecondPass(Location locA, Unit u, List<int> expectedMapLayers, List<Func<Location[], Location, Unit, List<int>, double>> pathfindingDelegates)
// Base-Game
bool onPathfinding_AllowSecondPass(Location locA, Unit u, List<Func<Location[], Location, Unit, double>> pathfindingDelegates)

It recieves the location the path is from (loc), the unit that is seeking the path (u) which may be null, the list of map layers that the destination should be on (List expectedMapLayers) if you are developing for the DLC branch, and the list of pathfinding delegates that have already been assigned to the path (pathfindingDelegates), including the unit's movement type and safemove requirements.

If your mod has introduced a strict pathfinding delegate that may prevent a valid path from being found, but that constraint is not mandatory, it should be removed in this hook by removing the reference to the delegate from the pathfiondingDelegates list, and the hook should return true.

If any instance of these hooks returns true, a second pass will be made after all instances of the hook are called. This ensures that all required pathfinding delegates have been removed before attempting to find a new path.

NOTE: The Community Library automatically handles the removal of the LAYERBOUND delegate if the DLC is enabled and the unit should be allowed to navigate via the underground and surface. This results in the pathfinding algorithm always prefering a path that only passes through the layers of the origin and destination, even if a shorter path is available through a different map layer. A path wll only be made through a different map layer if there is no valid path that does not. If your mod introduces a new map layer that shouldn't be accessible to agents from one or more specific other map layers, you will need to control access through your own delegate.

Example Delegates

The Community Library contains the following delegates, some of which match move types or safe move requirements, and others which serve as examples.

Note: All of these delegates are listed in their DLC-compatible form. For the non-DLC versions, check the Pathfinding file in the 1.1-Stable branch. Functionally, they are identical excpet that they don't receive the List<int> targetMapLayers parameter.

Move Types

Aquaphibious

public static double delegate_AQUAPHIBIOUS(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (location.isOcean || location.isCoastal)
    {
        return 0.0;
    }
    return 10000.0;
}

This delegate matches the aquabhibious move type, and is automatically added to any pathfinding request for a unit with that move type.

Desert Only

public static double delegate_DESERT_ONLY(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (location.hex.terrain == Hex.terrainType.ARID || location.hex.terrain == Hex.terrainType.DESERT || location.hex.terrain == Hex.terrainType.DRY)
    {
        return 0.0;
    }
    return 10000.0;
}

This delegate matches the desert only move type, and is automatcially added to any pathfinding request for a unit with that move type. Currently, the only base-game unit that makes use of this move type is the First Daughter.

Safe Move

public static double delegate_SAFE_MOVE(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (u == null || location.soc == null || !location.soc.hostileTo(u))
    {
        return 0.0;
    }
    return 10000.0;
}

This delegates matches the checks performed when a unit is ordered to use sfaeMove, and is automatically added to any pathfinding request that was made with safeMove equal to true.

Behavioural Modifications

Favourable Winds

public static double delegate_FAVOURABLE_WIND(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (u != null && u.isCommandable() && location.isOcean)
    {
        return -1.0;
    }
    return 0.0;
}

This delegate is applied to player controlled agents, and gives them a 10% bias in favour of travelling via the ocean. This matches the 10% frequency of the Favourable Winds event, which gives the agent an additional movemenet point.

Avoid Tresspass

public static double delegate_AVOID_TRESSPASS(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (u == null || location.soc == null || location.soc == u.society || u.society.getRel(location.soc).state != DipRel.dipState.hostile)
    {
        return 0.0;
    }

    return 30.0;
}

This delegate is added to military units when safe move is false, and forces the units to avoid territory belonging to a social group with which they are hostile, but not already at war. The aim of this delegate is to reduce the instances where armies get in fights by passing through the territory of a hostile third party, when on their way to or from a war.

DLC Specific Delegates

Layer Bound

public static double delegate_LAYERBOUND(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (targetMapLayers == null || targetMapLayers.Count == 0 || targetMapLayers.Contains(location.hex.z))
    {
        return 0.0;
    }
    return 10000.0;
}

This delegate prevents the agent from pathing through any layers that are not the origin's layer or destination's layer.

The pathfinding algorithm automatically includes this delegate when attempting to find a path. If that attempt fails, this delegate is removed from the pathfindingDelegates list, and a second attempt is made to find a valid path, if the unit is allowed to do so.

Examples

Aquatic

public static double delegate_AQUATIC(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (location.isOcean)
    {
        return 0.0;
    }
    return 10000.0;
}

This delegate only allows a unit to travel through ocean locations.

Landlocked

public static double delegate_LANDLOCKED(Location[] currentPath, Location location, Unit u, List<int> targetMapLayers)
{
    if (location.isOcean)
    {
        return 10000.0;
    }
    return 0.0;
}

This delegate prevents a unit from travelling over water.

Shadow Only

public static bool delegate_SHADOW_ONLY(Location[] currentPath, Location location, Unit u, Location origin, Location destination)
{
    return location.getShadow() >= 0.5;
}

This delegate prevents a unit from travelling through any location that is not at least 50% enshadowed.

Shadowbound

public static bool delegate_SHADOWBOUND(Location[] currentPath, Location location, Unit u, Location origin, Location destination)
{
    int hp = 0;

    if (u != null)
    {
        hp = u.hp;
    }

    if (u != null && location.getShadow() < 0.5)
    {
        if (currentPath.Where(l => l.getShadow() < 0.5).Count() + 1 < hp)
        {
            return true;
        }

        return false;
    }

    return true;
}

This delegate allows a unit to path through any number of locations with shadow at least 50%, and a number of locations with shadow less than 50% that is smaller than it's current HP. This would prevent the Shadow agent that you can create using She Who Will Feast's Split Shadow power, from killing itself while travelling between enshadowed areas, but will not prevent it from travelling to locations that have less than 50% shadow.

Trade Route Pathing

The getTradeRoute functions:

Location[] getTradeRouteTo(Location start, Location end)

// DLC
Location[] getTradeRouteFrom(Location start, List<Location> endpointsAll = null)
Location[] getTradeRouteFrom(Location start, int endPointMapLayer, List<Location> endpointsAll = null)
Location[] getTradeRouteFrom(Location start, List<int> endPointMapLayers, List<Location> endpointsAll = null)
// Base-Game
Location[] getTradeRouteFrom(Location start, List<Location> endpointsAll = null)

The getTradeRouteTo function allows you to create a trade route from a specific place, to a specific place, while the getTradeRouteFrom function allows you to get the cheapest, or one of the cheapest, trade routes from a location, to any other trade route endpoint.

If you do not pass endpoints into getTradeRouteFrom, it will automatically gather the list of valid trade route endpoints, otherwise it will use the list provided.

The range of Path Costs used for the pathfinding calculation are the same as those for general pathfinding, listed above.

Trade Route Delegates

In order to actually change how the pathfinding is conducted, you need to make use of the onPopulatingTradeRoutePathfindingDelegates hook described below. The hook is called by the Community Library's pathfinding algorithm whenever it is called to create a new trade route, and can be used to assign pathfinding delegates and endpoint validity delegates, to the pathfinding algorithm, or remove delagates fom it.

These delegates are functions with a very specfic format that they must follow.

Trade Route Path-Cost Delegates

Their signitures must match the following return and parameter types and order perfectly:

// DLC
double delegate_Trade_YourDelegateName(Location[] currentPath, Location location, List<int> targetMapLayers)
// Base-Game
double delegate_Trade_YourDelegateName(Location[] currentPath, Location location)

They receive the path up to this point as a Location array (Location[] currentPath), the location that is being tested to see if it is a valid next step in the path (Location location), and, if you are developing for the DLC branch, the list of map layers that the destination may be on and the origin is on (List targetMapLayers). It returns the cost of the path, as a double.

If any delegate returns a path cost of, or the total cost of the path is ever 10,000 or greater, the location is considered inaccessable for that path.

Here is an example delegate that prevents trade routes from crossing between the surface and the underground while awareness of the underground is below 100%:

public static double delegate_TRADE_UNDERGROUNDAWARENESS(Location[] currentPath, Location location, List<int> targetMapLayers)
{
    Location start = currentPath[0];
    if (start.map.awarenessOfUnderground < 1.0)
    {
        if ((start.hex.z == 0 && location.hex.z == 1) || (start.hex.z == 1 && location.hex.z == 0))
        {
            return 10000.0;
        }
    }

    return 0.0;
}

Each delegate should be a simple, self contained unit that only concerns itself with one soecific constraint that you wish to add to the pathfidning system. That way, other mods can remove delegates without worrying about loosing multiple conststarints at a time.

When using the onPopulatingTradeRoutePathfindingDelegates hook, all you need to do is add a reference to the delegate, to the pathfindingDelegates list that is passed into the hook, like so:

// DLC
public virtual void onPopulatingTradeRoutePathfindingDelegates(Location start, List<int> expectedMapLayer, List<Func<Location[], Location, List<int>, double>> pathfindingDelegates, List<Func<Location[], Location, List<int>, bool>> destinationValidityDelegates)
{
    if (start.map.awarenessOfUnderground < 1.0f)
    {
        pathfindingDelegates.Add(CommunityLib.Pathfinding.delegate_TRADE_UNDERGROUNDAWARENESS);
    }
}

// Base-Game
public virtual void onPopulatingTradeRoutePathfindingDelegates(Location start, List<Func<Location[], Location, double>> pathfindingDelegates, List<Func<Location[], Location, bool>> destinationValidityDelegates)
{
    if (start.map.awarenessOfUnderground < 1.0f)
    {
        pathfindingDelegates.Add(CommunityLib.Pathfinding.delegate_TRADE_UNDERGROUNDAWARENESS);
    }
}

The Community Library comes with some delegates already made, both to fulfill the move type and safe move requirements, and some just for fun. These are detailed below.

NOTE: Before any mod's onPopulatingTradeRoutePathfindingDelegates hook is called, the pathfinding system will have already added any required path cost delegates for the vanilla path costs, to restrict trade routes to navigating only using the layers of the origin and destination, and to manager awareness of the underground. It will also have added destination validity delegates for the destination being on the target map layer, and to prevent duplicate trade routes.

NOTE 2: Since this hook is called on each mod in the order that they are loaded, you will only be able to detect and remove delegates from mods that load before your mod. This may be changed at a later date, through the addition of a second hook, if there is demand.

Trade Route Destination Validity Delegates

This delegate is what the pathfinding algorithm uses to determine if the location it is checking is a valid trade route endpoint.

Its signitures must match the following return and parameter types and order perfectly:

// DLC
bool delegate_TradeValid_YourDelegateName(Location[] currentPath, Location location, List<int> targetMapLayers)
// Base-Game
bool delegate__TradeValid_YourDelegateName(Location[] currentPath, Location location)

They receive the path up to this point as a Location array (Location[] currentPath), the location that is being tested to see if it is a valid destination, and, if you are developing for the DLC branch, the list of map layers that the destination may be on (List targetMapLayers). It returns if the location is a valid destination, as a bool.

If any delegate returns false, the destination is considered invalid for that path.

Here is the delegate that handles ensuring the destination is on the target map layers, if any were provided:

public static bool delegate_TRADEVALID_LAYERBOUND(Location[] currentPath, Location location, List<int> endPointMapLayers)
{
    return endPointMapLayers == null || endPointMapLayers.Count == 0 || endPointMapLayers.Contains(location.hex.z);
}

NOTE: If any target map layers were passed into the function, the Layerbound delegate is automatically added to the destination validity delegates for that path. Your delegate does not, therefore, need to handle map layers in this situation.

Trade Route Hooks

The Community Library provides four hooks for trade route pathfinding pathfinding. (see hooks, pathfinding.

onGetTradeRouteEndpoints

public virtual void onGetTradeRouteEndpoints(Map map, List<Location> endpoints)
{
    return;
}

This hook fires when the game gathers all locations that should be connected to a tarde route. It recieves the map (map), and the list of locations that should be connected to a trade route (endpoints).

To force a location that is not the capitol city of a society to be connected to the network of trade routes, add it to the list of endpoints.

onPopulatingTradeRoutePathfindingDelegates

The second set of hooks is called when the Community Library's pathfinding function has been called.

// DLC
onPopulatingTradeRoutePathfindingDelegates(Location start, List<int> expectedMapLayer, List<Func<Location[], Location, List<int>, double>> pathfindingDelegates, List<Func<Location[], Location, List<int>, bool>> destinationValidityDelegates)
// Base-Game
onPopulatingTradeRoutePathfindingDelegates(Location start, List<Func<Location[], Location, double>> pathfindingDelegates, List<Func<Location[], Location, bool>> destinationValidityDelegates)

It recieves the location the trade route is from (start), the list of map layers that the destination should be on (List expectedMapLayers) if you are developing for the DLC branch, the list of pathfinding delegates that have already been assigned to the path (pathfindingDelegates), and the list of destination validity delegates that have already been assigned to the path (destinationValidityDelegates).

In order to modify how the path is calculated, add one or more new pathfinding delegates to the pathfindingDelegates variable, and in order to change what endpoints are condidered valid, add one or more new destination validity delegates to the destinationValidityDelegates variable.

onPathfindingTradeRoute_AllowSecondPass

The third hook is called if the pathfinding algorithm failed to find a valid path on the first pass. This could be due to overly strict pathfinding constraints introduced by a mod, or due to layer contstraints in the DLC.

// DLC
onPathfindingTadeRoute_AllowSecondPass(Location start, List<int> expectedMapLayer, List<Func<Location[], Location, List<int>, double>> pathfindingDelegates, List<Func<Location[], Location, List<int>, bool>> destinationValidityDelegates)
// Base-Game
onPathfindingTadeRoute_AllowSecondPass(Location start, List<Func<Location[], Location, double>> pathfindingDelegates, List<Func<Location[], Location, bool>> destinationValidityDelegates)

It recieves the location the trade route is from (start), the list of map layers that the destination should be on (List expectedMapLayers) if you are developing for the DLC branch, the list of pathfinding delegates that have already been assigned to the path (pathfindingDelegates), and the list of destination validity delegates that have already been assigned to the path (destinationValidityDelegates).

If your mod has introduced a strict pathfinding or destination validity delegate that may prevent a valid path from being found, but that constraint is not mandatory, it should be removed in this hook by removing the reference to the delegate from the corresponding delegates list, and the hook should return true.

If any instance of these hooks returns true, a second pass will be made after all instances of the hook are called. This ensures that all required pathfinding delegates have been removed before attempting to find a new path.

NOTE: The Community Library automatically removes the LAYERBOUND delegate and allows a second pass, if the DLC is enabled and the onPathfindingTadeRoute_AllowSecondPass hook is called. This results in the pathfinding algorithm always prefering a path that only passes through the layers of the origin and destination, even if a shorter path is available through a different map layer. Awareness of the Underground is managed through a dedicated delegate for trade routes. A path wll only be made through a different map layer if there is no valid path that does not. If your mod introduces a new map layer that shouldn't be accessible to agents from one or more specific other map layers, you will need to control access through your own delegate.

onBuildTradeNetwork_EndOfProcess

void onBuildTradeNetwork_EndOfProcess(Map map, ManagerTrade tradeManager, List<Location> endpoints)

This hook fires after the trade network has been rebuilt. It recievs the map (map), the trade manager (tradeManager), and the list of trade route endpoints (endpoints). It can be used to force a specific trade route to exist if it hasen't already been made.

NOTE: When playing the DLC, the Community Library automatically detects if the current god is Mammon, and if it is, uses this hook to ensure that The Mountaijn has a trade route connecting it to both the surface and the underground map layers.

Example Trade Route Delegates

The Community Library contains the following delegates for trade routes.

Note: All of these delegates are listed in their DLC-compatible form. For the non-DLC versions, check the Pathfinding file in the 1.1-Stable branch. Functionally, they are identical excpet that they don't receive the List<int> expectedMapLayer parameter.

Base Path Costs

Vanilla

public static double delegate_TRADE_VANILLA(Location[] currentPath, Location location, List<int> targetMapLayers)
{
    double result = 0.0;
    Location locLast = currentPath[currentPath.Length - 1];
            
    if (location.soc == null)
    {
        if (location.isOcean)
        {
            result += 7.5;
        }
        else
        {
            result += 15.0;
        }
    }
    else if (location.soc is Society society)
    {
        if (location.settlement is Set_City || location.settlement is Set_DwarvenCity)
        {
            result += 10.0;
        }
        else
        {
            result += 7.5;
        }
    }
    else
    {
        result += 30.0;
    }

    // Even when not layerbound, there is preference to sticking to the layer or layers that the end points are on.
    if (endPointMapLayers != null && endPointMapLayers.Count > 0 && !endPointMapLayers.Contains(location.hex.z))
    {
        result += 5.0;
    }

    return result;
}

This mimics the vanilla pathfinding algorithm, but also includes some minimal fine tuning relating to map layers.

Realistic Trade Routes

public static double delegate_TRADE_REALISTIC(Location[] currentPath, Location location, List<int> targetMapLayers)
{
    double result = 0.0;
    Location locLast = currentPath[currentPath.Length - 1];
    if (location.soc == null)
    {
        if (location.isOcean)
        {
            if ((locLast.settlement is Set_City || locLast.settlement is Set_DwarvenCity) && locLast.settlement.subs.Any(sub => sub is Sub_Docks))
            {
                result += 2.5;
            }
            else
            {
                result += 7.5;
            }
        }
        else
        {
            result += 15.0;
        }
    }
    else if (location.soc is Society society)
    {
        if (location.settlement is Set_City || location.settlement is Set_DwarvenCity)
        {
            if (locLast.isOcean && location.settlement.subs.Any(sub => sub is Sub_Docks))
            {
                result += 2.5;
            }
            else if (location.settlement.subs.Any(sub => sub is Sub_Market))
            {
                result += 2.5;
            }
            else
            {
                result += 5.0;
            }
        }
        else
        {
            result += 7.5;
        }
    }
    else
    {
        result += 30.0;
    }

    // Consideration for low habitability. Strong avoidance of uninhabitable lands. Weak avoidance of low habitability lands. Ignored on ocean.
    if (!location.isOcean)
    {
        float habitability = location.hex.getHabilitability();
        if (habitability < currentPath[0].map.param.mapGen_minHabitabilityForHumans)
        {
            result += 30.0;
        }
        else if (habitability < currentPath[0].map.param.mapGen_minHabitabilityForHumans * 2)
        {
            result += 10.0;
        }
    }

    // Even when not layerbound, there is preference to sticking to the layer or layers that the end points are on.
    if (endPointMapLayers != null && endPointMapLayers.Count > 0 && !endPointMapLayers.Contains(location.hex.z))
    {
        result += 5.0;
    }

    return result;
}

This delegate is used instead of the vanilla delegate if the Realistic Trade Routes option is enabled.

No Duplicates

public static bool delegate_TRADEVALID_NODUPLICATES(Location[] currentPath, Location location, List<int> targetMapLayers)
{
    return !location.map.tradeManager.routes.Any(r => (r.start() == currentPath[0] && r.end() == location) || (r.start() == location && r.end() == currentPath[0]));
}

This validity delegate prevents the formation of duplicate trade routes.

DLC Specific Delegates

Layebound

public static double delegate_TRADE_LAYERBOUND(Location[] currentPath, Location location, List<int> targetMapLayers)
{
    if (targetMapLayers == null || targetMapLayers.Count == 0 || targetMapLayers.Contains(location.hex.z))
    {
        return 0.0;
    }
    return 10000.0;
}

This delegate prevents trade routes from passing through layers that the start point and endpoints are not on.

public static bool delegate_TRADEVALID_LAYERBOUND(Location[] currentPath, Location location, List<int> targetMapLayers)
{
    return targetMapLayers == null || targetMapLayers.Count == 0 || targetMapLayers.Contains(location.hex.z);
}

Underground Awareness

public static double delegate_TRADE_UNDERGROUNDAWARENESS(Location[] currentPath, Location location, List<int> targetMapLayers)
{
    Location start = currentPath[0];
    if (start.map.awarenessOfUnderground < 1.0)
    {
        if ((start.hex.z == 0 && location.hex.z == 1) || (start.hex.z == 1 && location.hex.z == 0))
        {
            //Console.WriteLine($"CommunityLib: Location {location.getName()} ({location.hex.z}) violates Underground Awareness rules for trade route originating from {currentPath[0].getName()} ({currentPath[0].hex.z})");
            return 10000.0;
        }
    }

    return 0.0;
}

This delegate prevents trade routes from crossing between the surface and the underground if awareness of the underground is less than 100%.

⚠️ **GitHub.com Fallback** ⚠️