The Map Building Process

Temudjin

Chieftain
Joined
Oct 16, 2007
Messages
90
I've mentioned this already in the SDK forum, but here may be a better place for a reference list. I hope this can be of use for aspiring map-makers: :hatsoff:
Code:
[FONT="Courier New"][B]-------------------------------------------------------------------
The Map Building Process according to Temudjin
    --> also see Bob Thomas in "CvMapScriptInterface.py"
    (in ..\Assets\Python\EntryPoints)
-------------------------------------------------------------------[/B]
0)     - Get Map-Options
0.1)     getNumHiddenCustomMapOptions()
0.2)     getNumCustomMapOptions()
0.3)     getCustomMapOptionDefault()
0.4)     isAdvancedMap()
0.5)     getCustomMapOptionName()
0.6)     getNumCustomMapOptionValues()
0.7)     isRandomCustomMapOption()
0.8)     getCustomMapOptionDescAt()
0.9)     - Get Map-Types
0.9.1)     isClimateMap()
0.9.2)     isSeaLevelMap()
1)     beforeInit()
2)     - Initialize Map
2.2)     getGridSize()
2.3.1)   getTopLatitude()            # always use both
2.3.2)   getBottomLatitude()         # always use both
2.4.1)   getWrapX()                  # always use both
2.4.2)   getWrapY()                  # always use both
3)     beforeGeneration()
4)     - Generate Map
4.1)     generatePlotTypes()
4.2)     generateTerrainTypes()
4.3)     addRivers()
4.4)     addLakes()
4.5)     addFeatures()
4.6)     addBonuses()
4.6.1)     isBonusIgnoreLatitude()*
4.7)     addGoodies()
5)     afterGeneration()
6)     - Select Starting-Plots
6.1)     minStartingDistanceModifier()
6.2)     assignStartingPlots()
7)     - Normalize Starting-Plots
7.1)     normalizeStartingPlotLocations()+
7.2)     normalizeAddRiver()
7.3)     normalizeRemovePeaks()
7.4)     normalizeAddLakes()
7.5)     normalizeRemoveBadFeatures()+
7.6)     normalizeRemoveBadTerrain()+
7.7)     normalizeAddFoodBonuses()+
7.7.1)     isBonusIgnoreLatitude()*
7.8)     normalizeGoodTerrain()+
7.9)     normalizeAddExtras()
7.9.1)     isBonusIgnoreLatitude()*
8 )    startHumansOnSameTile()

* used by default 'CyPythonMgr().allowDefaultImpl()' in:
  addBonuses(), normalizeAddFoodBonuses(), normalizeAddExtras()
+ ['Fall From Heaven 2' and modmods] Mods in which the Flavour Start
  option is selected, will NOT call these functions.
[/FONT]
 
Er--I can't tell--what does all that do???
 
These are the functions that are called by Civ4 to build a map.

If you define those functions in your own map-script, you can control how to build the map. For those functions you don't define, a default process - which you can access with CyPythonMgr().allowDefaultImpl() - will be called.

You might want to have a look at the map-scripts in the 'Public Maps' folder to see how it is done. Also see "CvMapScriptInterface.py" (in ..\Assets\Python\EntryPoints) for a more detailed explanation.
 
Is there a key like this for the xml for editing the terrain itself?
 
Some notes on the items that decide and grab the pull-down options for random maps, based on looking at Totestra's (PW2's) source code:

getNumHiddenCustomMapOptions() Not in Totestra (optional)

getNumCustomMapOptions(): Input: None. Output: Int, number of parameters (besides size, climate, and sea level) that can be adjusted by the user

getCustomMapOptionDefault(): Input: Array. One element: The option number. Output: Int, the default value for this option

isAdvancedMap(): Input: None. Output 0 if this map is "not" an "advanced" map (a map which needs "custom game" to be seen) Make this 0; some mods don't work with "custom game".

getCustomMapOptionName(): Input: Array, one element. One element: The option number. The output is the name for this option. Keep in mind that in "quick game", this is preceded by the string "Select a" so it should ideally be a grammatically singular noun.

getNumCustomMapOptionValues(): Input: Array, one element. Element 0: The option number. Output: The number of possible values this option can have.

isRandomCustomMapOption(): Input: Array, one element. Element 0: The option number. Output: False if this option's value should be randomly chosen, True if it should be random.

getCustomMapOptionDescAt(): Input: Array, two elements. Element 0: The option number. Element 1: The choice for this option. Output: A string describing a given choice for a given option.

isClimateMap(): Input: None. Output: 0 if we are not allowed to select the climate for this map; 1 if we are allowed the select the climate for this map (Temperate, Arid, Cold, Rocky, and Tropical, but not in that order).

isSeaLevelMap(): Input: None. Output: 0 if we are not allowed to select the sea level for this map; 1 if we are

beforeInit(): Input: None. Output: None. Used by the map script to store the options selected by the user. To get options, start by having CyGlobalContext() create an object. Use the method getMap() in the CyGlobalContext()-created object to get map information. For example: foo = CyGlobalContext() ; bar = foo.getMap(). This second "getMap()" object has the following methods of interest for getting map script options:

  • getClimate() Get the "climate" option. Input: None. Output: 0: Temperate, 1: Tropical, 2: Arid, 3: Rocky, 4: Cold
  • getSeaLevel() Get the "sea level" option. Input: None. Output: 0: Low 1: Medium (normal) 2: High
  • getCustomMapOption() Get any other option. Input: Integer (the option in question). Output: Integer (the value for this option)

getGridSize(): Input: Array; one element. Array element: Integer with map size (0: Dual, 1: Tiny, 2: Small, 3: Standard, 4: Large, 5: huge). Presumably mods with even larger maps will give this function values like 6, 7, 8, etc. Output: If this element has the value -1, the Python script must return an empty array. Otherwise, return a tuple in the form (width, height), where width is the width of the map to be generated (in a unit which represents 4 squares), and height is the height divided by 4.

In other words, if this returns (36,24), that means Civilization 4 should generate a 144x96 map (144 squares wide, 96 squares high). Note that the "multiply by 4" rule was removed in the Colonization total conversion.
 
Once getGridSize() is called, the random map generator has all of the information it needs to generate a map. The subsequent functions are used strictly to give to Civilization 4 the information about the generated map.

They are:

getTopLatitude(): Input: none. Output: An integer between -90 and 90. Does not appear to affect gameplay, but the higher this value is to 90, the more likely trees near the north of the map will be drawn with snow on them. Value needs to be higher than getBottomLatitude().

getBottomLatitude(): Input: none. Output: An integer between -90 and 90. Does not appear to affect gameplay, but the lower this number is, the more likely trees near the south of the map will be drawn with snow on them. Value needs to be lower than getTopLatitude().

To be pedantic: we can have getTopLatitude() return, say, -89, and getBottomLatitude() return, say, -90, and have maps with lots of snowy trees on them. Civ4, presumably, calculates the latitude of each horizontal line in the map based on these two values and performing interpolation; the higher the absolute value for a given line based on this calculation determines how frequently we have snowy trees.

getWrapX(): Input: None. Output: True if the map wraps on the X axis, otherwise false. A flat map would have "False" here; a cylindrical or toric map would have "True" here.

getWrapY(): Input: None. Output: True if the map wraps on the Y axis, otherwise false. A non-toric map would usually have "False" here; a toric map would have "True" here. Note that this should be set to "False" for maps played in vanilla Civ4 or in Civ4 Warlords; the rendering of toric maps was buggy until Beyond the Sword.

beforeGeneration(): A map script can safely function without this call

The following functions return one-dimensional arrays the correspond to the Civ4 map thusly:

The one-dimensional array starts in the lower left corner of the map, and goes left to right, bottom to top. For example, the point on the lower left of a 144x96 map will be the first element of the array (which is index number 0 because Python uses 0-indexed arrays). The point to the right of that will be the second element (element #1), and element #144 (the 145th element) will be the point directly above the lower left corner of the map.

In a 5x10 map (not that a map script can actually make a 5x10 map, but humor me), it goes like this:

Code:
40 41 42 43 44 45 46 47 48 49
30 31 32 33 34 35 36 37 38 39
20 21 22 23 24 25 26 27 28 29
10 11 12 13 14 15 16 17 18 19
 0  1  2  3  4  5  6  7  8  9

generatePlotTypes(): Input: None. Output: A flat one-dimensional array of "plot types". A single plot type can have one of four possible values:

  • CvPythonExtensions.PlotTypes.PLOT_OCEAN
  • CvPythonExtensions.PlotTypes.PLOT_LAND
  • CvPythonExtensions.PlotTypes.PLOT_HILLS
  • CvPythonExtensions.PlotTypes.PLOT_PEAK

generateTerrainTypes(): Input. None. Output: A flat one-dimensional array of integers. The values are 0: Grass 1: Plains 2: Desert 3: Tundra 4: Snow 5: Coast 6: Ocean 7: Peak 8: Hill.

addRivers(): Input: None. Output: None.

Rivers are placed between squares. To add a river to a given square (x, y) on the map, do something like this:

Code:
foo = CyGlobalContext()
bar = foo.getMap()
baz = bar.plot(x, y)

And then one or more of:

Code:
baz.setWOfRiver(True,CardinalDirectionTypes.CARDINALDIRECTION_SOUTH)
baz.setNOfRiver(True,CardinalDirectionTypes.CARDINALDIRECTION_EAST)
baz.setWOfRiver(True,CardinalDirectionTypes.CARDINALDIRECTION_NORTH)
baz.setNOfRiver(True,CardinalDirectionTypes.CARDINALDIRECTION_WEST)

The second argument determines which direction the river flows.

addLakes(): Input: None. Output: None.

To add a lake to the map at point (x,y), do something like this:

Code:
foo = CyGlobalContext()
bar = foo.getMap()
baz = bar.plot(x, y)
baz.setTerrainType(5, True, True)

5 corresponds to a "coast" (shallow water) terrain type.

addFeatures(): Input: None. Output: None.

This is one place where we can add forests, jungles, ice, oasis, and flood plains. To put an oasis at point (x,y), do something like:

Code:
foo = CyGlobalContext()
bar = foo.getMap()
baz = bar.plot(x, y)
baz.setFeatureType(2,0)

Possible values to give setFeatureType():

  • (0,0) Ice
  • (1,0) Jungle
  • (2,0) Oasis
  • (3,0) Flood plains
  • (4,0) Leafy forest
  • (4,1) Evergreen forest
  • (4,2) Snowy forest

addBonuses(): Input: None. Output: None.

This is one possible place to add bonuses (resources).

To put a bonus (resource) at point (x,y), do something like:

Code:
foo = CyGlobalContext()
bar = foo.getMap()
baz = bar.plot(x, y)
count = foo.getNumBonusInfos()
if count > 0:
    baz.setBonusType(0)

isBonusIgnoreLatitude(): A map script can safely run without declaring this function

addGoodies(): This adds huts to the map. If this is not defined, default huts are added. If it is defined, it can be used to add huts, or to disable huts being added (by simply returning without doing anything)

afterGeneration(): A map script can safely run without declaring this function

minStartingDistanceModifier(): A map script can safely run without declaring this function

assignStartingPlots(): Input: None. Output: None.

To make a given square a given player's starting plot, where (x, y) is where they will start and playerNum is the number for this player:

Code:
foo = CyGlobalContext()
bar = foo.getMap()
baz = bar.plot(x, y)
boo = foo.getPlayer(playerNum)
boo.setStartingPlot(baz, true)

The "normalize" functions are not used by Totestra:

Code:
def normalizeAddRiver():
    return
def normalizeAddLakes():
    return
def normalizeAddGoodTerrain():
    return
def normalizeRemoveBadTerrain():
    return
def normalizeRemoveBadFeatures():
    return
def normalizeAddFoodBonuses():
    return
def normalizeAddExtras():
    return
def normalizeRemovePeaks():
    return
 
Here is a very simple map script. This makes a tiny 12x12 map where most of the squares are desert; there are only two non-desert squares on the map, as well as two oases near these non-desert squares.

It sets the starting plots for only two players, and can be played as a very simple 2-player game.

Here is the source code:

Code:
# How simple can we make a functioning map script
# Placed in the public domain 2012 by Sam Trenholme

from CvPythonExtensions import *
import CvUtil
import CvMapGeneratorUtil 

def getNumCustomMapOptions():
        return 0
def getCustomMapOptionDefault(x):
        return 0
def isAdvancedMap():
        return 0
def getCustomMapOptionName(x):
        return ""
def getNumCustomMapOptionValues(x):
        return ""
def isRandomCustomMapOption(x):
        return False
def getCustomMapOptionDescAt(x):
        return ""
def isClimateMap():
        return 0
def isSeaLevelMap():
        return 0
def beforeInit():
        return
def getGridSize(x):
        return (3,3) # Tiny 12x12 map
def getTopLatitude():
        return 0
def getBottomLatitude():
        return 0
def getWrapX():
        return False
def getWrapY():
        return False
def generatePlotTypes():
        out = []
        for a in range(12 * 12):
                out.append(PlotTypes.PLOT_LAND)
        return out
def generateTerrainTypes():
        out = []
        for a in range(12 * 12):
                if a != 27 and a != 116:
                        out.append(2) # Desert
                else:
                        out.append(0) # Grassland
        return out
def addRivers():
        return
def addLakes():
        return
def addFeatures():
        gc = CyGlobalContext()
        map = gc.getMap()
        plot = map.plot(3,1)
        plot.setFeatureType(2,0)
        plot = map.plot(8,10)
        plot.setFeatureType(2,0)
def addBonuses():
        return
def addGoodies():
        return # Disables huts
def assignStartingPlots():
        gc = CyGlobalContext()
        map = gc.getMap()
        player1 = gc.getPlayer(0)
        plot = map.plot(3,3)
        player1.setStartingPlot(plot, true)
        player2 = gc.getPlayer(1)
        plot = map.plot(9,9)
        player2.setStartingPlot(plot, true)
def normalizeAddRiver():
        return
def normalizeAddLakes():
        return
def normalizeAddGoodTerrain():
        return
def normalizeRemoveBadTerrain():
        return
def normalizeRemoveBadFeatures():
        return
def normalizeAddFoodBonuses():
        return
def normalizeAddExtras():
        return
def normalizeRemovePeaks():
        return
 
Top Bottom