Well it seems a lot of people have asked about increasing the unit limit in OP2, so here is a little info on the subject. Not an answer, just some basic info. If you know a little assembly, you can probably figure out how to patch it yourself. But I'll leave that for someone eager enough to learn to be pushed over the edge and try it out themselves. This is written with the assumption that you will follow along with a debugger, but probably take a bit of time to explore on your own at each part.
First of all, I'd like to discuss the hard unit limit. If you look at the map file format, posted somewhere on the forums, you'll see that every tile has 11 bits reserved for a unit index. If the game ever needs to know if there is a unit on any given tile, it can just check this field to see what unit. If this field is 0, then no unit occupies this tile. That means for the correct operation of the game, unit indexes must fit within 11 bits. There is just too much code relying on this fact to change it all, and even if you could, all the other bits are used for important stuff anyways. That means there are 2^11 = 2048 possible unit indexes, and 0 represents no unit. This means the game has a hard limit of 2047 units before you'd need major code rewrites to increase the limit.
I'm sure many of you feel the actual limit imposed by OP2 is actually much lower than this value. I will refer to this limit as the soft limit (whatever the limit actually is), and this is what we would like to change.
I'll be using OllyDbg to take a look at the code, so if you think something looks like it was copied and pasted out of a debugger, it was, and it was done with OllyDbg. It's a free download, just google it if you're looking for it, and it has everything you'd really need in a debugger. There are other ones out there with many cool and useful features, but they generally cost something. I know IDA has some really nice code analysis features which can be really handy in identifying standard C/C++ functions and making sense of the code, although I haven't found that of too much use for most OP2 work. Also, I've added many labels and comments to the code. OllyDbg will save these as a seperate file. If you don't have that file, or you don't insert the comments or labels yourself, your output won't be quite the same. It will have raw hex memory addresses in place of labels, and no comments at all. I've also collapsed the hex representation of the machine instructions since it's not very relevant here. We will simply work with the assembly nmonics.
Ok, so where do we start? Well, I added most of my comments and labels a long time ago, so I can't really remember where I started with those. I guess I'll just jump in to some relevant code and assume we figured out how to find it easily enough without really exaplaining how. I'll point out some of the relevant sections and hopefully it won't be too cryptic. One way these functions can be found within Outpost2.exe, is to take a look at the exported functions, and their parameters (which were largely figured out by playing around with them to see what happened) and then tracing inwards to see where these parameters went, and what Outpost2 did with them. If you need info on the parameters to exported functions, check the header files in the latest release of the SDK. Just search the forums for it.
A good place to start for unit limits, is with unit creation. By tracing the exported CreateUnit function we can determine a little more about how units are created internally. The exported function is only used when the level DLL tries to create a unit, not when a unit is created internally by the game engine, such as when a player builds a vehicle at the vehicle factory.
The (decorated) exported name of the CreateUnit function looks like this:
CreateUnit@TethysGame@@SIHAAVUnit@@W4map_id@@ULOCATION@@H1H@Z
By using the names window, we can jump to the code address that this export points to. The first line of the function looks like this:
00478780 >/$ 8>SUB ESP,0C
I won't explain this function since it doesn't contain what we are looking for, but I'll give just a few lines showing access to some memory I've labelled. That way you can take a look at the code on your own and see if you can figure out a little bit of what it does. (It helps to know what the memory being referenced is actually used for). Note that you will have raw memory addresses instead of labels unless you have added the labels or are using a comment file that already has a label entered for that address. As a useful exercise, you might want to try and identify the lines that access the parameters of the function. Also, take a look how OllyDbg tries to identify functions, loops, and function calling sequences with the solid black lines. These are not always correct, and sometimes a few are missed, but they are still a very useful visual cue as to the program's structure.
004787C6 |. A>MOV EAX,DWORD PTR DS:[<Map.tileWidth>]
...
004787EB |. A>MOV EAX,DWORD PTR DS:[<Map.tileHeight>]
...
00478819 |. 8>MOV EBX,DWORD PTR DS:[EDI*4+<UnitTypeInfo*[]>]
...
0047886D |> 8>MOV EAX,DWORD PTR SS:[ESP+28] ; Load param4 (playerNum)
00478871 |. 8>MOV ECX,DWORD PTR SS:[ESP+24] ; Load param3 (LOCATION.y)
00478875 |. 8>MOV EDX,DWORD PTR SS:[ESP+20] ; Load param3 (LOCATION.x)
00478879 |. 5>PUSH EAX ; /Arg4 - playerNum
0047887A |. 5>PUSH ECX ; |Arg3 - tileY
0047887B |. 5>PUSH EDX ; |Arg2 - tileX
0047887C |. 5>PUSH EDI ; |Arg1 - unitType (map_id enum)
0047887D |. B>MOV ECX,OFFSET <Outpost2.Map> ; |Load "this" pointer (MapData)
00478882 |. E>CALL Outpost2.00436AE0 ; \Outpost2.00436AE0
Eventually near the bottom of this function, we find that it calls another function. It turns out that this function is a sort of internal CreateUnit function that is called whenever a unit is created, even when it's done by a player ordering a vehicle factory to create a unit. (Although, the function isn't called when the player issues the order, but only when construction of the vehicle is complete). This brings us a little closer to info on the hard limit, since if a unit is created, it must be done so with this function. On the other hand, it doesn't lead directly to the soft limit, since if you've reached the unit limit, you can't even order a factory to produce a unit, which happens much earlier than this function would be called when a unit is compled. The function call line is the following:
00478906 |. E>CALL <Outpost2.???.CreateMapUnit(map_id unitType, >; \???.CreateMapUnit
Note that before the function is called, the parameters to that function must first be pushed onto the stack. Take a quick look at the preceeding lines and see if you can figure out what parameters are being passed to it. It is often useful to trace where the parameters being sent in come from, such as from the parameters to the current function. That is, by looking in the SDK header files and seeing what the parameters to the exported CreateUnit function are, we can look to see if these are passed in further to the internal create unit function. Take a look at the first parameter:
00478905 |. 5>PUSH EDI ; |Arg1
By looking up through to code to find where the value of EDI is set, we eventually come to the line (right near the top):
00478791 |. 8>MOV EDI,EDX
Furthermore, EDX is not set before this. This means that this value is being set by the calling function (or in the case of a bug, it's an unitialized value
). Now typically parameters are passed on the stack, and not in registers. But it so happened that the exported CreateUnit function using a "fastcall" calling convention. This means that the first two parameters to the function are passed in registers instead of on the stack. [roughly anyways, things are a little more complicated when passing objects since you may need a memory reference to them, thus forcing them onto the stack, plus it only considers the first two parameters that fit within 4 bytes as valid candidates. This is one of those detail things that you look up if and when you need it.] It also happens that the registers used by the fastcall calling convention are EDX and ECX. Thus we can see that EDX is one of the parameters.
Ok, so which parameter is it? Well, take a look at how the CreateUnit function is declared in the SDK.
static int __fastcall CreateUnit(class Unit &returnedUnit, enum map_id unitType,
struct LOCATION location, int playerNum, enum map_id weaponCargoType, int rotation);
We should find the first parameter in ECX, the second in EDX, and the remaining parameters pushed onto the stack in a right to left order. This means that EDX is "enum map_id unitType". Thus the first parameter to the internal create unit function is the unit type that is being created.
Now let's take a look at the internal create unit function. Following the address used in the call, we find the first line of this function looks something like this:
004467C0 >/$ 8>MOV EAX,DWORD PTR SS:[ESP+1C] ; ???.CreateMapUnit(map_id unitType, int pixelX, int pixelY, int creatorIndex, map_id cargoOrWeapon, int unitIndex, bool centerInTile)
Note that I added a comment giving the function some kind of name so I can remember what it does and a list of parameters used and their meaning and types.
Now this function is a little more complicated, but if you've added labels from the previous function, you should probably see that some of those memory locations are reused here.
Jumping to a rather important, but somewhat inconspicuous line, we have:
0044682D |. F>CALL DWORD PTR DS:[EBX] ; UnitTypeInfo.CreateUnit(int pixelX, int pixelY, int unitIndex)
What's going on here, is a call is being made to a function, whose address is stored in EBX. [Why it's doing this is to do with how the MSVC compiler handles object inheritance, and how it selects which function to call based on the object it's working with. The exact call address is not known until runtime, since the exact type of the object isn't know. To make the call correctly, the functions are declared as virtual and the call is made through a virtual function table. This allows objects of a different type to have pointers to different functions, allowing them to have different behavior when the corresponding function is called on each object type. This is sort of like how a laser weapon has a different affect than a sticky foam weapon when it is "fired", but they can both be "fired" in the same way. Using the virtual function table is a quick and easy way to get the compiler to handle which function gets called, and has fairly minimal impact on memory usage and runtime speed. That's not to say you don't need to know what you're doing to program something using this feature. ] The virtual function table is really just a table of function pointers. You should get used to seeing these as Outpost2 uses a lot of these.
These virtual function calls do pose a bit of a problem when analysing code however. Since the call address is not know until runtime, we don't know what code that call is being made to unless we actually run the program and stop it at that line. Also, even if we do know where it calls, that location can change depending on the object type that function is being called on. But not to worry, since once you know what one call destination does, they all essentially have to do the same thing, or rather similar.
It turns out that this line creates a unit of the given type. For each unit type, the virtual function pointer points to a different function, thus by making a "create" call, we can create a unit of any given type, just by calling the "create" function using a correct object. For each unit type, there is an object that creates units of that type. Hence the comment "UnitTypeInfo.CreateUnit". This is sort of like saying "Scout[UnitInfo].CreateUnit", or "Lynx[UnitInfo].CreateUnit", or "CommandCenter[UnitInfo].CreateUnit", although it would have been more like "genericUnitTypeInfo.CreateUnit" since we are using virtual functions. The "enum map_id unitType" parameter is used as an index into an array that contains pointers to all the "UnitInfo" for each unit type. Thus we can call the "create" function through the virtual function defined for that unit type to create the type of unit we want.
Take a look at the line:
00446815 |. 8>MOV EDI,DWORD PTR DS:[EAX*4+<UnitTypeInfo*[]>] ; Load UnitTypeInfo*[unitType]
This is where it loads the pointer to the object that creates units of the desired type. Here we see that EAX is used as an array index [pointers are 4 bytes long]. By looking up a bit:
0044680B |> 8>MOV EAX,DWORD PTR SS:[ESP+24] ; Load param1 (map_id unitType)
We can see that EAX is loaded with the first parameter, the unit type. This was used in the array lookup.
Ok, so where does this get us? Well by examing all these CreateUnit functions defined for each unit type, we soon start to see some similarities. And yes, there are better ways of finding all these functions than by setting runtime breakpoints. Looking back at the array lookup, you might notice that this array is all filled in nicely before we even start up the program, and it remains fixed. Thus we don't need to wait until runtime to know what all possible objects can have create unit called on them. We have a list of all their addresses. Following each address, we come to the memory layout of the class that stores all the info for units of that type (such as max hitpoints which is the same for all units of a given type, whereas the current hitpoints would be stored with each unit, since it will vary from unit to unit of the same type, and thus need more copies stored).
Slight note here: The memory for all these objects will be 0 until the program starts up and calls the constructors on each object. The constructors will fill in the virtual function table pointers allowing us to do the next part, even if not all the unit data gets read in yet. That might not happen until a level is loaded, but all we need for this is the virtual function tables. You don't need to pause the program or use any kind of breakpoints when inspecting this memory though. Just start up the program so the virtual function table pointers get filled in and you can inspect the data while the program is still running. (Idling at the main menu, or pause it manually yourself at any random code location - WITHIN Outpost2.exe! You don't want to be looking at code from Kernel32.dll really. It won't be a good use of your time. Make sure to check the title bar of the code window after pausing the app to make sure you've paused in the rigth module.)
Now, the first 4 bytes of any class with virtual functions should be the pointer to the virtual function table. If you follow the address stored in the first 4 bytes of the class you'll end up with the virtual function table pointer, which will be a list of pointers into the code section. The first of these functions is the create unit function. Follow this pointer to view the code for the create unit function for units of that type.
Note: Make sure you follow data references in the data window, and code references in the code window. Looking at code in the data window is not very useful. Keep in mind that the virtual function table is stored in the data section. The pointers however point into the code section.
After looking at a few of these functions, it should become obvious that they all call the same function. The call looks something like this:
00444F82 . E>CALL <Outpost2.GetNewUnitAddress(???, int pixelX, int pixelY, int unitIndex)>
If this function returns a non zero value, than that address is used as the "this" pointer to a unit object in a constructor call. In other words, if this function can return memory for the unit object, then the unit object is constructed at this address and returned.
Ok, so we follow this function in. The first line looks like:
00439A10 >/$ 5>PUSH EBX ; Function: GetNewUnitAddress(???, int pixelX, int pixelY, int unitIndex)
Look down a few lines until you see:
00439A39 |> A>MOV EAX,DWORD PTR DS:[<GameMap.numUnits>] ; Load GameMap.numUnits
00439A3E |. 3>CMP EAX,3FF ; Check if numUnits != 1023 (0x3FF)
00439A43 |. 7>JNZ SHORT Outpost2.00439A4E ; -> Continue
If that jump isn't taken, the function returns 0. That is, no memory could be allocated to create the unit. But look at the compare. If some number is not 1023, then the function continues on. One of the first things it does after it continues is increment that value. See below:
00439A4E |> F>INC DWORD PTR DS:[<GameMap.numUnits>] ; GameMap.numUnits++
Now I already have the label added, so it should be pretty obvious what's going on here. But after some quick experimentation, and looking at code that uses this memory reference, it seems fairly obvious its the current number of units. As long as it's not 1023, a new unit can be created, and the count is updated. But going back to the beginning, the hard limit seemed to be 2047, since 11 bits were used for the unit index. I can't imagine why this value is 1023, other than a possible off by 1 error that is so common in programming. It seems they used 2^10 - 1, instead of 2^11 - 1. If it is an error, it'd likely be hard to find since it won't cause a crash or anything. It just artificially lowers the unit limit to some safe value that is lower than it needs to be. And off by one error in the other direction would be much more noticable though, since crashes could occur if the safe unit limit was exceeded. I could be wrong though, maybe there is a good reason for this limit. But hey, why not try changing it and find out.
So what more is there to change? Well, if you've hit the unit limit, you can't order a vehicle factory to start producing, so we never get to the code we've just analysed. But, we know now where it stores the current number of units, and if it needs to check if a unit limit is exceeded, this might be a number it uses to do that. Don't get your hopes up though, since it'd be more fair to have a unit limit per player, thus preventing one player from building lots of units while someone else has very few. So what happens if we search for references to this memory address? Well, not too many are found, which means it's easy to check what they all do. Unfortunatly, they don't seem to perform max unit checks. The other references to this value that pop up decrement the value, which probably means you've found the code for when units are destroyed, or are somehow removed from game play (such as driving off the map). If it was a max unit check, you would expect some soft of compare instruction (probably CMP), followed by a conditional jump.
But don't give up hope yet, the reference finding abilities of OllyDbg are limited to static references (understandably!). There might still be a dynamic reference to that variable. So how do you find those? Well, set a (hardware) memory breakpoint on that data, and start the game up and see if it fires. Click on a vehicle factory, if it displays the build button when you select something, then it first had to check if it was buildable right now. Which means the code to perform the unit limit check had to run. If it referenced the memory you set a breakpoint at, then it'll halt at that code and let you examine it. If it doesn't halt, then it probably uses a different variable elsewhere, such as a per player unit count.
One last note: If you set a memory breakpoint on that piece of memory, expect it to fire a lot when you start a level and all the unit and structures are created. It would be wise to start the program, load a level, and THEN set the breakpoint. Otherwise you might find yourself stopping at the same place a few hundred times in code we've just looked at. We want something new. I'd also recommend against using levels with an AI, or disasters if possible, since they might decide to create units on you, which would lead to breakpoint firing when you don't want them to. Also, since a compare require only an access and not a write, you need to set the hardware memory breakpoint on access, and not on write.
I'll stop here for now. This post is already huge and I'd like to see someone try something and reply before I post further.