[SDK Tut] Calling from the SDK a custom python modules function.

Gerikes

User of Run-on Sentences.
Joined
Jul 26, 2005
Messages
1,753
Location
Massachusetts
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.

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 :p), 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.
 
Very usefull and clear.
Thanks!

I might be using this later if CCP doesn't add the calls that I want.
 
First of all an error on your part. On step 2 you say that the bit in brackets should be indentical to the filename minus the .py. Yet the python file is PyStarcraftInterface.py, and you wrote "CvStarcraftInterface".
Also, when I try to copy what you did I always get the error "failed to find file PySpellsInterface".
 
Lord Olleus said:
First of all an error on your part. On step 2 you say that the bit in brackets should be indentical to the filename minus the .py. Yet the python file is PyStarcraftInterface.py, and you wrote "CvStarcraftInterface".
Also, when I try to copy what you did I always get the error "failed to find file PySpellsInterface".


...Wow, that only makes the entire tutorial useless doesn't it?

Actually, the name of the python file is "CvStarcraftInterface.py". Part 2 is correct, it's the filenames that I used in part 1 that are incorrect. That is probably why you're getting the errors. I think if you use "CvSpellsInterface" rather then "PySpellsInterface" and make sure to name the file "CvSpellsInterface.py" that it will work.

Sorry about that. Thanks for the catch.

Hope it didn't cause too many problems :(
 
Still doesn't work, same error as before. Note that I am working on a mod comp and one of the Lopez's files CvABInterface is also not loading. Any idea as to what might be causing this?
 
Found the bug!

The game engine does not search your custom assets folder for Cv[ModName]Interface.py. I will try to report this bug to firaxis.
 
Lord Olleus said:
Found the bug!

The game engine does not search your custom assets folder for Cv[ModName]Interface.py. I will try to report this bug to firaxis.

So it works in a mod's Assets/python/entrypoints, but not in the Custom Assets/python/entrypoints?

I'm not able to do really anything until the weekend so I can't check this out.
 
Yep thats what I have found. Wierd and annoying, but not really important.
 
Just an info to anybody interested: 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.

Matze
 
I've been running into crashes with the argsList.add(...) command. It would appear that the game needs to be at a certain state of initialization for this to actually work. I've been trying to use it in CvGameAI.cpp, and it isn't working.

This only reason that I can think of is that this class seems to be inialized during game load (before main screen), and, more crucially perhaps, before the python is initialized.

Fairly easy fix for me involving counting the amount of players initialized. Will probably work in most cases.
 
Yes. I can do it using the standard python interface (exposing functions), but that seems to be a bit glitchy so I wanted to test it using this method. It's neater as well for what I want to do.
 
These are the possible callFunction functions you can use:

Code:
virtual bool moduleExists(const char* moduleName, bool bLoadIfNecessary) = 0;
virtual bool callFunction(const char* moduleName, const char* fxnName, void* fxnArg=NULL) = 0;
virtual bool callFunction(const char* moduleName, const char* fxnName, void* fxnArg, [b]long* result[/b]) = 0;
virtual bool callFunction(const char* moduleName, const char* fxnName, void* fxnArg, [b]CvString* result[/b]) = 0;
virtual bool callFunction(const char* moduleName, const char* fxnName, void* fxnArg, [b]std::vector<byte>* pList[/b]) = 0;
virtual bool callFunction(const char* moduleName, const char* fxnName, void* fxnArg, [b]std::vector<int> *pIntList[/b]) = 0;
virtual bool callFunction(const char* moduleName, const char* fxnName, void* fxnArg, [b]int* pIntList, int* iListSize[/b]) = 0;
virtual bool callFunction(const char* moduleName, const char* fxnName, void* fxnArg, [b]std::vector<float> *pFloatList[/b]) = 0;
virtual bool callPythonFunction(const char* szModName, const char* szFxnName, int iArg, [b]long* result[/b]) = 0; // HELPER version that handles 1 arg for you

The only ones I've used are returning a long and the CvString, both of which I have been able to use. However, I was having some problems with any extended characters, since the CvString can't handle wide characters (eventually I encoded the python string into UTF-8 and wrote a c++ function to convert the returned CvString in UTF-8 encoding as a CvWString, which is what I needed).
 
Top Bottom