[TUTORIAL] How to Convert a Civ 4 Mapscript

TC01

Deity
Joined
Jun 28, 2009
Messages
2,216
Location
Irregularly Online
How to Convert a Civ 4 Mapscript

Since I actually did this, I chose to make a tutorial about how to do it. The point of this tutorial is to show how to convert a Civ 4 mapscript to Civ4Col (for people who aren't amazing mapscripters, but might know a little python). There are actually issues you will run into, depending on the mapscript, so you might not be able to just drop the mapscript into Civ 4. Sometimes, you can. But not always, especially if you're working with a regional mapscript.

This guide will hopefully help fix any issues in converting a regional mapscript to Civilization IV Colonization.

So I'm going to be converting Oasis.py. It was one of the original Civ 4 scripts, and simulates a Sahara-like area with water to the north. Let's assume we're making this to simulate the colonization of Egypt (perhaps for Mare Nostrum?) Anyway, the first step is to copy Oasis.py from Civilization IV\Public Maps into Civilization IV Colonization\Public Maps.

Note that I'm not using the Warlords/BTS version. As far as I can tell, the only thing that script changes is it adds the ability to "wrap" the map and to change the resource placement system Both of these I don't really care about, because Colonization doesn't use them (The Standard vs. Balanced resources and the edge wrapping, that is). Therefore I suggest you take your maps from Civ 4 vanilla, not Civ 4BTS/Warlords.

Anyway, after you've copied the map, open the copy of the map in your editing program.

For now, we'll keep the script the same name as Oasis.py (until I think of a better one). We do want to change this, the first function you see:

Code:
def getDescription():
	return "TXT_KEY_MAP_SCRIPT_OASIS_DESCR"

It is referencing a text key from Civ IV vanilla which is not in Civ 4 Col. We want to add our own text. I'm going to add new text, if you want you can add the original Oasis script description, which reads "The desert region in the middle holds the strategic minerals. The fertile region in the north has less land (but more resources) than the south."

Code:
def getDescription():
	return "[COLOR="Red"]This is a conversion of the Oasis mapscript from Civilization IV."[/COLOR]

Now, depending on the script, you may have custom sizes per WorldSize. You may also reference the WorldSizes elsewhere through the file. Since Civ IV has the "Duel" Worldsize, and Civ4Col does not, we must remove any reference to WORLDSIZE_DUEL from Oasis.py. In Oasis, it appears several times. A search for "Duel" should first return the def getGridSize function. It should actually be on the screen of your editor as well.

Code:
def getGridSize(argsList):
	# Grid sizes reduced. Smaller maps reduced two steps. Larger maps reduced one and a half steps.
	grid_sizes = {
		WorldSizeTypes.WORLDSIZE_DUEL:      (6,4),
		WorldSizeTypes.WORLDSIZE_TINY:      (8,5),
		WorldSizeTypes.WORLDSIZE_SMALL:     (10,6),
		WorldSizeTypes.WORLDSIZE_STANDARD:  (14,9),
		WorldSizeTypes.WORLDSIZE_LARGE:     (18,11),
		WorldSizeTypes.WORLDSIZE_HUGE:      (23,14)
	}

To remove it, we're only going to comment it out with a "#" sign, so it looks like this:

Code:
def getGridSize(argsList):
	# Grid sizes reduced. Smaller maps reduced two steps. Larger maps reduced one and a half steps.
	grid_sizes = {
[COLOR="red"]#		WorldSizeTypes.WORLDSIZE_DUEL:      (6,4),[/COLOR]
		WorldSizeTypes.WORLDSIZE_TINY:      (8,5),
		WorldSizeTypes.WORLDSIZE_SMALL:     (10,6),
		WorldSizeTypes.WORLDSIZE_STANDARD:  (14,9),
		WorldSizeTypes.WORLDSIZE_LARGE:     (18,11),
		WorldSizeTypes.WORLDSIZE_HUGE:      (23,14)
	}

It will also appear in the def generatePlotsByRegion function, the def addBonusType function, and the def addRivers function in Oasis.py. It may appear in more, less, or none of those places in other mapscripts. But wherever you see it, either delete or comment it out.

Now, we should run a test. With logging and python exceptions enabled, we can quickly find errors. There may be syntax differences between a Civ 4 function or a Civ 4 Col function.

So, start a game with Oasis.py. We get a python exception that the "pPlot.getBonusType(-1)" function is incorrect on line 624. Specifically, here, highlighted in blue:

Code:
		else:
			NiTextOut("Placing Desert Maize (Corn - Python Oasis) ...")
			crops = CyFractal()
			crops.fracInit(iW, iH, 7, dice, 0, -1, -1)
			iCropsBottom1 = crops.getHeightFromPercent(24)
			iCropsTop1 = crops.getHeightFromPercent(27)
			iCropsBottom2 = crops.getHeightFromPercent(73)
			iCropsTop2 = crops.getHeightFromPercent(75)
			cropNorth = int(iH * 0.66)
			cropSouth = int(iH * 0.32)
			for y in range(cropSouth, cropNorth):
				for x in range(iW):
					# Fractalized placement of crops
					pPlot = map.plot(x,y)
					if (not pPlot.isFlatlands()) or pPlot.getFeatureType() != -1: continue
					cropVal = crops.getHeight(x,y)
					if [COLOR="Blue"]pPlot.getBonusType(-1)[/COLOR] == -1 and ((cropVal >= iCropsBottom1 and cropVal <= iCropsTop1) or (cropVal >= iCropsBottom2 and cropVal <= iCropsTop2)):
						map.plot(x,y).setBonusType(iBonusType)
			return None

The issue is because in Civ 4, you need to identify the player who can see the bonus. So, if you said "pPlot.getBonusType(0)", you wouldn't be checking if the bonus was actually on the plot but instead if you (well, the human player in Single Player) could see it. (Using -1, as you see here, checks if the bonus is actually there). But in Colonization, everyone can see all bonuses. So this bit of syntax was taken out. We must find change the code above to:

Code:
		else:
			NiTextOut("Placing Desert Maize (Corn - Python Oasis) ...")
			crops = CyFractal()
			crops.fracInit(iW, iH, 7, dice, 0, -1, -1)
			iCropsBottom1 = crops.getHeightFromPercent(24)
			iCropsTop1 = crops.getHeightFromPercent(27)
			iCropsBottom2 = crops.getHeightFromPercent(73)
			iCropsTop2 = crops.getHeightFromPercent(75)
			cropNorth = int(iH * 0.66)
			cropSouth = int(iH * 0.32)
			for y in range(cropSouth, cropNorth):
				for x in range(iW):
					# Fractalized placement of crops
					pPlot = map.plot(x,y)
					if (not pPlot.isFlatlands()) or pPlot.getFeatureType() != -1: continue
					cropVal = crops.getHeight(x,y)
					if [COLOR="Red"]pPlot.getBonusType() == -1[/COLOR] and ((cropVal >= iCropsBottom1 and cropVal <= iCropsTop1) or (cropVal >= iCropsBottom2 and cropVal <= iCropsTop2)):
						map.plot(x,y).setBonusType(iBonusType)
			return None

All we have to do is remove the -1. You should search the mapscript for other instances of .getBonusType, and when you find them, remove the -1 as well. It only appears one other time in Oasis.py, on line 685:

Code:
		for x in range(iW):
			for y in range(iH):
				# First check the plot for an existing bonus.
				pPlot = map.plot(x,y)
				if [COLOR="Blue"]pPlot.getBonusType(-1) != -1[/COLOR]: continue # to next plot.
				# Check plot type and features for eligibility.
				if (pPlot.canHaveBonus(iBonusType, True) and unforced): pass
				elif forceHills and pPlot.isHills(): pass
				elif forceForest and pPlot.getFeatureType() == gc.getInfoTypeForString("FEATURE_FOREST"): pass
				elif forceGrass and pPlot.isFlatlands() and pPlot.getTerrainType() == gc.getInfoTypeForString("TERRAIN_GRASS") and pPlot.getFeatureType() == -1: pass
				else: continue # to next plot.

Again, we have to change it to this:

Code:
		for x in range(iW):
			for y in range(iH):
				# First check the plot for an existing bonus.
				pPlot = map.plot(x,y)
				if [COLOR="Red"]pPlot.getBonusType() != -1[/COLOR]: continue # to next plot.
				# Check plot type and features for eligibility.
				if (pPlot.canHaveBonus(iBonusType, True) and unforced): pass
				elif forceHills and pPlot.isHills(): pass
				elif forceForest and pPlot.getFeatureType() == gc.getInfoTypeForString("FEATURE_FOREST"): pass
				elif forceGrass and pPlot.isFlatlands() and pPlot.getTerrainType() == gc.getInfoTypeForString("TERRAIN_GRASS") and pPlot.getFeatureType() == -1: pass
				else: continue # to next plot.

Now, we try another test. It works! However, there are two issues. One: The mapscript is incredibly small, even on huge. Two: You all start on land.

The first issue is because of this function. We altered it earlier to remove Duel from the list of mapsizes:

Code:
def getGridSize(argsList):
	# Grid sizes reduced. Smaller maps reduced two steps. Larger maps reduced one and a half steps.
	grid_sizes = {
#		WorldSizeTypes.WORLDSIZE_DUEL:      (6,4),
		WorldSizeTypes.WORLDSIZE_TINY:      (8,5),
		WorldSizeTypes.WORLDSIZE_SMALL:     (10,6),
		WorldSizeTypes.WORLDSIZE_STANDARD:  (14,9),
		WorldSizeTypes.WORLDSIZE_LARGE:     (18,11),
		WorldSizeTypes.WORLDSIZE_HUGE:      (23,14)
	}

	if (argsList[0] == -1): # (-1,) is passed to function on loads
		return []
	[eWorldSize] = argsList
	return grid_sizes[eWorldSize]

Colonization maps and Civilization IV maps have different default map sizes. The easiest way to solve the problem is to delete or comment out this entire function. Now, if you wanted to make it work with your mapscript's default values (the ones above) you need to multiply each value by 4. Below, I commented out to show the difference. Before, only the Duel line was commented out. Now, the whole function is. (As I said, if you wanted to, you could multiply each value by 4 instead).

Code:
[COLOR="Red"]#def getGridSize(argsList):
#	# Grid sizes reduced. Smaller maps reduced two steps. Larger maps reduced one and a half steps.
#	grid_sizes = {[/COLOR]
#		WorldSizeTypes.WORLDSIZE_DUEL:      (6,4),
[COLOR="red"]#		WorldSizeTypes.WORLDSIZE_TINY:      (8,5),
#		WorldSizeTypes.WORLDSIZE_SMALL:     (10,6),
#		WorldSizeTypes.WORLDSIZE_STANDARD:  (14,9),
#		WorldSizeTypes.WORLDSIZE_LARGE:     (18,11),
#		WorldSizeTypes.WORLDSIZE_HUGE:      (23,14)
#	}

#	if (argsList[0] == -1): # (-1,) is passed to function on loads
#		return []
#	[eWorldSize] = argsList
#	return grid_sizes[eWorldSize][/COLOR]

The second issue (starting on land) occurs because you are using the "def findStartingPlot" function to get a starting point. This function tries to place you in the center of the map to start, i.e., on land. So we must comment out this function. Here is what it looks like before:

Code:
def findStartingPlot(argsList):
	[playerID] = argsList

	def isValid(playerID, x, y):
		teamID = CyGlobalContext().getPlayer(playerID).getTeam()
		iH = CyMap().getGridHeight()
		
		if int(teamID/2) * 2 == teamID: # Even-numbered team.
			isOdd = False
		else:
			isOdd = True
		
		if isOdd and y >= iH * 0.7:
			return true
		
		if not isOdd and y <= iH * 0.3:
			return true
			
		return false
	
	return CvMapGeneratorUtil.findStartingPlot(playerID, isValid)

And here it is commented out.

Code:
[COLOR="Red"]#def findStartingPlot(argsList):
#	[playerID] = argsList

#	def isValid(playerID, x, y):
#		teamID = CyGlobalContext().getPlayer(playerID).getTeam()
#		iH = CyMap().getGridHeight()
		
#		if int(teamID/2) * 2 == teamID: # Even-numbered team.
#			isOdd = False
#		else:
#			isOdd = True
		
#		if isOdd and y >= iH * 0.7:
#			return true
		
#		if not isOdd and y <= iH * 0.3:
#			return true
			
#		return false
	
#	return CvMapGeneratorUtil.findStartingPlot(playerID, isValid)[/COLOR]

We've now got the conversion done- that is, all errors have been fixed and you could play a game. There's now two more things to get working. First, we need to add Light Forest and Marsh to the script. Then, we could have to fix the resource placement, since Oasis.py uses regional resource placement. (I'll revisit this later after adding Light Forest and Marsh). These aren't essential for a first version of the mapscript, but it's good to have all of your resources and terrains on the map.

So first, we'll add the Light Forest. There's a "backdoor" way to do it, by using a random number right before we place a Forest, and then potentially making that a light forest instead. I recommend doing this if you don't really understand a mapscript. This is the method we're going to use.

First, though, let's define the Light Forest in the def _initFeatureTypes function:

Code:
	def __initFeatureTypes(self):
		self.featureJungle = self.gc.getInfoTypeForString("FEATURE_JUNGLE")
		self.featureForest = self.gc.getInfoTypeForString("FEATURE_FOREST")
		self.featureOasis = self.gc.getInfoTypeForString("FEATURE_OASIS")
[COLOR="Red"]		self.featureLightForest = self.gc.getInfoTypeForString("FEATURE_LIGHT_FOREST")[/COLOR]

Then, below, we see this function:

Code:
	def addForestsAtPlot(self, pPlot, iX, iY, lat):
		# No evergreens.
		if pPlot.canHaveFeature(self.featureForest):
			if self.forests.getHeight(iX+1, iY+1) <= self.iForestLevel:
				pPlot.setFeatureType(self.featureForest, 0)

It is called, under certain conditions, to add a Light Forest to the plot. We want to get a random number right before we add the forest. If it is above some number, we'll make it a light forest, otherwise, it's a normal forest. Here is the added function.

Code:
	def addForestsAtPlot(self, pPlot, iX, iY, lat):
		# No evergreens.
		if pPlot.canHaveFeature(self.featureForest):
			if self.forests.getHeight(iX+1, iY+1) <= self.iForestLevel:
[COLOR="red"]				game = CyGame()
				iForestRnd = game.getSorenRandNum(20, "Light Forests")
				if iForestRnd >= 12:
					pPlot.setFeatureType(self.featureLightForest, 0)
				else:
					pPlot.setFeatureType(self.featureForest, 0)[/COLOR]

This way, instead of needing to add the Light Forest functions to the map, and define where they should appear, all you have to do is take where the creator wanted a Forest to appear, and have a "chance" of making it a light forest.

A test reveals this works.

Now, we need to add marsh. Again, instead of "adding" it the "real" way, I'm going to work in the feature placer section of the script. We first define the Marsh in the def _initFeatureTypes function, like this:

Code:
	def __initFeatureTypes(self):
		self.featureJungle = self.gc.getInfoTypeForString("FEATURE_JUNGLE")
		self.featureForest = self.gc.getInfoTypeForString("FEATURE_FOREST")
		self.featureOasis = self.gc.getInfoTypeForString("FEATURE_OASIS")
		self.featureLightForest = self.gc.getInfoTypeForString("FEATURE_LIGHT_FOREST")
		
[COLOR="Red"]		self.terrainMarsh = self.gc.getInfoTypeForString("TERRAIN_MARSH")[/COLOR]

Now, we want to do the same thing as we did for the Light Forest. We get a random number when the def addFeaturesAtPlot function is checking if it should call the actual function to place a Jungle. Depending on the number, we make the plot a Marsh. Here is what the part of the function relating to Jungles looks like:

Code:
		if (pPlot.getFeatureType() == FeatureTypes.NO_FEATURE):
			# Jungles only in the deep south or in the Oasis!
			if lat < 0.16:
				self.addJunglesAtPlot(pPlot, iX, iY, lat)
			elif lat > 0.32 and lat < 0.65 and (pPlot.getTerrainType() == self.gc.getInfoTypeForString("TERRAIN_GRASS")):
				pPlot.setFeatureType(self.featureJungle, -1)

Now, here it is with the added code:

Code:
		if (pPlot.getFeatureType() == FeatureTypes.NO_FEATURE):
			# Jungles only in the deep south or in the Oasis!
			if lat < 0.16:
[COLOR="Red"]				if not pPlot.isWater():
					if not pPlot.isHills():
						game = CyGame()
						iSwampRnd = game.getSorenRandNum(20, "Marsh Placement")
						if iSwampRnd >= 10:
							pPlot.setTerrainType(self.terrainMarsh,true,true)[/COLOR]
				self.addJunglesAtPlot(pPlot, iX, iY, lat)
			elif lat > 0.32 and lat < 0.65 and (pPlot.getTerrainType() == self.gc.getInfoTypeForString("TERRAIN_GRASS")):
				pPlot.setFeatureType(self.featureJungle, -1)

Notice I also check if the plot is water or a hill. This is just a good habit- you don't want to turn water tiles into Marsh, and Marsh on hill doesn't look that good. You can also change the cutoff for the random number (10, currently) to increase/decrease the amount of marsh.

We actually are done. Remember how I said that we might have to add resources? Well, we actually don't, since Oasis.py uses all of the resources in Colonization. If you look at the "Master Key" of the "Sahara Regional Bonus Placement" for Oasis.py, you see:

Code:
resourcesInOasis = ('BONUS_ALUMINUM', 'BONUS_IRON', 'BONUS_OIL', 'BONUS_STONE',
                     'BONUS_GOLD', 'BONUS_INCENSE', 'BONUS_IVORY')
resourcesInNorth = ('BONUS_HORSE', 'BONUS_MARBLE', 'BONUS_FUR', 'BONUS_SILVER',
                    'BONUS_SPICES', 'BONUS_WINE', 'BONUS_WHALE', 'BONUS_CLAM',
                    'BONUS_CRAB', 'BONUS_FISH', 'BONUS_SHEEP', 'BONUS_WHEAT')
resourcesInSouth = ('BONUS_DYE', 'BONUS_FUR', 'BONUS_GEMS', 'BONUS_SILK', 'BONUS_SUGAR',
                    'BONUS_BANANA', 'BONUS_DEER', 'BONUS_PIG', 'BONUS_RICE')
resourcesToEliminate = ()

resourcesToForce = ('BONUS_FUR', 'BONUS_SILVER', 'BONUS_DEER')
forcePlacementInForest = ('BONUS_FUR')
forcePlacementOnGrass = ('BONUS_DEER')
forcePlacementOnHills = ('BONUS_SILVER')
forceRarity = ('BONUS_SILK', 'BONUS_WHALE')
forceAbundance = ('BONUS_FUR', 'BONUS_IRON', 'BONUS_IVORY', 'BONUS_HORSE', 'BONUS_OIL')
oasisCorn = ('BONUS_CORN')

Most Col bonuses are listed there. The ones that aren't appear anyway, as they aren't listed in resourcesToEliminate. We might have to fix something if a resource was listed in resourcesToEliminate, but since nothing is there, we don't have to add anything in.

One thing we could do is remove the resources that are not in Colonization, like Horses or Oil. It might increase the amount of other bonuses (or it might not), as I'm not sure. And then you make this section look like this:

Code:
resourcesInOasis = ('BONUS_IRON')
resourcesInNorth = ('BONUS_FUR', 'BONUS_SILVER', 'BONUS_CLAM', 'BONUS_CRAB', 'BONUS_FISH')
resourcesInSouth = ('BONUS_FUR', 'BONUS_SUGAR', 'BONUS_BANANA', 'BONUS_DEER')
resourcesToEliminate = ()

resourcesToForce = ('BONUS_FUR', 'BONUS_SILVER', 'BONUS_DEER')
forcePlacementInForest = ('BONUS_FUR')
forcePlacementOnGrass = ('BONUS_DEER')
forcePlacementOnHills = ('BONUS_SILVER')
forceRarity = ()
forceAbundance = ('BONUS_FUR', 'BONUS_IRON')
oasisCorn = ('BONUS_CORN')

I don't actually know if this has an effect or not, though.

Also, we could have removed this entire resource placement system. If you wanted to do this, all you would have to do is comment out the def addBonus function, which is right under all of these lists. However, doing so would mess up the idea of the mapscript- corn wouldn't appear in the oasis, iron wouldn't appear in the desert only... so I wouldn't advise doing that.

We're finished! Congratulations, you've converted a Civilization IV mapscript to Colonization!

The process might be different depending on what mapscript you're dealing with. However, generally, it's the same- go through the map, remove all references to the Duel mapsize, if the map has a set of custom dimensions, either remove them or adjust them (*4), and then see if you get any errors when launching the map. Then go through the script and fix whatever those errors are that are preventing it from loading (if there are any).

Questions? Post them here.
 
A very useful tutorial! Thank you very much, TC01. :)
 
You're welcome. :)

I updated it explaining the difference between Civ gridsizes and Col gridsizes.

Then, I released the mapscript I converted in this tutorial as NorthAfrica.py, if you're interested. It might be more useful in a mod because of the massive amount of land versus amount of water, though.
 
I am working on adding some new maps to the game and I have one question atm. The Civ4 maps all have Civ4 ocean terrains, what do I need to change to get the Col ocean terrains as they are lots prettier :)

Mapscripts with "wrap x" or "wrap y" (or both) enabled have a weird colored ocean terrain.

If you disable wrap x and wrap y, the weird ocean goes away and is replaced by the Colonization ocean.
 
Mapscripts with "wrap x" or "wrap y" (or both) enabled have a weird colored ocean terrain.

If you disable wrap x and wrap y, the weird ocean goes away and is replaced by the Colonization ocean.

I also had the same problem with ocean terrains color when used "wrap X".

There is a simple solution of this error. You have to edit water_001.dds located in ...\Assets\Art\Terrain\Water\RRWater directory. After couple attempts you can create the water coloration as you like.

Please, remember, that the real water coloration you could see only in the game. Preview in any graphics viewer gives a little bit distorted coloration.

You can try this edited file.

 
I made a semi-working conversion of the archipelago map, but it's spawning all europeans on the same time, which is also the only "europe" tile.

Any thoughts on how to correct this issue?
 
Top Bottom