Author Topic: Level Dll Modding  (Read 32743 times)

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« on: February 16, 2009, 08:44:42 PM »
For this tutorial, you'll need OllyDbg 1.10. You can find it at: http://www.ollydbg.de/. There is a download link in the menu on the left under "Files".

You will also of course need Outpost 2.

Any version of Outpost 2 will work, but I've noticed OllyDbg will pause due to an exception if you try to load an Outpost 2 DLL with a patched version of Outpost 2 that contains op2ext.dll. This isn't actually a problem, but it leaves you with a blank looking screen, and if it bothers you, consider working with an older copy of Outpost 2. [OllyDbg will want to load LoadAll.exe first if you try to open a .dll file, and it loads to the address that Outpost2.exe normally occupies. When op2ext.dll loads, it tries to patch Outpost2.exe, assuming it's at it's usual known address, which it isn't, and this of course causes a problem].

Open your favourite level DLL in OllyDbg. For this, I'll use CES1.DLL (Colony, Eden Starship). OllyDbg will probably tell you something about launching LoadAll.exe. Once it's loaded, you should be sitting at the "Module Entry Point" in the code window, or, in the case of op2ext.dll, the screen will be blank and it will say something like "Access violation when executing ..." in the status bar. In the latter case, open the "Memory" window (Alt+M, or the M button on the menu bar), find CES1 in the list, and follow it's .text section (code) in the disassembler view.


You should now be staring at a chunk of code in the disassembler view (at the Module Entry Point in the first case, and at the start of the code section in the second case). Press Ctrl+N to see a list of Names in this module. This list will show all the exported symbols from the DLL, as well as all the imported symbols, and any user defined labels that you set. Note that the code frame must be active for Ctrl+N to work. If you've clicked on the stack, data, or CPU frames, or Ctrl+N otherwise seems unresponsive, click on the code frame first. You'll probably run into this from time to time.


I find sorting the Name list by Address to be the most effective, as it tends to put related items closer together. You can start typing names here as a quick keyboard shortcut to jump to that line (not case sensitive). Let's go down to "DoQuake", and follow it in the Disassembly view (Enter or with the right-click menu). We should now be at the start of the function that creates earthquakes. If you look down at the end of the function (it's only about a page), you'll see the RETN instruction. Sometimes it will have a number after it for functions that take parameters. The number is how many bytes of parameters to pop off the stack when returning. In this case there are none. If you want to remove a function, the easiest way is to replace it's first instruction with the appropriate RETN instruction. Note that RET will usually work in place of RETN. (RETN stands for Return Near, but Windows uses a flat memory model, so everything is near). To replace the instruction, highlight that line, and hit spacebar (or just start typing). It will popup an "Assemble" window, where you can type in "ret", and it will replace the instruction.

At this point, you can select the new RETN line (or the whole edit), right-click, and choose Copy to executable. (You made a backup, right?)


The level should now play without Earthquakes. You can do the same thing for the other disaster functions too. They are (for CES1.dll) "DoLightning", "DoTornado", "DoMeteor", and the "Lava?_Flow" functions.
 
« Last Edit: May 31, 2020, 11:57:37 PM by Arklon »

Offline Sirbomber

  • Hero Member
  • *****
  • Posts: 3237
Level Dll Modding
« Reply #1 on: February 16, 2009, 09:03:59 PM »
Now make a guide for replacing all other disasters with Blight. :evil laugh:
"As usual, colonist opinion is split between those who think the plague is a good idea, and those who are dying from it." - Outpost Evening Star

Outpost 2 Coding 101 Tutorials

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #2 on: February 17, 2009, 11:12:40 AM »
Thanks Hooman! Once I make the edits to the .dll I will be able to test some things about surpassing the unit limit. This way I know removing the blight/lava isn't some programming disaster on my part.

R.I.P. Blight : (
« Last Edit: February 17, 2009, 12:01:40 PM by Kamikaze088 »

Offline Sirbomber

  • Hero Member
  • *****
  • Posts: 3237
Level Dll Modding
« Reply #3 on: February 17, 2009, 12:10:33 PM »
Quote
R.I.P. Blight : (
NEVER!  Blight owns.
"As usual, colonist opinion is split between those who think the plague is a good idea, and those who are dying from it." - Outpost Evening Star

Outpost 2 Coding 101 Tutorials

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #4 on: February 17, 2009, 08:29:15 PM »
I always thought the blight could have been utilized more in the game. I never used the microbe walls to win a mission - which is sad because they are purple. Would be neat to program a way to "rescue" buildings from blight. Get on that programming and I might not delete the blight  :whistle:  

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #5 on: February 18, 2009, 12:39:14 AM »
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.
Code: [Select]
	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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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.
 
« Last Edit: February 18, 2009, 01:00:11 AM by Hooman »

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #6 on: February 18, 2009, 03:13:06 PM »
I successfully made the edits to cps2.dll:

Replaced the start of the functions of Lava_vent, Lava_flow and Microbe with a RETN instruction. The .dll loads fine with no error messages.

My next question is: If I've saved an old game with the old version of the cps2.dll can I now load that saved game and it will play off the new cps2.dll file?

Offline Sirbomber

  • Hero Member
  • *****
  • Posts: 3237
Level Dll Modding
« Reply #7 on: February 18, 2009, 04:13:23 PM »
No, you'll have to start fresh.
I think.
"As usual, colonist opinion is split between those who think the plague is a good idea, and those who are dying from it." - Outpost Evening Star

Outpost 2 Coding 101 Tutorials

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #8 on: February 20, 2009, 12:10:15 AM »
Hmm, good question. I don't know. Try it out and tell us.

It should try to load the new DLL when you load the game, but it's possible it will do some kind of checksum on it or something and realize it's changed. I have a feeling it doesn't though.

Of course if it works, then disasters that have already been created will still be there. It just shouldn't create any more.
 

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #9 on: February 20, 2009, 09:39:24 PM »
It loads the saved file with no problems! No error messages. I think it's interesting how it can do that. It's like I picked up where I left off. This potentially means I could add/remove disasters or settings based on what .dll I use. Hmm...

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #10 on: February 28, 2009, 05:27:28 PM »
I took a quick glance at that function, and discovered the mistake. Here's the correction:

Code: [Select]
	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.
Code: [Select]
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
« Last Edit: February 28, 2009, 05:28:12 PM by Hooman »

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #11 on: August 18, 2012, 03:53:53 PM »
I recently tried to make other edits to .dll levels but have not been able to get OllyDbg 1.10 to work the same way as before? Loads the .dll with some exception error. Not sure if it's the new OS or something else. Is there a better program to use now? In any case, I want to make it so my modified cps2.dll file does not have daylight. I've already removed the blight, electrical storms, lava ; )

I've gone through a lot of the forum posts on the programming guides, but I'm finding out-dated software with compatibility issues, and headaches trying to figure it all out. Even saw a post on the code for daylight settings. Just don't have the background skills to make it all happen.

Anyone willing to assist? Attached below is the file I'm working on.

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #12 on: August 19, 2012, 02:16:36 AM »
Some of the modifications to Outpost 2 can cause errors to appear when loading level DLLs directly in a debugger. If you load the EXE first, run it, and then start the level, there will be no errors displayed. But if you open the DLL directly, it will ask to load LOADALL.EXE (basically starts up a generic process to load the DLL into), and this method causes errors to be displayed. The problem is caused by LOADALL.EXE using the default EXE image base of 0x00400000, which Outpost2.exe is also set to load to. Since the DLL references Outpost2.exe as a required module, this EXE needs to be loaded after you've loaded the level DLL, but the address has already been taken by LOADALL.EXE, so Outpost2.exe must be relocated. The problem is, some of the patches are not designed for relocated code. They expect addresses to always be based off of the 0x00400000 base address and cause addressing errors if things shift around.

You can still use OllyDbg to load a DLL directly, analyse it's code, change it, and rewrite it. You can't run in such an environment though. Instead, you'd need to open Outpost2.exe in the debugger, and then have Outpost2 load the level. Even without the patch problems, that's still the only way to run the DLL, so there isn't really a loss of functionality here. The one annoyance though, is the CPU and data dumps don't automatically display the code and data segments from the DLL when loading the DLL directly, due to the error. You can still view this data though by going to the memory map window (the "M" button on the toolbar), and finding the segments for the module of interest (such as Outpost2 or the DLL). From there you can choose "view in disassembler" (Enter) on the .text (code) section, and "dump in CPU" on the .data section. This will basically set the view to what you would normally see when loading that module directly and no load errors have occurred.
 

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #13 on: August 19, 2012, 01:50:22 PM »
Ok, I think I got it looking how it was before. Thanks Hooman! Now, if I'm going to edit the .dll so it's pitch black all the time, where and what would I modify in this string of code? Can this be accomplished with the RETN function, or is it more complicated?

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

 

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #14 on: August 19, 2012, 11:10:05 PM »
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:
Code: [Select]
	// 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:
Code: [Select]
	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
Code: [Select]
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
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
	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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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.
 

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #15 on: August 21, 2012, 05:40:10 PM »
Thanks Hooman for your excessively detailed reply! I did find it useful and learned a bit more ;) I was able to modify the code to stop the daylight band from moving by changing [MOV ECX,1] to [MOV ECX,0] before the function:

110012D7 FF15 6CE40011 CALL DWORD PTR DS:[<&Outpost2.?SetDaylightMoves@TethysGame@@SIXH@Z>]

Game loaded fine, with no damage (that I can tell; file attached if anyone cares). The problem is though, the map starts with some daylight already so it doesn't get to my end goal. I didn't see a "SetInitialLightLevel" anywhere either, so I might be out of luck with that already existing in the code. As for actually making the level completely dark, I think the task might be beyond what I can do. I'm such a noob programmer haha.
 

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #16 on: August 21, 2012, 09:03:05 PM »

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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #17 on: August 22, 2012, 08:12:59 PM »
I took the "less silly" approach and it works! No more sunshine for the colonists, and they can all thank Hooman!  :D

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #18 on: August 23, 2012, 07:51:19 PM »
So, this is odd. After I save the file, and re-load, the daylight band returns, but does not move. Can you explain this?

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #19 on: August 25, 2012, 03:02:41 AM »
Quote
I took the "less silly" approach and it works! No more sunshine for the colonists, and they can all thank Hooman!
Umm..... *shifts eyes left* *shift eyes right* ... sunlight causes skin damage?


Quote
So, this is odd. After I save the file, and re-load, the daylight band returns, but does not move. Can you explain this?
I am uncertain about the daylight band effect at this time. I just tried it out and I observe the same phenomena. Perhaps the negative value for the daylight position was not saved, or was modified before or after saving.

The code that was modified is only called during level initialization (InitProc), which does not run when loading a game. Hence there is no opportunity to re-set that value.

Perhaps an equivalent positive value might work better. I'll have to find some time to look at the code. Perhaps on Sunday or so.
 

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #20 on: August 27, 2012, 03:49:36 AM »
I took a look at the code today to see what was going on with that band of daylight. The daylight value that was set is saved and loaded just fine. However, when setting the value there is some additional work done which is not run after loading a saved game. I didn't look at that additional code in much detail, but I believe it may loop over a buffer and sets some kind of daylight intensity values. If such a buffer were not properly initialized, that may account for the band of daylight.

If the value passed to SetInitialLightLevel is negative, the game will add the width of the map to that value. I suspect there is an assumption this value is bounded between 0 and the map width, or perhaps twice the map width. Based on the map buffer size and alignment differences between normal maps and around-the-world maps, I would expect the bound, or at least it's interpretation, would depend on the map type. I have not verified this though. I would guess  around-the-world maps probably have this value bounded by the true map width, and would always have a band of daylight visible. Non-around-the-world maps probably have this value bounded by twice the map width, and so can have the daylight completely off the map. Again, I haven't verified that. Note that true modulo arithmetic is not used when adjusting the value passed to SetInitialLightLevel. If the value passed is more negative than the map width, it will still remain a negative number.


I suppose as a workaround, you could call SetInitialLightLevel from AIProc to ensure it is continually reset, which would fix things after the game was reloaded. However, there may be a brief glitch period when the band of daylight would be noticeable. You might get better results by inserting a call from DllMain, although that might not be safe to do. There is no explicit callback function for a DLL when a saved game is loaded, so there is no particular way to detect this event and deal with it in such a manner.
 

Offline Kamikaze088

  • Newbie
  • *
  • Posts: 35
Level Dll Modding
« Reply #21 on: August 29, 2012, 08:48:52 PM »
Maybe the colonists will have sunshine after all : )

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #22 on: August 30, 2012, 02:34:27 AM »
We'll see about that! Patch!


This does actually appear to be a bug. None of the original game levels did what you're trying to do here, so coming across this sort of odd behavior isn't too surprising.

I tried inserting a call to that mysterious function after loading game data, but the daylight band was still visible. I'll have to look further into this to figure out what's going on.

Curiously though, I've never noticed daylight band position glitches with other saved games.
 

Offline Hidiot

  • Hero Member
  • *****
  • Posts: 1018
Level Dll Modding
« Reply #23 on: September 10, 2012, 10:25:32 AM »
Have you tried setting the position to a positive value greater than the map width in the meantime?
"Nothing from nowhere, I'm no one at all"

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4954
Level Dll Modding
« Reply #24 on: September 12, 2012, 02:50:20 AM »
I'm uncertain if that will do anything, but it might be worth trying. The first thing SetInitialLightLevel does is to adjust it's parameter by adding the map width if it is negative. Values in the range [-mapWidth, mapWidth] will be mapped to [0, mapWidth]. A value outside of that range, such as one exceeding the mapWidth may produce a different result. The daylight band is cyclic though, so eventually things should map back to the beginning. Guess I should find out at what point....


Ok, it calculates (((param * 65536) / mapTileWidth) mod 65536), and stores that value internally. This effectively calculates ((param mod mapTileWidth) * (65536 / mapTileWidth)). This would make the values cyclic every mapTileWidth.


This all seems to imply that any value is essentially taken modulo the map width before being used, so values outside of the range [0, mapWidth] will converted to a value in that range.


Ok, so I'm feeling pretty certain it won't do anything. But that doesn't mean I'm not wrong.