Cookbook:Dynamically Changing an Event Handler

From BF2 Technical Information Wiki
Jump to navigation Jump to search

Problem

The standard Python code that comes with BF2 includes an event handler that you want to change. You know it's a Bad Idea™ to screw around with the original source code, so how can you do it?

Solution

The solution is to put a "wrapper" around the original event handler, and use some of Python's more esoteric features to substitute your wrapper function for the original handler.

As an example, let's take this fix by Metac0m: the default autobalance algorithm used by BF2 will sometimes annoyingly autobalance the commander and squad leaders to the opposite team. The autobalance mechanism is implemented by a handler for the PlayerDeath event in the standard_admin.autobalance module. Metac0m's solution: add a couple of lines to the handler, so that when it runs it checks to see if the player who just died was a commander or squad leader, and if they were, it does nothing; otherwise, the handler continues to execute normally. This simple approach works great, but requires modifying the standard BF2 Python code--which can create a mess when, for example, it's time to upgrade to a new version of BF2.

Instead, here's a different approach that doesn't require any modification to the standard_admin.autobalance module:

import new
 import standard_admin.autobalance
 
 # Define wrapper function that implements our new functionality
 def newOnPlayerDeath(playerObject, vehicleObject):
     # If the player is a commander or squad leader, we're done
     if playerObject.isCommander(): return None
     if playerObject.isSquadLeader(): return None
    
     # If player is NOT a commander or squad leader, we pass the call on to
     # the original function
     onPlayerDeath.origFunc(playerObject, vehicleObject)
 
 # Create a new function from the original function's code object and it's
 # namespace, and save it as an attribute of the original function; this
 # is just so we can access the original function later.
 wrappedFunc = new.function(standard_admin.autobalance.onPlayerDeath.func_code,
                            standard_admin.autobalance.onPlayerDeath.func_globals)
 standard_admin.autobalance.onPlayerDeath.origFunc = wrappedFunc
 
 # Replace the code object of the original function with the code object
 # of our wrapper; this leaves the original function object intact, but with
 # a different code object.
 standard_admin.autobalance.onPlayerDeath.func_code = newOnPlayerDeath.func_code

When this code is executed, it dynamically replaces the original PlayerDeath event handler in standard_admin.autobalance with a new handler; the the new handler does Metac0m's checks as to whether the player who died is a commander or squad leader, and if not, it executes the original handler function.


Discussion

There is some serious Python voodoo in this example. Normally, there are better ways of substituting wrapper functions or methods for an original, but they won't work here because in this case, the original function has already been registered as an event handler. This means that the BF2 event system already has a reference to the function it's going to call when the event occurs--so we have to make our changes to the existing handler function object, not just create a new one.

This recipie takes advantage of the fact that a Python function object carries around with it a func_code attribute that contains a "code object"--the compiled "byte code" that executes the logic of the function. Since the event system is locked in on the original function object, what we do is replace it's code object with our own "wrapper" code object, leaving everything else about the original function object alone. We then turn the original code object into a new function object, and call that new function from our wrapper.

Confused yet?


Submitted By

--Woody