Exposing new functions to Python

jdog5000

Revolutionary
Joined
Nov 25, 2003
Messages
2,601
Location
California
Background:
For my forth coming Tech Cost mod, I wanted to create a second cost value for Techs. This modified cost would then go down as more civilizations acquired the tech with other factors as well, with the modifications done in Python for easier tweaking. Anyway, to do this I needed to create some new CvInfos functions and expose them to Python. If you want to check out the mod, see TechCost Mod.


Declare Yourself
Ofcourse, to create new functions and expose them to Python, you have to declare them in the header file and implement them in the appropriate cpp file. Skip to next section if this is old hat for you.

For my mod, I added the following function declarations to CvInfos.h in the CvTechInfo class:
(EDIT: As pointed out by Gerikes below, the DllExport is not needed to expose these functions to Python, it exposes them to the game exe ... so it's harmless, but unnecessary)

// ---------------- TECH_COST_MOD start -----------------------
DllExport int getOrigResCost() const; // Exposed to Python
DllExport void setModResCost( const int newModCost ); // Exposed to Python
// ---------------- TECH_COST_MOD end -----------------------

You would add your function declarations in the appropriate .h file, and you should get in the habit of marking your changes very clearly so that you and others can find them. Makes merging much easier! This is classic C++ declaration, except for the DllExport command at the beginning of the line, which I believe just indicates the function should be exposed in the dll. The Python binding is done in a different file.

You ofcourse also implement these new functions in the cpp file associated with the class:

// ---------------- TECH_COST_MOD start -----------------------
// This function returns the unmodified research cost, will be exposed to Python
int CvTechInfo::getOrigResCost() const
{
return m_iResearchCost;
}

// Function that will be exposed to python to allow changes to cost of techs
// during game play
void CvTechInfo::setModResCost( const int newModCost )
{
m_iModResCost = newModCost;
}
// ---------------- TECH_COST_MOD end -------------------------

Expose Yourself :mischief:
This is the somewhat tricky part. The exposure to Python is done in file with a name like Cy____Interface#.cpp, where the blank is filled by a word or too also in the name of the cpp file where your functions are. The # is a small integer, 1-3 and also may not be there for your particular file. For the Tech Cost mod, the file name is CyInfoInterface1.cpp. All the CvTechInfo bindings occur in this file, though other info class bindings are in CyInfoInterface2.cpp, etc. Not sure where to put it? Try a search in files for some already Python exposed function name (leave off ()!) in the class you're adding to.

(Note: see Gerikes post below for more information ... some Cv___.cpp files have a Cy___.cpp version as well, where you will have to create a wrapper for your Cv___.cpp function.)

Alright, here are the additions for the Tech Cost mod:

// ---------------- TECH_COST_MOD start -----------------------
.def("getOrigResCost", &CvTechInfo::getOrigResCost, "int ()")
.def("setModResCost", &CvTechInfo::setModResCost, "void (int newModCost)" )
// ---------------- TECH_COST_MOD end -----------------------

The three fields are:
1) The text of your new function name, as it will appear in Python.
2) Address of the new function (class::function).
3) Return type of your function and arguments it takes in

Compile your source code in Final Release (or maybe just Release) mode, then place the newly created CvGameCoreDLL.dll in your mods Assets folder. Congradulations, your new functions are exposed to Python!

How to use your new function
The interesting part comes in using your new functions in Python. There is a slight pitfall here to avoid. Your functions have been added to CyGlobalContext, and you have to refer to them using this. Many of the other functions for a particular class are essetially wrapped in PyHelpers.py but yours will not be. (Note: you could add yours to PyHelpers.py, but it doesn't appear to me that this loads with your mod, so you'd have to change the original ... bad! Comment if this is incorrect and I'll change this text)

Most of the .py files have a line at the beginning that reduces the amount of typing you have to do later:

gc = CyGlobalContext()

With this at the top of your python file, calls to your new functions should look something like:

OrigCost = gc.getTechInfo(techID).getOrigResCost()

gc.getTechInfo(techID).setModResCost( 12 )

The dots take you from Global Context -> Class -> your function. Now that you can access your new functions, your imagination is the limit! A few pointers: comment your additions! do as much in Python as reasonable! when you release your brilliant mod, release the source code as well so people can include it in their ideas!
 
good work! I will test this...
 
Thanks! Really helped me through.

One thing, though, incase someone else runs into this: In your case, the interface connects directly to the Cv* file, whereas you might have to connect it to the Cy* file as well. If you're just exposing the XML changes, you should be fine, but exposing things like functions from any of the objects (unit objects, city objects, area objects, selection group objects, etc.) will require some other steps.

For example:


I'm trying to expose already created functions in CvUnit.cpp called "setFortifyTurns" and "changeFortifyTurns" (note: I'm not sure if there's a better way to get around setting fortify turns that don't expose these functions, but let's just assume that there isn't Edit: In fact, it doesn't do at all what I want it to :p)

First, the CvUnit.cpp file and the CvUnit.h file already has the needed code written, so there's no change needed there. if I were to be making a new function, I would have to write the code in those files.

I'll start by editting the CyUnit.h..

Code:
/* ----------------Edited by Gerikes--------------- */
void setFortifyTurns(int iNewValue);
void changeFortifyTurns(int iChange);
/* --------------End Edited by Gerikes------------- */

Pretty simple, just add the declaration. Now, to make the implementation in CyUnit.cpp...

Code:
/* This Function Added by Gerikes */
void CyUnit::setFortifyTurns(int iNewValue)
{
	if (m_pUnit)
		m_pUnit->setFortifyTurns(iNewValue);
}

/* This Function Added by Gerikes */
void CyUnit::changeFortifyTurns(int iChange) {
	if (m_pUnit)
		m_pUnit->changeFortifyTurns(iChange);
}

You'll see that the m_pUnit variable is actually a pointer to the unit in question's CvUnit object. Obviously, to prevent any null errors we check the pointer.

Finally, I need to actually expose it to python by editting CyUnitInterface1.cpp:

Code:
.def("setFortifyTurns", &CyUnit::setFortifyTurns, "void (int iNewValue)")
.def("changeFortifyTurns", &CyUnit::changeFortifyTurns, "void (int iChange)")

Now I can call this function on a python object using any method to get the python object, such as...

Code:
CyMap().plot(14,9).getUnit(0).setFortifyTurns(5)



Hope this helps someone!
 
Gerikes said:
Thanks! Really helped me through.

One thing, though, incase someone else runs into this: In your case, the interface connects directly to the Cv* file, whereas you might have to connect it to the Cy* file. For example:

A very good point! Thank you for clarifying this for others. Glad it has proven useful.
 
Another usefull tutorial but please forgive me for asking.

Couldn't that effect your trying to create have been done in 10 lines of Python alone?

Just check the other Civs for the tec currently being researched and give a bonus. Even if you felt it nessary to create a container holding for data that could be done in Python as well.
 
Impaler[WrG] said:
Another usefull tutorial but please forgive me for asking.

Couldn't that effect your trying to create have been done in 10 lines of Python alone?

Just check the other Civs for the tec currently being researched and give a bonus. Even if you felt it nessary to create a container holding for data that could be done in Python as well.

While doing everything possible in Python is often good, I don't believe it's possible to acheive what I wanted using Python alone. When I post my mod (soon), I invite you to critique and post your ideas in the mod thread ... I'm always open for suggestions!

Edit: mod thread is here TechCost Mod
 
Gerikes said:
Code:
/* This Function Added by Gerikes */
void CyUnit::setFortifyTurns(int iNewValue)
{
	if (m_pUnit)
		m_pUnit->setFortifyTurns(iNewValue);
}

/* This Function Added by Gerikes */
void CyUnit::changeFortifyTurns(int iChange) {
	if (m_pUnit)
		m_pUnit->setFortifyTurns(iChange);
}

Small nitpick, it looks like you're calling the wrong function from CyUnit::changeFortifyTurns(). As you have it listed, both of the functions do the same thing. :)

Daniel
 
dsplaisted said:
Small nitpick, it looks like you're calling the wrong function from CyUnit::changeFortifyTurns(). As you have it listed, both of the functions do the same thing. :)

Daniel


Yup. I guess I could classify that as a "copy-and-paste" bug? :p

Anyway, fixed it in the thread. Thank you.
 
The Great Apple said:
A question:

How would I go about passing a python list to C++ as an array?

There are functions of that which you can probably use as a guide. I haven't done it myself, but it looks fun :p

Code:
void CyMapGenerator::setPlotTypes(boost::python::list& listPlotTypes)
{
	if (!m_pMapGenerator)
	{
		return;
	}

	int* paiPlotTypes = NULL;
	[b]gDLL->getPythonIFace()->putSeqInArray(listPlotTypes.ptr() /*src*/, &paiPlotTypes /*dst*/);[/b]
	m_pMapGenerator->setPlotTypes(paiPlotTypes);
	delete [] paiPlotTypes;
}

Also, putSeqInArray() will return to you the size of the array, which is probably helpful.

Code:
int iSize = gDLL->getPythonIFace()->putSeqInArray(listPlotTypes.ptr() /*src*/, &paiPlotTypes /*dst*/);

The trick is making sure you know what types of data will be in the list. Not sure exactly what would happen if the data in the list wasn't all of the same type.
 
Also, to jdogg, I'm not sure about you, but I've come to the conclusion that DllExport does nothing in regards of allowing DLL functions to be called by python. I'm starting to think that it's only there for allowing calls to be called by the exe. If that's the case, there is no need for a modder to use DllExport with their functions.
 
Top Bottom