Cookbook:Changing Objects At Runtime
Problem
You don't like how some of the standard Python code that comes with BF2 works, and want to change it. Maybe you want to add your own RCon commands; maybe you want to change the way scores are kept. You could just hack the default code and make whatever changes you wanted, but if you do very much of that it becomes a maintenance nightmare, and things really could get ugly when you try to apply the next game patch that EA will be releasing "soon", and all your changes get overwritten.
Solution
Python is a dynamic language, which, among other things, means that programs can change themselves even while they are running. We can take advantage of Python's dynamic nature to write code that dynamically modifies the properties of standard BF2 objects.
Example 1: Adding A New Property To An Object
Let's say you're working on a new mod for BF2 and need for Player Objects to track a new property, called "karma". If you look in the Object Reference, you'll see that Player Objects don't have a karma property, and you want to add one.
In Python, if all you want to do is add a new attribute to an object, you can Just Do It:
playerObject.karma = 0
There--it's done! If playerObject
is an instance of class bf2.PlayerManager.Player
, then we just created a new attribute called "karma" within that instance, and assigned it the value "0". There really isn't anything else to it--we can read and change that attribute elsewhere in our code at will, just as though it had been defined as part of the class from the beginning.
One note, though: we didn't add this attribute to the bf2.PlayerManager.Player
class--we added it to a specific instance of that class, so only that one player will have a karma attribute. If we want other players to have a karma attribute, then we need to find their playerObjects and do the same thing. Assuming we've imported bf2
, we could loop through and assign a karma attribute to all players by doing something like:
for playerObject in bf2.playerManager.getPlayers(): playerObject.karma = 0
If we really wanted to build the karma attribute into the class itself, we could do so by dynamically replacing the class's __init__
method using the technique shown in Example 2, below. . . but that would be overkill for most applications; just adding attributes to individual instances will be sufficient for most things you would want to do with new attributes.
Example 2: Adding A New Method To An Object
There is no direct way to get a player's CD Key Hash from within Python code. You can, however, do it by executing a console command and parsing the results--this Cookbook entry show how. Let's say you want to add a new method to the bf2.PlayerManager.Player
class, so that you can access each player's CD key in an object-oriented and intuitive way.
You can add a new getCDKey
method to player objects by running this code:
import bf2.PlayerManager import re import new def playerData(self): '''Returns a dictionary containing player data; dictionary keys are playerIDs, values are 4-element tuples, containing player name, IP address, IP port, and cd-key hash.''' rawData = host.rcon_invoke("admin.listplayers") pattern = re.compile(r'''^Id:\ +(\d+) # PlayerID \ -\ (\S+) # Player Name \ is\ remote\ ip:\ (\d+\.\d+\.\d+\.\d+): # IP Address (\d+).*? # Port Number hash:\ (\w{32}) # CD Key Hash ''', re.DOTALL | re.MULTILINE | re.VERBOSE) result = {} for player in pattern.findall(rawData): result[int(player[0])] = player[1:] return result[self.index][3] newMethod = new.instancemethod(playerData, None, bf2.PlayerManager.Player) bf2.PlayerManager.Player.getCDKey = newMethod
Let's go through what this program is doing:
- It imports
bf2.PlayerManager
because the class we want to modify lives in that module. - It imports
re
because that package gives it access to regular expressions, needed by theplayerData
function. - It imports
new
, which is where some of the magic comes from: thenew
module contains several functions that can be used to dynamically create code-related objects; we're going to use thenew.instancemethod
function, which binds functions to instances. - Then it defines the function we're going to use as our new method,
playerData
. At this stage,playerData
is just a function, quite separate from any objects.
Note, however, that the playerData
function shown here is slightly different from the one shown in the Cookbook entry: here, it takes a parameter self
. Right now playerData
is "unbound"; once we turn it into a "bound" function, self
will automatically be set to the instance.
- The program then assigns something unusual to the variable
newMethod
: this is where we use thenew.instancemethod
function to create a "bound" version ofplayerData
. The parameterNone
specifies that we don't want our new function bound to any specific instances--we want to leave the binding open, so that it will apply to every instances of thebf2.PlayerManager.Player
class. - Finally, we create a new attribute (
getCDKey
) for the classbf2.PlayerManager.Player
, and assigning to it the bound function objectnewMethod
. In Python, methods are just attributes that reference executable objects--so what we've done is to effectively add a new method to the Player class. All player objects are instances of that class, so we now, immediately, can find any player's key hash just by calling a method against their player object:
key = playerObject.getCDKey()
You may note that this is a spectacularly inefficient way to do things--every time you look up the key hash for a single player, it has to retrieve the key hashes for all players first--but the point here wasn't to be efficient, it was to demonstrate how to modify code on the fly. We could have just changed the Battlefield 2 Server/python/bf2/PlayerManager.py
file to add this method, but we've demonstrated a way to get the same effect without touching the original code.
See the related Adding New RCon Commands recipie for another example of how this method can be used.
Discussion
This cookbook entry just scratches the surface of what you can do with dynamic code modification. Before you start hacking around in the BF2's default Python code, please consider whether it might be cleaner to use an approach like this!
It may have occured to you, though, that something needs to be running that will make these dynamic modifications--and how does that something get to run in the first place, without itself modifying some of the standard code? The short answer is that these dynamic programming tricks are only possible if there is a "hook" you can use to cause this and other code to be run, and DICE put no such hook in their Python code. So, no matter what, at least one modification needs to be made to the standard BF2 code: at a minimum, an "import" statement needs to be added to one of the main files that will cause your code to be read in and executed. . . but that's a subject for another time. . .
Submitted By
--Woody