Skip to content

Enhanced canals: chainable canals, deep canals, and canal irrigation - #1219

Open
acato wants to merge 17 commits into
We-the-People-civ4col-mod:developfrom
acato:feature/enhanced-canals
Open

Enhanced canals: chainable canals, deep canals, and canal irrigation#1219
acato wants to merge 17 commits into
We-the-People-civ4col-mod:developfrom
acato:feature/enhanced-canals

Conversation

@acato

@acato acato commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR extends the existing canal system with three major enhancements that make canals a more strategic and versatile infrastructure option:

1. Chainable Canals

Canals are no longer limited to a single tile adjacent to water. Pioneers can now build canal chains across multiple land tiles by placing each new canal adjacent to an existing one. This enables connecting distant water bodies through multi-tile land crossings.

Cost scaling: Each additional tile in the chain increases both build time and gold cost exponentially (2x per connected canal tile), capped at 4x the base cost. This makes long chains expensive but not prohibitively so, encouraging strategic placement.

Implementation: CvPlot::canHaveImprovement() now checks for adjacency to existing canals (not just water). CvPlot::countAdjacentConnectedCanals() performs a BFS through the connected canal chain to determine the chain length for cost calculations. Cost scaling is applied in both CvPlot::getBuildTime() and CvPlayer::getBuildCost().

2. Deep Canals

A new improvement type (IMPROVEMENT_DEEP_CANAL) that is built by upgrading an existing regular canal. Deep canals allow larger lake-capable ships (sloops, schooners, smuggling ships, etc.) to pass through, while regular canals remain limited to coastal ships and fishing boats.

Ship filtering logic:

  • Regular canal: Only ships with TerrainImpassable(TERRAIN_OCEAN) (coastal-only ships) and gather boats with harbour space 1 (fishing boats) may enter
  • Deep canal: Any ship that can traverse lakes (!TerrainImpassable(TERRAIN_LAKE)) may enter — this includes sloops, schooners, smuggling ships, and similar mid-tier vessels, but excludes ocean-going warships like galleons and frigates

Water area bridge graph: A new subsystem in CvMap tracks which water areas (oceans, lakes) are connected via deep canal chains. This uses a union-find data structure with BFS discovery, rebuilt whenever deep canals are constructed or destroyed. The bridge graph enables the game's Europe-to-lake routing so that lake-capable ships spawning from Europe can reach inland lakes through deep canal chains. Cities adjacent to multiple water areas also create bridges.

Implementation files:

  • CvMap.h/cpp: WaterAreaBridge struct, rebuildWaterAreaBridges(), areWaterAreasConnected(), getBridgeLocation(), findWaterAreaRoot() (union-find)
  • CvInfos.h/cpp: New bDeepCanal flag on CvImprovementInfo, read from XML
  • CvPlot.h/cpp: isDeepCanal(), ship filtering in isValidDomainForAction() and canMoveInto(), canal BFS in getAdjacentSeaArea() for spawn routing
  • CvGame.cpp: Bridge graph rebuild on map regeneration and game load
  • CvSelectionGroup.cpp: isAmphibPlot() excludes canal tiles so ships aren't blocked from entering them
  • XML: IMPROVEMENT_DEEP_CANAL definition, BUILD_DEEP_CANAL build action, bDeepCanal schema tag, art defines, all pioneer-type units given the build ability

3. Canal Irrigation

Canals connected to a freshwater source (lake, large river, or river) via a canal chain enable pioneers to irrigate adjacent desert tiles into flood plains. The freshwater connection is verified by a BFS through the canal chain (CvPlot::isCanalChainConnectedToFreshwater()).

Key design decisions:

  • Irrigation is not automatic — a pioneer must manually build the flood plains improvement on each eligible desert tile. This prevents canals from being an overpowered free terrain upgrade
  • Only canals connected to freshwater enable irrigation; sea-only canal chains do not. The check uses isAdjacentToFreshwaterCanal() which verifies freshwater connectivity through the entire canal chain
  • The freshwater check is applied consistently in both canHaveFeature() (general feature placement) and canBuild() (terraforming validation)

Also included: Ice Hut improvement

A new IMPROVEMENT_ICE_HUT for frozen lake tiles (TERRAIN_ICE_LAKE), providing +2 food. Land units can now enter ice lake tiles (movement validation updated in isValidDomainForAction() and canMoveInto()).

Also included: Ice Hut improvement

A new IMPROVEMENT_ICE_HUT for frozen lake tiles (TERRAIN_ICE_LAKE), providing +2 food. Land units can now enter ice lake tiles (movement validation updated in isValidDomainForAction() and canMoveInto()).

Changed files

DLL Sources

File Changes
CvPlot.h/cpp Canal chaining, deep canal checks, freshwater BFS, ship filtering, canal BFS for getAdjacentSeaArea(), cost scaling
CvMap.h/cpp Water area bridge graph (union-find + BFS), rebuildWaterAreaBridges()
CvInfos.h/cpp bDeepCanal flag on CvImprovementInfo
CvPlayer.cpp Canal chain gold cost scaling
CvUnit.cpp Deep canal ship filtering in canMoveInto(), ice lake land unit access
CvGame.cpp Bridge graph rebuild on map regen and game load
CvGameTextMgr.cpp Deep canal tooltip text
CvSelectionGroup.cpp Exclude canal tiles from isAmphibPlot()
EventTrigger.cpp Unrelated one-line fix (missing push_back in setRandomNumbers())

XML

File Changes
CIV4ImprovementInfos.xml IMPROVEMENT_DEEP_CANAL, IMPROVEMENT_ICE_HUT; canal bNotAllowedNextToSameAsItself set to 0
CIV4TerrainSchema.xml bDeepCanal element type
CIV4BuildInfos.xml BUILD_DEEP_CANAL, BUILD_ICE_HUT
CIV4UnitInfos.xml BUILD_DEEP_CANAL added to all pioneer-type units
CIV4ArtDefines_Improvement.xml Art define for deep canal (reuses canal art)
XML_AUTO_UTF8_BuildInfo.xml Text keys for deep canal and ice hut builds (EN/FR/DE)
XML_AUTO_UTF8_ImprovementInfo.xml Civilopedia and tooltip text for deep canal and ice hut (EN/FR/DE/RU)

Test plan

  • Build a canal chain across 3+ land tiles and verify cost caps at 4x base
  • Build a regular canal and verify only coastal ships and fishing boats can enter
  • Upgrade a canal to deep canal and verify sloops/schooners can traverse it
  • Verify galleons/frigates cannot enter deep canals
  • Connect a lake to the ocean via deep canals and verify ships spawn/route correctly from Europe to the lake
  • Build a canal chain from a lake through desert and verify a pioneer can irrigate adjacent desert into flood plains
  • Build a canal chain from the ocean (no freshwater) adjacent to desert and verify irrigation is NOT possible
  • Build an ice hut on a frozen lake tile and verify +2 food yield
  • Verify land units can walk onto ice lake tiles
  • Load a savegame from before these changes and verify it loads correctly
  • Check Civilopedia entries for both canal types and ice hut

A test package (DLL + XML files) is included in test-packages/enhanced-canals/ for easy drop-in testing.

🤖 Generated with Claude Code

acato and others added 17 commits March 28, 2026 23:53
Allow canals to be built adjacent to existing canals (not just water),
enabling multi-tile canal chains connecting water bodies. Each additional
canal in a connected chain doubles both gold cost and build time.
Add BFS-based getAdjacentSeaArea() so mid-chain canals can resolve
their sea area for ship navigation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pioneers can now build Ice Huts on TERRAIN_ICE_LAKE tiles, providing
+2 Food (matching the total yield of Fishing Nets on regular lakes).
Build effort matches Fishing Nets (1000 time, 100 cost).

DLL changes allow land units to enter ice lake tiles, treating frozen
lakes like large rivers for movement purposes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EventTriggeredData::setRandomNumbers() creates a RandomContainer for
each event in the trigger, generates a random number via getSorenRandNum,
but never actually stores the container in m_RandomNumbers. The
push_back call was missing, so m_RandomNumbers remained empty after
the function completed.

This caused getRandomNumber() and getRandomNumberForIndex() to always
return 0 (the fallback default), which had two consequences:

1. Any Python PythonCanDo callback using getRandomNumberForIndex()
   for probability checks would always see 0. For example,
   canTriggerVolcanoDormant1() checks "getRandomNumberForIndex(0) < 250"
   which was always true (0 < 250), making the volcano dormant event
   fire 100% of the time instead of the intended ~25%.

2. The DLL-side TriggerChance fastpath in CvPlayer::canDoEvent()
   (line 14762) uses getRandomNumber(eEvent) to check event probability
   thresholds. With the vector always empty, this also always returned 0,
   bypassing intended probability gates for events validated through
   that code path.

The fix adds the missing m_RandomNumbers.push_back(container) so that
generated random numbers are actually persisted in the vector and
available to both Python and C++ callers.

Related: We-the-People-civ4col-mod#1205 (CvRandomInterfaceEvent tuple index out of range)
The primary cause of We-the-People-civ4col-mod#1205 was an argsList index mismatch fixed in
38b7518, but this bug compounded the issue by making the random
number check in canTriggerVolcanoDormant1 a no-op.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In CvPlayer::initTriggeredData(), after a PythonCanDo callback returns
successfully, the code reconstructs a Coordinates object from the
trigger data (since Python may have modified it). However, line 14416
passed m_iPlotX as both the X and Y arguments:

  Coordinates coord(pTriggerData->m_iPlotX, pTriggerData->m_iPlotX);

This meant pPlot was resolved to the wrong tile whenever the Python
callback modified the trigger's plot coordinates and X != Y. The
resolved plot would be at (X, X) instead of (X, Y), causing the
event to target the wrong map location for any subsequent logic
that uses pPlot (text generation, world news, event application).

Events where PythonCanDo does not modify coordinates would be
unaffected since pPlot was already correctly set before the callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deep canals are a new improvement built by upgrading an existing canal.
They allow lake-capable ships (sloops, schooners, etc.) to traverse land
tiles, enabling Europe-to-lake routing via deep canal chains.

- Cap canal chain cost scaling at 4x (was unbounded 2^n)
- Add IMPROVEMENT_DEEP_CANAL with bDeepCanal XML tag + schema
- Add BUILD_DEEP_CANAL (iTime=2500, iCost=50) with text keys (EN/FR/DE/RU)
- Ship filter: deep canals check TERRAIN_LAKE impassable instead of TERRAIN_OCEAN
- Water area bridge graph (union-find) tracks lake-ocean connectivity
  via deep canal chains; rebuilt on placement/destruction and game load
- Civilopedia: updated canal text, added deep canal pedia and helper text
- Art: reuses canal graphic (ART_DEF_IMPROVEMENT_DEEP_CANAL)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the previous test package. Now includes deep canals,
4x cost cap, and all modified XML/schema files.

Assert build for debugging. Apply on top of stock WtP 4.2.1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…adjacent desert

Canals connected to a freshwater source (lake, large river, or river) now
irrigate adjacent desert tiles by creating flood plains. Sea-only canals
do not irrigate. This makes canal chains from freshwater through desert
strategically valuable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every unit with BUILD_CANAL now also has BUILD_DEEP_CANAL in their
Builds list, so the deep canal upgrade option appears in the UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The canal filter only returned false to block disallowed ships, but
allowed ships fell through to the isWater() check which always fails
for canal tiles. Now returns true/false explicitly from the canal block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes for ship movement on canals:
- isAmphibPlot: exclude canal tiles from amphibious landing classification
  (pathfinder was rejecting canals as destinations)
- canMoveInto: bypass the sea-unit land gate for canal tiles
- isValidDomainForAction: already fixed in prior commit (explicit returns)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Canals connected to freshwater no longer instantly convert adjacent
desert to floodplains. Pioneers can still irrigate desert next to
canals, but only if the canal chain is connected to a freshwater source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AI_omniGroup used getPlot() (which dereferences *plot()) on the head
unit of every selection group, but off-map units (e.g. ships traveling
to Europe) have a NULL plot(). This crashed during any AI turn with
units in transit. Changed to use plot() pointer with explicit NULL
check, matching the pattern used by the old AI_group function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@acato
acato force-pushed the feature/enhanced-canals branch from 4970ef7 to bb85ca0 Compare March 29, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant