I had considered modding sticky foam into some sort of anti-bacterial spray, and probably increasing the blight growth rate. But then, I didn't really want to put that much effort into a fairly silly idea at the time.
Now, as for replacing all other disasters with blight.... ;)
Actually, that might not be too bad if the function to create the blight is already imported by the DLL. Or you can always hardcode the address from Outpost2.exe if it isn't. Mostly just a bunch of stack maintenance to get the call done right, and use the coordinates of whatever disaster you're replacing?
Btw, for the Data frame, I always set the display mode to "Long -> Address". It's by far the most useful.
Ok, so take a peek at cps2.dll. It's known to have blight appear if you play for a rather long time. Check the Names list, as in the first post, and you'll notice it exports a function called "Microbe". Follow this in the Disassembler view to see how it's done. You can also check the SDK header files for the GameMap class, and the TethysGame class. They use the functions GameMap.SetVirusUL, and TethysGame.SetMicrobeSpreadSpeed. They also use TethysGame.AddMessage, but that's just to be nice and warn the user. Technically, that's not a requirement. ;)
Ok, so find the line that loads the address of GameMap.SetVirusUL. Select it and right-click, Follow in dump -> Memory Address. Make sure the data view is set to Long -> Address. You can now see the address of that function in the Value column. There may be a catch though. If LoadAll.exe has taken the address normally used by Outpost2.exe (which it will), then Outpost2.exe will get relocated, and that address will be linked to the new location. If you modded the DLL to hardcode a call to that address, then it would crash when the game is run normally. But, no problem. It's fairly easy to convert this value to the correct value.
Open the Memory window, and find the load addresses of LOADALL and Outpost2. We just need to adjust for the difference in the addresses. Remember that all addresses are normally given in hexadecimal. For instance, I have:
LOADALL: 00400000
Outpost2: 00870000
Assuming that Outpost2 should load to address 00400000, which it should, we just need to subtract 00870000 - 00400000 = 00470000 from the address we are given to get the address we want. Back to the data view, and we see the function is at address 008E6EA0. So, if Outpost2 didn't get relocated, this function would be at address 008E6EA0 - 00470000 = 00476EA0. If you have trouble doing hexadecimal math in your head, then be aware that Windows Calc can do hexadecimal math when set in scientific mode, and selecting hex.
Ok, so now that we have the function address, we have to figure out how to call it. First, let's understand the calling conventions used. Take a look at the SetVirusUL function found in the SDK.
static void __fastcall SetVirusUL(LOCATION location, int spreadSpeed);
Normally, function parameters are passed on the stack, pushed from right to left. That is, the last parameter is pushed first in the assembly code. As a side note, this order makes implementing variable parameter lists with "..." at the end easier, as all non-variable parameters will still be at known offsets from the stack pointer. This would not be the case if they were pushed in the opposite order. Note that this also means that expressions are evaluated from right to left, and hence side effects are also seen in a right to left order. This is most evident with nested function calls, such as f(g(), h());, where g and h have noticable side effects, such as printing out a value. Here you would see the output from h() before the output from g(). You can also experiment with simpler expressions with side effects, such as i++, although, you may be getting into the realm of the undefined. (What is the result of "i = i++ + ++i;"? Seems the ordering of the stores to memory is undefined, so the compiler can really do whatever it wants. Different compilers can and do evaluate that differently, but usually in a consistent manner within a compiler family).
If the function is a class member function (as SetVirusUL is), then the "this" pointer used in C++ to access class member variables is normally passed in the ECX register. However, SetVirusUL has the static modifier. The static basically means don't pass a "this" pointer in ECX (hence the member function does not, and can not access any class member variables). This basically means treat the function much like a normal non-class member function, but still "hide" it by giving it class scope rather than global scope. This is also why you can use static class members as callback functions in place of a normal non member function, but you can't substitute a non-static member function. The non-static member functions requires that extra hidden "this" pointer in ECX, which makes them incompatible. Consider the following C++ function implemented in a more C (non-object) way:
void ClassName::FunctionName(int arg1, int arg2); Â // C++
void ClassName_FunctionName(struct ClassName* this, int arg1, int arg2); Â /* C */
Basically, think of class member functions of having a hidden arg0, that contains a pointer to a struct containing all the class member variables.
Now take another look back at SetVirusUL, and notice the __fastcall modifier. The __fastcall modifier means pass the first two arguments in the ECX and EDX registers, when possible. The "when possible" part usually means those arguments can't be a compound data type, such as a struct or a class. In our case, the first parameter is a LOCATION, which is a compound data type, so it can't be passed in registers (as it would have no memory to point the this pointer at when calling member functions). (Btw, "struct" and "class" keywords in C++ are nearly interchangable. Structs can have member functions just like classes. The only real difference is that structs default to public visibility, while class defaults to private visibility. There are also slight differences in name decoration, and the compiler will probably complain if you're not consistent with calling something either struct or class). So, the PUSHes closest to the CALL will be pushing the LOCATION struct onto the stack. After that, the spreadSpeed parameter will fit into ECX. There are no more parameters, so EDX doesn't get used.
Now, examining the CALL site, we see the code:
11007E7E Â |. Â >MOV EDI,DWORD PTR DS:[<&Outpost2.?SetVirusUL@GameMap@@SIXULOCATION@@H@Z>]
11007E84 Â |. Â >PUSH ECX
11007E85 Â |. Â >PUSH EAX
11007E86 Â |. Â >MOV ECX,1
11007E8B Â |. Â >CALL EDI Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â ; Â <&Outpost2.?SetVirusUL@GameMap@@SIXULOCATION@@H@Z>
That is the main part of our call sequence. Note that the LOCATION struct has two fields, the x and y coordinates, and so has the two PUSHes to get it on the stack. If you look up, you'll also see the coordinates being written to two consecutive memory locations, and then copied from that memory location and pushed onto the stack. This is because we are passing "by value" when we call the function. It is usually more efficient to pass objects by pointer or reference, i.e., passing "by reference". In that case, we would only pass a pointer to the original data, rather than copy it all. No matter though, in this simple case we can just "construct" the LOCATION object on the stack where it's being passed and avoid the copy. I should note that LOCATION has no side effects during construction, and also no destructor, and hence no side effects during destruction, which is why we can get away with this optimization. We wouldn't want to optimize away one of the copies of these objects if they both had visible effects on program output, such as say, printing a debug string from the constructor or destructor.
Ok, so where do we want to call this function from? I'll go with the CES1.DLL example, and change the Lava3_Flow function. If I'm right, that should be controlling the volcano just north of your base that erupts early in the game. In other words, this function should be called by a time trigger at the time the volcano normally erupts, and we can read the coordinates out of this function and use them when creating the blight instead. ;)
Let's try entering the following code over the old Lava3_Flow function:
1100B760 Â Â 68 BA000000 Â Â Â PUSH 0BA
1100B765 Â Â 6A 32 Â Â Â Â Â Â PUSH 32
1100B767 Â Â B9 00010000 Â Â Â MOV ECX,100
1100B76C Â Â E8 2FB746EF Â Â Â CALL 00476EA0
1100B771 Â Â C3 Â Â Â Â Â Â Â Â RETN
1100B772 Â Â 90 Â Â Â Â Â Â Â Â NOP
Remember to Copy-to-executable, and then Save-file. Start Outpost2 normally, and load up Colony, Eden Starship. While you're waiting for the volcano to erupt with purple lava, you can take a quick peek at the trigger creation and find out how long you'll need to wait.
If we go to the Names list, and find "CreateTimeTrigger" and press Enter, we'll see a list of all references to that function. We can now go through them all, and look up a bit to see the function name that's used as the callback function. (Use the Window menu to get back to the references list). Didn't find what you were looking for? Check the Names list again. There are two CreateTimeTrigger functions. It's an overloaded function with two possible parameter lists. The name decoration that encodes information about the parameter lists is slightly different between the two. (Having a dumb linker that doesn't know about overloaded functions is probably the main reason for having name decoration/mangling. That way, you can upgrade the language and compiler to allow function overloading without having to upgrade the linker too. The mangling keeps the names of the two functions distinct, so the linker doesn't get confused).
I suppose I can save you the time, and tell you that the CreateTimeTrigger call that setups up the trigger with the Lava3_Flow callback is right in the function above the callback (Lava3_Vent). Note that volcanos are usually two step processes. First, you see the vent start going, and you get the initial warning if you have the tech researched, then it eventually actually erupts, and places the kind of lava that spreads. We're replacing the second part. Looking at the parameters, and perhaps consulting the SDK, you can deduce that PUSH 5DC is the time value, which is 1500 ticks. That means 15 marks after the vent fist appears to start spewing, the lava actually erupts. This is of course relative time, and not absolute time. We still need to know when the vent first appears to figure out the absolute time.
Following the last CreateTimeTrigger reference on the list, we come to a function that sets up all Lava?_Vent timer callbacks. Looking below the "Lava3_Vent" parameter, we see PUSH 1388, so 5000 ticks from when this trigger is set, the vent will appear. I suppose we don't want to wait this long to test, so if you're not already running the level, feel free to shorten these values. Maybe try 0x100 (or 256). And to be quick about things, change both time triggers to this lower value. If OllyDbg complains at you about saving these changes to the file, just delete the old CES1.DLL file, and try again.
There you have it. After 5 marks, that volcano will now start spewing wonderful purple lava! :P
Edit: ... except the blight doesn't seem to be spreading. I see why though. It seems the last parameter to SetVirusUL isn't actually spreadSpeed, but some sort of boolean value. I'll have to investigate further and update the SDK. (SetEruption does have a similar parameter list that seems to use the last parameter for the spread speed). I suppose this means we also have to call TethysGame.SetMicrobeSpreadSpeed too then. Rats! I was hoping for a nice shortcut. Oh well, I'll leave it for someone else to see if they can figure it out by extension, unless anyone has some pressing need to see this extra bit of detail.
I took a quick glance at that function, and discovered the mistake. Here's the correction:
static void __fastcall SetVirusUL(LOCATION location, int bBlightPresent);
The last parameter isn't spread speed. It's whether blight is present or absent. You can use this function to remove blight from previously infected places.
Here's the modified code with the call to TethysGame::SetSpreadSpeed, with a speed parameter of 0x100. The blight now spreads like mad.
1100B760 > Â Â 68 BA000000 Â Â Â PUSH 0BA
1100B765 Â Â Â 6A 32 Â Â Â Â Â Â PUSH 32
1100B767 Â Â Â B9 00060000 Â Â Â MOV ECX,600
1100B76C Â Â Â E8 2FB746EF Â Â Â CALL 00476EA0
1100B771 Â Â Â B9 00010000 Â Â Â MOV ECX,100
1100B776 Â Â Â E8 55CB46EF Â Â Â CALL 004782D0
1100B77B Â Â Â C3 Â Â Â Â Â Â Â RETN
1100B77C Â Â Â 90 Â Â Â Â Â Â Â NOP
1100B77D Â Â Â 90 Â Â Â Â Â Â Â NOP
It's slightly more complicated. Allow me to digress. Excessively. ;)
Example C++ Code
Here's how that was done from the Hooville template level source code:
// Set map lighting conditions according to Uses Day/Night checkbox
// Force daylight everywhere if they don't want to use day/night
TethysGame::SetDaylightEverywhere(TethysGame::UsesDayNight() == 0);
// Now for some fun...: if day/night is enabled, force night everywhere all the time =)
TethysGame::SetDaylightMoves(false); // Freeze position of day/night
GameMap::SetInitialLightLevel(-32); // Set daylight off the map, to feeze it in darkness
You must enable day/night or it will always be daytime everywhere:
"SetDaylightEverywhere(true)"
Note: In Hooville, it's either always daytime everywhere, or it's always nighttime everywhere, depending on the checkbox to enable day/night in the multiplayer pre-game setup window. Hence the check for "UsesDayNight() == 0" in the above code.
Now with day/night enabled, there is going to be a rotating strip of daylight. What you can do is disable it from moving, and try to move the daylight position off the map:
"SetDaylightMoves(false)"
"SetInitialLightLevel(-32)" <= This value is dependent on the map size
The light level, if a remember correctly, is a value that increments as daylight moves across the map, and eventually loops back to some earlier value. It didn't appear to be a light level so much as a light position.
Quick note about boolean values:
Boolean (true/false) values are typically mapped as:
true = 1
false = 0
Aside: Boolean Values vs. Integer-"Boolean" Values
(This shouldn't affect you, but you should be aware of it)
As boolean values are stored in registers with much greater range, the assembly code is typically structured so that any non-zero value is considered true. However, you can't always rely on this. Treating any non-zero value as true sometimes requires more complicated code sequences. If the variables were actually declared as an integer type, the compiler won't emit these more complicated code sequences. This restricts how you can use and test integer-"boolean" values. In particular, it restricts how you test if a variable is true, since true is multivalued in such cases. Consider what might happen if a programmer wrote a test "if (booleanVariable == true)". If the variable was actually declared as an int, and held the value 2, this test would result in false, even though it should logically be true. A better test might simply have been "if (booleanVariable)". Another alternative could have been "if (booleanVariable != false)". Testing against false will be safe, since it is single valued (it is always 0). This is important because...
If you look in the TethysGame.h header file you'll see:
static void __fastcall SetDaylightEverywhere(int bOn);
static void __fastcall SetDaylightMoves(int bOn);
Note that parameters are declared as "int", not as type "bool". Hence, you must be careful about how true checks are done on these parameters. You should consider this for tests done by both your own code, and code in the Outpost2 functions you might be calling. At least, assuming you do any checks in your own code, or pass any values other than 0 or 1. (Which you just might find convenient here, when it comes to selecting opcodes and have limited space to work in).
Calling conventions
There are three main calling conventions used by the MSVC compiler. The main difference between them is how many values can be passed in registers (the remaining parameters are passed on the stack). The number of registers used for parameter passing and the calling convention names are:
0 __cdecl // Default for C functions
1 __stdcall // Default for C++ class member functions
2 __fastcall // Occasionally declared by programmer, typically for small simple functions
The registers used for parameter passing are ECX (first eligible parameter), and EDX (second eligible parameter). As a general rule, any variable that needs to have a memory address is not eligible to be passed in a register. Typically objects (and struct instances, which are basically equivalent) can not be passed in registers as all their member functions (which aren't static), take a hidden "this" pointer. As pointers can only point to memory, and not registers, these functions would only be callable if the object has a memory address, and hence can not be stored in a register. Note however it is perfectly acceptable to pass an object pointer in a register. (In that case, the object it points to must clearly have a memory address, which presents no problems to calling the object's member functions). Any parameter which is not passed in a register is passed on the stack, in reverse order (last parameter is pushed first).
Example: TethysGame::AddMessage
static void __fastcall AddMessage(Unit owner, char *message, int toPlayerNum, int soundIndex); // Note: toPlayerNum: -1 = PlayerAll
Although this method belongs to TethysGame, it is static (hence no hidden "this" pointer to pass). This method is also __fastcall, so ECX and EDX will be used for parameter passing. The first parameter is a Unit object (not a pointer or reference to one), and so is not eligible to be passed in a register. Hence, "message" will be passed in ECX, and "toPlayerNum" will be passed in EDX. The remaining parameter will be pushed onto the stack in reverse order. The last parameter "soundIndex" will be pushed onto the stack first, followed by the first parameter "owner".
Example: Unit::SetDamage
void SetDamage(int damage);
This method is a class member function, which is not static, and hence has a hidden "this" pointer as it's first argument. Translating this C++ declaration to an equivalent C statement would look something like:
Unit_SetDamage(Unit* this, int damage);
As this is a member function, it's default calling convention is __stdcall, so the first eligible parameter will be passed in ECX. Here the first parameter is a pointer to an object (and not just an object), and so can be passed in a register. Hence, "this" is passed in ECX, and "damage" is passed on the stack.
Translating Calling Conventions to Assembly
Now, getting back to the functions of interest:
static void __fastcall SetDaylightEverywhere(int bOn);
static void __fastcall SetDaylightMoves(int bOn);
These belong to TethysGame, but are both static (no hidden "this" pointer), and both __fastcall, so up to two registers will be used to pass parameters. There is however only one parameter for each of these functions, so for both of them, the first parameter "bOn" is passed in ECX.
Now, on to the assembly code you've posted:
110012CD E8 4E5C0000 CALL cps2.11006F20
110012D2 B9 01000000 MOV ECX,1
110012D7 FF15 6CE40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightMoves@TethysGame@@SIXH@Z>]
110012DD 33C9 XOR ECX,ECX
110012DF FF15 70E40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightEverywhere@TethysGame@@SIXH@Z>]
110012E5 33C9 XOR ECX,ECX
110012E7 FF15 74E40011 CALL DWORD PTR DS:[<&Outpost2.?SetCheatFastUnits@TethysGame@@SIXH@Z>]
110012ED 33C9 XOR ECX,ECX
Here you can see ECX is being set before each call. This is the value of the first parameter "bOn". Before the call to SetDaylightMoves, we see "MOV ECX,1", which sets bOn = 1 (true). Before the call to SetDaylightEverywhere, we see "XOR ECX, ECX", which sets bOn = 0 (false). But why not use "MOV ECX,0" here?
Using XOR of a register with itself has long been considered a clever way of setting it's value to 0. If you look to the left of each instruction above, you'll see the instruction encoding. For "MOV ECX,1" the encoding is "B9 01000000", which takes 5 bytes (2 hex digits per byte). If you look to the left of the "XOR ECX,ECX" instruction, you'll see it's encoding is "33C9", which is only 2 bytes. Had "MOV ECX, 0" been used instead, this instruction would also have 5 bytes. (Details below if you're interested). Hence, using XOR produces a smaller instruction. On early CPUs XOR was also supposedly faster. On newer CPUs, the relative speed of the two is a bit murkier of a situation. (A smaller instruction may mean a tight loop now fits in a cache line, but using the register as an input source on a pipelined or superscalar CPU may potentially cause delays if ECX was previously written to and it's new value has not yet been updated, provided the XOR instruction isn't automatically converted to something simpler internally).
Aside: Bitwise-XOR
XOR
A | B | Result
--------------
0 | 0 | 0 *
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0 *
If A == B, then you have either the first or last row, and so the result is 0.
If a register is being XOR-ed with another value, then XOR is applied to each of the corresponding bits.
Setting all bits to 0, results in the register having the overall value 0.
Aside: Instruction Encodings
The instruction opcode "B9" represents an assignment of a 32-bit immediate value (encoded in the instruction stream) into the ECX register. Here, the immediate value is the little endian 32-bit quantity 1 = (01 00 00 00) = "01000000". The "B?" opcodes all represent "MOV register, immediate". The B0-B7 opcodes move an immediate 1 byte value into a register. The B8-BF opcodes move an immediate word or double word value into a register. The size of the immediate is determined by the operand-size attribute. In 32-bit operating mode the immediate constant will be 32-bits, unless the instruction is prefixed with "66" (operand size), in which case the immediate constant will be 16-bits. In 16-bit operating mode, things are reversed, so the immediate constant will be 16-bits, unless the instruction is prefixed with "66" (operand size), in which case it will be 32-bits. The determine which register is being written to, add the register index to the opcode ("B0" for byte moves, and "B8" for word or double word moves). Hence, B9 = B8 + 1, where 1 represents ECX.
The register encoding order is:
32-bit: EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
16-bit: AX, CX, DX, BX, SP, BP, SI, DI (same as 32-bit, but only the lower 16 bits are affected)
8-bit: AL, CL, DL, BL, AH, CH, DH, BH (same as first 4 16-bit registers, broken into low bytes, followed by high bytes).
The instruction "33" in Intel notation is "XOR Gv, Ev". This basically means "XOR register, memory_or_register", where the "G" stands for a "general purpose register", "E" stands for "general purpose register or memory address", and "v" means an operand size of either 16 or 32 bits depending on the operand-size attribute. A "b" (as opposed to a "v") would have been a byte size quantity. The "C9" is a "MOD R/M" byte that follows the opcode, and is used to specify what Registers or Memory address is used. (What "G" and "E" refer to). It is broken into a 2-bit field representing the address mode, and two 3-bit fields representing the registers involved: (address mode, "G" field, "E" field).
Address Mode (slightly simplified, as there is some irregularity):
00 [Register] ("E" field is a memory location, whose address is in a register)
01 [Register] + displacement8
10 [Register] + displacement16/32 (controlled by address-size attribute)
11 Register ("E" field is an immediate value in a register)
The instruction "XOR ECX, ECX" uses register addressing (11) and so the "MOD R/M" byte = (11, 001, 001) = 0b1100_1001 = 0xC9.
Why Instruction Encodings Matter
Now, the instruction encoding matters mainly because of the variable length. Since you want to call "SetDaylightEverywhere(true)", you need to load ECX with the value 1 (true). Which is a problem when you examine the instruction lengths:
110012DD 33C9 XOR ECX,ECX
110012DF FF15 70E40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightEverywhere@TethysGame@@SIXH@Z>]
If you replace "XOR ECX, ECX" with "MOV ECX, 1", the increased instruction size will overwrite the first few bytes of the following "CALL" instruction.
To make matters worse you also need to call "SetInitialLightLevel". If that call doesn't already exist, you may have to overwrite more code to insert it.
Gaining and Using Space
Now, the good news. One of the official Sierra updates gutted the cheat functions, such as:
static void __fastcall SetCheatFastUnits(int bOn); // Useless
Essentially, these functions have been replaced with simple "RETN" stubs. As these functions no longer do anything, you're free to overwrite any calls to these functions without changing behavior. Hence, if you need to expand the size of previous instructions, you can gain a little bit of extra space by overwriting that last call. If you only overwrite part of the instruction, you can replace the remaining bytes with NOP (90) bytes. As NOP is a single byte instruction, it lets you easily pad code up to the next instruction boundary. I believe that's what you'll need to do here to make the necessary adjustments.
Another possibility is reusing existing values in registers. Technically, the C++ calling conventions assume the contents of the 4 registers (EAX, ECX, EDX, EBX) are all trashable during a function call. This means there is no guarantee they will have the same value after a function call as they did before the function call. This is why you see ECX set to zero multiple times, before each function call, in the code you posted. The compiler assumes ECX will be overwritten by the function, and so it's value is unknown after each function call. If you analyse the functions, you may determine that ECX, or another register is perhaps preserved, simply because the function had no need to overwrite it. That may allow you avoid setting it's value, or allow you to copy from another register, or increment/decrement a register to get the value you need. All these options should provide a shorter instruction than loading the register with an immediate constant. This is also where loading a ECX with a value other than 1 for true might come in handy. (Remember that true is multi-valued).
Procedure Overview
Now without knowing more about the surrounding code, I can't be certain if you have enough space to work with. But if you do, essentially you can copy the call instructions, update the assignment to the ECX register, and paste the call instruction back into it's new location, moved a few bytes later, and then NOP pad to the next instruction boundary (assume you're not overwriting anything important). It might be easier to NOP out useless instructions ahead of time, such as the call sequence for cheat functions, so you have a better sense of how much space you have to work with. Pay close attention to any important following instructions, and make sure you don't overwrite any of them.
Hopefully that's enough information that you may be able to figure out what to do here.
I took a look at cps2.dll to get a bit more context, and I believe what you want to do can be accomplished. A slightly larger copy of that code section is:
110012D2 |. B9 01000000 MOV ECX,1
110012D7 |. FF15 6CE40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightMoves@TethysGame@@SIXH@Z>] ; Outpost2.?SetDaylightMoves@TethysGame@@SIXH@Z
110012DD |. 33C9 XOR ECX,ECX
110012DF |. FF15 70E40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightEverywhere@TethysGame@@SIXH@Z>]; Outpost2.?SetDaylightEverywhere@TethysGame@@SIXH@Z
110012E5 |. 33C9 XOR ECX,ECX
110012E7 |. FF15 74E40011 CALL DWORD PTR DS:[<&Outpost2.?SetCheatFastUnits@TethysGame@@SIXH@Z>] ; Outpost2.?SetCheatFastUnits@TethysGame@@SIXH@Z
110012ED |. 33C9 XOR ECX,ECX
110012EF |. FF15 78E40011 CALL DWORD PTR DS:[<&Outpost2.?SetCheatFastProduction@TethysGame@@SIXH@Z>>; Outpost2.?SetCheatFastProduction@TethysGame@@SIXH@Z
110012F5 |. 33C9 XOR ECX,ECX
110012F7 |. FF15 7CE40011 CALL DWORD PTR DS:[<&Outpost2.?SetCheatUnlimitedResources@TethysGame@@SIX>; Outpost2.?SetCheatUnlimitedResources@TethysGame@@SIXH@Z
110012FD |. 33C9 XOR ECX,ECX
110012FF |. FF15 80E40011 CALL DWORD PTR DS:[<&Outpost2.?SetCheatProduceAll@TethysGame@@SIXH@Z>] ; Outpost2.?SetCheatProduceAll@TethysGame@@SIXH@Z
For the call to "SetDaylightMoves", changing the "MOV ECX, 1" to "MOV ECX, 0" is nice an easy.
The call to "SetDaylightEverywhere" doesn't need to be changed.
The SetCheatX functions can all be NOP-ed out, and that space reused.
To make a call to "SetInitialLightLevel", we'll need to find the address of that function. You can often find the addresses you need in the Names window (Ctrl+N), however, cps2.dll does not import this symbol from Outpost2.exe, so if you have code from the cps2.dll module active when you go to the Names window, you won't see that function anywhere. (*This is where silly me starts making things a little harder than needed*). Instead follow one of the function calls into the Outpost2.exe module. It doesn't matter which one, but let's say we follow "SetDaylightMoves". Just select that line of code so it's highlighted, and press enter. This should take you to this code:
008E8270 > 83EC 74 SUB ESP,74
008E8273 |. A1 1CEB5600 MOV EAX,DWORD PTR DS:[56EB1C] ; Load TethysGame.tick
008E8278 |. 66:894C24 12 MOV WORD PTR SS:[ESP+12],CX ; commandPacket.data[4] := param1 (???)
008E827D |. 53 PUSH EBX
...
From here, you can press Ctrl+N to bring up the Names window. You can start typing "SetInitialLightLevel" into this window (or just scroll through the list of function) until you have the correct entry highlighted:
008E6F90 | .text | Export | ?SetInitialLightLevel@GameMap@@SIXH@Z
If you press enter, it will jump to that function, but you can actually read the information you need right from the Names window. You just need the address from the left most column. In fact, it's probably easier to copy/paste it from the Names window. You can right-click on the line and choose "Copy to clipboard -> Address". Now be careful here. (*This is where silly me realized what I did wrong... after Outpost2 crashed on me*). If you've loaded cps2.dll directly in OllyDbg (as I have), the Outpost2.exe will have been relocated, and you'll be seeing the relocated address here. For instance, I see "008E6F90". The non-relocated address should be "00476F90". If you load Outpost2.exe directly in the debugger, this non-relocated address is the address you should see, and will be the addresses needed when you run the game. To determine what the address should be, subtract off the current module base address, and then add the normal module base address. You can find the current module base address from the Memory Map window (Alt+M). Here it shows Outpost2 starts at 00870000. The normal module base address is 00400000. Hence 008E6F90 - 00870000 + 00400000 = 00476F90. You can use Windows calc in hexadecimal mode if you need to. Note that 00400000 is the default executable base address set by the MSVC compiler, so most executables will use this address. To be sure though..., you can open up Outpost2.exe directly in a debugger so it's not relocated.
(*Here's the less silly alternative*)
Alternatively, you could have opened Outpost2.exe in a separate copy of OllyDbg, and looked up SetInitialLightLevel in the Names window. Since you've loaded Outpost2.exe directly, it shouldn't be relocated, so the address given here will be correct. You should see "00476F90" in the Names window for the SetInitialLightLevel function. Copy this, and return to the original copy of OllyDbg.
Now you can return to the code you are modifying. Use the "-" key to jump back to where you were before following an address (it's the reverse of "enter", and you can use it multiple times if needed). Once you're back to the cps2.dll code, you can begin constructing the needed function call in the space where the SetCheat functions were called.
Enter in the instructions:
MOV ECX, -64
CALL 00476F90
I tried with -32 first, but that turned out to not be enough. There was still a strip of partial daylight along the left edge of the map. If you want to experiment, you can set a breakpoint in the cps2.dll file just above the code you're modifying, close OllyDbg, and open Outpost2.exe in OllyDbg. Run the game, and load up the level. It should hit the breakpoint. If you forgot to set it, you'll need load the level, go to the appropriate address in OllyDbg, set the breakpoint, and then restart the level. Each time you restart the level though, it reloads the modules, which will undo any memory patches you've applied. To re-apply a memory patch to the code, you can go to the Patches window (Ctrl+P), or use the "/" button from the menu bar, find the address of the patch you made, and hit space bar to toggle it's active state. This lets you make small modifications to your patches without having to retype the whole thing each time.
The resulting patch ends up being:
110012D2 B9 00000000 MOV ECX,0
110012D7 |. FF15 6CE40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightMoves@TethysGame@@SIXH@Z>] ; Outpost2.?SetDaylightMoves@TethysGame@@SIXH@Z
110012DD |. 33C9 XOR ECX,ECX
110012DF |. FF15 70E40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightEverywhere@TethysGame@@SIXH@Z>]; Outpost2.?SetDaylightEverywhere@TethysGame@@SIXH@Z
110012E5 B9 CEFFFFFF MOV ECX,-64
110012EA E8 A15C47EF CALL Outpost2.?SetInitialLightLevel@GameMap@@SIXH@Z
110012EF 90 NOP
110012F0 90 NOP
110012F1 90 NOP
110012F2 90 NOP
110012F3 90 NOP
110012F4 90 NOP
110012F5 90 NOP
110012F6 90 NOP
110012F7 90 NOP
110012F8 90 NOP
110012F9 90 NOP
110012FA 90 NOP
110012FB 90 NOP
110012FC 90 NOP
110012FD 90 NOP
110012FE 90 NOP
110012FF 90 NOP
11001300 90 NOP
11001301 90 NOP
11001302 90 NOP
11001303 90 NOP
11001304 90 NOP