Modules and the Eventmanager

Flintlock1415

Emperor
Joined
Feb 24, 2008
Messages
1,057
Location
MN
I was working on converting my Python to Beyond the Sword, and I decided I wanted to make the code run in modules rather than exist in the eventmanager file. Most of the functions seemed pretty straight forward, but there was one that puzzled me. I'm not too sure about how Python modules work with each other, so I don't exactly know what I can place where.

My main goal is to minimize the amount of code that goes into the event manager.
Here is the code from my old eventmanager:
PHP:
def onChangeWar(self, argsList):
		'War Status Changes'
		bIsWar = argsList[0]
		iTeam = argsList[1]
		iRivalTeam = argsList[2]
		if (bIsWar):
			iCiv = gc.getPlayer(gc.getTeam(iTeam).getLeaderID()).getCivilizationType()
			iRivalCiv = gc.getPlayer(gc.getTeam(iRivalTeam).getLeaderID()).getCivilizationType()

			iNormandy = gc.getInfoTypeForString("CIVILIZATION_NORMANDY")
			iHRE = gc.getInfoTypeForString("CIVILIZATION_HRE")

			## Checks to make sure Normandy is attacking HRE
			if iCiv == iNormandy and iRivalCiv == iHRE:
				## Sends message to Normandy
				CyInterface().addImmediateMessage("The Pope does not approve of your actions", "")

				iPlayer = gc.getTeam(iRivalTeam).getLeaderID()
				pPlayer = gc.getPlayer(iPlayer)
				
				iHenry = gc.getInfoTypeForString("LEADER_HENRYIV")
				
				iKnight = gc.getInfoTypeForString("UNIT_KNIGHT")
				
				## Checks that HenryIV is the leader
				if pPlayer.getLeaderType() == iHenry:
					## Gives Henry a Knight in Liege
					pPlayer.initUnit(iKnight, 79, 14, UnitAITypes.NO_UNITAI)
What the code does is check to see that Normandy has declared war on the HRE, and pops a message as well as gives the HRE a knight in one of their cities. I just want to get as much of the code as possible into a separate module (as well as gain a better understanding as to how they work.)

Thanks! :)
 
If you don't want to use something like BUG to make this easier, the quickest way (other than modifying CvEventManager) is to extend the CvEventManager class, creating your own "custom" event manager:

Code:
# FlintlockEventManager.py

import CvEventManager

class FlintlockEventManager(CvEventManager):

    def __init__(self):
        CvEventManager.__init__(self)

    def onChangeWar(self, argsList):
        # put that function you posted here

and then swap your event manager in place of CvEventManager by modifying a copy of EntryPoints/CvEventInterface

Code:
...

import [B][COLOR="Red"]FlintlockEventManager[/COLOR][/B]

eventManager = [B][COLOR="Red"]FlintlockEventManager.FlintlockEventManager[/COLOR][/B]()

def getEventManager():
    return eventManager

...

What this does is define FlintlockEventManager as a subclass of CvEventManager which means that it is a CvEventManager with some stuff (a function) added to it. It fills the exact same role as CvEventManager but your onChangeWar() function will be called instead of the one in CvEventManager. Your version is said to override the one in the parent class.

BTW, you can look up the definition of all those italicized words by search for them with "object oriented programming".
 
Interesting, I think this the method you showed me when I first messed with Python. :lol: I guess I was leaning towards creating a function in my module and just calling that after an if statement. One thing I was wondering is whether I can place iCiv and iRivalCiv in my module, since it uses variables defined by the onChangeWar function (iTeam etc.). I don't feel like I fully understand how Python imports functions; i.e. does it bring the function to the eventmanager, or does it just tell my module to run the function and stay in its own module? (Thereby not allowing me to use variables defined by the onChangeWar function.)

Even if I can't mix variables from different modules/functions, I think I see a solution. Anyway, I was wondering what exactly did you mean by using BUG to help me? I was thinking about using some of the things from BUG, so it wouldn't be a bad idea to simply use it as a base. One thing that keeps me from doing this though is I feel like I would want to remove some things from BUG that don't really fit the theme of the mod and just cause clutter. I don't want to break BUG though, so I was wondering if you know how difficult it is to remove some aspects without completely wrecking the mod. As far as I can tell, its a very modular mod, but I've never stripped down a Python mod before, so idk. Merging things in is always an option as well...

Again thank you!
 
Nearly everything in BUG is configurable, including disabling it entirely. Don't like the scoreboard? Turn it off. Don't like the Autolog? Turn it off. Etc. Now, your mod's users will certainly be able to turn those things back on if they choose, so if you want to disallow this you'll need to do a little more work.

BUG hooks all of its features up using XML files described in the Modders' Corner of our wiki. There you will find a page called "Core Events" that describes how you would add this event manager to BUG.

When you import a module in Python, all it does is make the global variables, classes, and functions from that module available to the importer. A function's variables are always local to it, but you can pass their values to other functions. This would require modifying CvEventManager, and this is not the BUG Way. Every time you have to modify a file in BUG or BTS, you make it harder on yourself and other modders wishing to work with your code. BUG's goal is to eliminate that needs as much as possible.

To add your event to BUG you'd start by creating the Python module containing the event handler function copied from CvEventManager:

Code:
# FlintlockEvents.py

def onChangeWar(argsList):
    'War Status Changes'
    bIsWar = argsList[0]
    iTeam = argsList[1]
    ... entire function goes here ...

Next you need to tell BUG to call that when the "ChangeWar" event is fired. Create an XML file in the Assets/Config folder:

Code:
<!-- FlintlockMod.xml -->
<bug>
    <event type="ChangeWar" module="FlintlockEvents" function="onChangeWar"/>
</bug>

Note: I didn't double-check the type (ChangeWar) of the event. Make sure this matches the string in CvEventManager exactly--case included--or the event won't work. All of the event parameters come in argsList just as they do in CvEventManager. onChangeWar() lacks the first "self" parameter because it is a regular function not attached to a class.

That's it!
 
Ack! There's always a third step. You need to tell BUG to load your configuration XML file. BUG loads the file Config/init.xml automatically. Near the bottom of that file you need to point it to your file:

Code:
    ...
    <load mod="MapFinder"/>
    <load mod="Advanced Combat Odds"/>
    
    [B][COLOR="DarkOrange"]<load file="FlintlockMod"/>[/COLOR][/B]
    
    <!-- Options Screen -->
    ...
 
Thanks for the info. :D I really like the way you set this up in BUG, I had no idea how easy it was to add modules to BUG. :goodjob: I'm guessing the event type is the string under EventHandlerMap (in which case it would be 'changeWar'.) Also, how are the options saved? Is there like a cache file somewhere, or something else?
 
Yes, that's the type. By options do you mean user settings? These are stored in standard INI files under a UserSettings folder. You create options using the XML configuration files as well. For that you need to use the

Code:
<mod id="blahblah">
    ...
</mod>

element in your file instead of

Code:
<bug>
    ...
</bug>

because the options need a mod ID (blahblah above) to attach to. The Core Options wiki page describes this.
 
Okay, I've added in all of my Python, and I got an exception when I tried to load my scenario map. It seems to be complaining about this function in BugConfig.py:
Code:
def handle_data(self, data):
	self._element.addText(data)
I don't really know what it does, so my only guess is there is a Mac incompatibility. Anyway here is the PythonErr log.
Code:
Traceback (most recent call last):
  File "BugConfig", line 99, in unknown_starttag
  File "BugConfig", line 330, in startChild
  File "BugConfig", line 289, in start
  File "BugConfig", line 309, in flatten
ConfigError: Element <load> requires attribute 'name'
Traceback (most recent call last):
  File "BugConfig", line 99, in unknown_starttag
  File "BugConfig", line 326, in startChild
ConfigError: Element <None> does not accept child <screen>
Traceback (most recent call last):
  File "BugConfig", line 99, in unknown_starttag
  File "BugConfig", line 326, in startChild
ConfigError: Element <None> does not accept child <tab>
Traceback (most recent call last):
  File "BugConfig", line 77, in parse
  File "/System/Library/Frameworks/Python.framework/Versions/2.3/lib/python2.3/xmllib.py", line 171, in feed
    self.goahead(0)
  File "/System/Library/Frameworks/Python.framework/Versions/2.3/lib/python2.3/xmllib.py", line 268, in goahead
    self.handle_data(data)
  File "BugConfig", line 119, in handle_data
AttributeError: 'NoneType' object has no attribute 'addText'
Traceback (most recent call last):

  File "CvAppInterface", line 63, in preGameStart

  File "BugEventManager", line 327, in fireEvent

  File "BugEventManager", line 337, in _dispatchEvent

  File "BugEventManager", line 388, in _handleInitBugEvent

  File "BugEventManager", line 548, in initBug

  File "BugInit", line 48, in init

  File "BugInit", line 69, in loadMod

  File "BugConfig", line 77, in parse

  File "/System/Library/Frameworks/Python.framework/Versions/2.3/lib/python2.3/xmllib.py", line 171, in feed
    self.goahead(0)

  File "/System/Library/Frameworks/Python.framework/Versions/2.3/lib/python2.3/xmllib.py", line 268, in goahead
    self.handle_data(data)

  File "BugConfig", line 119, in handle_data

AttributeError: 'NoneType' object has no attribute 'addText'
ERR: Python function preGameStart failed, module CvAppInterface
 
The error that causes that whole cascade of failures is near the top:

Code:
ConfigError: Element <load> requires attribute 'name'

This means you have a <load ...> element in one of the Config XML files that doesn't have a "name" attribute. <load> only takes a name:

Code:
<load name="<file-name-without-dot-xml>"/>

so it should be easy to spot.
 
Okay, now I get a new error:
Code:
Traceback (most recent call last):
  File "BugEventManager", line 350, in _handleDefaultEvent
TypeError: onGameStart() takes exactly 2 arguments (1 given)
Traceback (most recent call last):
  File "BugEventManager", line 350, in _handleDefaultEvent
TypeError: onUnitPillage() takes exactly 2 arguments (1 given)
I'm guessing this has something to do with the fact that both of these functions have self and argslist as their arguments, but neither has an argslist defined. I'm not sure why this throws an error, really, since the normal BtS event manager has the same thing. Can I delete the argslist argument, or is there something else I need to do?
 
Is that the entirety of PythonErr.log? There should be a longer stack trace than that. No, you cannot remove argsList. Look in the code you've added for something calling the event manager. Look for this:

Code:
[B][COLOR="Red"]CvEventInterface.getEventManager()[/COLOR][/B].<something>

Use a multi-file search to look for code containing the red part. Post any that you find with several lines of context above and below for each.
 
MonkeyTools.py
Code:
######################################################
### 	some keyboard hooks
######################################################

def bShift():
	return [COLOR="Red"]CvEventInterface.getEventManager()[/COLOR].bShift

def bCtrl():
	return [COLOR="red"]CvEventInterface.getEventManager()[/COLOR].bCtrl

def bAlt():
	return [COLOR="red"]CvEventInterface.getEventManager()[/COLOR].bAlt
		
######################################################
### 	END 
######################################################
InputUtils.py
Code:
def handle(self, element, keys, module, function, dll):
		dll = BugDll.decode(dll)
		if self.isDllOkay(element, dll):
			[COLOR="red"]CvEventInterface.getEventManager()[/COLOR].addShortcutHandler(keys, BugUtil.getFunction(module, function, *element.args, **element.kwargs))
		else:
			BugUtil.info("InputUtil - ignoring <%s> %s, requires dll version %s", element.tag, keys, self.resolveDll(element, dll))
CvOverlayScreenUtils.py
Code:
def onKbdEvent(argsList):
	"""
	Event handler for keyboard events, checks keys against the hotkey list and opens the screen or closes it on match
	"""
	eventType, key, mouseX, mouseY, plotX, plotY = argsList
	eventManager = [COLOR="red"]CvEventInterface.getEventManager()[/COLOR]
	if ( eventType == eventManager.EventKeyDown ):
		stroke = InputUtil.Keystroke(key, eventManager.bAlt, eventManager.bCtrl, eventManager.bShift)
		if stroke in keys:
			if overlayScreen.isOpen():
				hideOverlayScreen()
			else:
				showOverlayScreen()
			return 1
	return 0
CvOptionsScreenCallbackInterface.py
Code:
# BUG - BugEventManager - start
	[COLOR="red"]CvEventInterface.getEventManager()[/COLOR].fireEvent("LanguageChanged", iValue)
# BUG - BugEventManager - end
and
Code:
# BUG - BugEventManager - start
	[COLOR="red"]CvEventInterface.getEventManager()[/COLOR].fireEvent("ResolutionChanged", iValue)
# BUG - BugEventManager - end
CvCustomizableDomesticAdvisor.py
Code:
def renamePage(self, inputClass):

		eventManager = [COLOR="red"]CvEventInterface.getEventManager()[/COLOR]

		if not self.renameEventContext or self.renameEventContext is None:

			for i in range(5000, 6000):
				if not eventManager.Events.has_key(i):
					self.renameEventContext = i
					eventManager.Events[self.renameEventContext] = ('DomAdvRenamePage', self.renameApply, self.renameBegin)
					CvUtil.SilentEvents.append(self.renameEventContext)
					break

		CvEventInterface.beginEvent(self.renameEventContext)
CvAppInterface.py
Code:
# BUG - core - start
	import CvEventInterface
	[COLOR="red"]CvEventInterface.getEventManager()[/COLOR].fireEvent("PreGameStart")
# BUG - core - end
BugUtil.py
Code:
def doHotSeatCheck(args):
	"""
	Called during EndPlayerTurn, fires SwitchHotSeatPlayer event during a hot seat
	game when the active player's turn ends.
	"""
	iGameTurn, ePlayer = args
	game = gc.getGame()
	if game.isHotSeat() and ePlayer == game.getActivePlayer():
		[COLOR="red"]CvEventInterface.getEventManager()[/COLOR].fireEvent("SwitchHotSeatPlayer", ePlayer)
BugConfig.py
Code:
def handle(self, element, module, clazz, dll):
		dll = BugDll.decode(dll)
		if self.isDllOkay(element, dll):
			BugUtil.callFunction(module, clazz, [COLOR="red"]CvEventInterface.getEventManager()[/COLOR], *element.args, **element.kwargs)
		else:
			BugUtil.info("BugConfig - ignoring <%s> from %s.%s, requires dll version %s", element.tag, module, clazz, self.resolveDll(element, dll))

class EventHandler(HandlerWithArgs):
	
	TAG = "event"
and
Code:
def handle(self, element, type, module, function, dll):
		dll = BugDll.decode(dll)
		if self.isDllOkay(element, dll):
			[COLOR="red"]CvEventInterface.getEventManager()[/COLOR].addEventHandler(type, BugUtil.getFunction(module, function, True, *element.args, **element.kwargs))
		else:
			BugUtil.info("BugConfig - ignoring <%s> %s, requires dll version %s", element.tag, type, self.resolveDll(element, dll))
 
Can you post PythonDbg.log after getting that error again, please? Are you adding event handlers for those two events in your code? If so, post the handler function code and the XML that you used to hook them up.

All of the usages you posted are from BUG and work as expected in BUG. Thus I suspect it's calling one of your event managers.
 
I'm not totally sure what you mean by event handler, but if it is the CvEventInterface.getEventManager() code, I've not added that anywhere in my Python code (should I?)
Here is the XML I used for hooking up my module to BUG:
Code:
<!-- HastingsEvents.xml -->
<bug>
    <event type="changeWar" module="CvHastingsEvents" function="onChangeWar"/>
    <event type="GameStart" module="CvHastingsEvents" function="onGameStart"/>
    <event type="BeginGameTurn" module="CvHastingsEvents" function="onBeginGameTurn"/>
    <event type="unitPillage" module="CvHastingsEvents" function="onUnitPillage"/>
</bug>
And here is the line in init.xml:
Code:
<load name="HastingsEvents"/>

Here is the relevant part of my PythonDbg.log (I don't think the whole thing will fit)
Code:
22:29:07 TRACE: onGameStart() takes exactly 2 arguments (1 given)
PY:saveDesc:/Users/Kristian/Documents/Civilization IV Beyond the Sword/Saves/WorldBuilder/WBQuickSave, curDir:/Users/Kristian/Documents/Civilization IV/Beyond the Sword
WBSave done

PY:OnPreSave
2 SCREEN TURNED ON

22:34:27 DEBUG: Timer - scores took 150 ms
22:34:42 DEBUG: Timer - scores took 140 ms
SCREEN OFF

22:35:27 TRACE: Error in unitPillage event handler &lt;function onUnitPillage at 0x242934b0&gt;
22:35:27 TRACE: onUnitPillage() takes exactly 2 arguments (1 given)
PY:OnUnInit
UnInit Python
 
That XML is hooking up the events, and the functions (e.g. onGameStart) in CvHastingsEvents are called event handlers because they handle events passed to them by Cv/BugEventManager. I'm going to bet that your functions look like this:

Code:
def onGameStart(self, argsList):
    ...

Remove the "self," part. That is only needed for functions defined inside classes. Yours are more module-level functions and only get one parameter: argsList. This is a Python thing unrelated to BUG. Do that for all of your event handlers (the four functions mentioned above).
 
Okay, thanks for that. I have another question though, when I subclass the CvEventManager functions (which is what I'm doing, right?) when you say the parent class is overridden, is that function replaced or supplemented by the subclass? I ask this because I'm not sure if I can leave out the statements that exist in the CvEventManager, or if I must include those in my module.

As I've probably said before, I've never programmed before, and google didn't really help give me a good understanding of how overrides work. (I'm not exactly proficient in programming jargon. :D )
 
Since you are using module-level functions instead of a class, you are neither subclassing nor overriding. I recommend the free online book How to Think Like a Computer Scientist.

However, the effect here is the same. The event handlers in CvEventManager are called in addition to your event handlers, so you don't need to replicate their effects. They are called in the order that BUG sees them, and the CvEventManager ones get called first since they are set up by CvEventManager itself before BUG adds its own and yours.

You can think of an event like the starter pistol in a race. Every runner registers interest in the event by listening for the gunshot. When they hear it--when the event is passed to their ears--they all take off running.

This mechanism is useful because any one can register interest in the event without having to tell the originator of the event--the person holding the gun. Sitting in the stands you could decide to toss your hotdog at your sister when the gun goes off without needing to coordinate with the gun holder or the racers.
 
Thanks a lot for the info and the link. I'll try to read when I have time. Also I like that analogy a lot, it really helps me to understand how modules work. I still don't know all about modules, but I feel like its getting better. I'm actually strongly considering taking a Post-Secondary intro class at one of the colleges here (since our High School doesn't offer any computer classes) so maybe I'll have a better feel for the terms then. :D

Again thanks for your help!
 
That book I linked is quite good. It uses Python (there are versions for other languages too) to walk you through beginning Object-Oriented programming.
 
Okay I ran into a different problem that I figured would be simple, but I don't get it. I'm trying to return the Gameturn number. The onGameStart function has it defined locally, but I want to use it in the canDeclareWar callback from the GameUtils. I tried seeing if this function from the API would do what I want:
Code:
INT getTurnYear (INT iGameTurn)
int (iGameTurn) - turn Time
But it doesn't seem to be the same as iGameTurn. Is iGameTurn a global variable, or is there some other way to get iGameTurn? (In case you don't know, iGameTurn just returns what the turn is in each game, with the first being 0 even in scenarios.)
 
Back
Top Bottom