Button allowing road dismantling, via Python

Herostratus

Grim Harbinger of Things to Come
Joined
Jul 24, 2009
Messages
116
In my mod I want to make it possible for gunpowder units (and only gunpowder units, at least for now) to dismantle roads via an action button that only shows up on the button bar when they're on a road.

My thinking is that pressing the button should trigger an event that uses iRouteChange. It seems to me that this should be possible. This thread was where I started, but I'm not able to make it work. When I start a game, it doesn't show the 4000 BC splash screen, the button bar, the city screen after founding the first city--all the interface is gone, really.

I should clarify that I'm still a Python noob. After turning on exceptions, I think where I'm going wrong is that I don't have the right code to actually trigger the event.

Here's what I have in MainInterface.py, adapted from the linked thread above:
Code:
					pUnit = g_pSelectedUnit
					iUnitCombatType = pUnit.getUnitCombatType()
					pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
					if pUnitOwner.isTurnActive( ):
						if iUnitCombatType == gc.getInfoTypeForString('UNITCOMBAT_GUNPOWDER'): 
							screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
							screen.show( "BottomButtonContainer" )
							iCount = iCount + 1
Code:
if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
			self.pPushedButtonUnit = g_pSelectedUnit
			iX = self.pPushedButtonUnit.getX()
			iY = self.pPushedButtonUnit.getY()
			pDismantle = CyMap().plot(iX, iY)
			if pDismantle.getRouteType('ROUTE_ROAD'): 
			triggerData = pPlayer.initTriggeredData(gc.getPlayer(0).trigger(gc.getInfoTypeForString("EVENT_TRIGGER_DISMANTLE")))
			g_pSelectedUnit.changeMoves(60)
The line in the second one containing "EVENT_TRIGGER_DISMANTLE" came up when I turned on exceptions. I'm sure I'm stumbling blindly here, so any guidance would be appreciated--whether it's rewriting my code :D or just pointing me to a mod that definitely has an action button that triggers an event. Or maybe there's an even simpler way that doesn't require events at all.

P.S. I'm sure there's a way through the SDK to enable such a button, but I'm not willing to go that deep down the rabbit-hole, so I gotta do it via Python.
 
Instead of using events can't you use pDismantle.setRouteType('NO_ROUTE') ?
Also you need to indent python statements for loops/ifs to work so the the following:
PHP:
if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
	self.pPushedButtonUnit = g_pSelectedUnit
	iX = self.pPushedButtonUnit.getX()
	iY = self.pPushedButtonUnit.getY()
	pDismantle = CyMap().plot(iX, iY)
	if pDismantle.getRouteType('ROUTE_ROAD'): 
		pDismantle.setRouteType('NO_ROUTE')
		g_pSelectedUnit.changeMoves(60)
 
Well, that definitely changed things :)
Now the interface looks normal, and the Dismantle button does appear-- but on any tile, not just tiles with roads. Also, pushing the button does nothing but make the little "boomp" sound.

The other issue (which I resolved myself) is that apparently I can't specify a unit COMBAT type, because gunpowder units didn't get the Dismantle button--only settlers and workers appeared to. I assume that has to do with settlers and workers lacking a combat type. So for now I'm just assigning the ability to one specific unit type.
 
The reason the code isn't doing anything is the call to check the existing route type is incorrect. There is some documentation on the Python API at http://civ4bug.sourceforge.net/PythonAPI/index2.html.

The CyPlot.getRoute() function doesn't take any arguments and returns the route type and I forgot that you need to pass the ID of the route to the function and not the name so the code would need to be updated to:
PHP:
if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
	self.pPushedButtonUnit = g_pSelectedUnit
	iX = self.pPushedButtonUnit.getX()
	iY = self.pPushedButtonUnit.getY()
	pDismantle = CyMap().plot(iX, iY)
	if pDismantle.getRouteType() == gc.getInfoTypeForString("ROUTE_ROAD"): 
		pDismantle.setRouteType(gc.getInfoTypeForString("NO_ROUTE"))
		g_pSelectedUnit.changeMoves(60)

What code do you have to determine when the button gets put on the screen as you should be able to select certain combat classes. If it only works with settlers/workers then I suspect that you are passing in the wrong value when you make the check.
 
IT'S WORKING :goodjob:
thank you thank you thank you.

The code works if I specify a particular unit (which I've changed to Great Engineer, and I'm actually happy with that). I just changed up the code in the first box I posted.
 
Follow-up question: how would I make that same button do the same thing to a railroad?
 
This says if there is any kind of route remove it:

PHP:
if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
    self.pPushedButtonUnit = g_pSelectedUnit
    iX = self.pPushedButtonUnit.getX()
    iY = self.pPushedButtonUnit.getY()
    pDismantle = CyMap().plot(iX, iY)
    if pDismantle.getRouteType() != gc.getInfoTypeForString("NO_ROUTE"): 
        pDismantle.setRouteType(gc.getInfoTypeForString("NO_ROUTE"))
        g_pSelectedUnit.changeMoves(60)
 
Excellent! I think you just made a lot of people happy, judging by how many old threads there are on this topic :king:
 
Looking at your original post the reason that the unit combat type didn't work is that you had an incorrect type name for gunpowder units, it should be UNICOMBAT_GUN (takes from Assets/XML/BasicInfos/CIV4UnitCombatInfos.xml). You could also limit the button to only appear when you are on a plot with a route with the following:

PHP:
pUnit = g_pSelectedUnit
pPlot = CyMap().plot(pUnit.getX(), pUnit.getX())
if pPlot.getRouteType() != gc.getInfoTypeForString("NO_ROUTE"):
	iUnitCombatType = pUnit.getUnitCombatType()
	pUnitOwner = gc.getPlayer( pUnit.getOwner())
	if pUnitOwner.isTurnActive():
		if iUnitCombatType == gc.getInfoTypeForString('UNITCOMBAT_GUN'): 
			screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
			screen.show( "BottomButtonContainer" )
			iCount = iCount + 1
 
Good call! Although I think you meant for that second X to be a Y ;)
 
Coding a python button without using mod messages lead to oos errors.
Plus, you didn't bother to check if the unit can move before executing the codes.
 
Yikes! Thanks Platy-- I am absolutely concerned about OOS errors, since I mainly play multiplayer.

But I don't know what a "mod message" even IS-- and I have no idea how to add a check for whether the unit can move. I have playtested it a bit (in multiplayer also) and I haven't noticed any issues, but I can't recall whether I attempted to push the button when the unit was out of moves...
 
In TCO1's tutorial, 2nd post, below this sentence: "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." ;)

Now, I'm surprised TCO1 doesn't address the onModNetMessage (CvEventManager) + handleInput (MainInterface). I hope I did not miss one file.

You can have examples in Platy's Happy Razing or Inquisition.
 
OK, so I assume this is the bit you're referring to in EventManager:
Code:
	def onModNetMessage(self, argsList):
		'Called whenever CyMessageControl().sendModNetMessage() is called - this is all for you modders!'
		
		iData1, iData2, iData3, iData4, iData5 = argsList
## Happy Razing ##
		if iData1 == 4444:
			pPlayer = gc.getPlayer(iData2)
			pCity = pPlayer.getCity(iData3)
			if pPlayer.canRaze(pCity):
				pPlayer.raze(pCity)
			else:
				pCity.kill()

		elif iData1 == 4445:
			pPlayer = gc.getPlayer(iData3)
			pCity = pPlayer.getCity(iData4)
			pCity.setNumRealBuilding(iData2, 0)
			pPlayer.changeGold(iData5)
## Happy Razing ##
					
		print("Modder's net message!")
		
		CvUtil.pyPrint( 'onModNetMessage' )

...and this from MainInterface:
Code:
	# Will handle the input for this screen...
	def handleInput (self, inputClass):
## Happy Razing ##
		if inputClass.getFunctionName() == "HappyRazing":
			pCity = CyInterface().getHeadSelectedCity()
			Razing.Razing().interfaceScreen(pCity.getOwner(), pCity.getID())
## Happy Razing ##
		return 0

Where I'm lost is how to adapt these to my button. Here is my current MainInterface stuff (I made it so that Great Engineers, gunpowder units, AND armored units get the button):
Code:
## Dismantle Road
					pUnit = g_pSelectedUnit
					pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
					if pPlot.getRouteType() != gc.getInfoTypeForString("NO_ROUTE"):
						iUnitCombatType = pUnit.getUnitCombatType()
						pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
						if pUnitOwner.isTurnActive( ):
							if iUnitCombatType == gc.getInfoTypeForString('UNITCOMBAT_GUN'):
								screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
								screen.show( "BottomButtonContainer" )
								iCount = iCount + 1
						iUnitCombatType = pUnit.getUnitCombatType()
						pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
						if pUnitOwner.isTurnActive( ):
							if iUnitCombatType == gc.getInfoTypeForString('UNITCOMBAT_ARMOR'):
								screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
								screen.show( "BottomButtonContainer" )
								iCount = iCount + 1
						iUnitType = pUnit.getUnitType()
						pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
						if pUnitOwner.isTurnActive( ):
							if iUnitType == gc.getInfoTypeForString('UNIT_ENGINEER'):
								screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
								screen.show( "BottomButtonContainer" )
								iCount = iCount + 1
## end Dismantle Road
Code:
## Dismantle Road
		if (inputClass.getNotifyCode() == 11 and inputClass.getData1() == 300 and inputClass.getData2() == 300):
			self.pPushedButtonUnit = g_pSelectedUnit
			iX = self.pPushedButtonUnit.getX()
			iY = self.pPushedButtonUnit.getY()
			pDismantle = CyMap().plot(iX, iY)
			if pDismantle.getRouteType() != gc.getInfoTypeForString("NO_ROUTE"):
				pDismantle.setRouteType(gc.getInfoTypeForString("NO_ROUTE"))
				g_pSelectedUnit.changeMoves(60)

## end Dismantle Road

So first question: for adding the movement point check, am I right in thinking it should go like this?:
Code:
## Dismantle Road
					pUnit = g_pSelectedUnit
					pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
					if pPlot.getRouteType() != gc.getInfoTypeForString("NO_ROUTE"):
						iUnitCombatType = pUnit.getUnitCombatType()
						pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
						if pUnitOwner.isTurnActive( ):
							[COLOR="Red"]if pUnit.getMoves() > 0:[/COLOR]
								if iUnitCombatType == gc.getInfoTypeForString('UNITCOMBAT_GUN'):
									screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
									screen.show( "BottomButtonContainer" )
									iCount = iCount + 1
						iUnitCombatType = pUnit.getUnitCombatType()
						pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
						if pUnitOwner.isTurnActive( ):
							[COLOR="Red"]if pUnit.getMoves() > 0:[/COLOR]
								if iUnitCombatType == gc.getInfoTypeForString('UNITCOMBAT_ARMOR'):
									screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
									screen.show( "BottomButtonContainer" )
									iCount = iCount + 1
						iUnitType = pUnit.getUnitType()
						pUnitOwner = gc.getPlayer( pUnit.getOwner( ))
						if pUnitOwner.isTurnActive( ):
							[COLOR="Red"]if pUnit.getMoves() > 0:[/COLOR]
								if iUnitType == gc.getInfoTypeForString('UNIT_ENGINEER'):
									screen.appendMultiListButton( "BottomButtonContainer", ArtFileMgr.getInterfaceArtInfo("INTERFACE_DISMANTLE").getPath(), 0, WidgetTypes.WIDGET_GENERAL, 300, 300, False )
									screen.show( "BottomButtonContainer" )
									iCount = iCount + 1
## end Dismantle Road

And second question: what, then, should be added to EventManager and MainInterface for the mod message? And is this a "message" in the sense that other players will get "news" text about a road having been dismantled? :confused:
 
1) right

2) I might be misleading you with my lack of real understanding of it.

I also used this tutorial to create an action button: it works with the onPlotPicked method and the PushButtonUtil file but I have nothing either in the onModNetMessage. As I don't play MP, I can't test it.

Edit: what I do have in handleInput (MainInterface) is:
Code:
		if inputClass.getNotifyCode() == 11 and inputClass.getData1() == 8888:
				PushButtonUtil.iPushButton = 1
				PushButtonUtil.pPushedButtonUnit = g_pSelectedUnit
				CyInterface().setInterfaceMode(InterfaceModeTypes.INTERFACEMODE_PYTHON_PICK_PLOT)
 
1) Wrong, getMoves is the opposite. movesLeft is more likely.

2) When you pressed a newly customized button on YOUR interface, only your computer system knows you pressed that button. So any codes done directly will only trigger on your system. Thus, you need to send a message to other computers to tell them "Hey, I pressed this button, please trigger the codes too".

So instead of "Button pressed => Do This"
It should be "Button pressed => Inform everyone via Mod Net Messaging => Do This"
 
OK, I think I understand the principle behind it. Where I'm lost is *how* to do that.

If it matters, this is what I have right now in GameUtils:
Code:
## Dismantle Road ##
		iType = WidgetTypes.WIDGET_GENERAL
		if (eWidgetType == iType):
			if (iData1 == 300):
				return CyTranslator().getText("TXT_KEY_BUTTON_DISMANTLE",())
## end Dismantle Road ##
 
Since the Dragon is asleep again (and I don't have much time to deal with this at the moment - sorry), all I can say is that what you have above is probably under getWidgetHelp (and you should specify this please), which is just the mouseover help.
 
Yeah, I figured that's what that code meant. Well, I don't hold out much hope, but I will experiment with derivations of others' mod-message code and see what happens :blush:
 
Back
Top Bottom