River Connections

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,727
Location
Near Portsmouth, UK
Lua classes, methods and functions to determine if two plots are connected by a river - see here for the general discussion

A River is one or more contiguous RiverSegments.
A river has a "known" headwaters and outflow (which may not be the actual headwaters/outflow if tiles have not been revealed)
A river flows from the headwaters to the outflow, it may have upstream branches (tributaries) and downstream branches (typically, but not necessarily, in deltas)

A RiverSegment is a single section of a River that flows between two adjacent tiles
A segment has both a primary tile (the one it is attached to) and an adjacent segment (which may be null if off the edge of the map)
A segment has a flow direction and can ascertain the next segment(s) both downstream and upstream.
A segment with no (known) upstream segment(s) is a headwater, one with no (known) downstream segment(s) is an outflow

A lake is treated as having a river flowing clockwise around its shoreline

Rivers and lakes are assumed to be immutable, users of IGE take note!!!
(use riverManager:rescanMap() after editing terrain/features/rivers to update the internal cache)

Sample usage

Code:
-- Get the river manager, only caring about passable terrain,
-- not if the player can actually traverse the plot
local riverManager = RiverManager:new(isPlotPassableTerrain)

-- Get the common rivers to the start and end plots
local rivers = riverManager:getCommonRivers(iStartX, iStartY, iEndX, iEndY)

-- If the start and end plots share at least one common river ...
if (#rivers > 0) then
  -- ... get the route between the plots on the first river
  local route = riverManager:getRiverRoute(rivers[1], iStartX, iStartY, iEndX, iEndY)

  -- If there is a route ...  
  if (#route > 0) then
    -- ... get the traversable plots along the bank of the shortest route
    --     from the start to end plot. (If there are more than one river
    --     between the plots, this may not be on the first river.)
    local bankPlots = riverManager:getRiverBankRoute(iStartX, iStartY, iEndX, iEndY)
  end
end
 

Attachments

How does the AI now respond in the case where you have a river connection but a road/railroad is possible to build?

I ask but I'm not even sure what the right answer should be for the AI. The road/railroad would have no gold advantage (only cost) but it does have potential military value.

The same issue comes up for Harbors connecting cities that are close on the same land-mass (this comes up particularly for Carthage). As a human player, I usually don't build roads/railroads in these cases, and remove them if they already existed but are no longer needed for connection (usually). I suppose that, short of adding real logic for this assessing military situation, it may be best for AI to follow this strategy.
 
Based only on the observation that the AI usually builds for itself what the player's advisors suggest, it doesn't start building road/rail connections until railways are discovered. Which should be the same for Carthage's harbour connections (the river connections are identical in function to harbour connections)

I though railways gave a production boost, so there is an advantage to them other than troop movement.
 
You get the production boost for railroad connection with Harbors without an actual rail connection. (IIRC, there was an early issue where you had to build at least one railroad out of the capital, but then this was not so after some patch.) So I'd assume that is true now for river connection.

From a flavor perspective, I think that's fine since river trade only got greater with steam and containers and so on.

So if I understand your AI comment: The AI will not build "extra" roads for cities otherwise connected, but they will build "extra" railroads. I think that's fine just based on military consideration only.
 
Code in post #1 updated to remove a "delta traversal" bug (ie a segment that has TWO downstream segments)
 
I've merged your MOD_EVENTS_CITY_CONNECTIONS dll changes and am about to add your Lua, so I have some questions about usage for the combined dll/Lua system.

I'm putting what I think is correct in code blocks, with questions/comments after.

Code:
local riverManager = RiverManager:new(isPlotPassableTerrain)
function IsRiverRoute(iStartX, iStartY, iEndX, iEndY)
	local rivers = riverManager:getCommonRivers(iStartX, iStartY, iEndX, iEndY)
	for i, river in pairs(rivers) do
		local route = getRiverRoute(river, iStartX, iStartY, iEndX, iEndY)
		if route then
			return true
		end
	end
	return false
end
Above is just a simple test for existence of river route. If I want to get more fancy, I can check bank plots for blockade before return true.

Code:
GameEvents.CityConnections.Add(function(iPlayer, bDirect) return true end)

function OnCityConnected(iPlayer, iCityX, iCityY, iToCityX, iToCityY, bDirect)
	return IsRiverRoute(iCityX, iCityY, iToCityX, iToCityY)
end
GameEvents.CityConnected.Add(OnCityConnected)
This "activates" and does the tests.

However: IIRC I noticed that the dll wants to test every city vs every city (holdover from Civ4?), but the only thing that really matters in Civ5 is connection to capital. Can't I just test iCityX, iCityY to see if it is capital and only test in that case? (Or would the capital be iToCityX, iToCityY? Or is it unpredictable? Or -dread- does it want to test each pair both ways?)
Or would it be more efficient to allow all pairwise tests but restrict all of this testing to bDirect = true only? (And to do that I would change the first event to return true for bDirect=true only, correct?)

Thanks for the system and the help!
 
However: IIRC I noticed that the dll wants to test every city vs every city (holdover from Civ4?), but the only thing that really matters in Civ5 is connection to capital. Can't I just test iCityX, iCityY to see if it is capital and only test in that case? (Or would the capital be iToCityX, iToCityY? Or is it unpredictable? Or -dread- does it want to test each pair both ways?)
Or would it be more efficient to allow all pairwise tests but restrict all of this testing to bDirect = true only? (And to do that I would change the first event to return true for bDirect=true only, correct?)

Thanks for the system and the help!

To answer the DLL questions - yes it tests every pair of cities, BUT if it's already tested B to A (as A to B) it just takes the cached result of A to B.

It has to test every city pair as you could have A to B by road B to C by harbour and then C to D by river. A is connected to D but not directly by any method (road, river or harbour)
 
And now for the Lua.

The city to city connection code in the DLL does TWO passes.

The first pass (indirect) looks for "magical" connections between pairs of cities - in the case of the standard game this is harbour to harbour connections. I've done a mod to enable airport to airport connections as well. Basically these are "if a unit gets to this tile can it teleport (ie magical) to any other tile to continue its journey".

The second pass (direct) looks for "physical" connections between pairs of cities - in the case of the standard game these are road and rail connections. When the DLL finds a physical connection it uses the path-finder to traverse the route and hook up any other cities on the route at the same time.

As the path-finder doesn't understand river connections we have to implement them as "indirect" connections.

Now, in order to keep the amount of events being sent to a sensible number, the Lua code has to "register an interest" in receiving them

Code:
function OnCityConnections(iPlayer, bDirect)
  -- Only interested in indirect city connection events for the civ with the river expansion trait and the sailing tech
  return ((not bDirect) and isRiverExpansion(iPlayer) and hasSailingTech(iPlayer))
end
GameEvents.CityConnections.Add(OnCityConnections)

Once the above returns true, the Lua code will start getting the CityConnected events

Code:
function OnCityConnected(iPlayer, iCityX, iCityY, iToCityX, iToCityY, bDirect)
  -- No need to test that the plots contain cities or that both cities belong to iPlayer as the DLL guarantees this
  if ((not bDirect) and isRiverExpansion(iPlayer) and hasSailingTech(iPlayer)) then -- another mod could be listening for these events
    -- Is there a known and passable river bank route between the cities?
    return (#(getRiverManager(iPlayer):getRiverBankRoute(iCityX, iCityY, iToCityX, iToCityY)) > 0)
  end

  return false
end
GameEvents.CityConnected.Add(OnCityConnected)

Code for implementing airport to airport connections is

Code:
local iBuildingAirport = GameInfoTypes.BUILDING_AIRPORT

--
-- Determine if two plots(cities) are connected by an air route
--
function OnCityConnections(iPlayer, bDirect)
  -- Only interested in indirect city connection events
  return (not bDirect)
end
GameEvents.CityConnections.Add(OnCityConnections)

function OnCityConnected(iPlayer, iCityX, iCityY, iToCityX, iToCityY, bDirect)
  if (not bDirect) then
    local pCity = Map.GetPlot(iCityX, iCityY):GetPlotCity()
    local pToCity = Map.GetPlot(iToCityX, iToCityY):GetPlotCity()

    return (pCity and pToCity and pCity:GetNumBuilding(iBuildingAirport) > 0 and pToCity:GetNumBuilding(iBuildingAirport) > 0)
  end

  return false
end
GameEvents.CityConnected.Add(OnCityConnected)

And the Morindim river connection code is ...

Spoiler :
Code:
--
-- Handles the Morindim's "River Connections" trait aspect
--

include("MorindimUtils")
include("RiverConnections")

local gPrereqTech = GameInfoTypes.TECH_BOAT_BUILDING or GameInfoTypes.TECH_SAILING

local gRiverManager = nil
local gNeedRescan = false

local gCurrentPlayer = nil

--
-- Catch terraforming events (after the map has been loaded) as these can change the river system
--
-- As there can be many sequential terraforming events when a river is added, rather then rescanning on every one,
-- just invalidate the river manager and then rescan when the next needing to check the river system
--
GameEvents.TerraformingMap.Add(function(iEvent, iLoad)
  GameEvents.TerraformingPlot.Add(OnTerraformingPlot)
end)

function OnTerraformingPlot(iEvent, iPlotX, iPlotY, iInfo, iNewValue, iOldValue, iNewExtra, iOldExtra)
  if (not gNeedRescan) then
    if (iEvent == TerraformingEventTypes.TERRAFORMINGEVENT_TERRAIN and (iNewValue == TerrainTypes.TERRAIN_COAST or iOldValue == TerrainTypes.TERRAIN_COAST)) then
      -- If a plot terrain has changed to/from coast (lake), invalidate the river manager
      gNeedRescan = true
    elseif (iEvent == TerraformingEventTypes.TERRAFORMINGEVENT_RIVER) then
      -- If a river segment changes, invalidate the river manager
      gNeedRescan = true
    end
  end
end


--
-- Lazily get the river manager, allowing for map rescans
--
function getRiverManager(iPlayer)
  if (gRiverManager == nil) then
    gRiverManager = RiverManager:new(function(pPlot) return isPlotPassablePlayer(pPlot, gCurrentPlayer) end)
    gNeedRescan = false
  end

  if (gNeedRescan) then
    gRiverManager:rescanMap()
    gNeedRescan = false
  end

  -- remember the current player for the isPlotPassablePlayer method
  gCurrentPlayer = Players[iPlayer]

  return gRiverManager
end


--
-- Determine if two plots(cities) are connected by a known river
--
-- Note: If we don't care if the river route is known to iPlayer, we can speed this up considerably
--       by checking the intersection of getRivers(iCityX, iCityY) and getRivers(iToCityX, iToCityY)
--
function OnCityConnections(iPlayer, bDirect)
  if ((not bDirect) and IsMorindimPlayer(iPlayer)) then
    return HasPrereqTech(iPlayer)
  end

  return false
end
GameEvents.CityConnections.Add(OnCityConnections)

function OnCityConnected(iPlayer, iCityX, iCityY, iToCityX, iToCityY, bDirect)
  -- No need to test that the plots contain cities or that both cities belong to iPlayer as the DLL guarantees this
  if ((not bDirect) and IsMorindimPlayer(iPlayer) and HasPrereqTech(iPlayer)) then -- another mod could be listening for these events
    -- Is there a known and passable river bank route between the cities?
    return (#(getRiverManager(iPlayer):getRiverBankRoute(iCityX, iCityY, iToCityX, iToCityY)) > 0)
  end

  return false
end
GameEvents.CityConnected.Add(OnCityConnected)


function HasPrereqTech(iPlayer)
  return (gPrereqTech == nil or Teams[Players[iPlayer]:GetTeam()]:IsHasTech(gPrereqTech))
end
 
Code:
GameEvents.CityConnections.Add(function(iPlayer, bDirect) return true end)
You are only interested in indirect routes, so should "return not bDirect" (as opposed to "return true")

Code:
function OnCityConnected(iPlayer, iCityX, iCityY, iToCityX, iToCityY, bDirect)
	return IsRiverRoute(iCityX, iCityY, iToCityX, iToCityY)
end
GameEvents.CityConnected.Add(OnCityConnected)

Not relevant for your Ea (total conversion) mod, but in case anybody else is trying to follow along, you should add an "if (not bDirect) then ... end" around the IsRiverRoute() call ... just in case some other mod has registered for the direct connections (like I say, not going to happen in Ea but ...)
 
If I want to get more fancy, I can check bank plots for blockade before return true.

The IsRiverRoute() function is correct, but if you want to get more fancy, you just need to add the logic into your own call-back and pass it to the function that creates the river manager

Code:
local riverManager = RiverManager:new(isPlotPassableTerrain)

isPlotPassableTerrain is a pre-defined convenience function, which boils down to

Code:
function isPlotPassableTerrain(pPlot)
  -- A plot is impassable if it is ...
  if (pPlot:IsImpassable()) then
    -- ... marked as impassable (eg a natural wonder)
    return false
  elseif (pPlot:IsWater() and not pPlot:IsLake()) then
    -- ... salt-water (should never happen)
    return false
  elseif (pPlot:IsMountain()) then
    -- ... a mountain
    return false
  elseif (pPlot:IsLake()) then
    -- ... water
    return false
  end

  return true
end

a river segment is passable if one or other bank is passable - so a river flowing between a mountain and a hill is passable, but one between two mountains isn't
 
OK, I understand bDirect and the passable function now.

Btw, there's no getRiverManager() method (it's in your post above). But I believe that RiverManager:new() gets me what you are referring to.

I have an agenda to strip this down to bare bones (it's going to apply to all full civs), so I'm doing this:
Code:
local riverManager = RiverManager:new(function(iPlot) return true end)
Maybe I'll add a stripped down passable function later.

Along the lines of bare bones, I don't need all the routes, or information about routes, or for routes to be "trimmed", etc. I just want to know as quickly as possible if there is a route. So I went an wrote a method in RiverManager:
Code:
  --Paz add
  isRiverRoute = function(self, iStartX, iStartY, iEndX, iEndY)
	for _, iRiver in pairs(self:getCommonRivers(iStartX, iStartY, iEndX, iEndY)) do
		local route = {}
		local startSegments = {}
		local endSegments = {}

		for _, segment in pairs(self:_getSegmentManager():getRiverSegments(iStartX, iStartY)) do
		  if (segment:getRiverId() == iRiver) then
			table.insert(startSegments, segment)
		  end
		end
		for _, segment in pairs(self:_getSegmentManager():getRiverSegments(iEndX, iEndY)) do
		  if (segment:getRiverId() == iRiver) then
			table.insert(endSegments, segment)
		  end
		end

		-- We can start on any segment of iRiver that borders (iStartX, iStartY), so choose the first
		local startSegment = startSegments[1]

		-- print("Looking UPSTREAM from ", startSegment:toString())
		local upstreamRoute = self:_getPartialRoute(startSegment, endSegments, true, false, route)
		if (upstreamRoute ~= nil) then
		  return true
		else
		  -- print("Looking DOWNSTREAM from ", startSegment:toString())
		  local downstreamRoute = self:_getPartialRoute(startSegment, endSegments, false, false, route)
		  if (downstreamRoute ~= nil) then
			return true
		  end
		end
	end
	return false
  end,
  --end Paz add
It's obviously hacked out of your other methods. Is this my quickest way to a true/false answer?
 
I left the riverManager assignment out for brevity ..

Code:
local riverManager = RiverManager:new(function(iPlot) return true end)

Omitting a call-back results in all plots being passable, so the above can be written as

Code:
local riverManager = RiverManager:new()

which will be significantly faster than calling an always true function.

Along the lines of bare bones, I don't need all the routes, or information about routes, or for routes to be "trimmed", etc. I just want to know as quickly as possible if there is a route.

The river connection code walks the cache, not the map, so unless you have hundreds of cities I wouldn't worry about the few extra cycles trimming the route and as most (if not all) cities will only ever be on one river, the check for "all routes" rarely kicks in (and the information about the route is just the list of segments, which it has to build anyway)

Now, as you currently have all plots as passable, the quickest way to determine if city A is on the same river as city B is ...


Code:
--
-- Determine if two plots(cities) are connected by a known river
--
-- [B]Note: If we don't care if the river route is known to iPlayer, we can speed this up considerably[/B]
--       [B]by checking the intersection of getRivers(iCityX, iCityY) and getRivers(iToCityX, iToCityY)[/B]
--

which translates to

Code:
function OnCityConnected(iPlayer, iCityX, iCityY, iToCityX, iToCityY, bDirect)
  local fromRivers = getRivers(iCityX, iCityY)
  local toRivers = getRivers(iToCityX, iToCityY)

  for _, iFromRiver in pairs(fromRivers) do
    for _, iToRiver in pairs(toRivers) do
      if (iFromRiver == iToRiver) then
        return true
    end
  end
  
  return false
end
GameEvents.CityConnected.Add(OnCityConnected)

Most of the time fromRivers and toRivers will be arrays of zero or one entries, so the double loop is not going to be a speed issue. And getRivers(x,y) is just pulling data from the cache.
 
I was thinking last night in my sleep that I was going about this all wrong focusing on route. All I wanted to know is if the the two cities share a river in common.

Got it! Thanks!
 
Back
Top Bottom