What happened to OC? - CLOSED Carnage?!

Tag manipulation in SAPP

Tag data manipulation sounds like pain, but it’s not, and there are many reasons why you’d want to manipulate the tag data, too. You may have seen some of my scripts do this, and perhaps you’ve wondered how I do it. If you're just trying to get a map modification running on your server rather than actually script anything, you can use Glazed Doughnut to generate your scripts for you, instead.


Before you start trying to figure out everything, you’ll first need to download a few things:

  • Eschaton. While you won’t be modifying map files with it, we’ll need the plugins that come with it to know where the data is. To efficiently find the tag you want to edit, you should set Eschaton’s preferences to organize by tag class. If you plan on editing the map in the future, then you should also set the editor style to “Classic Control” as “Scrolling Controls” is slow and crashy.

  • SAPP. Technically this is optional, but you will need a SAPP server in order to test your scripts.

  • Notepad++, Atom, or some other text editor with syntax highlighting. You CAN use plain Notepad, but having syntax highlighting makes your scripts easier to read. If you get Atom, you may want to get the Lua syntax highlighting through its package manager.


Also, here are a few notes to keep in mind before continuing:

  • If you don’t know how to write in Lua, then you should learn that, first. This tutorial assumes you know how to write in Lua without help. Please do not ask me for help with how to write in Lua, as this is not what this tutorial is for.

  • This tutorial assumes you know how to use SAPP’s Lua API. You can find it at http://halo.isimaginary.com/lua-scripting

  • This tutorial assumes you know how to use Eschaton as well as how to mod, and that you are using Classic Controls whenever modding is being referenced.

  • This tutorial assumes you know how to use hexadecimal.

  • This tutorial assumes you know how pointers work.


In order to begin manipulating tags, we’ll need to know what tags you want. Well… what tag do you want to manipulate? You can find the name of the tag by opening your map with Eschaton, such as weapons\pistol\pistol. If your tag name can vary from map to map, such as the map’s scenario (scnr) tag, then you’ll need the tag identity. For the principal scenario tag, you can use read_dword(0x40440004), and this will return the tag ID which you can use.


In SAPP, the function for getting the address to the tag you want is lookup_tag.


local tag = lookup_tag("weap","weapons\\pistol\\pistol")


Notice that I put two backslashes for every backslash. This is due to the fact that in string constants, backslashes are assumed as escape characters for special characters. If you escape a backslash with a backslash, it’s stored as a single backslash.


If you’re trying to use lookup_tag for a specific tag ID, use lookup_tag(tag_id), instead.


If the tag isn’t found, then it’ll return 0. Reading/writing to an invalid address such as 0 will result in Halo crashing, so be careful.


You’re not quite in the tag data itself, but there are a few useful things that can be read here.


local class_1 = read_dword(tag + 0x0)

local class_2 = read_dword(tag + 0x4)

local class_3 = read_dword(tag + 0x8)

local tag_id = read_dword(tag + 0xC)

local tag_path = read_string(read_dword(tag + 0x10))

local tag_data = read_dword(tag + 0x14)


You’ll want to look at tag_data, which is offset 0x14, or 20 bytes. This is a pointer, or an address to another part of the process’s memory.


Now this is where things start to get a little complex. You’ll want to open a weap.ent in a text editor. For the purposes of this tutorial, I’ll use Sparky’s Plugins’s weap.ent tag. You can optionally set your text editor to view .ent files as .xml files if you want, but it’s not a requirement.


There’s a lot of stuff you can modify here, though some data is worthless due to the fact that a lot of it is client-sided only, and won’t affect anyone on the server unless they also had that particular modification. For the sake of demonstration, let’s make it so the pistol spawns with more ammunition. To do this, we’ll first need to find where our initial ammunition count is located. If you were to open up Eschaton, you would go to Reflexives -> Magazines -> int16s -> Rounds Total Initial. In a script, you would first have to first go find the magazines. If you scroll down a bit, or use Control-F, you will find it like this:


<struct name="Magazines" note="" info="1 = primary, 2 = secondary" info_img="" offset="0x4F0" visible="true" size="112">


There’s a lot of stuff here, but you only have to concern yourself with three things here: The name, the offset, and the size. A “struct” is just another name for a reflexive, though just knowing the offset isn’t enough. You also need to know how a “struct” is structured. It’s structured as three 32-bit integers (dwords), but the only ones useful are the first two. The first one is the count, the second one is the address.


Getting the count isn’t necessary if you’re sure that this count will always be equal to 1, but you should know how to iterate reflexives in case you need to read/write more than one reflexive.


reflexive_count = read_dword(tag_data + offset + 0)

reflexive_address = read_dword(tag_data + offset + 4)


for i=0,reflexive_count-1 do

   local struct = reflexive_address + i * size



So, for the magazines, we’d do this:


magazines_count = read_dword(tag_data + 0x4F0 + 0)

magazines_address = read_dword(tag_data + 0x4F0 + 4)


for mag=0,magazines_count-1 do

   local mag_address = magazines_address + mag * 112

   -- This is where you can edit the magazine information.



Again, if you’re absolutely sure that there will only be one magazine, you can save yourself a few lines of code and just get magazines_address instead of iterating through it with a for loop.


Inside of the magazine reflexive, you can find the Rounds Total Initial short here.


<short name="Rounds Total Initial" note="" info="" info_img="" offset="0x6" visible="true"/>


This is a short, so you can just use write_short to make changes here. Because it’s in a reflexive, you will need to offset from the reflexive address. Let’s say you want to make it spawn with 120 rounds. You can do that pretty easily:


write_short(mag_address + 0x6, 120)


Or if you weren’t iterating through each magazine:


write_short(magazines_address + 0x6, 120)


If you want to execute this when the game starts, use the EVENT_GAME_START event.


Our script will look something like this when we’re done:


api_version = ""

function OnScriptLoad()



function OnScriptUnload() end

function OnStart()

   local pistol = lookup_tag("weap","weapons\\pistol\\pistol")

   if pistol == 0 then

       cprint("No pistol ;(")


   local pistol_data = read_dword(pistol + 0x14)

   local pistol_magazine = read_dword(pistol_data + 0x4F4)

   write_short(pistol_magazine + 0x6,120)



This should give you an idea for the basic tag manipulation with basic primitives such as chars, shorts, longs, and floats, as well as reflexives. It’s pretty simple once can get started, so the rest of the tutorial will just be how to edit the other data types.


enum8, enum16, and enum32 require the use of write_byte, write_word, and write_dword, respectively. They’re just integers.


<enum16 name="Example" offset="0x14" visible="true">

   <option name="A" value="00"/>

   <option name="B" value="01"/>

   <option name="C" value="02"/>

   <option name="D" value="03"/>



If we want to change Example to “A”, then use write_word(address,0), for “B”, write_word(address,1), and so on.


A dependency is another special structure of data consists of four 32-bit integers.


local tag_dependency_class_1 = read_dword(offset + 0x0)

local tag_dependency_path_unused = read_string(read_dword(offset + 0x4))

local tag_dependency_unused = read_dword(offset + 0x8)

local tag_dependency_id = read_dword(offset + 0xC)


Anything “unused” does not have to be modified when swapping a tag class. If we want something to shoot a tank shell, for instance:


local tank_shell = lookup_tag("proj","vehicles\\scorpion\\tank shell")


write_dword(address + 0xC,read_dword(tank_shell + 0xC))


For convenience sake, we can write a function that does it for us:


function write_dependency(Address,TagClass,TagPath)

   local tag = lookup_tag(TagClass,TagPath)


   write_dword(Address + 0xC,read_dword(tag + 0xC))


write_dependency(address,"proj","vehicles\\scorpion\\tank shell")


Lastly, you may have to deal with bitmasks at some point. Bitmasks are a collection of bits which can either be on (1) or off (0), and they’re stored in a single integer.


<bitmask32 name="Example" offset="0x14" visible="true">

   <option name="A" value="31"/>

   <option name="B" value="30"/>

   <option name="C" value="29"/>

   <option name="D" value="28"/>

   <option name="E" value="27"/>

   <option name="F" value="26"/>

   <option name="G" value="25"/>

   <option name="H" value="24"/>

   <option name="I" value="23"/>

   <option name="J" value="22"/>

   <option name="K" value="21"/>

   <option name="L" value="20"/>

   <option name="M" value="19"/>

   <option name="N" value="18"/>

   <option name="O" value="17"/>

   <option name="P" value="18"/>

   <option name="Q" value="15"/>

   <option name="R" value="14"/>

   <option name="S" value="13"/>

   <option name="T" value="12"/>



While it looks very similar to an enum, it is different. Unlike an enum, multiple bits in a bitmask can be activated at once. Example, for instance, can be a combination of any number of these bits, including none of them. To access these bits, you will need use of the read_bit/write_bit command. Note that the bits used by Eschaton are in the opposite order used by read_bit and write_bit. Bitmask32 starts at 31 and counts down to 0, Bitmask16 starts at 15, and Bitmask8 starts at 7.


SAPP’s read_bit and write_bit functions can only address one byte at a time, meaning they can only address bits 0 through 7. You’ll need to offset the number of bytes by 1 and subtract 8 bits if the bit number is higher than 8. I didn’t write SAPP’s Lua API, so don’t complain if it’s too difficult or annoying.


I’ll just throw at you some examples:


If you wanted to write 1 to bit 26, or F, you’d need to figure out what bit it is. Simply subtract it from the number of bits in the bitmask minus 1. It’s a bitmask32, so there are 32 bits. 32 - 1 = 31. Then, to get the bit, 31 - 26 = 5. Because it’s less than 8, you can use write_bit(offset, 5, 1).


If you want to write 0 to bit 21, or K, you first need to figure out what bit it is. Again, subtract it from 31 and you’ll get 10. This number is greater than 8, so you will need to subtract 8 bits and add 1 byte to your offset. So, you’ll use write_bit(offset + 1, 2, 0).


As a final example, if you want to write 1 to bit 12, or T, you can get the bit number by subtracting 12 from 31, thus you get 19. This number is greater than 8, so you will need to subtract 8 bits and offset 1 byte, getting 11. This number is still greater than 8, so you will need to do this again. You should end up with write_bit(offset + 2, 3, 1).


Bit (b32)   Bit (b16)   Bit (b8)   readbit (Phasor)       read_bit (SAPP)         
31          15          7          readbit(address, 0)    read_bit(address + 0, 0)
30          14          6          readbit(address, 1)    read_bit(address + 0, 1)
29          13          5          readbit(address, 2)    read_bit(address + 0, 2)
28          12          4          readbit(address, 3)    read_bit(address + 0, 3)
27          11          3          readbit(address, 4)    read_bit(address + 0, 4)
26          10          2          readbit(address, 5)    read_bit(address + 0, 5)
25          9           1          readbit(address, 6)    read_bit(address + 0, 6)
24          8           0          readbit(address, 7)    read_bit(address + 0, 7)
23          7                      readbit(address, 8)    read_bit(address + 1, 0)
22          6                      readbit(address, 9)    read_bit(address + 1, 1)
21          5                      readbit(address, 10)   read_bit(address + 1, 2)
20          4                      readbit(address, 11)   read_bit(address + 1, 3)
19          3                      readbit(address, 12)   read_bit(address + 1, 4)
18          2                      readbit(address, 13)   read_bit(address + 1, 5)
17          1                      readbit(address, 14)   read_bit(address + 1, 6)
16          0                      readbit(address, 15)   read_bit(address + 1, 7)
15                                 readbit(address, 16)   read_bit(address + 2, 0)
14                                 readbit(address, 17)   read_bit(address + 2, 1)
13                                 readbit(address, 18)   read_bit(address + 2, 2)
12                                 readbit(address, 19)   read_bit(address + 2, 3)
11                                 readbit(address, 20)   read_bit(address + 2, 4)
10                                 readbit(address, 21)   read_bit(address + 2, 5)
9                                  readbit(address, 22)   read_bit(address + 2, 6)
8                                  readbit(address, 23)   read_bit(address + 2, 7)
7                                  readbit(address, 24)   read_bit(address + 3, 0)
6                                  readbit(address, 25)   read_bit(address + 3, 1)
5                                  readbit(address, 26)   read_bit(address + 3, 2)
4                                  readbit(address, 27)   read_bit(address + 3, 3)
3                                  readbit(address, 28)   read_bit(address + 3, 4)
2                                  readbit(address, 29)   read_bit(address + 3, 5)
1                                  readbit(address, 30)   read_bit(address + 3, 6)
0                                  readbit(address, 31)   read_bit(address + 3, 7)


WaeV, aLTis, Takka and 1 other like this

Share this post

Link to post
Share on other sites


  • Recently Browsing   0 members

    No registered users viewing this page.