How To Make Python Action Buttons

TC01

Deity
Joined
Jun 28, 2009
Messages
2,216
Location
Irregularly Online


Table of Contents:
  1. Introduction
  2. Simple Button
  3. Advanced Button: Plot Picking
  4. AI Usage: Python Scripting
  5. Graphical Effects
Introduction

What is a python action button? It's simple- a button you can press on the unit's control panel (the buttons like move, sleep, skip turn, kill, etc) that does something in-game. Specific examples of python action buttons are the Afterworld abilities that each unit in Afterworld can get, and the Gods of Old religious missions available to Great Prophets.

This is something that you might not need to do and might prefer to handle by adding a command or mission to the SDK. But maybe you would rather make a python action button. Why? Well, there are a few reasons, some of which are listed below:

1. You've never edited the DLL before and are bad at C++, or are having trouble compiling a DLL.
3. You don't want to include a new DLL with your mod just for one button.

So, let's assume one of those conditions before has applied and you are going to make a python action button (hence why you are reading this tutorial).

To do this, you will need to edit at least two files:

-CvMainInterface.py
-CvGameUtils.py

Depending on how complicated this gets, you will need other files:

-CvEventManager.py
-PushButtonUtils.py (A custom python file we will create in this tutorial)

This tutorial, for now, will cover four topics:

1. A very simple button.
2. A button that picks a plot.
3. How the AI can be scripted to "use" buttons.
4. Graphical effects and sound when you press a button.


Okay, let's say we want to make a simple button, that when pressed, has a random chance of finding a resource on the tile you are standing on. We'll call this the Prospect ability.

First, you need to open up CvMainInterface.py (Assets\Python\Screens) in your editing program. I highly recommend Notepad++, whatever you like works (except for Notepad, which is a bad program). You need to search for/scroll down to the function "def updateSelectionButtons", which starts out looking like this:

Code:
	# Will hide and show the selection buttons and their associated buttons
	def updateSelectionButtons( self ):
	
		global SELECTION_BUTTON_COLUMNS
		global MAX_SELECTION_BUTTONS
		global g_pSelectedUnit

		screen = CyGInterfaceScreen( "MainInterface", CvScreenEnums.MAIN_INTERFACE )
		
		pHeadSelectedCity = CyInterface().getHeadSelectedCity()
		pHeadSelectedUnit = CyInterface().getHeadSelectedUnit()
		
		global g_NumEmphasizeInfos
		global g_NumCityTabTypes
		global g_NumHurryInfos
		global g_NumUnitClassInfos
		global g_NumBuildingClassInfos
		global g_NumProjectInfos
		global g_NumProcessInfos
		global g_NumActionInfos

We don't want to mess with any of this. So scroll down to the end of the function (it might take some time). Assuming this is normal BTS, you should see something like this:

Code:
		elif (not CyEngine().isGlobeviewUp() and pHeadSelectedUnit and CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_HIDE_ALL and CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_MINIMAP_ONLY):

			self.setMinimapButtonVisibility(True)

			if (CyInterface().getInterfaceMode() == InterfaceModeTypes.INTERFACEMODE_SELECTION):
			
				if ( pHeadSelectedUnit.getOwner() == gc.getGame().getActivePlayer() and g_pSelectedUnit != pHeadSelectedUnit ):
				
					g_pSelectedUnit = pHeadSelectedUnit
					
					iCount = 0

					actions = CyInterface().getActionsToShow()
					for i in actions:
						screen.appendMultiListButton( "BottomButtonContainer", gc.getActionInfo(i).getButton(), 0, WidgetTypes.WIDGET_ACTION, i, -1, False )
						screen.show( "BottomButtonContainer" )
				
						if ( not CyInterface().canHandleAction(i, False) ):
							screen.disableMultiListButton( "BottomButtonContainer", 0, iCount, gc.getActionInfo(i).getButton() )
							
						if ( pHeadSelectedUnit.isActionRecommended(i) ):#or gc.getActionInfo(i).getCommandType() == CommandTypes.COMMAND_PROMOTION ):
							screen.enableMultiListPulse( "BottomButtonContainer", True, 0, iCount )
						else:
							screen.enableMultiListPulse( "BottomButtonContainer", False, 0, iCount )

						iCount = iCount + 1

					if (CyInterface().canCreateGroup()):
						screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_BUTTONS_CREATEGROUP").getPath(), 0, WidgetTypes.WIDGET_CREATE_GROUP, -1, -1, False )
						screen.show( "BottomButtonContainer" )
						
						iCount = iCount + 1

					if (CyInterface().canDeleteGroup()):
						screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_BUTTONS_SPLITGROUP").getPath(), 0, WidgetTypes.WIDGET_DELETE_GROUP, -1, -1, False )
						screen.show( "BottomButtonContainer" )
						
						iCount = iCount + 1

		elif (CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_HIDE_ALL and CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_MINIMAP_ONLY):
		
			self.setMinimapButtonVisibility(True)

		return 0

Our code is going to go between the last iCount = iCount + 1 and the elif statement below that.

Here, we can control what units the action button appears to. You could set it to only appear to units in a city with a specific building, for instance. Or a unit with a certain promotion or combination of promotions, or a unit with specific script data. Or virtually anything. For our prospect button, we'll make it only appear to Workers.

Code:
					if (CyInterface().canDeleteGroup()):
						screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_BUTTONS_SPLITGROUP").getPath(), 0, WidgetTypes.WIDGET_DELETE_GROUP, -1, -1, False )
						screen.show( "BottomButtonContainer" )
						
						iCount = iCount + 1
						
[COLOR="Red"]					pUnit = g_pSelectedUnit
					iUnitType = pUnit.getUnitType()
					pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
					if pUnitOwner.isTurnActive( ):
						if iUnitType == gc.getInfoTypeForString('UNIT_WORKER'):
							screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_PROSPECT").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1
[/COLOR]

		elif (CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_HIDE_ALL and CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_MINIMAP_ONLY):
		
			self.setMinimapButtonVisibility(True)

		return 0

The new code is in red. What does all of it mean? Well, pUnit is the unit that you have selected. iUnitType is defined as it's unitType, and pUnitOwner is the player who owns the unit. ArtFileMgr.getInterfaceArtInfo("INTERFACE_PROSPECT").getPath() is a button that exists only in the art files (CIV4ArtDefines_Interface.xml). We have to create this later or get an error that will say "None has no attribute .getPath()".

The two widget values (here I've defined them as 300, 300, are arbitrary numbers. That is, they aren't entirely arbritrary. You will use them elsewhere in this file and in CvGameUtils.py (to control what happens when you press the button and what text you see when you mouseover the button). But as long as the game doesn't already use these numbers for something else, you can use anything.

Finally, the rest of the code simply appends the button to the other buttons in game. It will be added to the end of the buttons.


Next, you will need to go all the way down the page to def handleInput, which should read:

Code:
	def handleInput (self, inputClass):
		return 0

Basically, as of now, it does nothing.

We want to add code that will cause something to happen when we click the button. Remember, it needs to correspond with the two widget values we defined above.

Code:
	def handleInput (self, inputClass):
	
[COLOR="Red"]		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
			self.pPushedButtonUnit = g_pSelectedUnit
			iX = self.pPushedButtonUnit.getX()
			iY = self.pPushedButtonUnit.getY()
			pProspect = CyMap().plot(iX, iY)
			if pProspect.getBonusType(-1) == -1:
				iRnd = CyGame().getSorenRandNum(100, "Prospect")
				if iRnd >= 80:
					if pProspect.isHills():
						pProspect.setBonusType(gc.getInfoTypeForString("BONUS_IRON"))
			g_pSelectedUnit.changeMoves(60)[/COLOR]

		return 0

This is pretty simple, I could write a more complicated function. But basically what it does is checks what the numbers we defined above are (widgets/inputClass.getData()/whatever you want to call them). If they are 300, 300 (which is what we said they were above), it will trigger this code. This code gets the plot that the unit you pushed the button for was standing on and rolls a random number between 0 and 100. If the number is 80 or above, and if the plot is a hill, and if there's no resource on the hill already, then the plot will get an iron resource.

Also note that I added in a flat movement cost to using the ability. You can make this value whatever you want, but you should probably make it something. FFH prevents you from using any ability more then once a turn (no matter what). But it's easier to make it simply cost one movement point and thus prevent you from prospecting until you get a resource on the specific tile.

I could have, as I said, added in multiple checks, like randomly assign a minable resource to a hill, or randomly assign resources to specific terrains, but I decided not to make it so complicated for this tutorial.


Now, to the next file: CvGameUtils.py:

We want to scroll to the function "def getWidgetHelp" in this file. Here you can define what text will appear when you mouseover something. The function currently looks like this:

Code:
	def getWidgetHelp(self, argsList):
		eWidgetType, iData1, iData2, bOption = argsList
		return u""

We need to add code that checks what the value (300, 300) is and then adds text, like this:

Code:
	def getWidgetHelp(self, argsList):
		eWidgetType, iData1, iData2, bOption = argsList
		
[COLOR="Red"]		iType = WidgetTypes.WIDGET_GENERAL
		if (eWidgetType == iType):
			if (iData1 == 300):
				return CyTranslator().getText("TXT_KEY_BUTTON_PROSPECT",())[/COLOR]
		
		return u""

And you're done. You should test the game to make sure this works as intended. (It might not if I made a mistake in the tutorial).


So now we want to make a button that when pressed, causes a specific plot to be picked. This example came up recently in a thread in the SDK and Python forum, and it's actually the reason why I learned how to do this. After I figured out how to make a button work to answer the question, I started using them in my mods. This technically isn't part of making a python push button, but I decided to include it since it's an example of advanced button making and also is something used extensively by Firaxis. Both Afterworld and Gods of Old make use of python buttons and plot picking.

Anyway, we want to make a teleporter. This will only be visible if a unit has the "Teleport" promotion (I'm not making the promotion for you here, sorry), and will allow your unit to move to any plot on the map that it can see. This is because there are no real ways to pick a plot you cannot see.

We're going to start out with basically the same process- I'm going to keep the code I added in the above tutorial so you can compare, in fact. Anyway, open CvMainInterface.py and find the end of the updateSelectionButtons() function.

As I said, our code should make it so that the button only appears when the unit has the promotion Teleport. This means that instead of needing to create a new ArtInterface in the XML, we can simply use the button for the promotion. Thus, the code will be slightly different.

Code:
					if (CyInterface().canDeleteGroup()):
						screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_BUTTONS_SPLITGROUP").getPath(), 0, WidgetTypes.WIDGET_DELETE_GROUP, -1, -1, False )
						screen.show( "BottomButtonContainer" )
						
						iCount = iCount + 1
						
					pUnit = g_pSelectedUnit
					iUnitType = pUnit.getUnitType()
					pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
					if pUnitOwner.isTurnActive( ):
						if iUnitType == gc.getInfoTypeForString('UNIT_WORKER'):
							screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_PROSPECT").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1

[COLOR="Red"]						if pUnit.isHasPromotion(gc.getInfoTypeForString('PROMOTION_TELEPORT')):
							screen.appendMultiListButton( "BottomButtonContainer", gc.getPromotionInfo(gc.getInfoTypeForString('PROMOTION_TELEPORT')).getButton(), 0, WidgetTypes.WIDGET_GENERAL, 301, 301, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1[/COLOR]


		elif (CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_HIDE_ALL and CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_MINIMAP_ONLY):
		
			self.setMinimapButtonVisibility(True)

		return 0

The red code is the new stuff. As I mentioned, instead of using the Art file manager we're simply getting the promotion info and getting it's button for PROMOTION_TELEPORT. (if this promotion is not present, you will get an error "None type has no attribute getButton()"). Also, the numbers are 301, 301 instead of 300, 300. I'm allowing for the fact that I already used 300, 300 above in the first part of the tutorial (and kept that code there for this part).

Now, we want to add to the handleInput() function again. This time, though, since we are picking a plot, we don't want to actually do anything much here. We need to store some data and execute the plot picking command.

Code:
	def handleInput (self, inputClass):
	
		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
			self.pPushedButtonUnit = g_pSelectedUnit
			iX = self.pPushedButtonUnit.getX()
			iY = self.pPushedButtonUnit.getY()
			pProspect = CyMap().plot(iX, iY)
			if pProspect.getBonusType(-1) == -1:
				iRnd = CyGame().getSorenRandNum(100, "Prospect")
				if iRnd >= 80:
					if pProspect.isHills():
						pProspect.setBonusType(gc.getInfoTypeForString("BONUS_IRON"))
			g_pSelectedUnit.changeMoves(60)
			
[COLOR="Red"]		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 301 and inputClass.getData2() == 301):
			PushButtonUtils.iPushButton = 1
			PushButtonUtils.pPushedButtonUnit = g_pSelectedUnit
			CyInterface().setInterfaceMode(InterfaceModeTypes.INTERFACEMODE_PYTHON_PICK_PLOT)[/COLOR]

		return 0

What is "PushButtonUtils.iPushButton"? Well, those two lines are storing two pieces of data in a seperate file, PushButtonUtils, that I mentioned we'd have to create above. You can store any data you want here, as long as you create a define for it in Push Button Utils. And that name is arbitrary, for instance Gods of Old's file is called GodsOfOld.py and Afterworld's is Afterworld.py. But we'll cover creating that file next.

Finally, the last line of the added code simply sets the Python Pick Plot interface mode. You get the plot-sized cursor that, when it highlights an eligible plot, turns green, and when it is on a non-eligible plot, it turns gray.

But before we do anything with picking a plot, we need to create and import our new file, PushButtonUtils.py.

This may sound ominous, creating a new file, but really it's not that hard. All you have to do is create a new file (you can save a python file in Notepad++, for instance) in the Assets\Python directory. In this tutorial, we'll call it PushButtonUtils.py. There are two ways to make one, the easy way and the hard way. The easy way is good for only storing data, but you may want your file to do other stuff, and then it is better to do it the hard way. And it's not too much harder.

This is the easy way. Note this is the Gods of Old file from the Gods of Old mod, you can make the comments be whatever you want or have the variables listed here whatever you want:

Code:
## Sid Meier's Civilization 4
## Copyright Firaxis Games 2007
iPushButton = 0
pPushedButtonUnit = 0

That's it. iPushButton and pPushedButtonUnit are both 0 because they are integers. (Well, pPushedButtonUnit is really a CvUnit object, but it is being treated as an integer in this case). Booleans and strings would have to be defined as true or "".

The hard way is to actually import other files and create a class, like this:

Code:
## Sid Meier's Civilization 4
## Copyright Firaxis Games 2007

from CvPythonExtensions import *
from PyHelpers import PyPlayer

gc = CyGlobalContext()


class PushButton:
	def __init__(self):
		iPushButton = 0
		pPushedButtonUnit = 0

For this, you don't actually need the gc = CyGlobalContext(), but this is best if you start adding more stuff to this file, forget that you didn't define gc as CyGlobalContext(), and get errors when trying to use gc. instead of CyGlobalContext().

Now, you need to import your file from the MainInterface file (and other files, but we'll do that when we get there).

If you did it the easy way, well, it's easy. You just add one line to the top of CvMainInterface.py, and other files, where other import statements are listed:

Code:
import PushButtonUtils

If you did it the hard way, you need to not only do this but also add the line below to where globals are defined, usually directly after the imports. These globals are variables that can be called from the entire file, like gc for instance (instead of writing out CyGlobalContext() each time you wanted to get something from the XML).

Code:
PushButtonUtils = PushButton()

Then you can write PushButtonUtils.[something] and you know that [something] is being called from class PushButton in the PushButtonUtils.py file. I believe you don't actually need this line and could just write PushButton().[something], but it's easier this way in the tutorial because I can use the same variable, PushButtonUtils, for both methods.

Next, we want to control what happens when we pick a plot. To do this we have to open CvEventManager.py. Before we go to the plot picking function, you must add the import statements here as well, to the top of the file, so the event manager can read data stored in PushButtonUtils.py. After doing that, search for the function def onPlotPicked(self, argsList).It should look like this (in normal BTS):

Code:
	def onPlotPicked(self, argsList):
		'Plot Picked'
		pPlot = argsList[0]
		CvUtil.pyPrint('Plot was picked at %d, %d'
			%(pPlot.getX(), pPlot.getY()))

We want to add code that checks which button was pressed, and if it was, move the unit who pressed the button from it's current plot to the new plot, AND implement a flat movement cost. This isn't that hard.

I mention "check which button was pressed", because while we only have one pick plot button, you will need a way to tell which button was pressed in this function, and the best way to do that is to save a variable in the PushButtonUtils file as "iPushButton" and then check what that is in the Event Manager.

We need to add this code:

Code:
	def onPlotPicked(self, argsList):
		'Plot Picked'
		pPlot = argsList[0]
		CvUtil.pyPrint('Plot was picked at %d, %d'
			%(pPlot.getX(), pPlot.getY()))
[COLOR="Red"]		pCaster = PushButtonUtils.pPushedButtonUnit
		pCaster.changeMoves(60)
		iX = pPlot.getX()
		iY = pPlot.getY()
		if PushButtonUtils.iPushButton == 1:
			pCaster.setXY(iX, iY, false, true, true)[/COLOR]

This is pretty simple. I can change the caster's (FFH terminology) movement points as soon as the function is called because this only occurs after you actually pick a plot. I then check if the button is 1 (which is what I set iPushButton to in the handleInput() function after you pressed the Teleport button), and if it is, I set the caster's x, y coordinates to iX, iY (the coordinates of the plot).

Now, we need to add the mouseover text in GameUtils.py again. Here, the process is exactly the same as before.

Code:
	def getWidgetHelp(self, argsList):
		eWidgetType, iData1, iData2, bOption = argsList
		
		iType = WidgetTypes.WIDGET_GENERAL
		if (eWidgetType == iType):
			if (iData1 == 300):
				return CyTranslator().getText("TXT_KEY_BUTTON_PROSPECT",())
[COLOR="Red"]			if (iData1 == 301):
				return CyTranslator().getText("TXT_KEY_BUTTON_TELEPORT",())[/COLOR]
		
		return u""

Finally, there's one last thing we can do, and probably should. Before picking a plot, the canPickPlot function is called in CvGameUtils.py to check whether you can- it controls the green or gray cursor when you mouseover a plot in plot-picking mode. Here's the function currently:

Code:
	def canPickPlot(self, argsList):
		pPlot = argsList[0]
		return true

We probably want to make it so that you can't teleport onto a tile with enemy units or cities on them. (It would be awesome if we made it so that you would attack them, but unfortunately I don't know if that's doable so I'm not going to try. You're welcome to.) To do this, you're going to have to import PushButtonUtils.py in the CvGameUtils.py file using the same method you used in the Event Manager and in the Main Interface file.

Code:
	def canPickPlot(self, argsList):
		pPlot = argsList[0]
		
[COLOR="Red"]		pCaster = PushButtonUtils.pPushedButtonUnit
		pCasterTeam = gc.getTeam(pCaster.getOwner())
		if PushButtonUtils.iPushButton == 1:
			for i in range(pPlot.getNumUnits()):
				pUnit = pPlot.getUnit(i)
				if gc.getTeam(pUnit.getOwner()).isAtWar(pCasterTeam):
					return false
			if pPlot.isCity():
				pCity = gc.getPlotCity()
				if gc.getTeam(pCity.getOwner()).isAtWar(pCasterTeam):
					return false
			if not gc.getTeam(pPlot.getOwner()).isOpenBorders(pCasterTeam):
				return false[/COLOR]

		return true

So this is the code you should add to the canPickPlot function. It will check if the owners of the units on the plot are at war with the owner of the caster (the unit who pressed the button.) Then, it will check if the plot is a city, and if it is, if the city's owner is at war with the caster's owner. And finally, it will prevent you from teleporting into a plot whose owner does not have an open borders agreement with you. Again, though, note that the code is only triggered if the push button was "1", the value we assigned to the teleport button.

So we're done with the plot picking button. Again, if you're using this, you should probably test it in case I made a mistake. If I did, please mention it here and I'll fix it.


Now, we're going to create a third button that the AI will use. This sounds impossible, but it's not.

What we can do is, when certain conditions are met, "fake" the triggering of the button. For instance, for the Prospect button we created, scripting in onUnitMove could be set to trigger a function (that would do the prospect ability) if the unit that moved was a worker, on a tile with no resources, and had a movement point to spare. We can also do more advanced stuff to allow the game to "remember" to trigger the ability on the next turn if it does not have a movement point to spare- if it moved onto a tile where it could cast the ability using it's last movement point.

For now, the button we're making is a "Quell City Disorder" ability. It will allow any military unit to try and restore order in a rioting city. We can make it so that it also damages the unit (they take casualties trying to stop the rioters). But we're also going to make the AI use it.

First, let's open up CvMainInterface.py and go to the end of def updateSelectionButtons where our other buttons are. Here, we'll add the first bit of code.

Code:
					if (CyInterface().canDeleteGroup()):
						screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_BUTTONS_SPLITGROUP").getPath(), 0, WidgetTypes.WIDGET_DELETE_GROUP, -1, -1, False )
						screen.show( "BottomButtonContainer" )
						
						iCount = iCount + 1
						
					pUnit = g_pSelectedUnit
					iUnitType = pUnit.getUnitType()
					pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
[COLOR="red"]					pUnitPlot = CyMap().plot(pUnit.getX(), pUnit.getY())[/COLOR]
					if pUnitOwner.isTurnActive( ):
						if iUnitType == gc.getInfoTypeForString('UNIT_WORKER'):
							screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_PROSPECT").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1

						if pUnit.isHasPromotion(gc.getInfoTypeForString('PROMOTION_TELEPORT')):
							screen.appendMultiListButton( "BottomButtonContainer", gc.getPromotionInfo(gc.getInfoTypeForString('PROMOTION_TELEPORT')).getButton(), 0, WidgetTypes.WIDGET_GENERAL, 301, 301, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1
							
[COLOR="Red"]						if gc.getUnitInfo(pUnit).isMilitaryHappiness():
							if pUnitPlot.isCity():
								if (pPlot.getPlotCity()).isOccupation():
									screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_QUELL_REVOLT").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 302, 302, False )
									screen.show( "BottomButtonContainer" )
									iCount = iCount + 1[/COLOR]


		elif (CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_HIDE_ALL and CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_MINIMAP_ONLY):
		
			self.setMinimapButtonVisibility(True)

		return 0

Instead of checking for a specific promotion, I chose to get the unit info for the unit and then see if the <bMilitaryHappiness> XML tag was set to 1. This is a quick way of checking if the unit would already provide "happiness", or as the Modiki says, "Counts as a Military Unit in the eyes of the rabble." That seems to be what we would want here. I also checked if the plot had a city (I added a new line of code defining pUnitPlot as the unit's plot), and if that city was in revolt. These are rather specific conditions- you will only see the button if you meet them.

Also note that I use the ArtFile Manager again and define a new button, INTERFACE_QUELL_REVOLT (that you will have to make yourself) and that the numbers are 302, 302, since I am already using 300 and 301 for the previous two buttons.

Now, we go to def handleInput and add our code there.

Code:
	def handleInput (self, inputClass):
	
		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
			self.pPushedButtonUnit = g_pSelectedUnit
			iX = self.pPushedButtonUnit.getX()
			iY = self.pPushedButtonUnit.getY()
			pProspect = CyMap().plot(iX, iY)
			if pProspect.getBonusType(-1) == -1:
				iRnd = CyGame().getSorenRandNum(100, "Prospect")
				if iRnd >= 80:
					if pProspect.isHills():
						pProspect.setBonusType(gc.getInfoTypeForString("BONUS_IRON"))
			g_pSelectedUnit.changeMoves(60)
			
		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 301 and inputClass.getData2() == 301):
			PushButtonUtils.iPushButton = 1
			PushButtonUtils.pPushedButtonUnit = g_pSelectedUnit
			CyInterface().setInterfaceMode(InterfaceModeTypes.INTERFACEMODE_PYTHON_PICK_PLOT)
			
[COLOR="Red"]		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 302 and inputClass.getData2() == 302):
			PushButtonUtils.doQuellRevolt(g_pSelectedUnit)[/COLOR]

		return 0

Note that instead of doing anything here, I'm calling a function in PushButtonUtils, doQuellRevolt. The advantage of doing that in this case is that then, after I define the conditions I want for the AI to use the "button", I can call the same function if those conditions are met. This saves you from having to write the same code twice. However, doing it this way requires you to have used the "Advanced" mode of construction PushButtonUtils.py. It needs to have been set up like this:

Code:
## Sid Meier's Civilization 4
## Copyright Firaxis Games 2007

from CvPythonExtensions import *
from PyHelpers import PyPlayer

gc = CyGlobalContext()


class PushButton:
	def __init__(self):
		iPushButton = 0
		pPushedButtonUnit = 0

We want to add a function to class PushButton that, when called, does the effects of the Quell Revolt button. This will continue in the next post below:
 
Continued from above...

Below is our new function in red added to PushButtonUtils.py.

Code:
## Sid Meier's Civilization 4
## Copyright Firaxis Games 2007

from CvPythonExtensions import *
from PyHelpers import PyPlayer

gc = CyGlobalContext()


class PushButton:
	def __init__(self):
		iPushButton = 0
		iPushedButtonUnit = 0
		
[COLOR="Red"]	def doQuellRevolt(self, pUnit):
		pUnit.changeMoves(60)
		iX = pUnit.getX()
		iY = pUnit.getY()
		pPlot = CyMap().plot(iX, iY)
		pCity = pPlot.getPlotCity()
		pCity.changeOccupationTimer(-1)
		iRand = CyGame().getSorenRandNum(25, "Quell Revolt Injuries")
		pUnit.setDamage(pUnit.getDamage()+iDmgRnd, False)[/COLOR]

Occupation in this instance refers to what we would call "revolt", so pCity.changeOccupationTimer is a function that alters the turns of revolt a city is experiencing. Anyway, in addition to doing this, I apply the movement cost, and randomly deal some damage to the unit. The idea is that when you send armed troops to police a city, your forces will take casualties. However, note that we don't need any conditional statements because we already put them in. The button Quell Revolt only appears if the unit is in a revolting city.

We now need to add the help text to CvGameUtils.py, like before.

Code:
	def getWidgetHelp(self, argsList):
		eWidgetType, iData1, iData2, bOption = argsList
		
		iType = WidgetTypes.WIDGET_GENERAL
		if (eWidgetType == iType):
			if (iData1 == 300):
				return CyTranslator().getText("TXT_KEY_BUTTON_PROSPECT",())
			if (iData1 == 301):
				return CyTranslator().getText("TXT_KEY_BUTTON_TELEPORT",())
[COLOR="Red"]			if (iData1 == 302):
				return CyTranslator().getText("TXT_KEY_BUTTON_QUELL_REVOLT",())[/COLOR]
		
		return u""

Now, we should have our button. But the AI won't use it, which is what this section was about. We are going to script the AI in the Event Manager to press the button. We're going to start by editing the function def onUnitMove, which is called whenever a unit moves. Find that function in CvEventManager.py.

Code:
	def onUnitMove(self, argsList):
		'unit move'
		pPlot,pUnit,pOldPlot = argsList
		player = PyPlayer(pUnit.getOwner())
		unitInfo = PyInfo.UnitInfo(pUnit.getUnitType())
		if (not self.__LOG_MOVEMENT):
			return
		if player and unitInfo:
			CvUtil.pyPrint('Player %d Civilization %s unit %s is moving to %d, %d' 
				%(player.getID(), player.getCivilizationName(), unitInfo.getDescription(), 
				pUnit.getX(), pUnit.getY()))

This is what it should look like in normal Beyond the Sword. Despite what you might think, this function is not disabled in vanilla BTS, meaning it's okay to edit it. But it might be in some mods, so you should check the PythonCallbackDefines.xml file to make sure.

We want to add code here checking if a unit that can use Quell Revolt (isMilitaryHappiness) moves into a city causing revolt, and if that unit has 1 movement point to spare. If it does, we want the ability to trigger.

Code:
	def onUnitMove(self, argsList):
		'unit move'
		pPlot,pUnit,pOldPlot = argsList
		player = PyPlayer(pUnit.getOwner())
		unitInfo = PyInfo.UnitInfo(pUnit.getUnitType())
		if (not self.__LOG_MOVEMENT):
			return
		if player and unitInfo:
			CvUtil.pyPrint('Player %d Civilization %s unit %s is moving to %d, %d' 
				%(player.getID(), player.getCivilizationName(), unitInfo.getDescription(), 
				pUnit.getX(), pUnit.getY()))
				
[COLOR="Red"]		if pPlot.isCity():
			if (pPlot.getCity()).isOccupation():
				if gc.getUnitInfo(pUnit).isMilitaryHappiness():
					if pUnit.getMoves() > 0:
						PushButtonUtils.doQuellRevolt(pUnit)[/COLOR]

We've added a function that only is even looked at if the plot the unit moves onto is a city. Then, if that city is in revolt, and if the unit that moved "is Military Happiness", and if it has a free movement point, we call the doQuellRevolt function in PushButtonUtils.py.

That's it. The AI will run doQuellRevolt (our button) when it's unit satisfies the conditions above. This is basic scripting. We could have used more complicated buttons and conditions. However, there is more we can do.

We can make the AI "remember" it's unit is in a position to use the button, even if it has no movement points, and then on the beginning of it's next turn, use the ability. We can do this using script data, which can be attached to a number of objects: units, plots, cities, players, and games. Then in the Event Manager function def onEndPlayerTurn (which despite it's name is called before you move any units) we check if a unit has the script data "Do Quell Revolt" and if it does, run the function.

First, we need to add the script data being set in onUnitMove.

Code:
		if pPlot.isCity():
			if (pPlot.getCity()).isOccupation():
				if gc.getUnitInfo(pUnit).isMilitaryHappiness():
					if pUnit.getMoves > 0:
						PushButtonUtils.doQuellRevolt(pUnit)
[COLOR="Red"]					else:
						pUnit.setScriptData("Do Quell Revolt")[/COLOR]

All I added was an "else" statement that if the unit did not have greater then 0 moves (in other words, it had no moves) but had moved onto a revolting city, it should store "Do Quell Revolt" as it's script data.

Now, here is the def onEndPlayerTurn function in regular Beyond the Sword.

Code:
	def onEndPlayerTurn(self, argsList):
		'Called at the end of a players turn'
		iGameTurn, iPlayer = argsList
		
		if (gc.getGame().getElapsedGameTurns() == 1):
			if (gc.getPlayer(iPlayer).isHuman()):
				if (gc.getPlayer(iPlayer).canRevolution(0)):
					popupInfo = CyPopupInfo()
					popupInfo.setButtonPopupType(ButtonPopupTypes.BUTTONPOPUP_CHANGECIVIC)
					popupInfo.addPopup(iPlayer)
		
		CvAdvisorUtils.resetAdvisorNags()
		CvAdvisorUtils.endTurnFeats(iPlayer)

And here it is with our added code.

Code:
	def onEndPlayerTurn(self, argsList):
		'Called at the end of a players turn'
		iGameTurn, iPlayer = argsList
		
		if (gc.getGame().getElapsedGameTurns() == 1):
			if (gc.getPlayer(iPlayer).isHuman()):
				if (gc.getPlayer(iPlayer).canRevolution(0)):
					popupInfo = CyPopupInfo()
					popupInfo.setButtonPopupType(ButtonPopupTypes.BUTTONPOPUP_CHANGECIVIC)
					popupInfo.addPopup(iPlayer)
		
[COLOR="Red"]		(loopUnit, iter) = gc.getPlayer(iPlayer).firstUnit(false)
		while(loopUnit):
			if not loopUnit.isDead():
				if loopUnit.getScriptData() == "Do Quell Revolt"
					PushButtonUtils.doQuellRevolt(loopUnit)
					loopUnit.setScriptData(-1)
			(loopUnit, iter) = gc.getPlayer(iPlayer).nextUnit(iter, false) [/COLOR]
		
		CvAdvisorUtils.resetAdvisorNags()
		CvAdvisorUtils.endTurnFeats(iPlayer)

What is all of that? Well, I recently found out that the loop I normally use to get all cities or units a player owns doesn't work. So I borrowed this sequence from the PyHelpers.py file. Instead of calling a function from there I chose to just pull the loop out and use it here. Then, when I get loopUnit, I can check it's script data (instead of checking it's conditions), running the function, and then removing the script data from that unit.

So we're done. We've successfully scripted the AI to use a python action button. Using a simple function, though. For instance, we could make the AI choose whether to do it or not depending on what civ or leader it was, or what civics or religion it had. But I'm not going to do that now- now that I've shown you the process you should be able to do it on your own


When I say "visual effect" I mean a sound, a message saying the button was pressed, and something happening on the screen, a "graphical effect", which is called an Effect in Civ 4. CIV4EffectInfos.xml is a file containing not, say, the effects of having a worker build something or pressing the Go To button, but rather graphical effects.

The button I'm going to make is an alternative nuke ability, using the ping graphical effect as suggested by Civ Fuehrer. For those of you who don't know what I mean, when you press the P key in a game of Civ 4, you get to pick a plot. The plot you pick is "pinged", which creates a blue blast of some sort that spread across the map. (Try it to see what I'm talking about... or look here in this thread by Civ Fuehrer). It will essentially work the same as a nuke. However, it will use a different graphic.

Normally, you cannot use two effects for a nuke, as it is hardcoded in the XML, and cannot be changed without SDK modding. But by cheating and using either a mission/command or a python action button that does the same thing as a nuke, you will be able to.

Let's start by adding the graphics code to CvMainInterface's updateSelectionButtons function, at the end of the file.

Code:
					if (CyInterface().canDeleteGroup()):
						screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_BUTTONS_SPLITGROUP").getPath(), 0, WidgetTypes.WIDGET_DELETE_GROUP, -1, -1, False )
						screen.show( "BottomButtonContainer" )
						
						iCount = iCount + 1
						
					pUnit = g_pSelectedUnit
					iUnitType = pUnit.getUnitType()
					pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
					pUnitPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
					if pUnitOwner.isTurnActive( ):
						if iUnitType == gc.getInfoTypeForString('UNIT_WORKER'):
							screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_PROSPECT").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1

						if pUnit.isHasPromotion(gc.getInfoTypeForString('PROMOTION_TELEPORT')):
							screen.appendMultiListButton( "BottomButtonContainer", gc.getPromotionInfo(gc.getInfoTypeForString('PROMOTION_TELEPORT')).getButton(), 0, WidgetTypes.WIDGET_GENERAL, 301, 301, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1
							
						if gc.getUnitInfo(pUnit).isMilitaryHappiness():
							if pUnitPlot.isCity():
								if (pPlot.getPlotCity()).isOccupation():
									screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_QUELL_REVOLT").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 302, 302, False )
									screen.show( "BottomButtonContainer" )
									iCount = iCount + 1

						[COLOR="Red"]if pUnit.isHasPromotion(gc.getInfoTypeForString('PROMOTION_NUKE_2')):
							screen.appendMultiListButton( "BottomButtonContainer", gc.getPromotionInfo(gc.getInfoTypeForString('PROMOTION_NUKE_2')).getButton(), 0, WidgetTypes.WIDGET_GENERAL, 303, 303, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1[/COLOR]

		elif (CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_HIDE_ALL and CyInterface().getShowInterface() != InterfaceVisibility.INTERFACE_MINIMAP_ONLY):
		
			self.setMinimapButtonVisibility(True)

		return 0

I've created a new promotion, PROMOTION_NUKE_2. This promotion would say that a unit could use the second nuke ability. You would need to create it, create a graphic for it, and give it to all units (possibly new units?) that you want to be able to use the nuke that we are creating here. So, I've used the getButton command, rather then the art file manager. Anyway, now we add it to handleInput:

Code:
	def handleInput (self, inputClass):
	
		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
			self.pPushedButtonUnit = g_pSelectedUnit
			iX = self.pPushedButtonUnit.getX()
			iY = self.pPushedButtonUnit.getY()
			pProspect = CyMap().plot(iX, iY)
			if pProspect.getBonusType(-1) == -1:
				iRnd = CyGame().getSorenRandNum(100, "Prospect")
				if iRnd >= 80:
					if pProspect.isHill():
						pProspect.setBonusType(gc.getInfoTypeForString("BONUS_IRON"))
			g_pSelectedUnit.changeMoves(60)
			
		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 301 and inputClass.getData2() == 301):
			PushButtonUtils.iPushButton = 1
			PushButtonUtils.pPushedButtonUnit = g_pSelectedUnit
			CyInterface().setInterfaceMode(InterfaceModeTypes.INTERFACEMODE_PYTHON_PICK_PLOT)
			
		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 302 and inputClass.getData2() == 302):
			PushButtonUtils.doQuellRevolt(g_pSelectedUnit)

[COLOR="red"]		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 303 and inputClass.getData2() == 303):
			PushButtonUtils.iPushButton = 2
			PushButtonUtils.pPushedButtonUnit = g_pSelectedUnit
			CyInterface().setInterfaceMode(InterfaceModeTypes.INTERFACEMODE_PYTHON_PICK_PLOT)[/COLOR]

		return 0

This is a plot-picking button... so all of the effects will be handled in onPlotPicked, and the checks for it will be handled in canPickPlot in the game utils file. Note that I use 2 as the value of iPushButton instead of 1. 1 has already been used, as the Teleport ability. You should use an unique value for each button you create that picks a plot.

Before we add the effect, let's add the text to getWidgetHelp in CvGameUtils.py:

Code:
	def getWidgetHelp(self, argsList):
		eWidgetType, iData1, iData2, bOption = argsList
		
		iType = WidgetTypes.WIDGET_GENERAL
		if (eWidgetType == iType):
			if (iData1 == 300):
				return CyTranslator().getText("TXT_KEY_BUTTON_PROSPECT",())
			if (iData1 == 301):
				return CyTranslator().getText("TXT_KEY_BUTTON_TELEPORT",())
			if (iData1 == 302):
				return CyTranslator().getText("TXT_KEY_BUTTON_QUELL_REVOLT",())
			[COLOR="red"]if (iData1 == 303):
				return CyTranslator().getText("TXT_KEY_BUTTON_NUKE_2",())[/COLOR]
		
		return u""

Now, open up CvEventManager.py and find the onPlotPicked function. It should look like this (note that the code we added for example 2 is still there):

Code:
	def onPlotPicked(self, argsList):
		'Plot Picked'
		pPlot = argsList[0]
		CvUtil.pyPrint('Plot was picked at %d, %d'
			%(pPlot.getX(), pPlot.getY()))
		pCaster = PushButtonUtils.pPushedButtonUnit
		pCaster.changeMoves(60)
		iX = pPlot.getX()
		iY = pPlot.getY()
		if PushButtonUtils.iPushButton == 1:
			pCaster.setXY(iX, iY, false, true, true)

Now, rather then spend time trying to replicate what exactly the nuke function does, all I'm going to do for this example is make it randomly place Fallout around the "caster", and also kill the caster. If this was being done in an actual mod, you would need to add checks to weaken or kill nearby units, and whatever else happens when a nuke explodes (it varies from main Civ IV to mods). The basic code is below:

Code:
	def onPlotPicked(self, argsList):
		'Plot Picked'
		pPlot = argsList[0]
		CvUtil.pyPrint('Plot was picked at %d, %d'
			%(pPlot.getX(), pPlot.getY()))
		pCaster = PushButtonUtils.pPushedButtonUnit
		pCaster.changeMoves(60)
		iX = pPlot.getX()
		iY = pPlot.getY()
		if PushButtonUtils.iPushButton == 1:
			pCaster.setXY(iX, iY, false, true, true)
[COLOR="Red"]		if PushButtonUtils.iPushButton == 1:
			pCaster.kill(false, -1)
			iFallout = gc.getInfoTypeForString('FEATURE_FALLOUT')
			for iiX in range(iX-1, iX+2, 1):
				for iiY in range(iY-1, iY+2, 1):
					pFallout = CyMap().plot(iX, iY)
					iRand = CyGame().getSorenRandNum(20, "Fallout")
					if iRand <= 5:
						pFallout.setFeatureType(iFallout)[/COLOR]

Notice we haven't added the actual graphical or sound effect yet. All of that will be added here as well. The function to cause a graphical effect to occur is CyEngine().triggerEffect(), and the function to trigger a 2D sound script (which the nuke explosion sound is) is CyAudioGame().Play2DSound(). Here is the code we're adding:

Code:
point = pPlot.getPoint()
CyEngine().triggerEffect(gc.getInfoTypeForString('EFFECT_PING'), point.x, point.y, point.z)
CyAudioGame().Play2DSound('AS2D_NUKE_INTERCEPTED')

What is the point. stuff doing there? Well, this information is necessary for the triggerEffect command. I'm not entirely sure what it does, but you can get the point information ("NiPoint3") by using pPlot.getPoint().

Anyway, we can use all of this to make our function trigger this effect. Here it is added to the code:

Code:
	def onPlotPicked(self, argsList):
		'Plot Picked'
		pPlot = argsList[0]
		CvUtil.pyPrint('Plot was picked at %d, %d'
			%(pPlot.getX(), pPlot.getY()))
		pCaster = PushButtonUtils.pPushedButtonUnit
		pCaster.changeMoves(60)
		iX = pPlot.getX()
		iY = pPlot.getY()
		if PushButtonUtils.iPushButton == 1:
			pCaster.setXY(iX, iY, false, true, true)
		if PushButtonUtils.iPushButton == 2:
			[COLOR="red"]point = pPlot.getPoint()
			CyEngine().triggerEffect(gc.getInfoTypeForString('EFFECT_PING'), point.x, point.y, point.z)
			CyAudioGame().Play2DSound('AS2D_NUKE_INTERCEPTED')[/COLOR]
			pCaster.kill(false, -1)
			iFallout = gc.getInfoTypeForString('FEATURE_FALLOUT')
			for iiX in range(iX-1, iX+2, 1):
				for iiY in range(iY-1, iY+2, 1):
					pFallout = CyMap().plot(iX, iY)
					iRand = CyGame().getSorenRandNum(20, "Fallout")
					if iRand <= 5:
						pFallout.setFeatureType(iFallout)

Now, the only thing left to do is implement a range limit on the button via canPickPlot. So in CvGameUtils.py, find the "canPickPlot" function. Here it is, with our code from button example 2 already added:

Code:
	def canPickPlot(self, argsList):
		pPlot = argsList[0]
		
		pCaster = PushButtonUtils.pPushedButtonUnit
		pCasterTeam = gc.getTeam(pCaster.getOwner())
		if PushButtonUtils.iPushButton == 1:
			for i in range(pPlot.getNumUnits()):
				pUnit = pPlot.getUnit(i)
				if gc.getTeam(pUnit.getOwner()).isAtWar(pCasterTeam):
					return false
			if pPlot.isCity():
				pCity = gc.getPlotCity()
				if gc.getTeam(pCity.getOwner()).isAtWar(pCasterTeam):
					return false
			if not gc.getTeam(pPlot.getOwner()).isOpenBorders(pCasterTeam):
				return false

		return true

For simplicity, we will make it so that the targeted plot must be within 3 plots of the "caster"- the nuke unit. To do this, we will loop through all plots within 3 plots of the nuke unit, and then check their x and y coordinates against the targeted plot's iX and iY coordinates. If they are accurate, we will return true, but if after the entire loop has gone by and we haven't matched the x and y coordinates, we will return false.

Code:
	def canPickPlot(self, argsList):
		pPlot = argsList[0]
		
		pCaster = PushButtonUtils.pPushedButtonUnit
		pCasterTeam = gc.getTeam(pCaster.getOwner())
		if PushButtonUtils.iPushButton == 1:
			for i in range(pPlot.getNumUnits()):
				pUnit = pPlot.getUnit(i)
				if gc.getTeam(pUnit.getOwner()).isAtWar(pCasterTeam):
					return false
			if pPlot.isCity():
				pCity = gc.getPlotCity()
				if gc.getTeam(pCity.getOwner()).isAtWar(pCasterTeam):
					return false
			if not gc.getTeam(pPlot.getOwner()).isOpenBorders(pCasterTeam):
				return false
		
		[COLOR="Red"]if PushButtonUtils.iPushButton == 2:
			bPlotMatch = false
			iX = pCaster.getX()
			iY = pCaster.getY()
			for iiX in range(iX-3, iX+4):
				for iiY in range(iYi3, iY+4):
					if (iiX == pPlot.getX() and iiY == pPlot.getY()):
						bPlotMatch = true
						break

			if bPlotMatch = false:
				return False[/COLOR]

		return true

There's our new code added in red. We're finished! You could add more complicated range checks, but for this example, I will not.

That is the end of this tutorial. If you have problems with making your python action buttons, questions about something in the tutorial, ideas for additional examples of buttons, feel free to post them here.
 
Nice, good work :goodjob:.

I haven't dealt with the action buttons, and didn't want to do it, just because i have a sort of horror when it comes to GUI programming, but now...it doesn't really look complicated.

Maybe i'll add exactly this example (with other values), because i have a mining corp, and that would fit to their CEOs.



Any idea, how to force the AI to use them?
This example, i guess, onUnitMove could be used, to make some checks for the AI, and let it then hit the button.
 
Making the AI use them was going to be the subject of a future part of this guide, if I got it to work.

The easiest way, I think, is to make what you want to do a triggered function somewhere, triggered from the handleInput() code normally.

Then whenever your condition in onUnitMove is satisfied (they are on a tile in their territory without any resources, perhaps?) have the function be triggered and do the effect of pressing the button, without actually pressing the button.
 
Making the AI use them was going to be the subject of a future part of this guide, if I got it to work.

There's no doubt, what will happen ;).

The easiest way, I think, is to make what you want to do a triggered function somewhere, triggered from the handleInput() code normally.

Then whenever your condition in onUnitMove is satisfied (they are on a tile in their territory without any resources, perhaps?) have the function be triggered and do the effect of pressing the button, without actually pressing the button.

That's what i've meant.
If i click on a button, which will trigger a function onButtonClicked, or if i call the function somewhere...no difference.

very cool. i always wanted to make a button for the great prophet that would found a religion instead of techs doing it. i'll have to give this a try

mmmhh, is there not a modcomp, "prophet driven religion", somewhere?
Dosn't matter: Will be interested to see it :).
 
I've updated it with most of my second example- making a teleporting unit, or more specifically, how to make a button that picks a plot and does stuff to that plot. I still need to finish explaining/using the canPickPlot function in CvGameUtils.py, though.
 
I'm getting an error in the function handleInput (simplebutton example)

AttributeError: 'CyGlobalContext' object has no attribute 'getPlot'

I'm using BTS 3.19.
Also looked up the most recent Bts API reference and there is no getPlot listed either. But since all the API docs are quite outdated nowadays it's really confusing...
 
Huh. I though there was one. Or some GlobalContext() function to get a plot.

Oh well, you can use CyMap().plot(x, y) instead. I'll fix this, thanks.
 
I've updated the AI Scripting section. Three buttons are finished.

Now I'm going to make a fourth button (when I have a good idea) that is an example of how to trigger a message, graphical effect, and sound effect when you press the button.
 
maybe this is really obvious and im kinda slow...but when you say "action buttons", those are the buttons we press to do a mission?

eg, air patrol, intercept, build, etc...?

HDK
 
maybe this is really obvious and im kinda slow...but when you say "action buttons", those are the buttons we press to do a mission?

eg, air patrol, intercept, build, etc...?

HDK

Yes, that's correct. Civ refers to those buttons as two things: either a "Command" or a "Mission". I'm not really sure what the difference is... However, both commands and missions are hardcoded in the game core (the SDK), so this is for people who want to add a mission/command but don't want to have to compile a new SDK just for that.
 
Hey, great tutorial. Thanks for putting it up! I plan on using both the prospect and teleport additions in my mod. The question that I have is if I plan on adding several other "misisons or commands" which involve selecting a tile to target (for either war or peaceful purposes) than should I maybe figure out the SDK? What would be better for loading speed and performance? Thanks!

EDIT: I have meticulously followed steps 2 and 3, but when I test, I have NO buttons what so ever. I have no interface, the Civilopedia doesn't work on the main menu and in the game I have to use the keyboard to move units. I was adding your code in the python of the RevolutionDCM mod. It uses the Sevilopedia, BUG and many other mods. I imagine that the problem is that I missed adding "import PushButtonUtils" to other .py files. Should I just add that to any import file? What do you think?
Thanks again.
 
In general, adding stuff to the SDK is much better performance wise, both for speed, loading time, and AI usage (though I don't know how much of a speed there would be lost by this). I doubt a python button looses any speed, since the interface is all done in python. The issue with selecting a target is that you will have to make extensive use of canPickPlot and onPlotPicked to narrow down the plots you want to be able to pick. But I don't know how much easier it would be to do in the SDK... as the reason I learned to do this was I didn't want to mess around in the SDK to add missions.

The problem you describbe probably has to do with all those other mods you have added. BUG modifies the interface, and we're working in the python interface files. Does the first button (the one without adding PushButtonUtils) work if you don't do anything else?

I suggest enabling python exceptions, as there could be an error in the code that might be better found this way. (To do this, go into the Civilization IV.ini/_Civ4Config file in the main Beyond the Sword directory, and search for "Python", and change the HidePythonExceptions value from 1 to 0). This will give a popup that shows the location of any error in the code.
 
Excellent tutorial you have here :)

But there is something I want to do but I'm not sure how to do it. I have an action button and i want it to call an SDK function located in some CvWhatEver.cpp file. How would I do this ?
 
Excellent tutorial you have here :)

But there is something I want to do but I'm not sure how to do it. I have an action button and i want it to call an SDK function located in some CvWhatEver.cpp file. How would I do this ?

You cannot call a function from the Cv_____.cpp files from python.

However, if that function has been exposed to python (if it is in the Cy_____.cpp file, for instance CvUnit.cpp and CyUnit.cpp), then you can call it from the action button.

But if it hasn't been exposed to python, you would probably either have to expose it to python, or by making this a real mission, done entirely in the SDK.
 
But if it hasn't been exposed to python, you would probably either have to expose it to python, or by making this a real mission, done entirely in the SDK.

I admit that my action will require such SDK work. How do I declare a "real mission" in the SDK ?
 
Top Bottom