Cookbook:Changing Objects At Runtime

From BF2 Technical Information Wiki
Revision as of 19:27, 21 June 2018 by Pireax (talk | contribs) (Created page with "== 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...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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 the playerData function.
  • It imports new, which is where some of the magic comes from: the new module contains several functions that can be used to dynamically create code-related objects; we're going to use the new.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 the new.instancemethod function to create a "bound" version of playerData. The parameter None 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 the bf2.PlayerManager.Player class.
  • Finally, we create a new attribute (getCDKey) for the class bf2.PlayerManager.Player, and assigning to it the bound function object newMethod. 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