Gerikes
User of Run-on Sentences.
In this tutorial, I hope to show what I've learned about how to make a python file that has only the functions of your mod in it and let it be callable from the SDK, in a very clean manner. The bonuses of doing this rather than using a generic event which uses the CvEventManager.py file are these.
1.) All your mods functions are in a very easy to find spot.
2.) No modification to any original python files are done.
These tutorials were inspired by a thread showing the work of Sirkris.
---
1.) Making the Python file.
The first step is making the python file. It will reside in your mods "assets\python\entrypoints" directory, and should be called Cv[YourModsName]Interface.py. I'm not sure what happens if it's not called that, but why take chances? Since I've been doing this for the Starcraft mod, I will be using a function from that mod as an example. I've made a file called "CvStarcraftInterface.py", a placed it in this directory. Here are the contents of the file:
Note: Earlier this paragraph showed the filenames needing to be named "Py[YourModsName]Interface.py", and the example given was "PyStarcraftInterface.py". This was a mistake on my part, and was thankfully caught by Lord Olleus. The corrected text is showed now.
This file should be pretty self-explanatory in terms of how it's setup. There are simply two functions, called "meleeCombat" and "rangedCombat". Each function takes in a argument list that consists of two units (the attacker and defender). Both functions change the units depending on a set of reasons, and then return to the SDK (In fact, both functions do the exact same thing, but it's useful having two incase I want to change how the two different styles of combat work easily later on) Note that some of the functions used are specific to the Civcraft mod, and are not really Civ4 functions..
----
2.) Setting up your #define (Optional)
While this isn't necessary, it's probably a good idea. What's going to happen is you're going to set up a preprocessor directive defining your module name. As you'll see below, the c++ function used to call python functions takes in as the first parameter a string of the module name. In my example, my filename is "PyStarcraftInterface.py", so my modulename would be "CvStarcraftInterface" (For the Python guru, this would be "Cv%s"%filename.split(".")[0][2:], or something more to your liking). By defining this in one place, you can change the name of the module later on and only have to worry about changing the name in one place in the SDK code. As noted by MatzeHH, however, "the python file has to be in the entrypoints folder, but the filename doesn't matter. It doesn't has to be Cv[Name]Interface.py."
Modify your CvDefines.h files where it lists all the python module names (near the bottom of the code). You will be adding your pythons module to the bottom, like so...
Make sure that the name in quotes is the same as the filename you made, but without the .py.
----
3.) Calling your function from C++
Now the fun part. You can probably look through the SDK code and see examples of this everywhere, but incase you haven't yet, here's how to use the python interface.
Here's an example of calling my Starcraft function, which is located in CvUnit.cpp in the function updateCombat, which I'll explain below.
Here is line-by-line what's happening:
On line 1, we've created a new instance of a class which will hold all the arguments you are going to pass to the python function (it has a limit of 20 arguments).
On line 2, we've created a new CyUnit so that python can modify the unit's instance in C++, using the CyUnit as an interface. We've used 'this' (which is the instance of the CvUnit class that this function is called from) so that we can make a CyUnit of the current unit's instance.
On line 3, we've done exactly the same as line 2, except used a different CvUnit instance.
On lines 4 & 5, we are actually adding the two CyUnit variables to the arguments list. Note that we're using the helper function makePythonObject to convert the c++ object into a python object.
On line 6, you finally call the function. The first argument is the string that is the module to be called. As explained before, we've used the #define preprocessor to make this be the string we've defined in CvDefines.h. The second argument is the name of the function to be called. The last is the object of our argument list all ready to be delivered to python.
Lines 7 and 8 are to clean up the new objects we've made. This ensures that python releases the objects from memory as well as c++.
----
4.) Getting return values.
In the examples above, there are no values being returned, and the arguments you pass can not be changed. So, you'll need to use a slightly different style in order to return values from functions. While Starcraft doesn't (yet) use any custom python functions that need a value to be returned, here is what it would look like, for those interested. This code is taken from the "canTrain" method in CvCity.cpp (with what I've added bolded):
All the lines are the same up to lines labeled 1, 2, and 3. These are different because rather than pass in an object like a CyUnit or CyCity, you're passing in an enumerated type (an int) and two booleans. These work just as well.
In line 4, they've declared a long int and given it a value. It is this value whose contents the returned value will be stored in.
Line 5 uses an overloaded version of the callFunction method. Asides from obviously now going back to PYGameModule in the first argument, it also has added an argument. This argument is a reference to the long int made above. Note that this must be a reference (unless the returned object is itself an object, but I'm really not sure if that would even work, but if you got a spare chance try it out and see what happens ), otherwise you'll be passing by value (and probably get a compiler error). In the function that this calls, the return value defaults to false. However, if a modder modified the function to return true, then the value of lResult would change to 1. You could also change it to return 56, and so on.
----
I hope this will be helpful to others. Feel free to reply and make comments on any embarrassing errors you see, or questions you have.
1.) All your mods functions are in a very easy to find spot.
2.) No modification to any original python files are done.
These tutorials were inspired by a thread showing the work of Sirkris.
---
1.) Making the Python file.
The first step is making the python file. It will reside in your mods "assets\python\entrypoints" directory, and should be called Cv[YourModsName]Interface.py. I'm not sure what happens if it's not called that, but why take chances? Since I've been doing this for the Starcraft mod, I will be using a function from that mod as an example. I've made a file called "CvStarcraftInterface.py", a placed it in this directory. Here are the contents of the file:
Note: Earlier this paragraph showed the filenames needing to be named "Py[YourModsName]Interface.py", and the example given was "PyStarcraftInterface.py". This was a mistake on my part, and was thankfully caught by Lord Olleus. The corrected text is showed now.
Code:
## Starcraft Mod Specialized Python Functions
## Made by Gerikes
import CvUtil
from CvPythonExtensions import *
# globals
gc = CyGlobalContext()
def meleeCombat(argsList):
attackUnit, defendUnit = argsList
iDamage = 0
if (defendUnit.getDomainType() == DomainTypes.DOMAIN_LAND):
iDamage = attackUnit.getGroundDamage()
elif (defendUnit.getDomainType() == DomainTypes.DOMAIN_FLOAT):
iDamage = attackUnit.getAirDamage()
attackUnit.changeAttacksThisTurn(1) # Cost them one attack
attackUnit.changeMoves(gc.getMOVE_DENOMINATOR()) # Cost them one move
attackUnit.forceCooldown()
defendUnit.doDamage(iDamage, attackUnit.getOwner())
CvUtil.pyPrint("Player %ds %s attacked player %ds %s for %d damage (Melee)." %
(attackUnit.getOwner(), attackUnit.getName(),
defendUnit.getOwner(), defendUnit.getName(),
iDamage)
)
return;
def rangedCombat(argsList):
attackUnit, defendUnit = argsList
iDamage = 0
if (defendUnit.getDomainType() == DomainTypes.DOMAIN_LAND):
iDamage = attackUnit.getGroundDamage()
elif (defendUnit.getDomainType() == DomainTypes.DOMAIN_FLOAT):
iDamage = attackUnit.getAirDamage()
attackUnit.changeAttacksThisTurn(1) # Cost them one attack
attackUnit.changeMoves(gc.getMOVE_DENOMINATOR()) # Cost them one move
attackUnit.forceCooldown()
defendUnit.doDamage(iDamage, attackUnit.getOwner())
CvUtil.pyPrint("Player %ds %s attacked player %ds %s for %d damage (Ranged)." %
(attackUnit.getOwner(), attackUnit.getName(),
defendUnit.getOwner(), defendUnit.getName(),
iDamage)
)
return;
This file should be pretty self-explanatory in terms of how it's setup. There are simply two functions, called "meleeCombat" and "rangedCombat". Each function takes in a argument list that consists of two units (the attacker and defender). Both functions change the units depending on a set of reasons, and then return to the SDK (In fact, both functions do the exact same thing, but it's useful having two incase I want to change how the two different styles of combat work easily later on) Note that some of the functions used are specific to the Civcraft mod, and are not really Civ4 functions..
----
2.) Setting up your #define (Optional)
While this isn't necessary, it's probably a good idea. What's going to happen is you're going to set up a preprocessor directive defining your module name. As you'll see below, the c++ function used to call python functions takes in as the first parameter a string of the module name. In my example, my filename is "PyStarcraftInterface.py", so my modulename would be "CvStarcraftInterface" (For the Python guru, this would be "Cv%s"%filename.split(".")[0][2:], or something more to your liking). By defining this in one place, you can change the name of the module later on and only have to worry about changing the name in one place in the SDK code. As noted by MatzeHH, however, "the python file has to be in the entrypoints folder, but the filename doesn't matter. It doesn't has to be Cv[Name]Interface.py."
Modify your CvDefines.h files where it lists all the python module names (near the bottom of the code). You will be adding your pythons module to the bottom, like so...
Code:
// python module names
#define PYDebugToolModule "CvDebugInterface"
#define PYScreensModule "CvScreensInterface"
#define PYCivModule "CvAppInterface"
#define PYWorldBuilderModule "CvWBInterface"
#define PYPopupModule "CvPopupInterface"
#define PYDiplomacyModule "CvDiplomacyInterface"
#define PYUnitControlModule "CvUnitControlInterface"
#define PYTextMgrModule "CvTextMgrInterface"
#define PYPerfTestModule "CvPerfTest"
#define PYDebugScriptsModule "DebugScripts"
#define PYPitBossModule "PbMain"
#define PYTranslatorModule "CvTranslator"
#define PYGameModule "CvGameInterface"
#define PYEventModule "CvEventInterface"
/* Added by Gerikes for custom Starcraft function*/
#define PYStarcraftModule "CvStarcraftInterface"
/* End Added by Gerikes for starcraft */
Make sure that the name in quotes is the same as the filename you made, but without the .py.
----
3.) Calling your function from C++
Now the fun part. You can probably look through the SDK code and see examples of this everywhere, but incase you haven't yet, here's how to use the python interface.
Here's an example of calling my Starcraft function, which is located in CvUnit.cpp in the function updateCombat, which I'll explain below.
Code:
/* Added by Gerikes (Starcraft core combat (melee units)) */
CyArgsList pyArgs; [b]// *1*[/b]
CyUnit* pyAttacker = new CyUnit(this); [b]// *2* [/b]
CyUnit* pyDefender = new CyUnit(pDefender); [b] // *3* [/b]
pyArgs.add(gDLL->getPythonIFace()->makePythonObject(pyAttacker)); [b]// *4*[/b]
pyArgs.add(gDLL->getPythonIFace()->makePythonObject(pyDefender)); [b]// *5*[/b]
gDLL->getPythonIFace()->callFunction(PYStarcraftModule, "meleeCombat", pyArgs.makeFunctionArgs()); [b]// *6*[/b]
delete pyAttacker; [b]// *7*[/b]
delete pyDefender; [b]// *8*[/b]
/* End added by Gerikes */
Here is line-by-line what's happening:
On line 1, we've created a new instance of a class which will hold all the arguments you are going to pass to the python function (it has a limit of 20 arguments).
On line 2, we've created a new CyUnit so that python can modify the unit's instance in C++, using the CyUnit as an interface. We've used 'this' (which is the instance of the CvUnit class that this function is called from) so that we can make a CyUnit of the current unit's instance.
On line 3, we've done exactly the same as line 2, except used a different CvUnit instance.
On lines 4 & 5, we are actually adding the two CyUnit variables to the arguments list. Note that we're using the helper function makePythonObject to convert the c++ object into a python object.
On line 6, you finally call the function. The first argument is the string that is the module to be called. As explained before, we've used the #define preprocessor to make this be the string we've defined in CvDefines.h. The second argument is the name of the function to be called. The last is the object of our argument list all ready to be delivered to python.
Lines 7 and 8 are to clean up the new objects we've made. This ensures that python releases the objects from memory as well as c++.
----
4.) Getting return values.
In the examples above, there are no values being returned, and the arguments you pass can not be changed. So, you'll need to use a slightly different style in order to return values from functions. While Starcraft doesn't (yet) use any custom python functions that need a value to be returned, here is what it would look like, for those interested. This code is taken from the "canTrain" method in CvCity.cpp (with what I've added bolded):
Code:
CyCity* pyCity = new CyCity(this);
CyArgsList argsList;
argsList.add(gDLL->getPythonIFace()->makePythonObject(pyCity)); // pass in city class
argsList.add(eUnit); [b]// *1* [/b]
argsList.add(bContinue); [b]// *2* [/b]
argsList.add(bTestVisible); [b]// *3* [/b]
long lResult=0; [b]// *4* [/b]
gDLL->getPythonIFace()->callFunction(PYGameModule, "canTrain", argsList.makeFunctionArgs(), &lResult); [b]// *5* [/b]
delete pyCity; // python fxn must not hold on to this pointer
if (lResult == 1) [b]// *6* [/b]
{
return true;
}
All the lines are the same up to lines labeled 1, 2, and 3. These are different because rather than pass in an object like a CyUnit or CyCity, you're passing in an enumerated type (an int) and two booleans. These work just as well.
In line 4, they've declared a long int and given it a value. It is this value whose contents the returned value will be stored in.
Line 5 uses an overloaded version of the callFunction method. Asides from obviously now going back to PYGameModule in the first argument, it also has added an argument. This argument is a reference to the long int made above. Note that this must be a reference (unless the returned object is itself an object, but I'm really not sure if that would even work, but if you got a spare chance try it out and see what happens ), otherwise you'll be passing by value (and probably get a compiler error). In the function that this calls, the return value defaults to false. However, if a modder modified the function to return true, then the value of lResult would change to 1. You could also change it to return 56, and so on.
----
I hope this will be helpful to others. Feel free to reply and make comments on any embarrassing errors you see, or questions you have.