Is their an easier way to debug civ 4 functions in python

Capt_Blondbeard

Chieftain
Joined
Dec 20, 2008
Messages
49
Location
Mannheim, Germany
I'm still pretty much of a beginner with python for civ 4, but most of my errors seem to be "civ 4" function related, not problems with python logic (ie: infinite loops, etc.) IDLE has a function to check for python errors, but this doesn't help with "civ 4" errors.

Is there way to, for example step through python functions in a game, or some other way to debug, other than trial and error?

My current problem - working through the TC01 tutorial, importing stuff from a newly created file - causes the whole interface to crash. Changing one line at a time in each file, saving, running the game, having it not work, not knowing what which line of which file is causing the problem, trial and error is damn tedious - is there a better way to zero in on what is causing the problem??
:confused:
 
Alternatively, set LoggingEnabled to 1 in the Civ4.ini file and Hide Python Exceptions to 1. Now all the exceptions will be written to My Games / BTS / Logs / PythonErr.log.

Other things that have helped me: I created a bunch of logging functions in BugUtil for BUG Mod that let you dump out information to PythonDbg.log.
 
You can first run the game, and edit the python files, while it runs ;).
People say this, but it's never been true for me. I've always had to restart the game for python changes I've made. The two specific instances I can think of was changing the civilopedia display code, and Revolutions code. What python code can you mess with that doesn't require restarting the game to take effect?
 
People say this, but it's never been true for me. I've always had to restart the game for python changes I've made. The two specific instances I can think of was changing the civilopedia display code, and Revolutions code. What python code can you mess with that doesn't require restarting the game to take effect?

It works for me, but I have to exit the civilopedia and re-enter it. And usually, the game gives me some BUG error messages. If I edit python more than 10 times in a game, it usually locks up. :p
 
Also, I read that mapscripts aren't reloaded so you need to exit the game and restart it. Just saying :p
 
This has never worked reliably for me either. I see the effects of it and even created a new PythonReloaded event for BUG, but I see odd behavior. For one thing, the system doesn't initiate another initialization sequence, so you have objects like the event manager that don't get recreated.

This means that __init__() functions aren't executed again, but the module-level code is rerun. It leaves the system in a half-built state which is why you get BUG errors. I designed BUG like I build all my applications with a well-defined startup process. That process doesn't handle having all the modules execute some of their code again well.
 
I've had to reload the game too. Civ's python reloading only works for very simple changes in select places (if you've changed a function that is called dozens of times a second such as unitCannotMoveInto, forget it - the game's too busy calling the function and hitting the error you fixed to reload it). I've always had to reload from the desktop as simply letting it reload will break the game (my mod uses VERY extensive python code which is not reload friendly) and exiting to the main menu results in either a lockup or a CTD (can't remember which).
 
Heh heh, thats funny, I've noticed the same thing - I'll edit python with a game open, and SOMETIMES get the message "reloading python module", but sometimes not. I still haven't figured out why and when it will reload.

But back to my original question: Do the error logs help with civ 4 related errors - by that I mean errors that don't cause a "crash" but just don't work the way you want them to. I'll insert one of my "typical" problem children below

PHP:
if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 301 and inputClass.getData2() == 301):
			self.iPushedButtonUnit = g_pSelectedUnit
			pUnit = g_pSelectedUnit
			iX = self.iPushedButtonUnit.getX()
			iY = self.iPushedButtonUnit.getY()
			pMerchantLocation = CyMap().plot(iX, iY)
			pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
			if pMerchantLocation.isCity():
				TradeCity = pMerchantLocation.getPlotCity ()
				pTradePlayer = gc.getPlayer(TradeCity.getOwner()) # check syntax
				if pTradePlayer != pUnitOwner : # This doesn't work owner definitions probably different
					pUnitOwner.changeGold( game.getSorenRandNum(80,"Trade Result"))
					pUnitTeam = pUnitOwner.getTeam()
					CyInterface().addImmediateMessage ("%i "(pUnitTeam))
					pUnitTeam.changeResearchProgress (0,60,pUnitOwner) # Doesn't work
					gc.getTeam(pUnitOwner.getTeam()).setHasTech(0,True,pUnitOwner,False,False)
					g_pSelectedUnit.changeMoves(60)

Ok, this for a new unit, the "minor merchant" that I'm working on - what I want do is create a unit for before cities can set to produce gold, or research, that will randomly provide a boost to gold or research, or hammers, or culture. But for now I'm trying to do research or gold.

So, it runs, doesn't crash, but also doesn't do what its supposed to, for example the function thats supposed to check that you can run a mission in your own city doesn't work, and neither of the research function function work. The gold works. I suspect for the city check that it is comparing different types of variables - maybe one is a team and the other is a player value.

For the second problem, maybe the arguments are not specified correctly.

How do you guys usually go about programming/debugging these sorts of problems?
Is there some way to see what python is doing when running? For example to get python to list the values to be sure you are not comparing apples and oranges with an if statement, etc.
 
First, make sure you keep the BTS Python API handy. This will tell you that you need a PlayerTypes instead of a CyPlayer for that call to CyTeam::changeResearchProgress(). All of the FooTypes types are just integers from 0 to N-1, often with -1 signifying NO_FOO.

As for why the comparison is failing, this is more subtle. In the C++ DLL, each player has a single CvPlayer object that holds its information, and one such piece is its ID--a PlayerTypes value. When you call CyGlobalContext::getPlayer(PlayerTypes) it returns a CyPlayer which is a new Python object that holds a reference to a CyPlayer C++ object that holds a reference to the single CvPlayer C++ object. Yes, there is that much indirection.

The key is that every call to getPlayer() produces a new CyPlayer object (my assumption). The difference is between comparing two numbers vs. comparing two pieces of paper with those numbers written on them. 5 is the same as 5, but two pieces of paper with 5 written on them are not the same thing. They may be equal, but the Python CyPlayer class doesn't tell Python how to compare two of its objects.

Thus, if you want to know if two CyPlayers are the same player, use

Code:
if pPlayer.getID() == pOtherPlayer.getID():

Or just compare their PlayerTypes without calling getPlayer().

Code:
if pUnit.getOwner() == gc.getGame().getActivePlayer():

Both of the above functions return a PlayerTypes value, and you can compare those directly.
 
Ah ha, so thats why the comparison didn't work. And I suppose that adding in a CyInterface().addImmediateMessage line here and there that lists the objects wouldn't help, since apparently objects can have the same values but still not be comparable.

For several of the functions I am now working on, I am starting to work with loops and lists, meaning lots of possibilities for things to go wrong - thus the question about debugging tips.

For example the minor merchant - money can target a player, research seems to need a team (don't understand why not a player), but culture or hammers need to target a city. The logic should be easy, for example to do it randomly

get the number of player cities
generate a random number between 0 and number of player cities
get the city ID of the random numbered city
give the city a random number of hammers, culture etc.

Lots of possibilities to specify something incorrectly...

Anyway thank again for the help, and happy new year!
 
And I suppose that adding in a CyInterface().addImmediateMessage line here and there that lists the objects wouldn't help.

Actually, in this case it may have helped. You would have seen that the two objects were CyPlayer objects with different references (Python identifiers, C++ memory addresses). It may have been a clue. In Python you can define a function to be used to compare two objects, but Firaxis didn't do it for any of their objects unfortunately.

Look in the PyHelpers.py module. It has a PyPlayer class with some code that will show you how to loop over all cities or all units. Since the identifiers for a player's cities won't be a contiguous range (e.g. 1, 2, 5, 6, 8, ...) you can't just do a math calculation to choose a random city. Instead, pick a number n between 0 and N-1 and then loop to the nth city.

Oh, I almost forgot! If you have the cheat code entered into CivIV.ini (chipotle) you can open a Python console inside Civ4 with tilde ~ (SHIFT + `). Here you can use all of the stuff from CvPythonExtensions:

Code:
SHIFT + `
> p = gc.getPlayer(0)
> print p.getName()
Capt_Blondbeard
> print p.getGold()
5
> p.changeGold(1000)
 
Ok, enabling the python exception tracking has given me some interesting error messages. For example:

"int object has no attribute getCityList"

So I would assume "int object" is the integer value of the player, rather than a recognizable ID, meaning I need a getID () somewhere prior to the getCityList ?
 
When you get that type of error--"<some type> has no attribute <some name>"--it means you have code which looks like this:

Code:
<variable-of-some-type> . <attribute-of-some-name>

Objects have attributes (member variables and functions), but basic types like int and boolean do not. You can only pass ints and booleans to other functions or use them in expressions like "x + y". getCityList() is an attribute pf PyPlayer I believe, so you'll need to pass the ID to its constructor:

Code:
pPlayer = PyHelpers.PyPlayer(<id>)

Or you need to pass it the CyPlayer. I don't have the code in front of me. See if the __init__() function in PyPlayer uses gc.getPlayer() or not.
 
Ok, let's see if I have this straight; the follow are some sample definitions
PHP:
Start with a selected unit "pUnit"
Player attributes:
pOwner = pUnit.getOwner()  # What is sort of a value is "the owner" Can we do anything with it, or do we have to go straight to getID() to start doing things to the owner
pPlayer = gc.getPlayer(pOwner)  # will get the player - but what sort of a value is this? What can you do with it?
pPlayerID = pPlayer.getID()  # gets the players ID, but do you have to keep getting it each time you want to do something with it, or can you define it at the beginning and keep using it?
#What is a "player type" how does this differ from player ID

Team attributes:
pTeam = pUnit.getTeam()    # what does get team get you - can you do anything with it or do you need to getID() to do things to the team
pTeamID = pTeam.getID() # Do you have to keep getting the ID, or can you define it and forget it...

Now for my specific question:
PHP:
	if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 301 and inputClass.getData2() == 301):
			self.iPushedButtonUnit = g_pSelectedUnit
			pUnit = g_pSelectedUnit
			iX = self.iPushedButtonUnit.getX()
			iY = self.iPushedButtonUnit.getY()
			pMerchantLocation = CyMap().plot(iX, iY)
			pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
			if pMerchantLocation.isCity():
				TradeCity = pMerchantLocation.getPlotCity ()
				pTradePlayer = gc.getPlayer(TradeCity.getOwner())
				if pTradePlayer.getID() != pUnitOwner.getID() : # This now works 
					pUnitOwner.changeGold( game.getSorenRandNum(60,"Trade Result")) # will make this part of an if... elif once all functions work
					pUnitOwnerID = pUnitOwner.getID()
					gc.getTeam(pUnitOwner.getTeam()).changeResearchProgress (pUnitOwner.getCurrentResearch(),60,pUnitOwnerID) #works now, but seems cumbersome
					CityList = pUnitOwnerID.getCityList()
					iRnd = CyGame().getSorenRandNum(len(CityList), "Pick City")
					for i in range (0, len(CityList)):
						if i == iRnd :
							iTargetCity = CityList [i]
							iTargetCity.changePopulation(1) 
							iTargetCity.changeCulture (pUnitOwnerID, 60, True)
							break
					g_pSelectedUnit.finishMoves() # try this to end

The city list (15th line) doesn't work, the function is listed under pyhelpers as a pyPlayer function, but neither the defined pUnitOwner, nor pUnitOwnerID will work.

Thanks again. At least I've gotten the research function to work now...
 
PHP:
Start with a selected unit "pUnit"
Player attributes:
pOwner = pUnit.getOwner()  # What is sort of a value is "the owner" Can we do anything with it, or do we have to go straight to getID() to start doing things to the owner
pPlayer = gc.getPlayer(pOwner)  # will get the player - but what sort of a value is this? What can you do with it?
pPlayerID = pPlayer.getID()  # gets the players ID, but do you have to keep getting it each time you want to do something with it, or can you define it at the beginning and keep using it?
#What is a "player type" how does this differ from player ID

Owner: ID (integer value) of the player, who owns the unit.
getPlayer(ID): Gives you a CyPlayer class. You can use the CyPlayer functions on that class, which are listed in the API.
getID: Gives you here the same ID like pUnit.getOwner(). Yes, you can identify this at the beginning of your function, and just use it until the end.


PHP:
Team attributes:
pTeam = pUnit.getTeam()    # what does get team get you - can you do anything with it or do you need to getID() to do things to the team
pTeamID = pTeam.getID() # Do you have to keep getting the ID, or can you define it and forget it...

getTeam: Gives you a CyTeam instance. You can use all the functions on it, which are listed under CyTeam in the API.
getID: Same like for the player.

The city list (15th line) doesn't work, the function is listed under pyhelpers as a pyPlayer function, but neither the defined pUnitOwner, nor pUnitOwnerID will work.

You should see the error ;).
pUnitOwner and pUnitOwnerID are both integer values, they are just identifier for the player. But you need a PyPlayer instance. Would say anything to that, but i even can't find the commad getCityList :confused:.
But why not an easier way?

PHP:
ThisPlayer = gc.getPlayer(pUnitOwnerID)
NumberOfCities = ThisPlayer.getNumCities ()
iRnd = CyGame().getSorenRandNum(len(NumberOfCities), "Pick City") 
TargetCity = ThisPlayer.getCity(iRnd)
TargetCity.changePopulation(1)
...
 
One correction: CyUnit.getTeam() returns the ID (TeamTypes). You need to pass that to gc.getTeam() to get the CyTeam.

As for getCityList() versus picking a random ID in the list, it is my understanding that the city IDs for a player are not always 0..N-1. There are sometimes gaps, for example when you lose a city. Say you have 5 cities with IDs 0..4, and city #2 gets captured. You no longer have a city #2, you have 0, 1, 3, 4. There are times when these holes are filled in and cities are renumbered, but I don't know when that happens.
 
One correction: CyUnit.getTeam() returns the ID (TeamTypes). You need to pass that to gc.getTeam() to get the CyTeam.

As for getCityList() versus picking a random ID in the list, it is my understanding that the city IDs for a player are not always 0..N-1. There are sometimes gaps, for example when you lose a city. Say you have 5 cities with IDs 0..4, and city #2 gets captured. You no longer have a city #2, you have 0, 1, 3, 4. There are times when these holes are filled in and cities are renumbered, but I don't know when that happens.

I remember that too... If you do this:

Code:
for i in range(pPlayer.getNumCities()):
	pCity = pPlayer.getCity(i)

You get a list with holes.

But if you do this:

Code:
	def getCityList(self):
		' PyCitylist - list of PyCity player owns '
		lCity = []
		(loopCity, iter) = self.player.firstCity(false)
		while(loopCity):
			cityOwner = loopCity.getOwner()
			if ( not loopCity.isNone() and loopCity.getOwner() == self.getID() ): #only valid cities
				city = PyCity( self.getID(), loopCity.getID() )
				lCity.append(city)
			(loopCity, iter) = self.player.nextCity(iter, false)
		return lCity

No holes are created.

EDIT: According to this thread.
 
Back
Top Bottom