1. We have added the ability to collapse/expand forum categories and widgets on forum home.
    Dismiss Notice
  2. All Civ avatars are brought back and available for selection in the Avatar Gallery! There are 945 avatars total.
    Dismiss Notice
  3. To make the site more secure, we have installed SSL certificates and enabled HTTPS for both the main site and forums.
    Dismiss Notice
  4. Civ6 is released! Order now! (Amazon US | Amazon UK | Amazon CA | Amazon DE | Amazon FR)
    Dismiss Notice
  5. Dismiss Notice
  6. Forum account upgrades are available for ad-free browsing.
    Dismiss Notice

How to make a Python mod

Discussion in 'Civ4 - Modding Tutorials & Reference' started by Baldyr, Aug 6, 2010.

  1. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    This tutorial details how the Rebels Python mod was done. It was requested by a non-programmer looking to get into Python modding and the specifics are detailed in this post.

    Note however that this tutorial will only describe how this particular mod was done. You won't learn how to make your own Python mods, because this requires programming skill. If you wanna keep up with what happens in this tutorial you might wanna have this textbook handy. It will teach you all the Python you need to mod CivIV - and then some.

    What this tutorial however does go into some detail about is how to do CivIV specific Python. Because there is a distinction between the language itself and its implementation for modding. And the latter you won't learn in the textbook.

    I'll post one lesson at a time, each time adding some code and explaining what it does. Once all questions are answered I'll post another one, until the entire script is completed.

    The first homework assignment is to open up the built-in Python console in CivIV. First you enable cheat mode in the Civilization4.ini file. Then you figure out what shortkey opens up the console. On a US keyboard it is supposedly shift + ~ but that doesn't seem to be true for other keyboard layouts. (On my Swedish keyboard its shift + รถ.)

    You know that you have found it if it says "Python 2.4.1" - if it doesn't then that's not it. (There are other consoles and tools also.) I believe that the shortkey to open the Python console always include the shift key though, so non-US modders could just try all keys together with the shift key... :rolleyes: Wikipedia might be able to offer clues as to what key it could be.

    To test the Python console you write the following line on the command prompt:
    Code:
    print "Hello World!"
    If the console returns the words hello and world, then you have just joined the community of programmers, as you just executed your first program. :goodjob: Also, you can do math with the console - and the result will be shown as long as you include the print command. Like:
    Code:
    print (1 + 2) * 3 / 2
    (The print command isn't really used in modding though, but it can be used outside the console to print messages into the Python debug log.)

    We'll use the Python console in the first lesson and again at the end of the course. You can of course follow the tutorial even without using the console, but then you won't be able to join in on the fun. :D
     
  2. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    Lets get busy with some practical Python. Firstly; fire up the game. (You should have cheat mode enabled.) Then; open up the Python console. See the previous post for details.

    Now, at the command prompt, enter this line of code:
    Code:
    from Popup import PyPopup
    This is an import statement and it makes the PyPopup "class" available in the Python Console. Its located in a module called Popup, so it needs to be imported before it can be accessed.

    Python is a modular language and while there is a built-in library of key-words, functions and methods, all the rest needs to be imported before it can be used. As an example, you have to import all the CivIV specific Python modules before you can use them. This is important to know and we'll revisit the subject soon enough.

    There is another Popup class available in the CvPythonExtensions module - which incidentally is already available in the Python console - but we'll not be using that. Because PyPopup is much simpler to use. This is what you do:
    Code:
    modPopup = PyPopup()
    This is an assignment statement and it assigns something to the variable called modPopup. Variables are named by the programmer and can be anything, as long as it only contains valid characters - and no whitespace!

    What the actual value that is assigned here is a long story, but PyPopup() is an example of something that is called a "class constructor", and it creates a new instance of that class. So the modPopup variable refers to a PyPopup "object" then. If you wanna create another popup, then you call the class constructor one more time and assign it to another variable. You can do this as many times as you need to, depending on how many different popups you wanna use.

    Classes and their constructors are part of something called Object-Oriented Programming, but this field in computer science is not covered in this tutorial. You really wanna learn more about the subject though, as CivIV Python itself is an example of OOP. (It will all make so much more sense once you know about classes and their use.)

    More code:
    Code:
    modPopup.setHeaderString("Tutorial")
    modPopup.setBodyString("This is a Python tutorial.\n\nby Baldyr")
    These are two examples of method invocation.

    The PyPopup instance we're referencing with the modPopup variable has "methods" defined in its class, and we call on them with dot notation. This is a common practice in both regular and CivIV Python, and it entails putting an object (like an instance of a class - like PyPopup) first, followed by a dot, and lastly we add the method name. (In this case the names pretty much tell what they will do for the Popup.) Inside the parenthesis you put parameters, and in this case these are string values.

    A string can be any combination of characters inside a set of quotations. Text, basically - in this case text messages. The "\n" bit translates into "newline" and creates a line-break. There are other types of values also, like numerical values (integer and floating point values), and different types of data structures (like lists). CivIV Python also comes with its own types, which are inherited from the C++ code. We'll revisit all these in the upcoming lessons.

    And finally we display the popup with:
    Code:
    modPopup.launch()
    This is another method invocation and there is no value given as a parameter.

    Done! This is what the complete program looks like:
    Code:
    from Popup import PyPopup
    
    modPopup = PyPopup()
    modPopup.setHeaderString("Tutorial")
    modPopup.setBodyString("This is a Python tutorial.\n\nby Baldyr")
    modPopup.launch()
    This concludes the first lesson and now its time for questions and trouble shooting. The next lesson will be online soon.
     
  3. tchristensen

    tchristensen Chieftain

    Joined:
    Jul 21, 2010
    Messages:
    1,241
    Location:
    Grand Rapids, Mi
    Woots I have taken my first into programming. Baby steps for sure, but I am moving forward inch by inch.

    It took me about 5 times on each line to get the syntax right, but I got it!!

    Is this something we could put into the game for real? So that when the game starts, the box appears!!!

    As an after thought, I had a hard time getting out of the Python control console and eventually just three-fingered to the Task Manager and quit the program. Is there a command to close the window.
     
  4. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    :goodjob:

    Yeah, for sure. Come to think of it, we should also have a text message heralding the rebellion. But lets save that one to an advanced class. :p (Just remind me once we're done, ok?)

    Oh, I forgot. Its the same shortkey (with or without shift).

    Next lesson is up.
     
  5. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    Next up we will be setting up a Python script that contains all the code needed for the mod. At the end we'll save it as a Python module that can be imported by other Python code.

    The code from the previous lesson will also be included, but note that you can't run the code in following lessons in the Python console! You will be able to use it to test individual statements however, but not the whole script. Instead, we enter the lines of code in a text or code editor, like the default code editor that is downloaded with the Python language.

    Firstly we add more import statements so that we get a Application Programming Interface - or API - to work with:
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    The first module is actually not a Python module as such, but is rather the library of classes and methods that the game offers up for modders. So we need that.

    The second module is an additional API written in Python (as the py prefix indicates). It wraps some of the most useful CivIV Python methods into an alternate setup, but most importantly it adds a couple of key methods not at all present in the standard library! We could write that code ourselves though, but there is no need to redo what the professionals at Firaxis have already done for us.

    Also, we wanna define some variables. Lets start with these:
    Code:
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    All four assignments are of class instances, and the first couple of classes can be found in the CivIV Python API. The fourth one is part of PyHelpers. We need all these objects to use with methods for fetching game info in our script. But we could also create new instances of these every time we wanna use the methods, but that wouldn't be proper.

    Also, we add a global variable:
    Code:
    iPropability = 5
    First and foremost this is an integer value, as indicated by the i prefix. The name doesn't actually make it an integer value - I did by giving it an none-floating point value. This value defines the percentile chance of a given city spawning rebel units on a given game turn. We add it here at the beginning so that it will be easy to find later. Because we might wanna tweak this value when we play test the mod. Its also convenient for anyone else looking to use the mod as part of their own work.

    Also, this variable is a so called constant, because the iProbability value won't ever change while the script is running. Again, not because it is a constant - its rather a constant because we won't be changing the value with the script. (We will also be using boolean values and lists later, which are easily identified by the b and l prefixes.)

    edit: this was added to the post after it was originally posted:
    And lastly, we add a couple of string values:
    Code:
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    These correspond to the values we used in the last lesson. If we define string values as variables it will be easy to change them later, and the code will be less cluttered up. And we will want to use these constants to name the mod and display a description of the mod. But we'll wait a while with that and focus on the actual scripting.

    This is what the script looks like thus far:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    # show popup
    
    modPopup = PyPopup()
    modPopup.setHeaderString(popupHeader)
    modPopup.setBodyString(popupMessage)
    modPopup.launch()

    (Note the comment lines starting with the # character. These do nothing for the script but are rather a way of documenting the code.)

    We save your work as Rebel.py and that ends lesson #2. For the next lesson, you'd wanna be able to open the Python module to continue scripting, so figure out how to do that.
     
  6. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    In the previous lesson we setup and created the Rebels Python module. It doesn't actually do much at this point - just displays a popup - but now we'll get to work doing the actual mod now!

    So we want to run some code once on every single game turn. But we can worry about how to get the game to import and then execute the code later. Now we add content to the mod.

    Firstly we need to fetch references for all active Civs (or players) - and those will vary from game to game, from game turn to game turn. We can use the PyHelpers module to achieve this relatively painlessly:
    Code:
    lPlayers = pyGame.getCivPlayerList()
    This is actually yet another assignment statement. This time we define the variable lPlayers which will hold references to all current players on the current game turn. (So its not a constant - it changes dynamically as we want the variable to hold current values at all times.)

    Note that we use used dot-notation to fetch the player list. The object is the reference to PyGame - which is part of the PyHelpers module as indicated by the py prefix - and the method getCivPlayerList() is part of the PyGame class. (If you open up the PyHelpers.py file located in the game's \Assets\Python\ folder you can actually find the class definition followed by the method definition, confirming that this is true.)

    One thing about functions and methods (functions belonging to a class): They always "return" something - even if it would just be the value None. The getCivPlayerList() method for instance returns a list of valid player instances. This list value can therefore be assigned to any variable.

    Also, the regular CivIV Python works with "Cy" instances - there is CyPlayer, CyTeam, CyCity and CyUnit - along with CyGame and CyMap that we used in the previous lesson - and many, many more. But, the PyHelpers API works instead with "Py" objects - PyGame, PyPlayer and PyCity. :crazyeye: This is very confusing and the explanation lies in the Object-Oriented Programming setup of CivIV. The important thing to know is however that the CivIV Python can't work with Py objects, and that PyHelper module can't handler Cy objects.

    In short: The lPlayers list variable contains a set of PyPlayer references - not CyPlayer references. As long as we keep this in mind we'll be fine. There will also be a need to convert from one type of object to another - more on this below.

    Next we iterate through all the players with the commands for and in:
    Code:
    for pyPlayer in lPlayers:
    What this does is it loops through the contents of the lPlayers list - whatever its contents may be - one player reference at a time. Since we're dealing with PyPlayer references we can call the current value pyPlayer (as opposed to cyPlayer). So every time the loop starts over the pyPlayer variable changes to whatever is the next PyPlayer instance. And once they are all processed the loop terminates automatically. (With five active players we get five loops, with ten players we get ten loops.)

    The colon at the end of the line indicates that this iteration statement must have some content to call its own. In fact, it holds a "block" of code - the code that is repeated on every iteration. And the next block of code is this statement:
    Code:
      lCities = pyPlayer.getCityList()
    So, working with the PyHelpers setup, we fetch yet another set of values wrapped in a list value. Now we get all the active cities for each PyPlayer in the loop created by the previous line. So the first time around lCities will reference all the cities of the first player, and on the second iteration it will reference the cities of the second player. And so on.

    Note the blank spaces at the beginning of this last line! This is an important concept in Python as indentation is used to organize code and divide it into blocks. By using two blank spaces I just made a rule for this particular script: Every level of indentation is henceforth defined by two instances of blank space. So by adding or subtracting two blank spaces we increase or decrease the indentation level. (I could however have used any number of blank spaces - or tabs.) The important thing now is consistency and to preserve the integrity of the indentation trough-out the script.

    Next up we need to traverse also the lCities list to check every city in order, player by player. To achieve this we create another loop inside the first loop! :eek:
    Code:
      for pyCity in lCities:
    This is exactly the same thing as above - and the indentation level tells us that this line is part of the same block of code as the previous line. There is another colon at the end of this line though, which means that the indentation level goes up one more level:
    Code:
        cyCity = pyCity.GetCy()
        bDisorder = cyCity.isDisorder()
    We conclude this lesson by assigning a couple variables that will change with every loop through the list of players and the list of cities.

    The first line fetches a Cy instance corresponding to the current PyCity object with the GetCy() method belonging to the PyCity class. This value is stored in the temporary variable cyCity (which is different from the pyCity variable). Because we need the CyCity instance to be able to use methods belonging to the CyCity class.

    The second line invokes a CyCity method on the cyCity variable. The value returned is a boolean value, meaning that it is either True or False - no other value is valid (although 1 and 0 can double as these values also). Since the method in question looks up whether or not the city is in disorder - and if it is, then the bDisorder value will be True - otherwise its False.

    This is what our script looks like now:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    # show popup
    
    modPopup = PyPopup()
    modPopup.setHeaderString(popupHeader)
    modPopup.setBodyString(popupMessage)
    modPopup.launch()
    
    # code checked every turn
    
    lPlayers = pyGame.getCivPlayerList()
    for pyPlayer in lPlayers:
      lCities = pyPlayer.getCityList()
      for pyCity in lCities:
        cyCity = pyCity.GetCy()
        bDisorder = cyCity.isDisorder()

    In the next lesson we will check whether or not the conditions for the rebel unit spawn event are met or not. This will be decided on a city to city basis.

    The homework assignment, in the meanwhile, is to open up the CivIV Python API and click on the CyCity class in the menu (upper left corner). Now, scroll down to entry #328 and there it is: isDisorder(). The word "BOOL" indicates that the method returns a boolean value.

    Have the API handy going forward and use it to look up all the methods used in this tutorial. (The PyHelpers methods are instead found in the PyHelpers.py file as described above.) You will eventually wanna learn how to use the API to find all the methods you need for your own programming!
     
  7. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    In the previous lessons we used import statements to get hold of the tools we need to do our mod, and also assignment statements that linked references to values and different "objects" to variables. Additionally, we had the code execute loops while assigning different values to variables on each iteration. Now we'll try some conditional statements.

    So, with the code we already have, we are getting references to all cities in the game (in both PyCity and CyCity format). The cardinal condition for any of the rebel units appearing is that the city should be in a state of disorder. And we already have a variable holding the value True if it is - bDisorder. Now we employ the if command:
    Code:
        if bDisorder == True:
    This is pretty self-explanatory and you could also imagine the colon at the end being a replacement for a imaginary "then" command. So the statement simply asks the question: if the variable bDisorder is equal to the value True, then the following block of code will be executed. And that block of code is actually what makes up the rest of the code, so we increase the indentation level yet again:
    Code:
          iForeigners = cyCity.getCulturePercentAnger()
          bNeverLost = cyCity.isNeverLost()
    Time for some more assignment statements. Look up both these CyCity methods in the API (they are ordered alphabetically) and confirm that they indeed return a integer value and a boolean value respectively.

    The getCulturePercentAnger() method returns the number of citizens currently unhappy because of foreign culture. So those would be the foreigners in the city, then.

    The isNeverLost() method return the boolean True if the city has had a previous owner, and the value False if it hasn't.

    Now yet another conditional statement:
    Code:
          if iForeigners == 0 or bNeverLost == True: 
            continue
    Lets start with the continue statement on the second line (indented one level) first. What it does, is it interrupts the current loop - the one that is giving us city references from lCities, remember? This basically means that the condition for spawning rebels has failed, since the code will move on to the next city and firstly check if it is also in disorder. (The previous conditional statement.)

    This if statement is interesting however, as it contains the logical operator or. This means that the if statement will first look at the iForeigners variable and compare it to the value 0. This in fact makes it a boolean expression using the == operator - translating into "equal to" - and it can only result in a True or False outcome. So, if the first boolean expression is True, then the conditional statement has already passed and the second boolean expression isn't processed. Then the following block of code - the continue statement - is executed.

    But if the iForeigners == 0 expression doesn't pass - meaning that there in fact are culturally challenged citizens in the city - then the next boolean expression will be processed. If you replace the variable bNeverLost with the value its holding you either get the boolean expression True == True or the boolean expression True == False, and only the first expression would be True (while the second one is clearly False).

    Now, why didn't I check if the iForeigners value was greater than zero and the bNeverLost value was False? Because by using the continue statement I could prevent the indentation level for the rest of the code to go up another level, making the actual level of indentation harder and harder to distinguish as it is ever increasing. But it could have been done like this also:
    Spoiler :
    Code:
          if iForeigners > 0 and bNeverLost == False:

    Moving on...
    Code:
          iRandNum = cyGame.getSorenRandNum(100, "rebels")
    Now we're getting technical, as we invoke the getSorendRandNum() method on the cyGame reference we created earlier. This method can also be found in the API, under the class CyGame. Verify that it returns a integer value!

    This method is the default way of generating random numbers with Python within the CivIV game. The getSorendRandNum() method takes two parameters, which are defined as a integer value and a string value in the API. (See for yourself!) So we put one of each inside the parenthesis. The integer value 100 is in reference to 100% and the returned value is any number between 0 and 99. This is important to know: the computer always counts from zero and up, so the 100th value in order is hence 99 and not 100!

    The second value is any arbitrary - but valid - string value we choose to feed it. Its only used for debugging and here we flag this random number generation as "rebels" for easy identification. We could as easily have given it a empty string value ("") but you don't even have to think about this any more.

    The return (integer) value of the getSorendRandNum() method is assigned to the value iRandNum. Which, as described above, can be any value from 0 to 99 any given time the method is invoked.

    And we end this lesson with the last condition for the actual rebels code (which we will start making in the next lesson):
    Code:
          if iRandNum < iPropability:
    This is another if statement and the first value is randomly generated, while the second is actually a constant - the iProbability value of 5 (see lesson #2). The < operator stands for "less than" and the boolean expression is thus really iRandNum < 5. So the if statement will only pass any time the iRandNum value happens to be 0-4 - an interval of 5 on a 100 unit scale. This equals a 5% chance of the rebel units actually forming - this time around.

    This is what your Rebels module should look like:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    # show popup
    
    modPopup = PyPopup()
    modPopup.setHeaderString(popupHeader)
    modPopup.setBodyString(popupMessage)
    modPopup.launch()
    
    # code checked every turn
    
    lPlayers = pyGame.getCivPlayerList()
    for pyPlayer in lPlayers:
      lCities = pyPlayer.getCityList()
      for pyCity in lCities:
        cyCity = pyCity.GetCy()
        bDisorder = cyCity.isDisorder()
        
        if bDisorder == True:
          iForeigners = cyCity.getCulturePercentAnger()
          bNeverLost = cyCity.isNeverLost()
          if iForeigners == 0 or bNeverLost == True: 
            continue      
          iRandNum = cyGame.getSorenRandNum(100, "rebels")
          if iRandNum < iPropability:
     
  8. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    In the previous lessons we managed to get the script to loop through all players and all their respective cities. We have also added the conditions for the rebel units code to fire. This means that we are identifying actual cities that are in disorder, that have foreign nationals, and that have belonged to another Civ at some point. We have also passed the 5% chance condition. Now we do something unexpected but none-the-less necessary:
    Code:
            lPlots = list()
    We used list variables earlier and this time we assign an empty list to the lPlots variable. This can be done with the list() function built into Python. (The function returns an empty list because we haven't added any arguments inside the parenthesis. There are other ways of initializing empty lists also.)

    This list variable will soon be populated by CyPlot instances, but first we need to get the coordinates of the CyCity instance we identified as the target for the rebellion:
    Code:
            iCityX = cyCity.getX()
            iCityY = cyCity.getY()
    So what we do is assign the X and Y coordinates to a couple of integer variables. We invoke the getX() and getY() methods on the cyCity variable that is carried over from the previous code, and these will return the desired values. Please refer with the API, as always.
    Code:
            lXCoordinates = range(iCityX - 1, iCityX + 2)
            lYCoordinates = range(iCityY - 1, iCityY + 2)
    Ok, now it gets a bit hairy, but what we basically do is we create two more lists. The Python function range() is used to create lists containing integer values from the arguments we feed it. Here the first argument is the coordinate minus 1 and the second argument is the coordinate plus 1. But what a minute, it says +2. This is necessary because if the first value is the starting point for enumeration, then the second one is the last value in order. But the computer counts up from zero, remember? So iCityX + 1 would actually be the city's X coordinate - and not the next tile's. Confusing, but completely logical. :p

    Now we use both these list for iteration:
    Code:
            for iX in lXCoordinates:
              for iY in lYCoordinates:
    So, if we for the sake of argument say that the lXCoordinates variable holds the values [45, 46, 47] and the lYCoordinates variable refers to the values [23, 24, 25], then we get the coordinates of all tiles within one tile from the city tile itself. Because on the first iteration the iX value will be 45 and the iY value 23, and then only the iY value will change to 24 while the iX value stays the same. So next we get the tile iX = 45, iY = 24. Only when all the content of the lYCoordinates list have been looped through will iX change to the next value, which would be iX = 46 - and the iY value starts all over from iY = 23.

    But this is not enough, we wanna exclude invalid map tiles before we put any values into the lPlots list we created earlier. So we collect information from each tile as we loop through them. But first we need to fetch the CyPlot instance of each tile in order:
    Code:
                cyPlot = cyMap.plot(iX, iY)
    So what we do is we assign the return value (a CyPlot instance - look in the API under CyMap) of the plot() method to the variable cyPlot. The parameters we use to specify what plot we want is the iX and iY coordinates. And since we will eventually get all possible combinations due to the looping we're doing, the value of the variable will change 9 times in total.
    Code:
                bWater = cyPlot.isWater()
                bPeak = cyPlot.isPeak()
                bCity = cyPlot.isCity()
    We invoke three different methods belonging to the CyPlot class - look these up also! The return values of each are assigned to the corresponding boolean variables. Now we know if the current tile is valid for spawning units! But we still need a conditional statement to do something with this imformation:
    Code:
                if bWater == False and bPeak == False and bCity == False:
                  lPlots.append(cyPlot)
    The first line is an if statement with a logical expression consisting of three boolean expressions. The and logical operator requires each of the enclosed expressions to equal False for the entire if statement to be True and pass. We could have done this also, but it just adds lines - and even more indentation:
    Spoiler :
    Code:
                if bWater == False:
                  if bPeak == False:
                    if bCity == False:
                      lPlots.append(cyPlot)

    Anyways, the last line is special because we treat the lPlots variable as an object! Its the dot-notation that gives it away - list values are in fact data structures - basically data objects. The append() method is used to add a entry to a list. Once all the coordinates have been looped through we should have anything between zero and 8 valid CyPlot intances in the lPlots list. (The city plot in the middle will always be invalid though, because bCity will always be True for that tile.)

    Now we have the actual CyPlot instances where we wanna spawn rebel units, so lets do just that in the next lesson then! The script:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    # show popup
    
    modPopup = PyPopup()
    modPopup.setHeaderString(popupHeader)
    modPopup.setBodyString(popupMessage)
    modPopup.launch()
    
    # code checked every turn
    
    lPlayers = pyGame.getCivPlayerList()
    for pyPlayer in lPlayers:
      lCities = pyPlayer.getCityList()
      for pyCity in lCities:
        cyCity = pyCity.GetCy()
        bDisorder = cyCity.isDisorder()
        
        if bDisorder == True:
          iForeigners = cyCity.getCulturePercentAnger()
          bNeverLost = cyCity.isNeverLost()
          if iForeigners == 0 or bNeverLost == True: 
            continue      
          iRandNum = cyGame.getSorenRandNum(100, "rebels")
          if iRandNum < iPropability:
            
            lPlots = list()
            iCityX = cyCity.getX()
            iCityY = cyCity.getY()
            lXCoordinates = range(iCityX - 1, iCityX + 2)
            lYCoordinates = range(iCityY - 1, iCityY + 2)        
            for iX in lXCoordinates:
              for iY in lYCoordinates:
                cyPlot = cyMap.plot(iX, iY)
                bWater = cyPlot.isWater()
                bPeak = cyPlot.isPeak()
                bCity = cyPlot.isCity()
                if bWater == False and bPeak == False and bCity == False:
                  lPlots.append(cyPlot)
     
  9. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    Now we are approaching the finishing line. But first we have to backtrack a bit and decrease the indentation level equal to three levels, so that the next line belongs to the same block as the line "lPlots = list()". Because now we have populated the lPlots value with content and are ready to use it:
    Code:
            iNumPlots = len(lPlots)
            if iNumPlots > 0:
    The len() function is another built-in feature of Python and is actually short for "length". By feeding it any data structure (even a string value) as an argument, the function returns the number of entries. Since we requested the return value of len(lPlots) we get a value corresponding to the number of cyPlot instances in the list. This value is then assigned to the variable iNumPlots.

    The next line checks if the number of list entries is greater than zero - any, really - because otherwise where will be nowhere to spawn the rebel units. So this is actually important to include. If we can find at least one plot to spawn these, then the script will continue with this block of code:
    Code:
              iMilitaryUnits = cyCity.getMilitaryHappinessUnits()
              iNumRebels = max(1, iMilitaryUnits / 4)
              eUnitType = cyCity.getConscriptUnit()
    Line by line; the first line invokes the getMilitaryHappinessUnits() method on the cyCity variable from earlier. It should return a integer value - compare with the API. That value should correspond to the number of military units in the city, minus those that can't keep population in check - which should roughly be the same units that wont be able to defend the city properly from rebel attacks. (So I chose to use this method for convenience.)

    The next line uses the built-in max() function to compare two values and return the higher one. So the first value is the integer value of 1, and the next value is the iMilitaryUnits value divided by four. Since this is integer division, all fractions are counted down. This means that if the city has less than four military units then the result will be zero. Since we went through all this trouble to get this far, it feels kinda lame to let the city get away with no rebellion at this point. So the iNumRebels variable will be set to at least the integer value 1.

    The last line invokes yet another CyCity method, but since you have already checked this up in the API (right?) you can see that the returned value of getConscriptUnit() isn't anything we've seen before. Its actually "UnitType". Click on this text in the API and you will see all the available unitType values at the bottom left panel. These are CivIV specific enumerated types that are actually inherited from the main C++ portion of the game, originally generated from the XML files of the game/mod. (So if you change the XML for you mod, then the default index in the API won't be valid anymore!)

    So what the eUnitType variable will hold is a reference to one of these types, depending on which one the getConscriptUnit() returns. Which in turn depends on the city, its owner, what team that owner belongs to and what Technologies are available to that team. And also what strategic resources are available in the city, so the returned unitType will vary!

    As it turns out, we don't actually need to know what unitType the eUnitType variable is referring to, as long as the script knows it. So we don't worry about it much.
    Code:
              while iNumRebels > 0:
    This is another iteration statement, but instead of for and in commands we use while. So the block of code following this statement (note the colon at the end) will repeat itself as long as the variable iNumRebels is a positive value. So it could basically go on forever, so it is important that we don't forget to change the iNumRebels value along the way. (Don't worry, we wont. :D)
    Code:
                iRandPlot = cyGame.getSorenRandNum(iNumPlots, "spawn")
                cyPlot = lPlots[iRandPlot]
                iX = cyPlot.getX()
                iY = cyPlot.getY()
    You know about the getSorenRandNum() CyGame class method from earlier, but here it is returning a random integer value from zero up to the value of the iNumPlots variable. So if iNumPlots == 5 then it can be any number from 0 to to the fifth value, which is 4 (counting up from zero).

    Since we need to pick a random CyPlot instance from the lPlots list variable, we use it as an index and feed the iRandPlot value inside a set of brackets. So if iRandPlot == 0 then we get the first list entry with lPlots[0]. We assign the CyPlot instance - again - to the variable cyPlot. (The previous value from when we created the plot list is therefore replaced.)

    We also re-use the iX and iY variables to store the X and Y coordinates of the current CyPlot instance.

    Now, finally we make a unit:
    Code:
                pyBarbarian.initUnit(eUnitType, iX, iY)
    The initUnit() method belongs to the PyPlayer class in PyHelpers - because the variable we are invoking it on - pyBarbarian - is a PyPlayer instance. (See lesson #2.) Anyway, there are three parameters used with the method, and you know what they are by looking at the code. This is how a new unit is created - with PyHelpers. The corresponding CyPlayer.initUnit() method is somewhat more complex but works just the same. (In fact, the PyHelpers version just wraps it up in a more convenient format.)

    One single thing that we can't afford to forget - or the script will spawn units at these coordinates until the game crashes:
    Code:
                iNumRebels = iNumRebels - 1
    We need to decrease the InumRebels value by one for each unit we make, because otherwise the while loop will never end!

    Now that there is no more code in this block - or in this script for that matter - the while loop will start all over again, spawning yet another barbarian unit. Assuming iNumRebels > 0 of course.

    In the next lesson we will start to figure out what to do with our script. Because this is all that is needed to make the actual effects. This is the completed thing, finally:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    # show popup
    
    modPopup = PyPopup()
    modPopup.setHeaderString(popupHeader)
    modPopup.setBodyString(popupMessage)
    modPopup.launch()
    
    # code checked every turn
    
    lPlayers = pyGame.getCivPlayerList()
    for pyPlayer in lPlayers:
      lCities = pyPlayer.getCityList()
      for pyCity in lCities:
        cyCity = pyCity.GetCy()
        bDisorder = cyCity.isDisorder()
        
        if bDisorder == True:
          iForeigners = cyCity.getCulturePercentAnger()
          bNeverLost = cyCity.isNeverLost()
          if iForeigners == 0 or bNeverLost == True: 
            continue      
          iRandNum = cyGame.getSorenRandNum(100, "rebels")
          if iRandNum < iPropability:
            
            lPlots = list()
            iCityX = cyCity.getX()
            iCityY = cyCity.getY()
            lXCoordinates = range(iCityX - 1, iCityX + 2)
            lYCoordinates = range(iCityY - 1, iCityY + 2)        
            for iX in lXCoordinates:
              for iY in lYCoordinates:
                cyPlot = cyMap.plot(iX, iY)
                bWater = cyPlot.isWater()
                bPeak = cyPlot.isPeak()
                bCity = cyPlot.isCity()
                if bWater == False and bPeak == False and bCity == False:
                  lPlots.append(cyPlot)
    
            iNumPlots = len(lPlots)
            if iNumPlots > 0:
              iMilitaryUnits = cyCity.getMilitaryHappinessUnits()
              iNumRebels = max(1, iMilitaryUnits / 4)
              eUnitType = cyCity.getConscriptUnit()
              
              while iNumRebels > 0:
                iRandPlot = cyGame.getSorenRandNum(iNumPlots, "spawn")
                cyPlot = lPlots[iRandPlot]
                iX = cyPlot.getX()
                iY = cyPlot.getY()
                pyBarbarian.initUnit(eUnitType, iX, iY)
                iNumRebels = iNumRebels - 1
     
  10. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    Now we get to fool around with the Python console once again. But firstly put the Rebels.py file in the \Assets\Python\ folder of your mod. If you haven't started working on a mod yet, then you can put in the corresponding folder belonging to the game (\Beyond the Sword\Assets\Python\) - for now.

    Once the module is installed we can import it in-game with the Python console. You simply enter the import statement:
    Code:
    import Rebels
    What happens once you do this, is that our entire script is executed. We know this because the popup from the first lesson is shown again. Unless the conditions for a rebellion is met anywhere on the map, the script won't do anything else, but it should still basically work!

    Unfortunately you can only import a module once while the game is running, so this is not at all what we want. Because we want the popup message displayed when the game starts - and we want the rebels code to be executed every turn. Also, the entire script really shouldn't be executed when we import the module, because we will probably be importing it from another module on initialization. So clearly we need to do something.

    Well, the solution to our problems is called functions. We already used other peoples functions (and methods) - now we need to define our own. Firstly, we could just replace the comment lines with function definitions:
    Code:
    # show popup
    ...
    # code checked every turn
    Instead we can have:
    Code:
    def showPopup():
    ...
    def checkTurn():
    Now, these are function definition statements and they are used by entering the def command followed by the function name. The parenthesis can be used to define arguments that the function uses, but these two have none. They do however have colons at the end, so this means that we need to change the indentation levels! Otherwise the computer logic breaks down completely.

    So the result - with adjusted indentation - would be:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    def showPopup():
    
      modPopup = PyPopup()
      modPopup.setHeaderString(popupHeader)
      modPopup.setBodyString(popupMessage)
      modPopup.launch()
    
    def checkTurn():
      lPlayers = pyGame.getCivPlayerList()
      for pyPlayer in lPlayers:
        lCities = pyPlayer.getCityList()
        for pyCity in lCities:
          cyCity = pyCity.GetCy()
          bDisorder = cyCity.isDisorder()
          
          if bDisorder == True:
            iForeigners = cyCity.getCulturePercentAnger()
            bNeverLost = cyCity.isNeverLost()
            if iForeigners == 0 or bNeverLost == True: 
              continue      
            iRandNum = cyGame.getSorenRandNum(100, "rebels")
            if iRandNum < iPropability:
              
              lPlots = list()
              iCityX = cyCity.getX()
              iCityY = cyCity.getY()
              lXCoordinates = range(iCityX - 1, iCityX + 2)
              lYCoordinates = range(iCityY - 1, iCityY + 2)        
              for iX in lXCoordinates:
                for iY in lYCoordinates:
                  cyPlot = cyMap.plot(iX, iY)
                  bWater = cyPlot.isWater()
                  bPeak = cyPlot.isPeak()
                  bCity = cyPlot.isCity()
                  if bWater == False and bPeak == False and bCity == False:
                    lPlots.append(cyPlot)
    
              iNumPlots = len(lPlots)
              if iNumPlots > 0:
                iMilitaryUnits = cyCity.getMilitaryHappinessUnits()
                iNumRebels = max(1, iMilitaryUnits / 4)
                eUnitType = cyCity.getConscriptUnit()
                
                while iNumRebels > 0:
                  iRandPlot = cyGame.getSorenRandNum(iNumPlots, "spawn")
                  cyPlot = lPlots[iRandPlot]
                  iX = cyPlot.getX()
                  iY = cyPlot.getY()
                  pyBarbarian.initUnit(eUnitType, iX, iY)
                  iNumRebels = iNumRebels - 1

    If we save our Rebels module then all the Python in CivIV is instantly reloaded. Now we can import the new version of the Rebels module in the Python console. (See above for instructions.)

    Nothing happens! Well, that is what we were looking for! We wanna be able to execute our code on command - not when the module holding the code is imported. The only thing executed this time around was the statements that aren't part of any function definition:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"

    We can access these variables with the module name using dot-notation:
    Code:
    print Rebels.iPropability
    And if we wanna execute the rest of the code, we can make a function call:
    Code:
    Rebels.showPopup()
    And then, we can open up the console every turn and run the main script:
    Code:
    Rebels.checkTurn()
    Nothing will probably happen once you do this, but its not because the code isn't working, but rather that the mod design is such that the actual event will be rather rare. If you do this once a city is in disorder and all the other conditions for the event are met - including the random 5% chance - there should indeed be rebel units spawned.

    Now, before we end this rather practical lesson we will organize the code into more functions:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    
    # constants
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    
    iPropability = 5
    eBarbarian = gc.getBARBARIAN_PLAYER()
    pyBarbarian = PyPlayer(eBarbarian)
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    def showPopup():
      """Displays the welcome message on game start"""
      modPopup = PyPopup()
      modPopup.setHeaderString(popupHeader)
      modPopup.setBodyString(popupMessage)
      modPopup.launch()
    
    def checkTurn():
      """Checks all players and cities every turn, and executes the rebels event when applicable."""
      lPlayers = pyGame.getCivPlayerList()
      for pyPlayer in lPlayers:
        lCities = pyPlayer.getCityList()
        for pyCity in lCities:
          cyCity = pyCity.GetCy()
          if [B]checkCity(cyCity)[/B]:
            lPlots = [B]getPlotList(cyCity)[/B]
            [B]spawnRebels(cyCity, lPlots)[/B]
            
    def checkCity(cyCity):
      """Checks if the CyCity instance is valid for the rebels event."""
      bDisorder = cyCity.isDisorder()
      if bDisorder == True:
        iForeigners = cyCity.getCulturePercentAnger()
        bNeverLost = cyCity.isNeverLost()
        if iForeigners > 0 and bNeverLost == False: 
          iRandNum = cyGame.getSorenRandNum(100, "rebels")
          if iRandNum < iPropability:
            [B]return True[/B]
      [B]return False[/B]
    
    def getPlotList(cyCity):
      """Checks all adjacent plots and returns a list of CyPlot instances."""
      lPlots = list()
      iCityX = cyCity.getX()
      iCityY = cyCity.getY()
      lXCoordinates = range(iCityX - 1, iCityX + 2)
      lYCoordinates = range(iCityY - 1, iCityY + 2)        
      for iX in lXCoordinates:
        for iY in lYCoordinates:
          cyPlot = cyMap.plot(iX, iY)
          bWater = cyPlot.isWater()
          bPeak = cyPlot.isPeak()
          bCity = cyPlot.isCity()
          if bWater == False and bPeak == False and bCity == False:
            lPlots.append(cyPlot)
      [B]return lPlots[/B]
    
    def spawnRebels(cyCity, lPlots):
      """Spawns rebel units matching the defenses of the CyCity instance on surrounding random plots."""
      iNumPlots = len(lPlots)
      if iNumPlots > 0:
        iMilitaryUnits = cyCity.getMilitaryHappinessUnits()
        iNumRebels = max(1, iMilitaryUnits / 4)
        eUnitType = cyCity.getConscriptUnit()
        while iNumRebels > 0:
          iRandPlot = cyGame.getSorenRandNum(iNumPlots, "spawn")
          cyPlot = lPlots[iRandPlot]
          iX = cyPlot.getX()
          iY = cyPlot.getY()
          pyBarbarian.initUnit(eUnitType, iX, iY)
          iNumRebels = iNumRebels - 1

    This is what we want our modules to look like: Code neatly organized into functions - and I even added documentation for each function!

    Now, how this works is that instead of having that long sequence of continuous code, with ever incrasing indentation levels, the tree new function will be called on by the checkTurn() function. For this to work, I had to add return statements to two of the functions, so that they deliver some value back to the function that called upon it. So the checkCity() function is called on by the checkTurn() function and returns a boolean value - True or False. That return value is then used in an if statement by checkTurn(). And the getPlotList() function returns a list value that in turn is assigned to a variable in checkTurn().

    Here you can also see how arguments work - they are passed along to the function with the function call and then used as local variables inside the function.

    So its basically the same code (I only did some minor adjustments) and it works exactly the same. It will be easier to work with going forward though, as the mod is still to be tested, tweaked and perhaps even expanded.

    Before we end this lesson I thought I'd show you how the same code can be simplified, almost to the point where it becomes unreadable:
    Spoiler :
    Code:
    from CvPythonExtensions import *
    from PyHelpers import *
    from Popup import PyPopup
    gc = CyGlobalContext()
    cyGame = CyGame()
    cyMap = CyMap()
    pyGame = PyGame()
    pyBarbarian = PyPlayer(gc.getBARBARIAN_PLAYER())
    popupHeader = "Tutorial"
    popupMessage = "This is a Python tutorial.\n\nby Baldyr"
    
    def showPopup():
      modPopup = PyPopup()
      modPopup.setHeaderString(popupHeader)
      modPopup.setBodyString(popupMessage)
      modPopup.launch()
    def checkTurn():
      for pyPlayer in pyGame.getCivPlayerList():
        for pyCity in pyPlayer.getCityList():
          cyCity = pyCity.GetCy()
          if checkCity(cyCity):
            spawnRebels(cyCity, getPlotList(cyCity))
    def checkCity(cyCity):
      if cyCity.isDisorder() and cyCity.getCulturePercentAnger() and not cyCity.isNeverLost(): 
        return cyGame.getSorenRandNum(100, "rebels") < 5:
      return False
    def getPlotList(cyCity, lPlots=[]):
      iCityX, iCityY = cyCity.getX(), cyCity.getY()
      for iX in range(iCityX - 1, iCityX + 2):
        for iY in range(iCityY - 1, iCityY + 2):
          cyPlot = cyMap.plot(iX, iY)
          if cyPlot.isWater() or cyPlot.isPeak() or cyPlot.isCity(): continue
          lPlots.append(cyPlot)
      return lPlots
    def spawnRebels(cyCity, lPlots):
      iNumPlots = len(lPlots)
      if iNumPlots:
        iNumRebels = max(1, cyCity.getMilitaryHappinessUnits() / 4)
        eUnitType = cyCity.getConscriptUnit()
        while iNumRebels:
          cyPlot = lPlots[cyGame.getSorenRandNum(iNumPlots, "spawn")]
          pyBarbarian.initUnit(eUnitType, cyPlot.getX(), cyPlot.getY())
          iNumRebels -= 1

    This code is plain hard to read and undocumented to boot. It holds no real advantage to the previous edition and is also harder to edit. The point here is merely to show how things can be done in several different ways without being more "right" or "wrong". Because it all works just the same.

    In the next lesson we will be putting some final touches on our code and connecting it to the game, so that the code will run automatically when it is supposed to. Then this tutorial will be completed.
     
  11. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    Ok, here we go: Final lesson!

    What we have at this point is a fully working and practically complete Python module for all the code making up the Rebels mod. Now we have to find the hooks provided by the game (in the DLL file which is programmed in C++) on which we hang the code.

    What your mod needs is a copy of the CvEventManager.py file. This is the Event Manager module and it practically runs all the Python in any mod. The way it works, is that the DLL file will make calls to the Event Manager whenever a "game event" takes place. Like when the game starts, when a game turn begins or when a city or unit is lost. It is possible for the modder to interupt the game on these occations and add his or her own Python code to the mix.

    This is practically it. And we're talking basic fuction calls - stuff that we already know about! Firstly we have to make sure that your mod has what we're looking for: Open up \Mods\My Mod\Assets\Python\CvEventManager.py - if you don't already have this, then you copy-paste in the original one found in \Beyond the Sword\Assets\Python\ - because you don't wanna mess around with you original installation of the game!

    Ok, looking at the Event Manager module code - and there is a lot of it and it makes little sense at first - we notice all these import statements below the information at the top. So we include our own module to these import lines:
    Code:
    import Rebels
    Now the game/mod is aware of our code and it can be accessed directly through the Event Manager.

    Firstly, we wanna add the popup code to the mod, so that it is shown at the beginning of every game. Lookup the definition for the onGameStart() method and add a function call after all the other lines of code:
    Code:
    		Rebels.showPopup()
    Spoiler :
    Code:
    	def onGameStart(self, argsList):
    		'Called at the start of the game'
    		if (gc.getGame().getGameTurnYear() == gc.getDefineINT("START_YEAR") and not gc.getGame().isOption(GameOptionTypes.GAMEOPTION_ADVANCED_START)):
    			for iPlayer in range(gc.getMAX_PLAYERS()):
    				player = gc.getPlayer(iPlayer)
    				if (player.isAlive() and player.isHuman()):
    					popupInfo = CyPopupInfo()
    					popupInfo.setButtonPopupType(ButtonPopupTypes.BUTTONPOPUP_PYTHON_SCREEN)
    					popupInfo.setText(u"showDawnOfMan")
    					popupInfo.addPopup(iPlayer)
    		else:
    			CyInterface().setSoundSelectionReady(true)
    
    		if gc.getGame().isPbem():
    			for iPlayer in range(gc.getMAX_PLAYERS()):
    				player = gc.getPlayer(iPlayer)
    				if (player.isAlive() and player.isHuman()):
    					popupInfo = CyPopupInfo()
    					popupInfo.setButtonPopupType(ButtonPopupTypes.BUTTONPOPUP_DETAILS)
    					popupInfo.setOption1(true)
    					popupInfo.addPopup(iPlayer)
    
    		CvAdvisorUtils.resetNoLiberateCities()
    
    [COLOR="Red"]		# Rebels mod
    		Rebels.showPopup()[/COLOR]

    Note that this has to be entered on the second indentation level - and that the indentation level in the Event Manager module is one tab per level. So you add two tabs at the beginning of the line - and no blank spaces!

    Next up, lookup the onBeginGameTurn() method and add the other function call we wanna use:
    Code:
    		Rebels.checkTurn()
    Spoiler :
    Code:
    	def onBeginGameTurn(self, argsList):
    		'Called at the beginning of the end of each turn'
    		iGameTurn = argsList[0]
    		CvTopCivs.CvTopCivs().turnChecker(iGameTurn)
    
    [COLOR="Red"]		# Rebels mod
    		Rebels.checkTurn()[/COLOR]

    Done! What happens now when the mod is loaded, is that it looks for the CvEventManager module in the appropriate folder. And when it locates it the game will import the module, and the Event Manager will in turn import the Rebels module.

    Then, when the actual game session starts, the game will call on the Event Manager and run the block of code inside the onGameStart() method. This block of code includes the function call that fires the popup message. And then, during the game, the game will call the Event Manager method onBeginGameTurn() - and execute our function call.

    Now you can test the mod by starting a new game!

    And once you're done with that, we could change those string constants from the first lesson:
    Code:
    popupHeader = "Rebels mod"
    popupMessage = "This mod includes the Rebels Python mod. It spawns barbarian units around cities \
    with foreign citizens that are currently in disorder. The unit type and the number of units \
    adapts to the makeup of the city garrison and the outcome will vary with the circumstances.\n\n\
    You have been warned!"
    You can download the final (?) Rebels module below.

    edit: The file uploaded originally was actually a test version, sorry. The real file is available now.

    Some final thought on how the mod could be improved:
    • Instead of checking all players and all cities every single turn, it would cause less lag in between game turns to only run the code on every 20th turn (or so) and take away the 5% chance condition.
    • This would however make the rebellions appear clustered every 20 turns, so we might wanna only have one rebellion at a time.
    • If we limit the code to only one rebellion per run, then it would make sense to check the players in random order, so that it isn't always the first player who is hit with this.
    • Also, the number of rebel units (iNumRebels) could be equal to the number of foreign nationals (iNumForeigners).
    • If the human player gets the rebellion event, then it would be prudent to add a in-game text message telling about the event.
    • For flavor and to add more challenge to the rebellions, each rebel unit could have the Guerilla I promotion by default, regardless of unit type.
    These improvements, and others that you can come up with, are now up to you to make yourself. If the idea is learning how to do this, then you should consider reading up on Python and spending some time looking at how other Python mods are made.

    Now I will answer any questions from the class to the best of my ability.
     

    Attached Files:

  12. tchristensen

    tchristensen Chieftain

    Joined:
    Jul 21, 2010
    Messages:
    1,241
    Location:
    Grand Rapids, Mi
    Overload!!

    OK. I need to do step by step and slow down :(

    OK...I am like a bull in a China Shop. I put the codes in where I think they belong and going to test it. I think I understand what is going on -- but my first try failed. Got into the game and the End Turn button was gone, so I must have did something wrong.

    Back to the drawing boards.

    SECOND ATTEMPT WORKS!! Silly me spelled "Import Rebels", "Import Rebel" so of course the program didn't know what to do.
     
  13. tchristensen

    tchristensen Chieftain

    Joined:
    Jul 21, 2010
    Messages:
    1,241
    Location:
    Grand Rapids, Mi
    Question: Is there a way to CENTER that box in the game window? I looked for coordinates, but didn't fin anything in the code.

    Very excited.

    We played the Mod tonight and was dismayed at how quickly the barbarians spawned next to the city. I will need to look into something where they don't spawn for at least 100 turns or else they just destroy all the civilizations in the game. I will look at the code and see if I can do anything with it, but I may need some help.
     
  14. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    I just implemented the popup the easiest possible way, as an example. Since we are using the Popup module found in the \Sid Meier's Civilization 4\Assets\Python\pyHelper\ folder the solution lies there. What about the PyPopup.setPosition() method?
    Code:
    	def setPosition(self, iX, iY):
    		[B]"set the location of the popup"[/B]
    		self.popup.setPosition(iX, iY)
    According to the documentation it is just the thing you need. How to use it?

    Well, if you look at the Popup module you see that the the setPosition() method belongs to the PyPopup class:
    Code:
    class PyPopup:
    This means that you need an instance of the PyPopup class to invoke the method on. As luck would have it, we already have one of those in the Rebels module (line #22):
    Code:
      modPopup = PyPopup()
    As described in the the tutorial (lesson #1) we used the PyPopup() constructor to create - a PyPopup instance. It is referenced by the variable modPopup that identifies our popup (and not any other popup).

    Now we need to read the definition for the setPosition() method:
    Code:
    	def setPosition(self, [COLOR="Red"]iX, iY[/COLOR]):
    Inside the parenthesis are tree values. The first one is self and that you pretty much ignore. (It refers to the class instance. It makes no sense now but once you learn about Object-Oriented Programming it will.) So, the "real" parameters are iX and iY.

    The way to use the method, then, is that you firstly take the class instance, and then you invoke the method on it with dot-notation. Don't forget to put a pair of integer values (as indicated by the "i" prefix in iX and iY) for the actual coordinates as parameters.

    Just to save some time on trial-and-error for you, this is NOT the way you do it:
    Code:
    PyPopup.setParameter(iX, iY)
    If you read the tutorial and understand what it says, there is no reason that you can't set the coordinates for the popup just like we set the header and the body messages. Its actually exactly the same thing, you're just using a different method with some other parameters this time.

    So look at the code, look at it hard, ask questions if you must, but don't just try stuff at random until it appears to work. With Python this is a terrible way to learn, since every single error in your code will make some other part of the mod not work. (Once the Python interpreter encounters an exception it stops executing any more code.)

    I'm not spelling it all out for you because then you won't learn a thing. :rolleyes: And thats not helping you, unless you want me to do your programming for you. (I would, if you paid me. :lol:)

    :lol: You just downloaded the file at the last lesson - skipping the rest of all the classes - and tried to get it to work, right? Because that file was actually broken - new file is uploaded - use that instead.

    Thanks for making me notice that I actually uploaded a test version - with all the conditions disabled. :lol: My mistake, albeit a revealing one. ;)

    The current conditions for the rebels event should be (and I haven't really tested this myself in an actual game):
    1. city in disorder
    2. unhappiness from foreign culture in city (this excludes small cities, by the way)
    3. city mush have changed hands at some point
    But you can of course change them as you like. If you take the time to learn this programming stuff you will be able to do this.
     
  15. tchristensen

    tchristensen Chieftain

    Joined:
    Jul 21, 2010
    Messages:
    1,241
    Location:
    Grand Rapids, Mi
    Hey thanks for all the help.

    Its awful hard to really spend a huge amount of time on any one thing, but I did try to follow the instructions but I did skipped to the end and read through the code.

    I am trying to do too much I think all at one time; learning XML, learning python, learning how to record audio, adding new units & technology, and doing that while playtesting. Stacked on top of working, school, and family stuff.

    I will try the new code again today, trying to paw through some of the code and make sense of it.
     
  16. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    No, you need to take the time it takes. Just do it one lesson at a time - this is actually the idea.

    But you might eventually wanna take some time to really look into this stuff. Sure, it takes time, but its not nearly as hard as you probably make it out to be. So it'll be done with in practically no time at all once you commit to it.
     
  17. tchristensen

    tchristensen Chieftain

    Joined:
    Jul 21, 2010
    Messages:
    1,241
    Location:
    Grand Rapids, Mi
    At my age, I don't have the time :cry:. Starting in two weeks I go back to school after being away for 25 years; so on top of hectic work :mad:, stressed out family life :blush:, going back to school :cool:, modding for Civ is a luxury.

    I really want to express sincere gratitude for showing me the framework for python. And I will continue to walk through the lessons, but I am still in need of help just getting things in the game, first, learning second :crazyeye:.
     
  18. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    Take your time, there is no hurry, you know. Eventually, you will have some vacation time, right? (I don't have that luxury myself, by the way... :()
     
  19. tchristensen

    tchristensen Chieftain

    Joined:
    Jul 21, 2010
    Messages:
    1,241
    Location:
    Grand Rapids, Mi
    Don't you Europeans all get like 5 weeks of vacation a year!?!?
     
  20. Baldyr

    Baldyr "Hit It"

    Joined:
    Dec 5, 2009
    Messages:
    5,530
    Location:
    Sweden
    Only those who can afford it... I used my vacation pay to fix the car (so that I can work), so I've been at work all summer while my co-workers have been vacationing. Thankfully work is extremely busy or I'd had to find work elsewhere over the summer.

    And I will be working extra hours this fall. All work, no play for me, I'm afraid. :(
     

Share This Page