Memory Modding

From HaloDemoMods

Jump to: navigation, search


Contents

Newbie introduction

The first thing one should do is play around with The Cheat.

  • Learn how to find variables (such as ammunition, kills, deaths, etc) and know how to modify them.
  • Get a feel with what variables might be 32 bit (4 byte) integer, or a 16 bit (2 byte) integer, or a 8 bit (1 byte) integer, or a floating-point (also 4 bytes). Halo doesn't really use doubles and 64 bit integers in my experience. (What variable type is ammunition, what variable type is kills, what variable type is scores?, etc)
  • Play around with applying changes to variables every x amount of seconds, if desired.
  • At least learn the difference between signed and unsigned for integers.
  • If lost, consult to The Cheat's manual under its Help menu.

Programming introduction

(The rest of this tutorial assumes you have a platform that runs Mac OS X 10.5 or later)

Now, The Cheat is a fun tool, but imagine if one could create dynamic mods where things are changing based on gameplay. The rest of this tutorial focuses on that. To do such things, one would need to create his own programs. Yes, this means learning how to program. This guide uses a tool that runs scripts that are created in Python.

Before continuing on, learn the Python programming language. It's simple, yet powerful, unlike other programming languages (REALbasic, C, ObjC, etc). I will, however, mention that C has advantages in programming in this kind of hack-ish environment, but Python is a lot easier to begin with and is loads more friendlier to use.

  • Google for a Python tutorial (duh). Here is a random tutorial I got off from google [1]. To start up python's interactive shell, open up Terminal.app and type python and hit return. To run a source code file, just execute python <path_to_python_file>
  • Make sure to learn the fundamentals such as using if's, loops, arrays, calling/creating functions, constants, globals, and of course writing code in source code files.
  • Use a good coding/programmer editor, whether it be textmate, subethedit, textwrangler, smultron, or even xcode's editor.
  • Notice that Python is a case sensitive programming language, just like many others, but noticeably unlike REALbasic
  • Python uses indentation for scope. All of my code uses tabs for indentation, not [soft] spaces. Tabs and spaces can't be mixed for indentation. My recommendation is to make your coding editor treat tabs as tab characters, use whatever tab width you want (I recommend 4), use tabs for first indentation on a line, and use spaces for aligning things (but not tabs) if need be.

HaloDemoMemoryTemplate

Yeah, this is the tool. Like I said before, it will only run on Mac OS X 10.5 (PPC or intel) or a newer operating system..

File:HaloDemoMemoryTemplate.zip (engine build 0.1.6)

[Note: This template application can be updated with bug fixes or new features at *any* time, and when it is, the engine build version will differ. The engine build version can be found by running the application and opening up the debug window in the application menu]

Go ahead and try the sample script that comes with it. Run the application. Run Halo Demo and start your own server in Slayer, go to blue base, start the script, and observe the warthog move in action. Stop the script to like, stop it.

Neat, right?!

Now, in the menubar go to File > Reveal Script in Finder, and open HaloDemoTrainer.py in a coding editor.

The source code should look like this:

#
# HaloDemoTrainer.py
#
# Created by nil, thanks to modfox for finding me the warthog position addresses
# Copyright (c) 2008. All rights reserved.
 
#you need to include this
from VirtualMemory import *
#although this script in particular doesn't use writeLog() from Debug, you'll almost always want to include this
from Debug import *
#I'm using trig functions from math module
import math
 
#Basic script attributes
WINDOW_TITLE = "Circular warthog"
DESCRIPTION = "The mathematical warthog grapher! Drawing a circle with the parametric equation x = cos(t), z = sin(t). This must be played on Slayer."
#This is a time interval in seconds, indicating how often our script's execute function is called when it's running
EXECUTION_TIME_INTERVAL = 0.03
 
#my halo demo constants
WARTHOG_X_POSITION_ADDRESS = 0x4BB26798
WARTHOG_Z_POSITION_ADDRESS = 0x4BB267A0
WARTHOG_CURVE_Z_ORIGIN_OFFSET = 5.0
WARTHOG_CURVE_ALTITUDE = 4.0
 
#my global variables
warthogBaseX = 0
warthogBaseZ = 0
 
#this function is called every EXECUTION_TIME_INTERVAL seconds, as long as the script is running
def execute(timeElapsed):
	#I need to declare these as globals if I want to use them from inside this function
	global warthogBaseX
	global warthogBaseZ
 
	#timeElapsed will be 0 right when the user hits 'Start' (the first time this function is called)
	if timeElapsed == 0:
		#the x and z values of where the warthog orginally is at the beginning of this script are going to be stored in these base variables
		warthogBaseX = readFloat(WARTHOG_X_POSITION_ADDRESS)
		# I want baseZ to be a little bit higher than where the warthog currently is, which is what
		# WARTHOG_CURVE_Z_ORIGIN_OFFSET is used for
		warthogBaseZ = readFloat(WARTHOG_Z_POSITION_ADDRESS) + WARTHOG_CURVE_Z_ORIGIN_OFFSET
	else:
		#use basic trig to create a parametric curve with x = cos(t) and z = sin(t) where t is timeElapsed
		#this parametric formula is simply just a circle
		#so, this makes the warthog go move in a circle
		writeFloat(WARTHOG_X_POSITION_ADDRESS, warthogBaseX + math.cos(timeElapsed) * WARTHOG_CURVE_ALTITUDE)
		writeFloat(WARTHOG_Z_POSITION_ADDRESS, warthogBaseZ + math.sin(timeElapsed) * WARTHOG_CURVE_ALTITUDE)
 
#this is called when the either the user hits stop or the script stops unexpectedly
def finish(timeElapsed, forcedShutDown):
	#this is where we clean up our mess. Place the warthog back where it originally was before the script was executed.
	writeFloat(WARTHOG_X_POSITION_ADDRESS, warthogBaseX)
	writeFloat(WARTHOG_Z_POSITION_ADDRESS, warthogBaseZ - WARTHOG_CURVE_Z_ORIGIN_OFFSET)

Yay, pretty wiki formatting! Uh, well, the script is commented pretty heavily, so hopefully you can understand the logic of how it works.. HaloDemoTrainer.py is normally the only file that you will end up modifying for making mods.

Engine Revision History

  • 0.1.6
    • Fixed serious bug where a script would stop executing if another another application opened or quits
  • 0.1.5
    • Fixed a spacing/alignment issue with the text in the Description field that's displayed on the main window.
    • Added a 'Reveal Script in Finder' menu option under the File menu.
  • 0.1.4
    • Fixed byte order issues when reading UTF16 strings. Also fixed an issue with writing UTF16 strings where garbage may have been written in memory. Much thanks goes to Johnathon for helping me fix this.
  • 0.1.3
    • Reverting to standard 32 bit universal instead of 32/64 universal, as Python/PyObjC doesn't like that on all machines.
  • 0.1.2
    • Fixed an issue with reading UTF8/UTF16 strings where an extra 0 character (not '0') would be at appended at the end of the returned string
  • 0.1.1
    • Reading/writing UTF8/UTF16 strings now work
  • 0.1
    • Initial public release

Scripting Documentation

Every HaloDemoTrainer.py script shares a few things in common as you may or may not have noticed:

  • WINDOW_TITLE and DESCRIPTION constants, which are really obvious if you run and test the application. They are displayed on the window of the application
  • EXECUTION_TIME_INTERVAL constant, which indicates how often execute() should be called. It's in seconds. The first time execute is called, timeElapsed argument that is passed is 0.
  • execute() is called when the script starts, and every EXECUTION_TIME_INTERVAL seconds after that until the script is stopped
  • finish() is called when the user stops the script or the script stops unexpectedly in execute() (like by raising an un-handled exception)

Memory Handling Functions

def readInt8(memoryAddress)
def writeInt8(memoryAddress, value)
def readUInt8(memoryAddress)
def writeUInt8(memoryAddress, value)
 
def readInt16(memoryAddress, byteOrder=None)
def writeInt16(memoryAddress, value, byteOrder=None)
def readUInt16(memoryAddress, byteOrder=None)
def writeUInt16(memoryAddress, value, byteOrder=None)
 
def readInt32(memoryAddress, byteOrder=None)
def writeInt32(memoryAddress, value, byteOrder=None)
def readUInt32(memoryAddress, byteOrder=None)
def writeUInt32(memoryAddress, value, byteOrder=None)
 
def readInt64(memoryAddress, byteOrder=None)
def writeInt64(memoryAddress, value, byteOrder=None)
def readUInt64(memoryAddress, byteOrder=None)
def writeUInt64(memoryAddress, value, byteOrder=None)
 
def readFloat(memoryAddress, byteOrder=None)
def writeFloat(memoryAddress, value, byteOrder=None)
 
def readDouble(memoryAddress, byteOrder=None)
def writeDouble(memoryAddress, value, byteOrder=None)
 
#each character in a UTF8 string is 1 byte long
def readUTF8String(memoryAddress)
def writeUTF8String(memoryAddress, value)
 
#each character in a UTF16 string is 2 bytes long
def readUTF16String(memoryAddress, byteOrder=None)
def writeUTF16String(memoryAddress, value, byteOrder=None)

These functions are self explanatory for the most part. The read functions return the value at the memory address you specify. The write functions write the value at the memory address, which you both specify. For Ints, the 'U' prefix in some of the functions stands for Unsigned as opposed to the implicit Signed. None is returned on failure.

Byte order is an optional parameter (meaning you don't have to pass it), which takes one of the three constants:

  • NATIVE_ENDIAN_BYTE_ORDER
  • BIG_ENDIAN_BYTE_ORDER
  • LITTLE_ENDIAN_BYTE_ORDER

In addition, two more functions for dealing with byte order exist:

#Can return either NATIVE_ENDIAN_BYTE_ORDER, BIG_ENDIAN_BYTE_ORDER, or LITTLE_ENDIAN_BYTE_ORDER from the current byte order
def getEndianByteOrder()
 
#Can set either NATIVE_ENDIAN_BYTE_ORDER, BIG_ENDIAN_BYTE_ORDER, or LITTLE_ENDIAN_BYTE_ORDER to the current byte order
def setEndianByteOrder(newByteOrder)

Each time the HaloDemoTrainer script is loaded, the current byte order is set to a constant DEFAULT_ENDIAN_BYTE_ORDER. Good chances are it will be BIG_ENDIAN_BYTE_ORDER when you are modding Halo Demo. You can change the DEFAULT_ENDIAN_BYTE_ORDER in VirtualMemory.py to whatever you want if you like.

Debugging

#writes a string, message, to the debug window which is accessed from the application menu
def writeToLog(message)

This writeToLog function seems to be self-explantory enough. When you first open the Debug Window (cmd L), the engine build version is outputted. Then, whatever else your script may log during its execution is outputted with a date stamp.

Testing Mod Changes

When making changes to a script, you do *not* have to quit the application and restart it to test your changes. In fact, you shouldn't. You should just leave the application open and restart the script by hitting stop if it's running, and by starting it again. Also, you can force-reload the script without running it in the File menu (cmd R), which is useful for seeing instant changes such as when you modify the mod description or window title. Be sure to check the Debug Window (cmd L) as errors may be reported there.

Deploying an Application

Deploying your own application that has its own custom name and everything is easy.

  • Rename the application in Finder to what you want.
  • Go in the application's bundle (right or control click on application -> show package contents), and open up Contents/Info.plist in a text editor. Modify the strings corresponding to the following keys to your liking: CFBundleIdentifier, CFBundleName, CFBundleVersion, CFBundleShortVersionString, and CFBundleIconFile if you have an application icon. You probably will also want to modify the NSHumanReadableCopyRight string in Contents/Resources/English.lproj/infoPlist.strings
  • Start up your application and see if everything works as intended. Check the application name in the menus and check your About Application window!

Targeting Halo Full (Or another program) Instead

You will need to change one or two of the user-configurable constants in VirtualMemory.py

  • Change APP_TARGET to the program name you want to target (For halo full's case, "Halo")
  • Change DEFAULT_ENDIAN_BYTE_ORDER to an appropriate value. One would likely want it to be BIG_ENDIAN_BYTE_ORDER for rosetta-emulated programs, and NATIVE_ENDIAN_BYTE_ORDER for any other type of program (For halo full's case, one would likely want it to be NATIVE_ENDIAN_BYTE_ORDER)
  • Then restart the application for changes to take effect.

Engine Source Code

This is the current source code of the template application. It is updated as often as the template application. An Xcode project is included. You don't need to get this to create your own scripts/mods. You just need the template application for that. Anyway, here's the link:

File:HaloDemoMemoryTemplate source.zip (0.1.6)

Halo Demo Memory Documentation

First, two types of memory addresses that you can track down exist: static and dynamic. Static memory addresses never change, and always point to the same variable that you are looking for, which is really nice. Dynamic memory addresses can change whenever, which is not nice, but there's still some hope. You may be able to find a static address that points to a variable whose value holds the dynamic address you're trying to find.

I will also mention that some static addresses may not always be the same across different machines. Sometimes this is because a user trying out your memory mod may have modified his own bloodgulch map, but that doesn't always have to be the case.

Halo Trial/Demo's memory space has been reverse-engineered already somewhat and the information is available here [2]. (Read that page before continuing on, even if you aren't able understand everything on there). The offsets to the static structures given on that page are the same for Halo Trial and Halo Demo, surprisingly.

Here are data types that may be foreign to you, but may be similar to what you already know:

  • char -> Int8 (1 byte)
  • short, wchar_t -> Int16 (2 bytes)
  • long -> Int32 (4 bytes)
  • char someString[n] -> pretty much a UTF8/ASCII string with a maximum storage of (n - 1) 1 byte characters plus a 1 byte null terminator character
  • wchar_t someString[n] -> pretty much a UTF16 string with a maximum storage of (n - 1) 2 byte characters plus a 2 byte null terminator character

Structs or structures are just an array of data, and the fields are consecutive in memory. For example, the memory address to the Static_Player_Header is located at 0x4BD7AF94. This also means that the memory address 0x4BD7AF94 points to the char TName[32] field because it's the first one. The char TName[32] field is just 32 * (size of char bytes) long in size, so if I wanted to get the memory address to the MaxSlots field, I would calculate: 0x4BD7AF94 + 32 * 1 = 0x4BD7AFB4. Then in a script, I could do readUInt16(0x4BD7AFB4) and it would return 2048. Yay!

Misc. References

  • The size of every Static_Player structure is 0x200, or 512, bytes long which is useful to know for iterating through them

For example, printing everyone's name in the server (you don't have to be the host):

firstPlayerNameAddress = 0x4BD7AFD0
staticPlayerTableSize = 0x200
maxPlayers = 16
for playerIndex in range(maxPlayers):
	playerName = readUTF16String(firstPlayerNameAddress + staticPlayerTableSize * playerIndex)
	if len(playerName) > 0:
		writeToLog("Player " + str(playerIndex) + "'s name is " + playerName)
  • The first Object_Table_Array in memory is located at 0x4BB206EC (right after the Object_Table_Header structure)
  • The size of every Object_Table_Array is 12 bytes long
  • Each Object_Table_Array has a variable that holds a memory address (a pointer) to a Halo object, and the offset to the pointer is 0x8 within the object table array structure.
    • If you are able to get the object ID of a halo structure, you can get the memory address of the Object_Table_Array by calculating firstObjectTableArrayAddress + objectID * objectTableArraySize
      • Which means if you want to get the memory address to a halo structure, you would do readUInt32(0x4BB206EC + objectID * 12 + 0x8)
  • You should always check if an object id is "invalid"
    • An object id is typically invalid if its value is 0xFFFF (or 65535 in decimal). Notice that object id's are unsigned 16-bit integers.
    • For dynamic player structures, when an object id is invalid, it means that the player is not alive or not in the game.
      • Or, an object id of 0 for a dynamic player structure may mean that no player at that slot has been allocated yet.
Personal tools
click logo to return home