April 26, 2021
Extending BF2 Python API Part 2
Battlefield 2 object components
Objects in Battlefield 2 are created from “templates”. The engine allows us to create and configure templates in a simple DSL. For example:
ObjectTemplate.create PlayerControlObject us_tnk_m1a1
ObjectTemplate.collisionMesh us_tnk_m1a2
ObjectTemplate.hasCollisionPhysics 1
...
Creates a template for a PlayerControlObject
(vehicle), by the name us_tnk_m1a1
and then assigns it a specific collision mesh by name. (collision meshes have a similiar template system). The template is then used to create instances of the object.
The c++ class diagram would look like this
Templates can be configured to add optional components to the objects they create, which add features to the object. For example, a firearm may have a zoom component, which allows the holder to aim down the sights and zoom.
ObjectTemplate.createComponent DefaultZoomComp
ObjectTemplate.zoom.zoomDelay 0.4
ObjectTemplate.zoom.addZoomFactor 0
ObjectTemplate.zoom.addZoomFactor 1.099
ObjectTemplate.zoom.disableMuzzleWhenZoomed 0
It may have a fire component, describing when and how it may fire.
ObjectTemplate.createComponent SingleFireComp
ObjectTemplate.fire.roundsPerMinute 450
ObjectTemplate.fire.addFireRate 2
And instead of a SingleFireComp
that fires single shots, it can instead have MultiFireComp
, which supports multiple fire barrels, firing in a round robin order.
In the last part I’ve shown how to get to the object itself, however all of the real interesting object state is inside components, so to do anything interesting we need to get to an object’s components. VPTRs are going to help us figure out where to find them.
VPTR
Virtual methods in c++ allow you to call different methods depending on the object type, without having to remember which type the object is. A classic example for this is shapes. You have a base class called Shape
, and two classes that inherit from it called Square
and Circle
. Both of these classes implement a getArea()
function, which are obviously different from eachother. Now, your code has an instance of a shape and needs to do some logic based on its area. One way to do it is to implement something that remembers what type each object is and then call the fitting function yourself. With virtual methods, the objects themselves remember their type and store which function should be used.
The implementation used by most (if not all?) compilers is to place a pointer to a Virtual Method Table at the start of the object. The compiler remembers that shape->getObject()
is a virtual call, and the call is done by dereferencing the pointer to the table and then calling a specific entry on it.
How do VPTRs help us figure out Object components?
Every object that implements a virtual function has a VPTR at the start of it. This is huge, we can learn exactly what an object in memory is just by seeing what his VPTR is. We take the Battlefield 2 object and dereference anything that looks like a pointer in it, then read the first few bytes for a vptr. If this offset on the Battlefield 2 object actually pointed to a virtual type, then the vptr would point to something that looks like this.
Then, using left over debug string, or strings related to the DSL in the area, or comparing interfaceIDs to linux binaries like I mentioned in here, we can learn what type of component sits on this offset of the object.
Putting it all together in Python and controlling an object’s velocity.
One of the disappointing issues with the existing Python API is the inability to control an object’s velocity. Calling SetPosition() every tick of the game to simulate it does not work well. It doesn’t actually affect an object’s velocity and the function itself also checks for collision in a way that constantly fails, sometimes deciding the put the objects 30m above the requested position. We want to be able to set the object’s velocity and let the game engine physics system to deal with all things related to collision resulting from that speed. Using ctypes.Structure, we can define the C object structs in Python in a (somewhat) clean way.
Define the base class. For breivity, lets show the relevant Physics Component.
class CObject(Structure):
_fields_ = [("vptr", c_void_p), # 0
....
("objPhysics", c_void_p), # 44
....
]
When reversing the Physics Component, we notice that there are a few possibles types for it. We can see it by checking the constructor of the object (finding it by checking who references the vptr). Any constructor that calls this constructor is a child type, and any constructor this constructor calls is the parent type. Its then easy to create a diagram to help us understand what to expect when reading memory.
It is important when modifying memory to be sure you know what you’re modifying, and also be sure you know the object you’re modifying is the same object type you are expecting. Writing velocity to a staticPhysicsNode will write past the object’s memory, corrupting random memory in the heap. We must know what object type we are currently messing with before doing any writes. The best way to verify an object’s type, is to just read and compare its vptr.
objPhysics_normalPhysicsVPTR = 0x007FA700
After reading the component’s memory and seeing how it behaves, we write down some interesting offsets.
class objPhysics_normal(Structure):
_fields_ = [("vptr", c_void_p), # 0
("unknown", c_int8 * 0x1C), # 0x04
("vx", c_float), # 0x20 (velocity)
("vy", c_float), # 0x24
("vz", c_float), # 0x28
("unknown2", c_int8 * 0x2C), # 0x2C
("gravitymod", c_float), # 0x58
("unknown3", c_int8 * 0x08), # 0x5C
("sleepinessMax", c_int32), # 0x64
("sleepiness", c_int32), # 0x68
("unknown4", c_int8 * 0x28), # 0x6C
("rx", c_float), # 0x94 (rotational velocity)
("ry", c_float), # 0x98
("rz", c_float), # 0x9C
("unknown5", c_int8 * 0x80), # 0xA0
("lagCompensationPositions", POINTER(c_float * (16 * 3))), # 0x120
("lagCompensationQuats", POINTER(c_float * (16 * 4))), # 0x124
("lagCompensationCyclicBufferIndex", c_int32) # 0x128
]
At last, we can write some (realtively) normal Python for a change.
A physics getter function.
def _getObjectPhysics(obj):
cobj = _getCObject(obj)
if cobj.objPhysics is None:
return None
physicsVptr = c_void_p.from_address(cobj.objPhysics).value
# Nothing to do with static physics (for now)
if physicsVptr == objPhysics_staticVPTR:
return None
if physicsVptr == objPhysics_normalPhysicsVPTR:
return objPhysics_normal.from_address(cobj.objPhysics)
if physicsVptr == objPhysics_pointVPTR:
return objPhysics_point.from_address(cobj.objPhysics)
if physicsVptr == objPhysics_soldierVPTR:
return objPhysics_soldier.from_address(cobj.objPhysics)
if physicsVptr == objPhysics_rotationalPointVPTR:
return objPhysics_rotationalPoint.from_address(cobj.objPhysics)
raise Exception("Unknown physics VPTR %x" % physicsVptr)
And finally, the API that other Pythoners will be calling.
(Sleepiness is a physics optimization feature. Objects that do not move for long go to sleep. So we should reset its sleepiness to tell the game knows it should perform physics updates on it)
def setVelocity(obj, speed):
physics = _getObjectPhysics(obj)
if physics is None:
raise Exception("Object %s does not have physics or is static" % obj.templateName)
physics.sleepiness = physics.sleepinessMax
physics.vx, physics.vy, physics.vz = speed
def getVelocity(obj):
physics = _getObjectPhysics(obj)
if physics is None:
raise Exception("Object %s does not have physics or is static" % obj.templateName)
return physics.vx, physics.vy, physics.vz
#... Same for angular velocity, with the added check if the physics type even has angular velocity
This opens the door for all kinds of useful and crazy features that were never possible.