1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

Modding CvGameUtils

Discussion in 'Civ4 - SDK/Python' started by EmperorFool, Jun 10, 2009.

  1. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    There are three ways to modify the callbacks in CvGameUtils. I will cover all three in this tutorial and compare the differences so you can pick the method that best suits your mod and goals.

    I have attached the modified files for all three methods to this post.

    Important: You should not normally need to modify the CvGameInterface.py module. The only case where that would be necessary is creating new callback functions to call from a custom DLL.

     
  2. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    This is the best method overall because it requires very little work and makes merging mods very easy. It can seem daunting if you don't understand classes or subclassing in Python, but you honestly don't need to in order to make use of it. It has the added benefit that if someone else modifies CvGameUtils in their CustomAssets folder, your mod will work with it.

    There are two steps.

    First, create a new module called MyGameUtils.py. It will contain a single class that subclasses the CvGameUtils class from the CvGameUtils.py module. Copy the callback functions from CvGameUtils that you wish to modify and make your changes to them here.

    For this example capturing a city will always give 1000:gold:. :goodjob:

    Code:
    ## MyGameUtils.py
    
    import CvGameUtils
    
    class MyGameUtils(CvGameUtils.CvGameUtils):
    	def doCityCaptureGold(self, argsList):
    		return 1000
    
    Second, tell BTS about MyGameUtils. This is done in the CvGameInterfaceFile.py module. You need to import your new module instead of CvGameUtils and set a module-level variable called "GameUtils" to your class. Simply replace the three occurrences of "CvGameUtils" with "MyGameUtils".

    Code:
    ## CvGameInterfaceFile.py
    
    import [B]MyGameUtils[/B]
    
    GameUtils = [B]MyGameUtils[/B].[B]MyGameUtils[/B]()
    
    Now whenever CvGameInterface.py dispatches a callback, it looks in MyGameUtils.py for the matching function. For doCityCaptureGold(), the function above is used. If the callback isn't found here, it looks in CvGameUtils.py. This means you only need to define the callbacks that you want to change.

    If you later wanted to change all pillaging to 20:gold:, you would add this to MyGameUtils.py:

    Code:
    class MyGameUtils(CvGameUtils.CvGameUtils):
    	...
    	[B]def doPillageGold(self, argsList):
    		return 20[/B]
    
     
  3. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    This method is easy as well, but it makes merging very difficult and error-prone. It is similar to method 1 except you are copying the entire CvGameUtils class instead of subclassing it.

    It has two steps as well.

    First, create a new module called MyGameUtils.py by copying CvGameUtils.py and changing the "class CvGameUtils" line to "class MyGameUtils". Next for our example, change the doCaptureCityGold() function as above:

    Code:
    ## [B]MyGameUtils.py[/B]
    
    ...
    
    class [B]MyGameUtils[/B]:
    	...
    
    	def doCityCaptureGold(self, argsList):
    		[B]return 1000[/B]
    
    	...
    
    Note that the "..."s represent unchanged code. This module will be just as big as CvGameUtils because it is a modified copy. It must supply all the behavior of CvGameUtils--even the callbacks you aren't changing.

    Finally, perform the second step from method 1: point CvGameInterfaceFile to your new module and class.

    Code:
    ## CvGameInterfaceFile.py
    
    import [B]MyGameUtils[/B]
    
    GameUtils = [B]MyGameUtils.MyGameUtils()[/B]
    
    The main reason that this is less desirable than method 1 is that when merging two mods that use this method, the merger must compare the entire module to the original CvGameUtils to figure out which callbacks have been changed and copy those to the merged module. Tools like WinMerge make this easier.

    Also, anyone wanting to understand how your mod modifies the game must do the same thing. Using method 1, your module will contain only the modified behavior. All of the original behavior is left in the original CvGameUtils module.
     
  4. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    This method is nearly identical to method 2, and it has the same deficiencies. Here you are replacing the CvGameUtils.py module with your own module of the same name so BTS no longer sees the original.

    It has one step.

    Copy CvGameUtils.py to Assets/Python in your mod and make your modifications to the copy. For our example, just change the doCaptureCityGold() function as above:

    Code:
    ## CvGameUtils.py
    
    ...
    
    class CvGameUtils:
    	...
    
    	def doCityCaptureGold(self, argsList):
    		[B]return 1000[/B]
    
    	...
    
    You don't need to modify CvGameInterfaceFile.py because you aren't changing the name of the module or class from the original.
     
  5. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    If you choose method 2 or 3, modders merging your mod into their own will be forced to do the WinMerge Dance and hope they don't miss something. Anyone reading your code will also need to use WinMerge just to figure out what it does differently from the normal game.

    With method 1, not only do you have less work to do, but you make merging and reading your code easier. For example, say two modders create CityGoldGameUtils and PillageGoldGameUtils from the examples above, and now you want to merge them.

    You can either copy the functions from one into the other, calling it CityPillageGoldGameUtils, or you can modify one to subclass the other as was done in method 1. Let's have CityGold subclass PillageGold:

    Code:
    ## CityGoldGameUtils.py
    
    import [B]PillageGoldGameUtils[/B]
    
    class CityGoldGameUtils([B]PillageGoldGameUtils[/B].[B]PillageGoldGameUtils[/B]):
    
    	def doCityCaptureGold(self, argsList):
    		return 1000
    
    Next you point CvGameInterfaceFile.py to CityGoldGameUtils.py. This forms a chain of callback-handling responsibility from CityGold to PillageGold to CvGameUtils. The first one in the chain that has the function gets used.
     
  6. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    Hopefully I've convinced you to use method 1 due to its advantages:

    • Easy to create
    • Easy to see your changes
    • Easy to merge
    • Small code size
    • Easy to plug into BUG Mod
    Please feel free to post questions, comments, or suggestions for this tutorial below. I hope you enjoyed it and found it useful.

    Creating New Callbacks

    If you are creating a mod with a custom DLL, you can add your own Python extension points (callbacks) like those in CvGameUtils for other modders to use.

    Example: Let's block the ability to upgrade to Infantry on even turns.​

    First, add a function for the new callback to CvGameInterface.py. This function will look like all the others: it merely passes on the function call to the game utils object and returns the result.

    Code:
    def cannotUpgrade(argsList):
        return gameUtils().cannotUpgrade(argsList)
    
    Next, add the handling function to your game utils class using one of the above methods.

    Code:
    def cannotUpgrade(self, argsList):
        pUnit, eUnitType = argsList
        eInfantry = gc.getInfoTypeForString("UNIT_INFANTRY")
        return eUnitType != eInfantry or gc.getGame().getGameTurn() % 2 == 1
    
    Finally, call this callback from the SDK, probably in CvUnit::canUpgrade(). This step is beyond the scope of this tutorial.
     
  7. davidlallen

    davidlallen Deity

    Joined:
    Apr 28, 2008
    Messages:
    4,743
    Location:
    California
    I agree that method 1 is better than methods 2,3. However, even with method 1, there are some things to watch out for, if you are not the only python modder in your mod. For example, very often a mod may start out with some existing python, and you want to add in your python.

    The problem arises when both you, and another modder, have changed the same function. You may not even realize it has happened, until some functionality in the previous mod no longer works. Method 3, which is the hardest to maintain, actually makes this problem the easiest to spot. If you want to make some changes to doCityCaptureGold, you look into CvGameUtils.py, and you detect (by the comments ... the previous modder did put in comments right?) somebody else has already changed the function for your mod. Then you are aware of the merging problem and you can design your solution.

    In method 1 or 2, it would be possible to overlook that the other modder has also defined your function. If his subclass has higher priority, then your function will never be called. If your subclass has higher priority, then his function will never be called. As long as you think to search for other definitions of your function in all the places it might be defined, this is not hard; it's just easy to forget.

    Of course (advertisement) it would be better if multiple modders could define callbacks for these entry points rather than overriding the function. Then at least all the functions from the different mods would be called. They could still cause unexpected effects, but at least it would be possible to have a clean merge.
     
  8. The_J

    The_J Say No 2 Net Validations Retired Moderator Supporter

    Joined:
    Oct 22, 2008
    Messages:
    32,874
    Location:
    DE/NL/FR
    I used the 3. way until now :blush:.
    Didn't know, that the other ways are possible, and i'm not sure, if i'll change my workflow, because more files mean more sources for errors.
    But nice to see, what's possible :cool:.
     
  9. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    Method 2 and 3 aren't wrong in any way, just different. Most people aren't fluent in OOP (Object Oriented Programming), and the Civ code doesn't make use of it so there's no good example to follow when modding.

    Yes, changing what is already working has its drawbacks. I'm hoping this tutorial helps future modders, but I wrote it more as a background explanation for the thread about making CvGameUtils modular. :mischief: I started explaining a lot of this in the other thread and decided to split them in the hopes someone would find the tutorial useful.

    As for two files, the second file can be literally 2 lines long. There's not a lot of room for error in

    Code:
    import MyGameUtils
    
    GameUtils = MyGameUtils.MyGameUtils()
    
     
  10. The_J

    The_J Say No 2 Net Validations Retired Moderator Supporter

    Joined:
    Oct 22, 2008
    Messages:
    32,874
    Location:
    DE/NL/FR
    I know it's not wrong, but sometimes it's just stupid.
    I have in my CvRandomEventInterface.py 3 times nearly the same code, because i was to lazy to create a new function :blush:.

    And yes, there's no good example for OOP, i even hadn't really realized, that every .py-file here is one big class.


    Right, should be useful :).
     
  11. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    Well, technically not. Each module has attributes and functions like a class, but it can also define multiple classes. Just like you can define classes in functions and functions in functions and . . .

    Most people build their modules like a class, and I've done that to a certain extent in BUG. Usually I'll have a single class in a module with some module-level constants and global variables where necessary. The BUG Core modules, however, often have multiple classes per module. It really depends on the complexity of the feature.
     
  12. OrionVeteran

    OrionVeteran Deity

    Joined:
    Dec 25, 2003
    Messages:
    2,443
    Location:
    Newport News VA
    I tried Method 1: Subclass CvGameUtils

    I like the concept of modularizing the python using a subclass, but I encountered a problem where two of my Subclass functions failed to perform the same way they had performed in the original CvGameUtils.py file. The functions that failed were:

    doHolyCity
    doHolyCityTech

    There were no python errors at all. The two functions just failed to do their tasks. For example a religion was founded, when the code in the Subclass function should have denied it. The failure was repeated. I'm not sure what to do to fix it.

    Orion Veteran :cool:
     
  13. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    Start by posting some code. That's always the best first step. ;) Without seeing it I can only assume you did it exactly as I did, and in that case it "should" have worked and I can't say anything else. :confused:

    I have successfully built the code in BUG to completely modularize CvGameUtils, and so far so good. I have only replaced a few functions: getWidgetHelp(), doPillageGold() and doCityAcquiredGold(). Perhaps these other functions don't work the same way. If you show me what you did, I can try it here and see what I get.
     
  14. OrionVeteran

    OrionVeteran Deity

    Joined:
    Dec 25, 2003
    Messages:
    2,443
    Location:
    Newport News VA
    Sure:
    Spoiler :

    Code:
    # # Sid Meier's Civilization 4
    # # Copyright Firaxis Games 2007
    # # InquisitionGameUtils
    
    import CvUtil
    from CvPythonExtensions import *
    import CvEventInterface
    import CvGameUtils
    import Popup as PyPopup
    import PyHelpers
    import GodsOfOld
    import Inquisition
    
    # globals
    gc = CyGlobalContext()
    PyPlayer = PyHelpers.PyPlayer
    PyInfo = PyHelpers.PyInfo
    PyCity = PyHelpers.PyCity
    PyGame = PyHelpers.PyGame
    
    # Modular Python to eliminate Merging 
    class InquisitionGameUtils(CvGameUtils.CvGameUtils):
    


    The Functions:

    Spoiler :

    Code:
    def doHolyCity(self):
    	# Runs Third
    	# Purpose: Determine if Religion should be allowed to be founded
    	# Returns True if religion should not be founded
    	# Returns False if religion should be founded (Default)
    		
    	# If game option is NOT Limited Religions
    	if not Inquisition.isOC_LIMITED_RELIGIONS():
    		# Allow religion to be founded
    		return False
    		
    	# Stop religion from being founded
    	return True
    


    And...

    Spoiler :

    Code:
    def doHolyCityTech(self,argsList):
    	eTeam = argsList[0]
    	ePlayer = argsList[1]
    	eTech = argsList[2]
    	bFirst = argsList[3]
    	eTeam = argsList[0]
    	ePlayer = argsList[1]
    	eTech = argsList[2]
    	bFirst = argsList[3]
    	# Runs Second
    	# Purpose:  Determine if Religion Popup should be dispalyed
    	# Returns True if Religion Popup should not be dispalyed
    	# Returning False if Religion Popup should be dispalyed	(Default)		
    		
    	# If game option is NOT Limited Religions
    	if not Inquisition.isOC_LIMITED_RELIGIONS():		
    		# Allow the Religion popup to be displayed
    		return False
    			
    	# If game option is Limited Religions
    	elif Inquisition.isOC_LIMITED_RELIGIONS():
    		# If game option is set to choose religions
    		if CyGame().isOption(GameOptionTypes.GAMEOPTION_PICK_RELIGION):
    			# Does the player have a Holy City?
    			if not Inquisition.OwnsHolyCity(ePlayer):
    				# Allow the Religion popup to be displayed
    				#CyInterface().addMessage(CyGame().getActivePlayer(),True,25,'Message 2','AS2D_DISCOVERBONUS',1,'Art/Interface/Buttons/TerrainFeatures/Forest.dds',ColorTypes(8),0,0,False,False)
    				return False					
    					
    	# Stop Religion Popup from being dispalyed
    	return True
    


    The functions work fine in CvGameUtils but not in InquisitionGameUtils.

    Respectfully,

    Orion Veteran :cool:
     
  15. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    Did you perform step #2?

    Code:
    ## CvGameInterfaceFile.py
    
    import [B]InquisitionGameUtils[/B]
    
    GameUtils = [B]InquisitionGameUtils[/B].[B]InquisitionGameUtils[/B]()
    
     
  16. OrionVeteran

    OrionVeteran Deity

    Joined:
    Dec 25, 2003
    Messages:
    2,443
    Location:
    Newport News VA
    Yup! It looks like this:

    Spoiler :

    Code:
    import CvUtil
    import CvGameUtils
    import CvGameInterfaceFile
    import CvEventInterface
    from CvPythonExtensions import *
    import InquisitionGameUtils
    
    # globals
    gc = CyGlobalContext()
    normalGameUtils = CvGameInterfaceFile.GameUtils
    GameUtils = InquisitionGameUtils.InquisitionGameUtils()
    


    Orion Veteran :cool:
     
  17. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    That's your CvGameInterfaceFile.py module? It looks like it should work, but it's very odd that it imports itself and references a variable that hasn't been defined yet. Use exactly the code I just posted.

    If instead that is the top of your CvGameInterface.py file, you should remove the file entirely. Instead, replace CvGameInterfaceFile.py with the one I posted.

    CvGI should define a normalGameUtils variable that it assigns the value from CvGIF.GameUtils. You don't need to and shouldn't modify CvGI unless you are creating entirely new callback functions in a custom DLL (not covered yet by this tutorial).
     
  18. OrionVeteran

    OrionVeteran Deity

    Joined:
    Dec 25, 2003
    Messages:
    2,443
    Location:
    Newport News VA
    It was the top of my CvGameInterface.py file. I replaced it as you said with the CvGameInterfaceFile.py and now the two functions work correctly. :) :dance:

    This is a huge improvement to the Inquisition mod, as it eliminates the need for merging of the CvGameUtils functions. :goodjob: All of my CvGameUtils functions will now go into the new InquisitionGameUtils.py file.

    I desire to expand upon this process. With that in mind, does BUG work the same way? If so, how did you name the files? I'm looking to make these changes to RevDCM. Lastly, can a similar file be created to run specified functions from the CvMainInterface.py file?

    Orion Veteran :cool:
     
  19. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    I cover how I did this for BUG in this thread on the subject. The code is in BUG now and I'm working on the piece that makes configuring your game utils modules from XML easier.
     
  20. EmperorFool

    EmperorFool Deity

    Joined:
    Mar 2, 2007
    Messages:
    9,633
    Location:
    Mountain View, California
    I added a note to the first post that you should not modify the CvGameInterface.py module. I also added an addendum after the conclusion explaining how to create your own callbacks. This is the one case where you will need to modify that module, but it should be pretty rare since you can just call your own function directly instead of using GameUtils.
     

Share This Page