April 27, 2021

Extending BF2 Python API Part 3 - Inherited types

Subclasses

In the last part we reverse engineered some of the base class for game objects and showed how to get offsets for components. These components usually contain the more interesting parts we want to play with. However, some components only exist in specific subclasses. For example, while the physics component we showed in the last part exists in every subclass (as it is part of the base class), an Ammo Component only exists in a FireArm class. This means we have to redo the component search for every subclass. But it does not mean we need to do it from scratch every time.

Quick reminder about C++ Object oriented: Inheriting from a class also extends the object’s size in memory, and any new fields the object will have will be stored in a place that doesn’t conflict with the base class. If for example the parent class is 32 bytes, and the subclass needs another 32 bytes, the size of the subclass will be 64 bytes. The important thing about this is that the first 32 bytes of the subclass will have the same memory layout as the base class. (This does get messy with multiple inheritance, but its not relevant for our needs, yet.) What it means for us is that when we start reversing a new object type, we can just copy what we learned about the base over to it.

It’s good to know what the base size exactly is, so we know at which offset in memory to start searching for fields we want to reverse. The best way to do this is to find the constructor of the base class (through any subclass constructor, which has to call the parent’s constructor), and then find references to it that try to construct the base class itself (without inheritance). If it was generated by code using the new statement, it will feature a call to a memory allocation function. The size that was passed to that function is the size of the class.

`new SimpleObject()`.

If it’s an abstract class then the constructor does not get called except from other constructors. We can try to estimate the size by seeing which offsets the base constructor writes to, knowing it will never write to outside of its area.

Reading/Writing supply count

One of the subclasses is called SupplyObject. An object of this type periodically checks its area for other objects to rearm and heal them. It’s possible to set up the object to have finite resources and have it self destruct when these resources are depleted. There is no API in Battlefield 2 for controling the supplies.

Since it inherits from SimpleObject (or as we called it CObject), we can just place CObject at the start of it (After making sure we pad CObject to be the correct size). SupplyObjects are pretty simple, they have no extra components and they store their current counts in their struct as floats. A SupplyObject has 3 types of resources, heal, ammo and shared. The first two are not really important for our use cases, so we didn’t document them.

class CSupply(Structure):
    _fields_ = [
        ("CObject", CObject),  # 0
        ("unknown1", c_int8 * 0x14),  # 150 <----- Ammo and heal somewhere here, if we ever need
        ("sharedammo", c_float),  # 164
    ]

We want to make sure that Python doesn’t end up writing sharedammo to an object that isn’t a SupplyObject. So we create a function to verify the object type by asking the game engine which type the template was created for, no hacky methods here. This could have been done with VPTRs too like last time, but VPTRs require us to maintain 3 copies of them: windows client, windows server, linux server. So this is the lazier way

def _verifyObjectType(obj, expectedType):
    '''
    :param obj: Python object
    :param expectedType: object type string
    :return: None. Raises exception if assertion fails.
    '''
    actualType = rcore.getObjectType(obj.templateName)
    if actualType.lower() != expectedType:
        raise Exception("Expected type %s but got type %s. template: %s" % (expectedType, actualType, obj.templateName))

And we create a function to cast an object to a requested type:

def _getCObjectCasted(obj, CType):
    '''
    :param obj: Python object
    :param CType: C Type to cast to
    :return:
    '''
    _verifyObjectType(obj, CType.TYPENAME)
    return CType.from_address(_getObjectPtr(obj))

Finally, we can write the public API.

def getSupplyCrateAmmo(supplyCrate):
    _verifyObjectValidity(supplyCrate)

    CSupplyObject = _getCObjectCasted(supplyCrate, CSupply)
    return CSupplyObject.sharedammo


def setSupplyCrateAmmo(supplyCrate, ammo):
    _verifyObjectValidity(supplyCrate)

    CSupplyObject = _getCObjectCasted(supplyCrate, CSupply)
    if ammo < 0:
        ammo = 0

    CSupplyObject.sharedammo = ammo

Passing an object that isn’t a supplyObject to these calls will raise an exception, we can trust this function to not crash the server when someone makes a mistake and gives it an object of a different type.

This allowed us to implement two features. We can make weapons (kits) cost supplies from crates, and we can have the server send events to the player which tell him how much supplies a crate has left (this value is not networked to the player normally)

Written on April 27, 2021