onCityDoTurn

Xyth

History Rewritten
Joined
Jul 14, 2004
Messages
4,106
Location
Aotearoa
From the comment in EventManager.py this function seems to be used when a city updates its production. Does anyone know how often this gets called? Is it once per city per turn? I'm trying to get an idea of the potential for slowdown from attaching python to it.
 
Once per turn. Like the names says. ;)

I guessed that, but does mean 'once per turn per city', 'once per turn per civilization' or even 'once per turn for all civilizations'?
 
Or before the beginning of the next turn, depending on how you look at it. :mischief:

Keep in mind that the AI's turns come after your doCityTurn() calls.

What are you trying to do that is having trouble?
 
We added a Capture Slave feature using python to our mod. I playtest the mod pretty regularly (we are on our fourth playtest), and have not had any lag or MAF issues until now. I will admit that we did add a bunch of python features, so while they may not be DIRECTLY the cause of the MAFs and lag (it is a pretty art heavy mod) I think they are contributing greatly. I think this because, when you compare the previous playtest to this one, the difference in performance is HUGE.

We are basically just trying to figure out which python changes are the most damaging to performance. Personally I think it has to do with the Machu Picchu wonder which uses all of this python:

Code:
	def onBuildingBuilt(self, argsList):
		'Building Completed'
		pCity, iBuildingType = argsList
		game = gc.getGame()          
		
		if ( iBuildingType == gc.getInfoTypeForString("BUILDING_MACHU_PICCHU") ):

			pPlayer = gc.getPlayer(pCity.plot().getOwner())
			iPID = pPlayer.getID()
			iTID = pPlayer.getTeam()
			iX = pCity.getX()
			iY = pCity.getY()

			### check inner city radius for Peaks (MachuPicchu Wonder prefers to be in the inner city radius of the home city of this wonder) ###
			lPeakPlotX = []
			lPeakPlotY = []
			for iXLoop in range(iX - 1, iX + 2, 1):
				for iYLoop in range(iY - 1, iY + 2, 1):
					pPlot = CyMap().plot(iXLoop, iYLoop)
					if ( pPlot.isPlayerCityRadius(iPID)==true ):
						if ( pPlot.getTeam()==iTID ):
							if ( pPlot.isPeak()==true  ):
								lPeakPlotX.append(iXLoop)
								lPeakPlotY.append(iYLoop)

			if ( len(lPeakPlotX) < 1 ):

				### check all city radius plots for Peaks if no inner peak available ###
				lPeakPlotX = []
				lPeakPlotY = []
				for iXLoop in range(iX - 2, iX + 3, 1):
					for iYLoop in range(iY - 2, iY + 3, 1):
						pPlot = CyMap().plot(iXLoop, iYLoop)
						if ( pPlot.isPlayerCityRadius(iPID)==true ):
							if ( pPlot.getTeam()==iTID ):
								if ( pPlot.isPeak()==true  ):
									lPeakPlotX.append(iXLoop)
									lPeakPlotY.append(iYLoop)

			if ( len(lPeakPlotX) > 0 ):
				it_machu_picchu = gc.getInfoTypeForString( "IMPROVEMENT_MACHU_PICCHU" )

				### set bonus for peaks in all city radius plots ###
				for iCity in range(pPlayer.getNumCities()):
					ppCity = pPlayer.getCity(iCity)
					iiX = ppCity.getX()
					iiY = ppCity.getY()
					### check all city radius plots for peak ###
					for iXLoop in range(iiX - 2, iiX + 3, 1):
						for iYLoop in range(iiY - 2, iiY + 3, 1):
							lPlot = CyMap().plot(iXLoop, iYLoop)
							if ( lPlot.isPlayerCityRadius(iPID)==true ):
								if ( lPlot.getTeam()==iTID ):
									if(lPlot.getTerrainType() != -1):
										if ( lPlot.isPeak()==true  ):
											lPlot.setImprovementType(it_machu_picchu)

				### now set bonus only with matchu picchu and remove matchu picchu (overlapping city radius) ###
				for iCity in range(pPlayer.getNumCities()):
					ppCity = pPlayer.getCity(iCity)
					iiX = ppCity.getX()
					iiY = ppCity.getY()
					### check all city radius plots for peak ###
					for iXLoop in range(iiX - 2, iiX + 3, 1):
						for iYLoop in range(iiY - 2, iiY + 3, 1):
							lPlot = CyMap().plot(iXLoop, iYLoop)
							if ( lPlot.isPlayerCityRadius(iPID)==true ):
								if ( lPlot.getTeam()==iTID ):
									if(lPlot.getTerrainType() != -1):
										if ( lPlot.isPeak()==true  ):
											if ( lPlot.getImprovementType() == it_machu_picchu ):
												CyGame().setPlotExtraYield(iXLoop, iYLoop, YieldTypes.YIELD_FOOD, 1)
												CyGame().setPlotExtraYield(iXLoop, iYLoop, YieldTypes.YIELD_PRODUCTION, 2)
												CyGame().setPlotExtraYield(iXLoop, iYLoop, YieldTypes.YIELD_COMMERCE, 1)
												lPlot.setImprovementType(-1)

												### ausgabe ###
												CyInterface().addMessage(iPID,True,0,' ','',1,'Art/Interface/Buttons/MachuPicchu_Python/MachuPicchu_Python_211.dds',ColorTypes(5), iXLoop, iYLoop,True,True)


				### set machu picchu on hill ###
				### get random Peak from list ###
				iRand = CyGame().getSorenRandNum( len(lPeakPlotX), "Random Peak")
				iXPeak = lPeakPlotX[iRand]
				iYPeak = lPeakPlotY[iRand]
				pPeakPlot = CyMap().plot(iXPeak, iYPeak)
				#pPeakPlot.setFeatureType(gc.getInfoTypeForString( "FEATURE_FOREST" ), 1)
				pPeakPlot.setImprovementType(gc.getInfoTypeForString( "IMPROVEMENT_MACHU_PICCHU" ))
				CyGame().setPlotExtraYield(iXPeak, iYPeak, YieldTypes.YIELD_FOOD, 1)
				CyGame().setPlotExtraYield(iXPeak, iYPeak, YieldTypes.YIELD_COMMERCE, 6)
				plot_culture_1_10 = int(pPeakPlot.getCulture(iPID) * 0.10)
				pPeakPlot.changeCulture(iPID, plot_culture_1_10, true)

				### ausgabe ###
				CyInterface().addMessage(iPID,false,10,CyTranslator().getText("TXT_KEY_MACHU_PICCHU_GAMETXT1",(iXPeak,iXPeak)),'',0,gc.getBuildingInfo(gc.getInfoTypeForString("BUILDING_MACHU_PICCHU")).getButton(),ColorTypes(5), iXPeak, iYPeak,True,True)
				### message: The Machu Picchu provides some benefits from Peaks! ###

				### set additional bonus for peaks in the wonder city radius ###
				for iXLoop in range(iX - 2, iX + 3, 1):
					for iYLoop in range(iY - 2, iY + 3, 1):
						pPlot = CyMap().plot(iXLoop, iYLoop)
						if ( pPlot.isPlayerCityRadius(iPID)==true ):
							if ( pPlot.getTeam()==iTID ):
								if(pPlot.getTerrainType() != -1):
									if ( pPlot.isPeak()==true  ):
										CyGame().setPlotExtraYield(iXLoop, iYLoop, YieldTypes.YIELD_COMMERCE, 1)

										### ausgabe ###
										if ( iXLoop != iXPeak or iYLoop != iYPeak ):
											CyInterface().addMessage(iPID,True,0," ",'0',1,'Art/Interface/Buttons/MachuPicchu_Python/MachuPicchu_Python_212.dds',ColorTypes(5), iXLoop, iYLoop,True,True)
										else:
											CyInterface().addMessage(iPID,True,0," ",'0',1,'Art/Interface/Buttons/MachuPicchu_Python/MachuPicchu_Python_218.dds',ColorTypes(5), iXLoop, iYLoop,True,True)

First of all, it is a lot of python code, secondly I think what the wonder does is causing a lot of memory to be used. Basically when you build the wonder it gives commerce to every peak tile in your empire. I think that the city that builds it gets extra bonuses on their peaks, plus a special improvement that gives even more yields. So I'm thinking this is what is creating issues for us.

He's asking about onCityDoTurn becaue it requires a python callback, or something like that, I have two things in there. One is for the slaves, I believe it is for a Slave Specialist which I have actually removed so I could probably remove this anyway (at least I think that is what this does), but this is why Xyth is asking:

Code:
	def onCityDoTurn(self, argsList):
		'City Production'
		pCity = argsList[0]
		iPlayer = argsList[1]
[COLOR="Red"]		###from here
		pPlayer = gc.getPlayer(iPlayer)
		if not pPlayer.isCivic(gc.getInfoTypeForString("CIVIC_SLAVERY")):
			iSlaveSpecialist = gc.getInfoTypeForString("SPECIALIST_SLAVE")
			iSlaveSpecialistInCity = pCity.getSpecialistCount(iSlaveSpecialist)
			pCity.alterSpecialistCount(iSlaveSpecialist,-iSlaveSpecialistInCity)
				###to here[/COLOR]
		
# no anger defying UN resolutions start #
		iGovernmentCivicOption = CvUtil.findInfoTypeNum(gc.getCivicOptionInfo,gc.getNumCivicOptionInfos(),'CIVICOPTION_GOVERNMENT')
		iPoliceState = CvUtil.findInfoTypeNum(gc.getCivicInfo,gc.getNumCivicInfos(),'CIVIC_POLICE_STATE')
		pPlayer = gc.getPlayer(iPlayer)
		iGovernmentCivic = pPlayer.getCivics(iGovernmentCivicOption)

		if (iGovernmentCivic == iPoliceState):
			pCity.changeDefyResolutionAngerTimer(pCity.getDefyResolutionAngerTimer())
# no anger defying UN resolutions end #

I put the slave part in red, the other part is, well obviously no anger from defying UN resolutions while under Police State civic.
 
Code:
	def onCityDoTurn(self, argsList):
		'City Production'
		pCity = argsList[0]
		iPlayer = argsList[1]
		...
# no anger defying UN resolutions start #
		iGovernmentCivicOption = CvUtil.findInfoTypeNum(gc.getCivicOptionInfo,gc.getNumCivicOptionInfos(),'CIVICOPTION_GOVERNMENT')
		iPoliceState = CvUtil.findInfoTypeNum(gc.getCivicInfo,gc.getNumCivicInfos(),'CIVIC_POLICE_STATE')
		pPlayer = gc.getPlayer(iPlayer)
		iGovernmentCivic = pPlayer.getCivics(iGovernmentCivicOption)

		if (iGovernmentCivic == iPoliceState):
			[COLOR="Red"]pCity.changeDefyResolutionAngerTimer(-pCity.getDefyResolutionAngerTimer())[/COLOR]
# no anger defying UN resolutions end #
You need to add the "-" before pCity.getDefyResolutionAngerTimer() to get it work. It causes "few" extra angry people when it's missing.
Sorry, it was my mistake when I wrote that...
 
The Machu Picchu code only runs once when the building is built. All other times a building is built it will run instantaneously (simple if X == Y comparison).

The onCityDoTurn() function could be sped up in a few ways:

1. Replace all uses of findInfoTypeNum() with getInfoTypeForString(). Do this everywhere.

2. Look up these values once in __init__() and store them in variables (minor speed improvement).

3. Change the anger timer for defying resolutions only once when a resolution is actually defied. You would do this in onBeginPlayerTurn() by testing any city (e.g. the capital) for a non-zero anger timer. If it was non-zero and they are running the civic, loop over all cities setting their anger timers to zero. If not, do nothing.
 
The onBuildingBuilt part of the Machu Picchu code is not the issue, it's this part (and a bit of Next War) in GameUtils.py that I'm suspicious of as I think it requires a Python Callback to be enabled:

Code:
	def cannotConstruct(self,argsList):
		pCity = argsList[0]
		eBuilding = argsList[1]
		bContinue = argsList[2]
		bTestVisible = argsList[3]
		bIgnoreCost = argsList[4]
		
		# player can't build an arcology if they have shielding or advanced shielding
		if eBuilding == gc.getInfoTypeForString("BUILDING_ARCOLOGY"):
			if pCity.getNumRealBuilding(gc.getInfoTypeForString("BUILDING_ARCOLOGY_SHIELDING")) or pCity.getNumRealBuilding(gc.getInfoTypeForString("BUILDING_DEFLECTOR_SHIELDING")):
				return True
		
		# player can't build shielding if they have advanced
		if eBuilding == gc.getInfoTypeForString("BUILDING_ARCOLOGY_SHIELDING"):
			if pCity.getNumRealBuilding(gc.getInfoTypeForString("BUILDING_DEFLECTOR_SHIELDING")):
				return True
				
		### MachuPicchu Mod begins ###
		###########################################################################################

		if ( eBuilding == gc.getInfoTypeForString("BUILDING_MACHU_PICCHU") ):

			### find peaks within the city radius controlled by your team ###
			pPlayer = gc.getPlayer(pCity.plot().getOwner())
			iPID = pPlayer.getID()
			iTID = pPlayer.getTeam()
			iX = pCity.getX()
			iY = pCity.getY()
			for iXLoop in range(iX - 2, iX + 3, 1):
				for iYLoop in range(iY - 2, iY + 3, 1):
					pPlot = CyMap().plot(iXLoop, iYLoop)
					if ( pPlot.isPlayerCityRadius(iPID)==true ):
						if ( pPlot.getTeam()==iTID ):
							if ( pPlot.isPeak()==true  ):
								return False
			return True

		###########################################################################################
		### MachuPicchu Mod ends ###                 

		return False

The onCityDoTurn() function could be sped up in a few ways:

1. Replace all uses of findInfoTypeNum() with getInfoTypeForString(). Do this everywhere.

2. Look up these values once in __init__() and store them in variables (minor speed improvement).

3. Change the anger timer for defying resolutions only once when a resolution is actually defied. You would do this in onBeginPlayerTurn() by testing any city (e.g. the capital) for a non-zero anger timer. If it was non-zero and they are running the civic, loop over all cities setting their anger timers to zero. If not, do nothing.

1 I can do for you Capo. 2 and 3 I don't know how to do, I'll give them a try though.

He's asking about onCityDoTurn becaue it requires a python callback, or something like that, I have two things in there. One is for the slaves, I believe it is for a Slave Specialist which I have actually removed so I could probably remove this anyway (at least I think that is what this does), but this is why Xyth is asking:

That sections checks if a civ is running the slavery civic and if not, removes any slave specialists in each city. Sounds like you can just delete it entirely.
 
Oh my, yes that callback will eat a lot of time. One thing you can do is mark each city as to whether there are peaks in its radius. That's what it does, right? I didn't read it just saw the nested loops. In onCityBuilt (or wherever it founds a city) you would scan its radius and set a flag if you find at least one peak. This won't ever change during the life of the game so there's no need recalculating it constantly.

The easiest way to store this information is to stick a value in the city's script data: a string attached to every CyXXX game object:

Code:
[B]# in onCityBuilt[/B]
if ...find a peak...
    pCity.setScriptData("yes")
else:
    pCity.setScriptData("no")

[B]# in cannotConstruct[/B]
if eBuilding == MACHU_PICCHU and pCity.getScriptData() == "no":
    return True
 
before trying to speed something up you should test how much performance it really needs. Simply put a "return false" before the ### MachuPicchu Mod begins ###. Since this doesn't break saves you can get instant results by comparing two turn times with and without the modification (well compare some more turns to be sure). I doubt that this loop takes more than 0.1 ms a turn in total but you never know.
 
The cannotConstruct callback is used a lot by the AI code, but I agree that timing before optimization is a good idea. if you're using BUG for your mod you can use BugUtil.Timer() to calculate and report the timing.

Code:
timer = BugUtil.Timer("look for peaks")
... your code to time ...
timer.log()
 
Back
Top Bottom