# ../memory/manager.py
"""Provides extended memory functionality."""
# =============================================================================
# >> IMPORTS
# =============================================================================
# Source.Python Imports
# Core
from core import GameConfigObj
# Memory
from memory import Convention
from memory import DataType
from memory import EXPOSED_CLASSES
from memory import TYPE_SIZES
from memory import alloc
from memory import find_binary
from memory import get_object_pointer
from memory import make_object
from memory import memory_logger
from memory.helpers import Array
from memory.helpers import BasePointer
from memory.helpers import Key
from memory.helpers import MemberFunction
from memory.helpers import NO_DEFAULT
from memory.helpers import Type
from memory.helpers import parse_data
# =============================================================================
# >> ALL DECLARATION
# =============================================================================
__all__ = ('CustomType',
'TypeManager',
'manager',
)
# =============================================================================
# >> GLOBAL VARIABLES
# =============================================================================
manager_logger = memory_logger.manager
# =============================================================================
# >> CustomType
# =============================================================================
[docs]class CustomType(BasePointer):
"""
Subclass this class if you want to create a new type.
Make sure that you have set the metaclass
attribute to a valid TypeManager instance.
"""
# This should always hold a TypeManager instance. It's set automatically
# by the metaclass.
_manager = None
# This is required if TypeManager.function() is used. It should contain
# the path to the binary that defines this type
_binary = None
# Optional -- this gets passed to BinaryFile.find_binary()
_srv_check = True
# Optional -- specifies the size of the type
_size = None
# Optional -- will be called when an instance of the type is created
_constructor = None
# Optional -- will be called when an instance of the type is deleted
_destructor = None
[docs] def __init__(self, *args, wrap=False, auto_dealloc=True):
"""Initialize the custom type."""
# _manager must be an instance of TypeManager. Otherwise the type
# wasn't registered by a TypeManager.
if not isinstance(self._manager, TypeManager):
raise ValueError(
'Attribute _manager must be an instance of "TypeManager".')
# Make sure the _manager attribute wasn't set manually
if self.__class__.__name__ not in self._manager:
raise TypeError(
'Custom type was not registered at a type manager.')
# This set will contain internally allocated pointers.
self._allocated_pointers = set()
# This dict will hold pointers, so they don't deallocate if
# auto_dealloc was set. {<offset>: <pointer>}
self._pointer_values = {}
# Do we want to wrap a pointer?
if wrap:
# Check if only the this pointer was passed
if len(args) != 1:
raise ValueError(
'If <wrap> is true only one argument is accepted.')
super().__init__(args[0])
# Obviously, we want to create a new instance
else:
# Was a size specified?
if self._size is None:
raise ValueError(
'In order to create an instance _size is required.')
# Allocate some space
super().__init__(alloc(self._size, False))
self.auto_dealloc = auto_dealloc
# Optionally, call a constructor
if self._constructor is not None:
self._constructor(*args)
# No constructor, but arguments? Hmm, the user is doing something
# wrong
elif args:
raise ValueError(
'No constructor was specified, but arguments were passed.')
def _ptr(self):
"""Return the pointer of the object."""
return self
@classmethod
def _obj(cls, ptr):
"""Wrap the given pointer."""
return cls(ptr, wrap=True)
[docs] def on_dealloc(self):
"""Call the destructor.
This method is automatically called, when the pointer gets
deallocated. It then calls the destructor if it was specified.
"""
# Call the destructor if it was specified
if self._destructor is not None:
self._destructor()
# =============================================================================
# >> TypeManager
# =============================================================================
[docs]class TypeManager(dict):
"""Class able to reconstruct almost every possible data type."""
[docs] def __init__(self):
"""Initialize the instance."""
# Initialize the dictionary
super().__init__()
# This dictionary will hold global pointer instances
self.global_pointers = {}
# Stores converters
self.converters = {}
# Stores function typedefs
self.function_typedefs = {}
# Storage for custom calling conventions
self.custom_conventions = {}
def __call__(self, name, bases, cls_dict):
"""Create and registers a new class."""
# Set the manager attribute. This is required, so CustomType.__init__
# can verify that we have registered the class
cls_dict['_manager'] = self
# Create the class object
cls = type(name, bases, cls_dict)
# Check if it's a subclass of CustomType
if not issubclass(cls, CustomType):
raise ValueError(
'Custom type "{0}" has to '.format(name) +
'be a subclass of "CustomType".')
self[name] = cls
return cls
[docs] def custom_calling_convention(self, cls):
"""Register a custom calling convention class by its name.
Example:
.. code:: python
@manager.custom_calling_convention
class MyCustomCallingConvention(CallingConvention):
pass
# Equals to...
class MyCustomCallingConvention(CallingConvention):
pass
manager.register_convention(
'MyCustomCallingConvention',
MyCustomCallingConvention)
"""
self.register_convention(cls.__name__, cls)
return cls
[docs] def register_convention(self, name, convention):
"""Register a custom calling convention.
:param str name:
Name of the custom calling convention.
:param CallingConvention convention:
The custom calling convention.
"""
self.custom_conventions[name] = convention
[docs] def unregister_convention(self, name):
"""Unregister a custom calling convention."""
self.custom_conventions.pop(name, None)
[docs] def register_converter(self, name, obj):
"""Register a callable object as a converter.
The callable object should only accept a pointer as an argument.
"""
# Make sure we can call the object
if not callable(obj):
raise ValueError('Object is not callable.')
self.converters[name] = obj
[docs] def unregister_converter(self, name):
"""Unregister a converter."""
self.converters.pop(name, None)
[docs] def convert(self, name, ptr):
"""Convert the pointer.
Tries to convert a pointer in the following order:
Attempts to convert the pointer...
1. to a custom type
2. to a exposed type
3. to a function typedef
4. by using a converter
"""
cls = self.get_class(name)
if cls is not None:
# Use the class to convert the pointer
return make_object(cls, ptr)
# No class was found. Maybe we have luck with a function typedef
converter = self.function_typedefs.get(name, None)
# Is there no function typedef?
if converter is None:
converter = self.converters.get(name, None)
# Is there no converter?
if converter is None:
raise NameError(
'No class, function typedef or ' +
'converter found for "{0}".'.format(name))
# Yay, we found a converter or function typedef!
return converter(ptr)
[docs] def create_converter(self, name):
"""Create a new converter for the given name."""
return lambda ptr: self.convert(name, ptr)
[docs] def get_class(self, name):
"""Return the custom type for the given name.
Tries to return a custom type that matches the given name. If no
custom type was found, it tries to return a class that was exposed on
the C++ side. If that fails too, None will be returned.
"""
return self.get(name, None) or EXPOSED_CLASSES.get(name, None)
[docs] def create_type(self, name, cls_dict, bases=(CustomType,)):
"""Create and registers a new class."""
# This is just a wrapper for __call__
return self(name, bases, cls_dict)
@staticmethod
[docs] def create_pipe(cls_dict):
"""Create a new pipe class that acts like a collection of functions."""
# Just create a container for all the functions
return type('Pipe', (object,), cls_dict)
[docs] def create_pipe_from_file(self, f):
"""Create a pipe from a file or URL."""
return self.create_pipe_from_dict(GameConfigObj(f))
[docs] def create_pipe_from_dict(self, raw_data):
"""Create a pipe from a dictionary."""
# Prepare functions
funcs = parse_data(
self,
raw_data,
(
(Key.BINARY, Key.as_str, NO_DEFAULT),
(Key.IDENTIFIER, Key.as_identifier, NO_DEFAULT),
(Key.ARGS, Key.as_args_tuple, ()),
(Key.RETURN_TYPE, Key.as_return_type, DataType.VOID),
(Key.CONVENTION, Key.as_convention, Convention.CDECL),
(Key.SRV_CHECK, Key.as_bool, True),
(Key.DOC, Key.as_str, None)
)
)
# Create the functions
cls_dict = {}
for name, data in funcs:
cls_dict[name] = self.pipe_function(*data)
return self.create_pipe(cls_dict)
[docs] def pipe_function(
self, binary, identifier, args=(), return_type=DataType.VOID,
convention=Convention.CDECL, srv_check=True, doc=None):
"""Create a simple pipe function."""
# Create a converter, if it's not a native type
if return_type not in DataType.values:
return_type = self.create_converter(return_type)
# Find the binary
binary = find_binary(binary, srv_check)
# Find the address and make it to a function
func = binary[identifier].make_function(convention, args, return_type)
# Add documentation
func.__doc__ = doc
return func
[docs] def create_type_from_file(self, type_name, f, bases=(CustomType,)):
"""Create and registers a new type from a file or URL."""
return self.create_type_from_dict(
type_name, GameConfigObj(f), bases)
[docs] def create_type_from_dict(self, type_name, raw_data, bases=(CustomType,)):
"""Create and registers a new type from a dictionary."""
# Prepare general type information
data = tuple(parse_data(
self,
# Discard all subkeys and add the new dict to a another dict to
# make it work with parse_data(). Okay, this can be improved...
{0: dict((k, v) for k, v in raw_data.items() if not isinstance(
v, dict))},
(
(Key.BINARY, Key.as_str, CustomType._binary),
(Key.SRV_CHECK, Key.as_bool, CustomType._srv_check),
(Key.SIZE, Key.as_int, CustomType._size)
)
))[0][1]
cls_dict = dict(zip(('_binary', '_srv_check', '_size'), data))
# Prepare pointer and instance attributes
for method in (self.instance_attribute, self.pointer_attribute):
attributes = parse_data(
self,
raw_data.get(method.__name__, {}),
(
(Key.TYPE_NAME, Key.as_attribute_type, NO_DEFAULT),
(Key.OFFSET, Key.as_int, NO_DEFAULT),
(Key.DOC, Key.as_str, None)
)
)
# Create the attributes
for name, data in attributes:
cls_dict[name] = method(*data)
# Prepare arrays
for method in (
self.static_instance_array,
self.dynamic_instance_array,
self.static_pointer_array,
self.dynamic_pointer_array):
arrays = parse_data(
self,
raw_data.get(method.__name__, {}),
(
(Key.TYPE_NAME, Key.as_attribute_type, NO_DEFAULT),
(Key.OFFSET, Key.as_int, NO_DEFAULT),
(Key.LENGTH, Key.as_int, None),
(Key.DOC, Key.as_str, None)
)
)
# Create the arrays
for name, data in arrays:
cls_dict[name] = method(*data)
# Prepare virtual functions
vfuncs = parse_data(
self,
raw_data.get('virtual_function', {}),
(
(Key.OFFSET, Key.as_int, NO_DEFAULT),
(Key.ARGS, Key.as_args_tuple, ()),
(Key.RETURN_TYPE, Key.as_return_type, DataType.VOID),
(Key.CONVENTION, Key.as_convention, Convention.THISCALL),
(Key.DOC, Key.as_str, None)
)
)
# Create the virtual functions
for name, data in vfuncs:
cls_dict[name] = self.virtual_function(*data)
# Prepare functions
funcs = parse_data(
self,
raw_data.get('function', {}),
(
(Key.IDENTIFIER, Key.as_identifier, NO_DEFAULT),
(Key.ARGS, Key.as_args_tuple, ()),
(Key.RETURN_TYPE, Key.as_return_type, DataType.VOID),
(Key.CONVENTION, Key.as_convention, Convention.THISCALL),
(Key.DOC, Key.as_str, None)
)
)
# Create the functions
for name, data in funcs:
cls_dict[name] = self.function(*data)
# Now create and register the type
return self(type_name, bases, cls_dict)
[docs] def instance_attribute(self, type_name, offset, doc=None):
"""Create a wrapper for an instance attribute.
Examples:
Vector vecVal;
bool bVal;
"""
native_type = Type.is_native(type_name)
def fget(ptr):
"""Return the instance attribute value."""
# Handle custom type
if not native_type:
return self.convert(type_name, ptr + offset)
# Handle native type
return getattr(ptr, 'get_' + type_name)(offset)
def fset(ptr, value):
"""Set the instance attribute value."""
# Handle custom type
if not native_type:
cls = self.get_class(type_name)
if cls is None:
raise NameError('Unknown class "{0}".'.format(type_name))
get_object_pointer(value).copy(
ptr + offset,
cls._size
)
# Handle native type
else:
getattr(ptr, 'set_' + type_name)(value, offset)
return property(fget, fset, None, doc)
[docs] def pointer_attribute(self, type_name, offset, doc=None):
"""Create a wrapper for a pointer attribute.
Examples:
Vector* pVec;
bool* pBool;
"""
native_type = Type.is_native(type_name)
def fget(ptr):
"""Get the pointer attribute value."""
# Get the base address of the pointer. We are now on
# "instance level"
ptr = ptr.get_pointer(offset)
# Handle custom type
if not native_type:
return self.convert(type_name, ptr)
# Handle native type
return getattr(ptr, 'get_' + type_name)()
def fset(ptr, value):
"""Set the pointer attribute value."""
# Handle custom type
if not native_type:
# Set the pointer
ptr.set_pointer(value, offset)
# Make sure the value will not deallocate as long as it is
# part of this object
ptr._pointer_values[offset] = value
# Handle native type
else:
# Go down to "instance level"
instance_ptr = ptr.get_pointer(offset)
# Is there no space allocated?
if not instance_ptr:
# Allocate space for the value
instance_ptr = alloc(TYPE_SIZES[type_name.upper()])
# Add the pointer to the set, so there will be a reference
# until the instance gets deleted
ptr._allocated_pointers.add(instance_ptr)
# Set the pointer
ptr.set_pointer(instance_ptr, offset)
# Set the value
getattr(instance_ptr, 'set_' + type_name)(value)
return property(fget, fset, None, doc)
[docs] def static_instance_array(self, type_name, offset, length=None, doc=None):
"""Create a wrapper for a static instance array.
Examples:
Vector vecArray[10];
bool boolArray[10];
"""
def fget(ptr):
"""Get the static instance array."""
return Array(self, False, type_name, ptr + offset, length)
def fset(ptr, value):
"""Set all values in the static instance array."""
array = fget(ptr)
for index, val in enumerate(value):
array[index] = val
return property(fget, fset, None, doc)
[docs] def dynamic_instance_array(self, type_name, offset, length=None, doc=None):
"""Create a wrapper for a dynamic instance array.
Examples:
Vector* pVecArray;
bool* pBoolArray;
Those arrrays are mostly created by the "new" statement.
"""
def fget(ptr):
"""Get the dynamic instance array."""
return Array(
self, False, type_name, ptr.get_pointer(offset), length)
def fset(ptr, value):
"""Set all values for the dynamic instance array."""
array = fget(ptr)
for index, val in enumerate(value):
array[index] = val
return property(fget, fset, None, doc)
[docs] def static_pointer_array(self, type_name, offset, length=None, doc=None):
"""Create a wrapper for a static pointer array.
Examples:
Vector* pVecArray[10];
bool* pBoolArray[10];
"""
def fget(ptr):
"""Get the static pointer array."""
return Array(self, True, type_name, ptr + offset, length)
def fset(ptr, value):
"""Set all values for the static pointer array."""
array = fget(ptr)
for index, val in enumerate(value):
array[index] = val
return property(fget, fset, None, doc)
[docs] def dynamic_pointer_array(self, type_name, offset, length=None, doc=None):
"""Create a wrapper for a dynamic pointer array.
Examples:
Vector** pVecArray;
bool** pBoolArray;
Those arrays are mostly created by the "new" statement.
"""
def fget(ptr):
"""Get the dynamic pointer array."""
return Array(
self, True, type_name, ptr.get_pointer(offset), length)
def fset(ptr, value):
"""Set all values for the dynamic pointer array."""
array = fget(ptr)
for index, val in enumerate(value):
array[index] = val
return property(fget, fset, None, doc)
[docs] def virtual_function(
self, index, args=(), return_type=DataType.VOID,
convention=Convention.THISCALL, doc=None):
"""Create a wrapper for a virtual function."""
# Automatically add the this pointer argument
args = (DataType.POINTER,) + tuple(args)
# Create a converter, if it's not a native type
if return_type not in DataType.values:
return_type = self.create_converter(return_type)
def fget(ptr):
"""Return the virtual function."""
# Create the virtual function
func = ptr.make_virtual_function(
index,
convention,
args,
return_type
)
# Wrap it using MemberFunction, so we don't have to pass the this
# pointer anymore
return MemberFunction(self, return_type, func, ptr)
return property(fget, None, None, doc)
[docs] def function(
self, identifier, args=(), return_type=DataType.VOID,
convention=Convention.THISCALL, doc=None):
"""Create a wrapper for a function."""
# Automatically add the this pointer argument
args = (DataType.POINTER,) + tuple(args)
# Create a converter, if it's not a native type
if return_type not in DataType.values:
return_type = self.create_converter(return_type)
class fget(object):
def __get__(fget_self, obj, cls=None):
if cls is None:
if obj is None:
return fget_self
else:
cls = obj.__class__
if cls._binary is None:
raise ValueError('_binary was not specified.')
# Create a binary object
binary = find_binary(cls._binary, cls._srv_check)
# Create the function object
func = binary[identifier].make_function(
convention,
args,
return_type
)
# Called with a this pointer?
if obj is not None:
# Wrap the function using MemberFunction, so we don't have
# to pass the this pointer anymore
func = MemberFunction(self, return_type, func, obj)
func.__doc__ = doc
return func
return fget()
[docs] def function_typedef(
self, name, args=(), return_type=DataType.VOID,
convention=Convention.CDECL, doc=None):
"""Create a new function typedef.
When a class has an attribute that contains a pointer of a function,
the attribute will return a Function object that will be created
by this method.
"""
# Create a converter, if it's not a native type
if return_type not in DataType.values:
return_type = self.create_converter(return_type)
def make_function(ptr):
"""Return the function typedef."""
func = ptr.make_function(convention, args, return_type)
func.__doc__ = doc
return func
self.function_typedefs[name] = make_function
return make_function
[docs] def create_function_typedefs_from_file(self, f):
"""Create function typedefs from a file."""
# Read the data
raw_data = GameConfigObj(f)
# Prepare typedefs
typedefs = parse_data(
self,
raw_data,
(
(Key.ARGS, Key.as_args_tuple, ()),
(Key.RETURN_TYPE, Key.as_return_type, DataType.VOID),
(Key.CONVENTION, Key.as_convention, Convention.CDECL),
(Key.DOC, Key.as_str, None)
)
)
# Create the typedefs
for name, data in typedefs:
self.function_typedef(name, *data)
[docs] def global_pointer(
self, cls, binary, identifier, offset=0, level=0, srv_check=True,
accessor=False, accessor_offset=0):
"""Search for a global pointer and wrap the it."""
manager_logger.log_debug(
'Retrieving global pointer for {}...'.format(cls.__name__))
# Get the binary
binary = find_binary(binary, srv_check)
# Get the global pointer
if accessor:
ptr = binary[identifier]
ptr = (ptr + offset + ptr.get_pointer(accessor_offset)).make_function(
Convention.CDECL,
(),
DataType.POINTER
)()
for _ in range(level):
ptr = ptr.get_pointer()
else:
ptr = binary.find_pointer(identifier, offset, level)
# Raise an error if the pointer is invalid
if not ptr:
raise ValueError('Unable to find the global pointer.')
# Wrap the pointer using the given class and save the instance
ptr = self.global_pointers[cls.__name__] = make_object(cls, ptr)
return ptr
[docs] def create_global_pointers_from_file(self, f):
"""Create global pointers from a file."""
# Parse pointer data
pointers = parse_data(
self,
GameConfigObj(f),
(
(Key.BINARY, Key.as_str, NO_DEFAULT),
(Key.IDENTIFIER, Key.as_identifier, NO_DEFAULT),
(Key.OFFSET, Key.as_int, 0),
(Key.LEVEL, Key.as_int, 0),
(Key.SRV_CHECK, Key.as_bool, True),
(Key.ACCESSOR, Key.as_bool, False),
(Key.ACCESSOR_OFFSET, Key.as_int, 0)
)
)
# Create the global pointer objects
for name, data in pointers:
cls = self.get_class(name)
if cls is None:
raise NameError('Unknown class "{0}".'.format(name))
self.global_pointer(cls, *data)
[docs] def get_global_pointer(self, name):
"""Return the global pointer for the given class."""
# Allow passing class objects
if not isinstance(name, str):
name = name.__name__
# Raise an error if no global pointer was found.
if name not in self.global_pointers:
raise NameError('No global pointer found for "{0}".'.format(name))
return self.global_pointers[name]
# Create a shared manager instance
manager = TypeManager()