StarGraft Actions Tutorial

A Tutorial for StarCraft

StarGraft Actions "Assembly" Tutorial: Part III

Because we're still finding new variables and stuff in memory literally every day, this section is probably not up to date, but I will try to frequently update it. If it seems dead, then go and ask about new stuff over at Camelot System's Black Smith. More Variables to Mess with Short and sweet, here's a few more memory locations detailing the current selected unit, courtesy of Ius: Recall that the offset to the unit data of the currently selected unit is a dword located at 68D310h. (So, remember that you need to grab the value of the dword at that location into a register and then go to that value's offset. E.G., if you do MOV EBX,[68d310h] then [EBX] is where the unit data is.) Remember this number! You always need it to get to the unit data. Starting from the unit data offset (the one you get inside [68D310h]), add the following to find the corresponding variable: +0x0000 - [dword] The offset to the start of the unit data for the previous unit (here, by 'previous' we mean the previous unit that happens to be in memory; they aren't in any particular order) +0x0004 - [dword] The offset to the start of the unit data for the next unit (again, no particular order, just the unit that happens to be in the next memory slot allocated) +0x0009 - [word] Current number of Hitpoints. +0x000C - [dword] The offset to this unit's sprite data. (See below for more info) +0x008F - [byte] Current number of Kills. +0x0061 - [word] Current number of Shield Points. +0x0064 - [byte] The unit type this unit is. (units.dat number) +0x00A3 - [byte] Current amount of Energy. +0x010D - [byte] Stim pack time remaining (how much longer this unit has stim). +0x0117 - [byte] Ensnare time remaining (if this unit is ensnared). +0x0118 - [byte] Lockdown time remaining. +0x0119 - [byte] Parasite status. (Um... I'll get back to you on the format of these bytes :) +0x011A - [byte] Stasis time remaining. (invincibility) +0x0120 - [byte] Plague time remaining. So, for example, if you wanted to get the current unit's number of hitpoints, you'd first move the value at [68D310h] into a register like EBX and then goto the word at [EBX+09h]. You'll notice that the value at +0x000C is yet another offset that points to some more sprite data about the current unit. Starting from that offset, you can add the following to find more variables: +0x0008 - [word] Sprites.dat entry to use. +0x000A - [byte] Unit team color (0 to 10 or 11 are actual team colors, the rest start indexing from the main palette). +0x000E - [byte] ? Selection display: if this value is 7h then display the health/mana bars when this unit is deselected; else if this value is 8h then display the selection circle when this unit is deselected. +0x0014 - [byte?] Horizontal position of sprite (graphical pixel offset). +0x0015? - [byte?] Vertical position of sprite. So, for example, if you wanted to get to the team color variable, you'd have to first move the dword at [68D310h] into a register, then move the dword at [register+0x000C] into a register (to get the offset of the sprites data), and then finally the byte value of [register2+0x000A] is what you're looking for. Not complicated, just a couple steps. The switches (like the ones you set with triggers) are four bytes at 0x0050D9A8. The first byte contains the first 8t switches, and the next (at 0x0050D9A9) contains the next 8t, one after that contains the next 8t, etc. (switches 1 to 32). I'm not sure where the ones that were added in Broodwar (33 to 256) are. How they work is kind of complicated. (If you don't understand this, then don't worry about it -- it is only required for messing with switches) Recall that each byte can also be written as 8 bits (the binary number). For example 10h (or 16t) can be written as 00010000b and FFh (255t) can be written as 11111111b. (You can use a conversion calculator to convert number bases for you). Well, each switch is represented by each one of these bits (from right to left, so the bit closest to the right of the first byte represents switch #1, the one next to that switch #2, etc.). If the bit is 1, the switch is set, otherwise, the switch is clear. Complicated? It is. :) To set anything else other than switch #1, #9, #17, or #25 takes some work, because you have to add/subtract the correct hexadecimal number to clear the bits. You can get some help from logical operators, but I'm not going to get into those in this tutorial (I may add it later). To toggle switch number 1, 9, 17, or 25, just add or subtract 1 from the corresponding byte. This works because the lowest bit on each of the four bytes is always one of those switches, and if you add or subtract one, then that bit will change from 1 to 0 or 0 to 1. (Note that this may also shift some other bits in that byte, since you have to carry if you added 1b to 1b) If none of this made any sense to you, then don't worry about it. Dealing with switches is much more complicated than dealing with other things. A Couple More Instructions The set of instructions I am presenting to you is no where near the full instruction set Intel made (I don't even know close to that :), and will even be neglecting several important ones, but I don't want to overload anyone or confuse people with instructions that are quite complicated (e.g., the logical instructions will be ignored, though if you are familiar with boolean algebra they are quite simple; though if there is enough interest in these -- they are quite essential in setting switches -- I will add them later). Here goes a few more then: SUB location,value Almost like ADD, SUB takes the value of the location, subtracts the value from it, and then leaves the result in location (first argument). All argument syntax that is valid for ADD is also valid for SUB. CMP value1,value2 CMP stands for "compare," meaning it compares 2 values you give it. When you compare two values, it will determine if value1 is greater than value 2, or if value1 is equal to value2, or if value1 is less than value2. The result of the last CMP instruction used is always saved on a special place on the CPU (either greater-than, equal, or less-than). Valid values for value1 and value2 are any constant, register, or memory location (in either argument), but, like MOV and ADD, you can not CMP two memory locations. What is the use of this you ask? The next couple instructions will show you: JE destination JNE destination JA destination JB destination These are called conditional "jump" statements. They cause your CPU to "jump" to destination in the code (a memory code-based offset) and continue executing from there. But these instructions are conditional, meaning they will only jump if a certain condition is fulfilled. In particular, they depend on the last CMP comparison that was carried out. JE will jump if CMP showed that the two values you compared were equal ("jump if equal"). JNE will jump if the two values were not equal ("jump if not equal"). JA will jump if the first value was greater than or "above" the second value ("jump if above"). JB will jump if the first value was less than or "below" the second value ("jump if below"). If the condition isn't met, then the jump is just ignored. There is also an unconditional jump instruction: JMP destination Which will always jump to destination in the code. Now, while you can put an offset in the destination argument to jump to a particular place in your code (or in the original Starcraft code), it would be a lot easier if you could use regular words to determine where you want to jump to. With a good assembler (like NASM :), you can do exactly that. Here's an example: MOV EAX,100h ADD EAX,EBX JMP letsGoHere . . . letsGoHere: SUB EAX,5h MOV [0x0001],EAX . . . Here, we had the JMP instruction jump to the 'letsGoHere' which would make the CPU skip all the code that was in the ... area and immediately start where the letsGoHere: line is, and thus begin executing at SUB EAX,5h. Simple, eh? As long as you don't use any of the regular assembly "words" as your jump destination name, you'll be fine. You also don't necessarily have to jump forward: backHereYouDope: MOV [0x1000], word 10h ADD [0x6800], byte 8h . . . CMP EAX,EBX JE backHereYouDope . . . Here, if EAX and EBX held equal values at the time of the CMP, then JE would have caused us to jump back to the start. CMPing and then jumping is how you use "if, then" statements in assembly. "If two values are then jump, else keep executing here." If you're clever enough, then you can create intricate conditional statements this way. ("If the unit has greater than X hit points, do this, else if the unit has Y energy, do this, else do this..." or something of the like) CALL subroutine Now don't go panicking because of new terminology. :) The CALL instruction is actually just like the jump instructions, except we will automatically get a jump back to where we came from. Specifically, CALL makes us jump to subroutine (which is just a destination name exactly like we used for jumps). When we meet up with a RET instruction in the subroutine, we will immediately jump back to the instruction just after the CALL. (Remember RET from before? :) For example: mySubroutine: MOV EAX,EBX MOV CX,DX MOV DI,SI PUSH DI POP DI RET . . . CALL mySubroutine MOV ECX,EDX . . . In this example, when we get to CALL mySubroutine, we will jump to the code starting at mySubroutine: and thus start executing again at MOV EAX,EBX. When we get to the RET instruction, we immediately jump back to just after the call, or to the MOV ECX,EDX instruction. Got it? You should now see how the Starcraft main program is working with our "button code." When we press the button, the main code CALLs our button code, so when we use the final RET instruction, we return to executing in the main code. Don't think you can only use Calls and jumps to your own word destinations though. You can also use offsets in the actual game code (e.g., you may want to CALL a preexisting subroutine -- like to combine your couple actions with a preexisting order). Though you will generally only CALL memory offsets we give you (unless you know how to find them by yourself :). Here is a complete example of using an in game subroutine (the offset of which is 0x004B5CB0): PUSH EAX MOV ECX,00000000h MOV EDX,00000000h PUSH ECX MOV EAX, 004B5CB0h CALL EAX POP EAX RET Before you start getting confused, let me explain. First let me tell you what the subroutine at 0x004B5CB0 is supposed to do. (KA found it :) It takes a number as an "argument" and then makes the unit perform that order number (order number as per orders.dat). [Actually, its a bit more complex than that, but I'll clarify later so we don't muddle things here.] But what do we mean by "giving it an argument"? We can't tell a subroutine a specific number, since all we're doing is jumping to some other point in the code. But look at what we did just before the CALL: we moved 0h into ECX, EDX, and onto the stack (PUSHing ECX onto the stack just moves its value there). In particular, this subroutine, when called, expects the order # we want to perform to be in ECX, EDX, and on the stack. So if we wanted to perform order 1, we would put 1 in ECX, EDX, and on the stack the same way. How do we know this? Well, kA told us. :) Calling a subroutine from in the game will usually require some "setting up the registers and the stack" like this because they expect certain values to be at certain locations, so you'll have to read carefully when Ius, kA, myself, or others tell you how to use one of these. So, in summary, the example above does this: First, we PUSH EAX onto the stack because we're going to use that register for ourselves. Second, we move 00000000h into ECX, EDX, and onto the stack (PUSH ECX) because the subroutine expects those to be there. Third, we move 004B5CB0h (the location of the subroutine) into EAX. Next, we call the subroutine by feeding the value of EAX to it (CALL EAX). [Why we don't call the offset directly is very complicated -- just trust us on this one. :) When CALLing code offsets in the regular game code, always put the value in a dword register first, then indirectly CALL the value in the register.] Once the subroutine returns, we POP EAX (since we saved the original value of it on the stack, remember?) and then RET to the main code. And we're finished. Now, let me clarify what this subroutine does a little bit. Specifically, when you call the subroutine, it will do "all the necessary things" so that when you return to the game code (from the CALL here and from your own button subroutine), your unit will get a targeting cursor. When you choose a target, the unit will do that order #. (whatever number you put in ECX, EDX, and the stack). While you can do other stuff in your button before/after the subroutine call, note that all this stuff will happen before you get the targeting cursor. One Last Tid-Bit For now, this is going to be it. There's a lot more, but its too unorganized to show to the public as of yet. :) The final note that I'll leave you with is this: when you call a stargraft action, recall that you can give SG an Act Var (action variable). When Starcraft calls your button code, this Act Var is actually on the CX register when your code starts. So you can use that variable to create some versatile actions. For example, this example will add X to a units' Hitpoints, where X is the Act Var that was input in that particular button: PUSH EBX MOV EBX,[68d310h] ADD [EBX+09h],CX POP EBX RET Since CX holds the value that was in the Act Var, we added that value to the current hitpoints. Simple. I lied. One more advanced Tidbit :) Oops, almost forgot one last thing "advanced" users might like to know. (Though this one is entirely optional) So far we've only been dealing with the "currently selected" unit, but what happens if we have more than one selected unit? We could have selected two SCVs, or 12 marines, and we'd still have all of their buttons to use. If we just used the methods we've had here, then the button we press would only effect the "top" unit in the group, all other units in our group would just ignore the action. How do we get all of our units in the group to carry out an action? First, we have to find out where they are. :) If you remember (as you should by now), the offset to the currently selected unit's data is a dword at 68D310h. If we have two units selected, then the offset to the second unit's data is a dword directly following that offset (so 68d310h+4h). And if we have a third, then after that, and fourth after that, etc. If there is no unit in that slot (e.g., in the fourth offset slot, if you only have 3 units selected) then it is just 00000000h. What good does this do us? Well, if you're up to speed with everything we've done so far (you'd have to be really good, actually :), then you'll see that we can use this fact to have every unit in our group carry out our action. You may see how to do this if we knew how many units were in our group (i.e., if we had two units, we'd just have to go to the unit data for the first unit, then go to the unit data of the second unit once we finished with the first), but to do this sucessfully, we have to model our action to work with unit group sizes of any size from 1 to 12. How? Well, you really shouldn't expect yourself to figure this out unless you're a programmer. What we do is create a kind of "loop" that will carry out our action on each successive unit in the group until there isn't anymore units in our group. Here's the pseudo code: Move the offset that holds the offset value of the first unit into EBX. (In other words, move 68D310h into EBX) startHere: Do our action on the unit data at [value at EBX]. (We aren't actually allowed to do this in one step, but what I mean by this is that first we get the value from EBX, that'd be the offset to the unit data, and then we go to that offset.) This means, whatever actions you do, like adding hitpoints, or whatever. Compare the next unit data offset (this is [EBX+04h]) with 0x00000000. If they are equal, that means there is no more units in our group, and we exit and return. Compare how many times we have done this already with 0x0C (12t). If they are equal, that means we have already done this on 12t units, and there can not be anymore units in our group, so we exit and return. Otherwise, we do have some units left in our group, so: First, we move the offset of where we'd find the next unit data offset into EBX (in other words, add 4 to EBX). Then, we jump back to startHere. This is what we call an iterative loop. If you concentrate on our pseudo code, you will easily see what is happening. We perform our action on one unit in the group, then we check if there are any more units in our group. If there are more units in our group, then we loop back to the start and perform our action on the next unit on our group. And so on. Because at the end of each action we perform, we check to see if there are additional units in our group, we account for there being anywhere from 1 to 12 units in our group. After each individual unit we deal with, we check to see if we exit. It may take a little while for you to see, but its relatively simple and elegant. Here is one complete way we could implement this in assembly (though not the only way). [Note: the semi-colons indicate that everything after it is a "comment"; in other words, you can use a semi-colon to make the assembler ignore all text after the semi-colon; kind of like notes to yourself.] BITS 32 ; (tell the assembler to compile in 32-bit mode) PUSH ebx ; PUSH the registers we will use. PUSH ax MOV ebx,0x68d310 ; This value is the location of the first unit offset. MOV ax,0x0 ; Here, we are setting up AX as an "incrementing variable" to ; keep track of how many units we've done our action on already. start: ; This is where we'll loop back to if we have more units to process ;------------------------ ; Main Code Start - (this is where we would place our action to effect one unit.) ; (Right now [EBX] would give us the offset to our current unit) ; Main Code End - (we would have done our action on whatever unit was at offset [EBX] ;------------------------ ADD ebx,0x4 ; Remember, in EBX is the offset where we found the last unit offset. ; By adding 4, this is now the location of the next unit offset, if it exists. ADD ax,0x1 ; We add 1 to ax to indicate that we've used the action on one additional unit. CMP dword [ebx],0x0 ; Compare the offset at EBX with 0x00000000. JE exit ; If they are equal, that means there are no more units, and we exit. CMP ax,0xc ; Compare AX with 12t. JE exit ; If they are equal that means we have used all 12t units already ; and we exit. JMP start ; Else, we have more units to process and we go back to the start. ; Remember that we've already added 4 to EBX, so now it points to the ; offset of the next unit. ;------------------------ exit: ; This is where go when we're done POP ax ; POP the values from the stack to clean up POP ebx RET ; Return, and we're done. This example is pretty long, and you shouldn't expect to understand it at once. But read the comments, trace through the code, and you'll see what's going on. Its just like our pseudo example. When you get it, you can use it as a template for your own actions to effect all units in a group. (Just be careful about what registers you are using, and to appropriately PUSH, POP them so that the correct values are always in them) If you're super intellegent and are some how completely up to speed, then you can also take a look at this action, which is the first real one I made. It causes the currently selected unit(s) to change team color when pressed. :) I'll let you play around with these for a while. I may eventually get to updating this. If not, then ask on the Camelot Forums or our own.
Sign up to access this!


None found


Share banner
Image URL
HTML embed code
BB embed code
Markdown embed code


Key Authors
DI (aka Magnus)


omgfire avatar
omgfire Joined 11y ago
769 points Ranked 6704th
9 medals 1 legendary 1 rare
  • 10 years a member Medal icon
  • 6 years a member Medal icon
  • Became a Club Leader Medal icon
  • One month a member Medal icon
  • 6 months a member Medal icon
  • 1 year a member Medal icon

Sign up to access this!
Sign up to access this!
Sign up to access this!


Sign up to access this!



Difficulty Level




  • Share on Reddit
  • Share on Twitter
  • Share on Facebook
  • Share on Google+


  • 10ySubmitted
  • 7yModified


  • Not yet rated
Sign up to access this!

More from Submitter

More Other/Misc Tutorials tracking pixel