Author Topic: Dual Turret Guard Posts  (Read 3243 times)

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Dual Turret Guard Posts
« on: January 05, 2007, 02:58:23 PM »
Part 1
-------

Dual Turret Behavior


This first post will tell you how to get a single guard post to behave as if it is a dual turret guard post. This is just a simple project (about a page of code, two lines of which handle the topic at hand), and only covers the double fire rate behavior of dual turret weapon systems. The graphics still appear to be single turret, but that issue will be discussed in a later post.

First of all, the dual turret fire rate is controlled by a single bit in the unit flags. If you've done any memory editing to units, you'll probably be well aware of this flags field at offset 0x44 within the unit records. We will be interested in the flag with bitmask 0x20. To get the double fire rate behavior we need only set this bit.

To get access to that bit, we need access to the internal unit records. This array of unit records can be found by following the pointer at address 0x0054F848. This value points to the start of the unit array. Each unit takes up 120 bytes. So, to find the flags field for the unit we are interested in, we read the value at 0x0054F848 to get to the start of the unit array, add 120*unitIndex to get to the start of the data for the unit we are interested in, and then add 0x44 to get to the address of the flags field.

To do the above, we still need to know the unitIndex of the unit we are interested in. Namely, the guard post. But this is easily obtained. When TethysGame::CreateUnit is called, the first parameter is a Unit class, which is really just a dummy exported class that you can use to manipulate the internal unit classes. To do this, it needs to know what unit it refers to, and it does it by storing the unit index. When you call TethysGame::CreateUnit and pass in a Unit class, it fills in the unitIndex field for the unit it just created.


The Project

I made a copy of the blank Multiplayer SDK template project. I changed the exports required by Outpost2.exe so I could test this out as a colony game, and remember what it did.
Code: [Select]
char MapName[]  	= "cm01.map";      	// The .map file used for this level
char LevelDesc[]  = "Dual Turret GP Test Level";   // Description appearing in the game list box
char TechtreeName[]  = "Multitek.txt";      // File to use for the tech tree
SDescBlock DescBlock = { Colony, 2, 12, false };    // Important level details (GameType, NumPlayers, MaxTechLevel, UnitOnlyMission)

Next, I added in the InitProc which creates the Guard Post and sets the Dual Turret flag. I also created some enemy units that will run up to the guard post, and a reference lynx of your own so you can compare hitpoints and armour. Also the guard post will need some power.

Code: [Select]
int InitProc()
{
Unit unit;
Unit guardPost;

// Make sure graphics look right
Player[0].GoEden();
Player[1].GoEden();

// Create a guard post
TethysGame::CreateUnit(guardPost, mapGuardPost, LOCATION(100,40), 0, mapLaser, 0);
// Create a power plant to power the guard post
TethysGame::CreateUnit(unit, mapTokamak, LOCATION(90, 40), 0, mapNone, 0);


// Get the address of the flags
int* flagsAddress = (int*)(*(int*)(0x0054F848) + guardPost.unitID*120 + 0x44);

// Set the dual turret flag
*flagsAddress |= 0x20;


// Create some enemy units
TethysGame::CreateUnit(unit, mapLynx, LOCATION(110,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(116,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(122,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in

// A refrence unit for you
TethysGame::CreateUnit(unit, mapLynx, LOCATION(80,40), 0, mapLaser, 0);

// Center the view on the action
Player[0].CenterViewOn(110, 40);

return 1; // return 1 if OK; 0 on failure
}


I compiled this DLL, and copied it to the Outpost2 folder as cGPTest.dll. It can then be run through the colony game menu.

I figured 3 enemy units would be good. That's enough to kill a single turret GP, but not enough to kill a dual turret GP. You can comment out the line that sets the flag to see the difference. Also, note that the fire rate of the guard post is higher than the fire rate of the lynx attacking it.

 

Offline Eddy-B

  • Hero Member
  • *****
  • Posts: 1186
    • http://www.eddy-b.com
Dual Turret Guard Posts
« Reply #1 on: January 06, 2007, 03:55:31 PM »
This is an interresting thing to put into Renegades. They ARE seperated from the main colony and as such may come up with other research/techs.

Once you have figured how to make the turrets LOOK dual, i'll put it into the Renegades campaign.
Rule #1:  Eddy is always right
Rule #2: If you think he's wrong, see rule #1
--------------------

Outpost : Renegades - Eddy-B.com - Electronics Pit[/siz

Offline BlackBox

  • Administrator
  • Hero Member
  • *****
  • Posts: 3093
Dual Turret Guard Posts
« Reply #2 on: January 06, 2007, 06:07:42 PM »
Quote
Once you have figured how to make the turrets LOOK dual, i'll put it into the Renegades campaign.
That might be a bit of a problem because the GP graphics are different from the vehicle graphics, and no dual turret graphics exist for the GPs.

Come to think of it, for all the games where I have dug into their datafiles for graphics and such, OP2 has very little waste / unused data when compared with other games. (There are only a handful of graphics in OP2_ART that aren't actually used in game in some manner).

Offline instigator

  • Jr. Member
  • **
  • Posts: 89
Dual Turret Guard Posts
« Reply #3 on: January 06, 2007, 09:51:35 PM »
Can't you just put a Dual turret from a tiger right into the ground? Lol. Tubes wouldnt have to be attached. i know that the chassis is just the wheels and the weapon type is added to this chassis. So why not just plant one in the ground without the chassis?

Offline Eddy-B

  • Hero Member
  • *****
  • Posts: 1186
    • http://www.eddy-b.com
Dual Turret Guard Posts
« Reply #4 on: January 07, 2007, 06:09:41 AM »
Quote
Come to think of it, for all the games where I have dug into their datafiles for graphics and such, OP2 has very little waste / unused data when compared with other games. (There are only a handful of graphics in OP2_ART that aren't actually used in game in some manner).
Isn't it possible to expand OP2_ART with new items ?
And besides .. i'm talking about Renegades: i can have some weapons unavailable, and use those graphics (alter them) to display a dual-turret.

Would be nice to have OP2 allow you to upgrade existing buildings, as many other RTS games do. Send out a convec to do an upgrade of your turret, to turn it into a dual-turret. Same could be done for upgrading a residence for example.
Rule #1:  Eddy is always right
Rule #2: If you think he's wrong, see rule #1
--------------------

Outpost : Renegades - Eddy-B.com - Electronics Pit[/siz

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Dual Turret Guard Posts
« Reply #5 on: January 07, 2007, 04:18:26 PM »
Yes, there is definately an issue with graphics. The combat vehicles have seperate graphics for the chasis and turret. You can simply draw the chasis you want, and then place the weapon turret over it (single or double). But the guardposts have the graphics all together. There is no seperate guard post structure followed by a turret palced on top of it. So, if you really want a guard post to look like it has a dual turret, you'll need to do some graphics editing. If it had been as simple as the vehicle case, I probably would have posted this long ago.


Also, I was going to post on how to add research topics which allow the dual turret. There are a few complications though, due to the way Outpost2 passes data around internally. When you're creating a unit, the player who owns the unit is not passed in to the actual unit creation part (where the class is instantiated). If it was, you could just simply override the virtual function that creates the class, and set the bit depending on whether or not the player building it has finished researching the topic yet. Checking if a topic is complete is fairly easy, or you could also use a trigger callback for when the research is complete to install the new unit creation function that always sets that bit. Of course since the player number isn't passed into that creation function, this would just give everyone dual turret GPs once anyone researched the tech. There are two ways around this. One is to intercept the unit creation at a different point, probably using code overwrites, or just keep in mind that the unit creation functions should only be called from one place, and lift the player number off the stack. It will be there at a fixed place backwards, provided your function is always called from the same place. The solution is a bit sick, but it does seem to be the easiest way.

I'd also like to point out that since this bit is stored in the unit records, and not the "UnitInfo" records, it won't automatically upgrade all existing GPs when you start setting this bit (such as when a research topic is completed). Personally I think this is a lot more realistic, so I'm happy with this.



The Graphics Functions
--------------------------

First we'll start at the virtual function table for the Guard Post unit. Once a label is added at the correct address, it can quickly be found using the Names window in OllyDbg (Ctrl+N). The entry with the label added looks something like this:
Code: [Select]
004CFFB0 .rdata User Unit:GuardPost.vtbl

This will take you to a place in the data window that starts with something like this:
Code: [Select]
004CFFB0 >0042D700  <Outpost2.Unit:GuardPost.GetUnitTypeInfo()>
If we look down 6 lines, we will see something like this:
Code: [Select]
004CFFC8  0042D450  <Outpost2.Unit:GuardPost.DrawUnit()>

Whenever a unit needs to be drawn on screen, it calls a draw function like this through the virtual function table. By overriding this function, we can control how any unit is drawn. In this particular case, we can control how Guard Posts are drawn.


Note: By changing the various functions in the virtual function table, we can change a fairly large number of unit behaviors. If you're looking to build new units, this is probably how you'd want to go about doing it. There are however a lot of functions in this table, and to prevent crashing the game, you'd need to have every function available for your unit, and all with the correct calling procedure. The number of functions needed changes depending on the type of unit (building, vehicle, disaster, resource), and for the Guard Post, it appears to use 65 functions. A much easier way to add "new" units, is to clone an old unit, and only override a few of the functions. This is essentially what we will do with the Guard Posts, since the only difference between our new unit and the existing one, is the fire rate, and dual turret display, and the existing unit can handle the fire rate already.


Now, there are two obvious ways of overriding the function we are interested in. The first way is to simply change the address of that one function in the virtual function table. This will ensure that all existing units of that type will now use the new function. This method has two disadvantages that I can see. One is that the virtual function table is meant to be read only data, and as such is often placed in a read only section of memory. To modify it successfully (indeed to avoid crashing the program) you'll have to unprotect that area of memory first before modifying it, and then you'll probably want to reprotect it again afterwards. The other disadvantage is that this limits you to changing an old unit to have new behavior. You can't really add a new unit and keep the old unit, unless the new unit can somehow be both.

Neither problem is much of an issue with creating a Dual Turret Guard Post. We have the dual turret flag in each unit, so the unit can use this flag to determine if it should draw a single turret unit or a dual turret one. Thus the new unit can handle both the new and old behaviors quite easily.

The other method that is of interest, is to copy the existing virtual function table to a new area of (read/write) memory, edit the table to update the address of the function you want to change to point to your new function, and then change the virtual function table pointer in the unit object. The unit object will reside in read/write memory, so you won't have to worry about unprotecting and reprotecting memory. The old virtual function table will also continue to exist, so old units can continue using it, while new units can use the updated table. This is a little closer to adding new units, and so I figure this method is more worth being explored for future use.


I'll begin by adding a new file to the existing project. For now, we'll just fill in enough to duplicate the existing virtual function table. We only really need to call one function to do the setup, and all the rest of the changes will be hidden away from the main program. For this reason, I will avoid creating a header file, since we will only need to call one function from the .cpp file. We can just add the function declaration to the file we need to call it from (main.cpp). Also, since I'm only interested in updating the virtual function table for a single unit in this test, I'll need the address of the new virtual function table to replace the old one with. To keep things simple, I'll just have that single function return this address.

Here is the code to duplicate the virtual function table:

DualTurretGP.cpp
--------------------
Code: [Select]
#include <memory.h>


// DualTurretGPvftbl
void* dualTurretGPvftbl[65];



void* SetupDualTurretGP()
{
// Copy the vtbl we want to edit
memcpy(&dualTurretGPvftbl, (void*)0x004CFFB0, 65*4);
// Update desired function pointers here

// Return the new virtual function table
return dualTurretGPvftbl;
}


To use this code, we add the function declaration to main.cpp just after the #include lines:
Code: [Select]
void* SetupDualTurretGP();

We then need to call this function from InitProc:
Code: [Select]
	void* dualTurretGPvftbl = SetupDualTurretGP();

Then we need to update the virtual function table of the guard post we created:
Code: [Select]
	// Get the address of the virtual function table pointer
int* vtblPtrAddress = (int*)(*(int*)(0x0054F848) + guardPost.unitID*120);

// Set the new virtual function table
*vtblPtrAddress = (int)dualTurretGPvftbl;

All this just clones the virtual function table and sets the guard post to use the new one. We have not yet changed any entries, so the guard post will still behave exactly as the old one did.



This would be a good time to run the test DLL to ensure there are no strange crashes. The changes made so far are summarized as follows: DualTurretGP.cpp was added to the project with the contents given above. The declaration for the setup function in DualTurretGP.cpp was added to main.cpp just after the #include lines. The InitProc function in main.cpp was updated to the following:
Code: [Select]
int InitProc()
{
Unit unit;
Unit guardPost;

void* dualTurretGPvftbl = SetupDualTurretGP();

// Make sure graphics look right
Player[0].GoEden();
Player[1].GoEden();

// Create a guard post
TethysGame::CreateUnit(guardPost, mapGuardPost, LOCATION(100,40), 0, mapLaser, 0);
// Create a power plant to power the guard post
TethysGame::CreateUnit(unit, mapTokamak, LOCATION(90, 40), 0, mapNone, 0);


// Get the address of the flags
int* flagsAddress = (int*)(*(int*)(0x0054F848) + guardPost.unitID*120 + 0x44);

// Set the dual turret flag
*flagsAddress |= 0x20;


// Get the address of the virtual function table pointer
int* vtblPtrAddress = (int*)(*(int*)(0x0054F848) + guardPost.unitID*120);

// Set the new virtual function table
*vtblPtrAddress = (int)dualTurretGPvftbl;


// Create some enemy units
TethysGame::CreateUnit(unit, mapLynx, LOCATION(110,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(116,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(122,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in

// A refrence unit for you
TethysGame::CreateUnit(unit, mapLynx, LOCATION(80,40), 0, mapLaser, 0);

// Center the view on the action
Player[0].CenterViewOn(110, 40);

return 1; // return 1 if OK; 0 on failure
}




Our next step is to override the DrawUnit function. To do this, we'll need to define a new function to replace the old one with, and update the virtual function table to point to this new function. For now, we will keep the new function a simple empty one. This will result in the guard post being invisible, since no code will run when the game asks it to draw itself.

Here is the replacement function to add to DualTurretGP.cpp:
Code: [Select]
// Replacement DrawUnit function
__declspec(naked) void DualTurretGP_DrawUnit(void *graphicsObject)
{
__asm RETN 4;
}

I'll place this near the top of the file so I don't need to add a forward declaration when I go to update the function address. Especially since I might very well change the function's signature when I go to implement something. But for now, this will do.

The virtual function table is updated in SetupDualTurretGP with the following line:
Code: [Select]
	// Update the DrawUnit function pointer
dualTurretGPvftbl[6] = &DualTurretGP_DrawUnit;

If you look at the virtual function table as an array of function pointers, than the DrawUnit function is at position 6 (starting from 0) in the table.

The new DualTurretGP.cpp file now looks like this:
Code: [Select]
#include <memory.h>



// Replacement DrawUnit function
__declspec(naked) void DualTurretGP_DrawUnit(void *graphicsObject)
{
__asm RETN 4;
}


// DualTurretGPvftbl
void* dualTurretGPvftbl[65];


void* SetupDualTurretGP()
{
// Copy the vtbl we want to edit
memcpy(&dualTurretGPvftbl, (void*)0x004CFFB0, 65*4);
// Update the DrawUnit function pointer
dualTurretGPvftbl[6] = &DualTurretGP_DrawUnit;

// Return the new virtual function table
return dualTurretGPvftbl;
}



Compile this and run it, and you'll see the the Guard Post is now invisible. The tubes and bulldozed terrain around it still give it away though. Plus, you can still select it and see the selection box around it.


Just a quick note on the calling conventions needed by the replacement DrawUnit function. There is one parameter to the function, a pointer to a graphics object that is used to do all the drawing to the game window. This is why we have the RETN 4, since the pointer is 4 bytes long, and needs to be cleaned off the stack when the function exits. There is also a hidden "this" pointer to the Guard Post unit object. The hidden "this" pointer is passed in the ECX register. When you write your replacement function you must keep this in mind and make no other assumptions about registers. In particular, you must make sure that exactly 4 bytes get poped off the stack when the function returns. This is not always an easy task when writing the function in C/C++. You may need to check your compiler documentation to determine how to get your function to conform.


Another suitable replacement function might look like this:
Code: [Select]
// Replacement DrawUnit function
void __stdcall DualTurretGP_DrawUnit(void *graphicsObject)
{
}

Or to get explicit access to the hidden "this" pointer:
Code: [Select]
// Replacement DrawUnit function
void __stdcall DualTurretGP_DrawUnit(void *graphicsObject)
{
void *thisGuardPost;

__asm MOV thisGuardPost, ECX;
}



The next topic will be on how to use the graphics object to do some drawing. We will start this off by looking at how the original DrawUnit functions work, and seeing what we can do to draw what we want.

I would also like to put in a call to graphics artists to design some nice dual turret Guard Post graphics. We will eventually need something to draw, and the game doesn't quite have the graphics we need already built it. Perhaps the existing graphics for vehicle turrets and the existing graphics for guard posts can somehow be put to work. Although, the same weapon tends to look quite different between the vehicle and guard post form. I'm sure anything you guys out there put together will look better than anything I can patch together.
 

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Dual Turret Guard Posts
« Reply #6 on: March 07, 2007, 05:55:14 PM »
Well I've had a bit of time to get back to work on this so I should post an update for a next step soon. I'll probably take a slight detour first and discuss how to make dual turret lynxes and panthers. This will be a little easier to do since the graphics already exist in game. Hopefully this post will also prompt someone to make some nice dual turret GP graphics. (*eyes gpgarretboast* I know you have extracted graphics files ready and waiting!).

I should point out that the difficulty with the guard posts over the lynxes and panthers is more than just the pre-existance of graphics. The internal Graphics drawing functions that the DrawUnit function uses take an index to an "animation" to draw. This index is into the data loaded from the .PRT file. For us to have a valid index into this data, we would have to place the new graphics into that file somehow. Either patch the file itself, or patch things up at runtime after the file is loaded. Which brings us to what Eddy said earlier.

Quote
Isn't it possible to expand OP2_ART with new items ?
And besides .. i'm talking about Renegades: i can have some weapons unavailable, and use those graphics (alter them) to display a dual-turret.

Both options given above about patching the .PRT data (either the file itself before loading, or in memory after loading) are sick for various reasons, so I will avoid both. But before I get to the next alternative I guess I should convince you why.

If you patch the graphics file, then you have a rather large file you must either redistribute for your addon (modem users beware), or some sort of patching utility for them to run before playing. Both are a bit of a pain. Also, if you overwrite graphics, then they must stay the same dimensions and size and you create two essentially incompatible versions of the graphics files, and then people have to worry about maintaining/patching/swapping them around to get the desired one they want when they go to play. This is a big pain for the end user. The addon should be added as transparently as possible, and not require them to redownload many megabytes of data they already have.

If you patch in memory after the load, then you can solve both of the major problems given above. However, if you're patching the data after the load, then the memory was already allocated and is probably tight. Not too much of a problem if you're overwriting graphics and keep the new ones the same size, but it's more of a problem if you want to add new graphics. You could potentially reallocate memory, copy the old data to the new memory, and add your stuff in. But then keep in mind that they've loaded many megabytes of data, and to expand the amount of allocated memory and do the copy, you'll need to have twice as much memory allocated to complete this step. It'll also be a little slow to move that much data around. Mind you, having a temporary copy of of 7MB file or so around isn't too much of a burden for modern computers, but let's avoid the waste anyways.


This brings me to the next idea. Don't use the graphics function that takes an index into the .PRT data. We can just trace into this function and follow it down to lower level functions. At the very worst, we'll figure out how they get access to the graphics buffer and can do the memory copy ourselves. There is no real need to use functions already written inside of OP2. Although, it is easier to do things this way if possible, and in this case it is. So, when we do get to drawing custom made unit graphics into OP2, such as a dual turret GP, we can just use these slightly lower down functions to do the drawing. It also appears we can use internal classes already existing in OP2 to load the graphics from a file into a form suitable for doing this drawing. Which is really nice since that leaves not a whole lot left for us to write ourselves.

So, graphics artists, get those images done!



But, that aside, we can turn to dual turret lynxes and panthers for the moment and use the usual drawing function that takes the index into the .PRT data for the next step. We can just draw the lynx or panther chasis as usual, and then replace the drawing of the single turret with the drawing of the dual turret that tigers use. Well, actually, once we know how the drawing function works, we'll realize we need to change even less. We can cheat just a tad in the case of the turret by changing an even smaller and simpler function. I might show both ways just for completeness though.

But, like I said, this next step is comming soon. So you'll just have to wait in anticipation for at least a few more hours.
 

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Dual Turret Guard Posts
« Reply #7 on: March 08, 2007, 03:38:23 AM »
Ok, so I went hunting down the details of a few loose ends, and came across more pieces of code that made me curious, and went and hunted down some more not quite so related things. [You know how CCs, power plants, and light towers can't be EMPed? Turns out any structure that doesn't require power can't be EMPed. If you edited a structure to remove it's power requirements, it can't be EMPed.] Lots more labels added about the internal structure of the game, but not too much time left now to write up about the DrawUnit function. I'll keep it as a simple overview for now. Nothing practical to implement.


I'll be discussing the following function:
Code: [Select]
00484CB0 >   8>SUB ESP,10                                       ;  Function: Unit:Tank.DrawUnit(???* ???)

I left a few question marks in there since I don't yet know enough about the object to really give it a good name, but it does relate to the viewport, and what area of the game map you can currently see. The parameter is a pointer to an object, hence the "*", but I've left out a name for it for now. It is a rather large object, so it may have many more uses than I've seen so far.


The really braod overview of what this function does is this:
1) Draw the chasis
2) Check if the unit is Stickyfoamed, and if it is
 2a) Draw stickyfoam over the chasis
3) Draw the turret
4) Check if the unit is ESGed, and if it is
 4a) Draw ESG over the unit
5) Check if the unit is EMPed, and if it is
 5a) Draw EMP over the unit

There are of course many more details as to how this is done. The light level must be calculated so the graphics get darker in night areas, but visibility is also affected by allied status. You are always set allied to yourself internally. The turret animationIndex must also change depending on the rotation of the turret, and stickyfoam, ESG, and EMP can't be draw if the unit is in the process of docking.

There is also the issue of drawing unit shadows. Mind you, this is not actually done in this function. This function does however set a boolean global variable as to whether or not Mobile Shadows should be drawn. This value is just copied from one global variable to another global variable right at the start of the function.

Much of the code in this function deals with getting the right animationIndex and frameIndex of the graphic to display at each step. There is also a chunk of code that deals with determining a light level based on the unit's X coordinate, and the current position of the band of daylight. All of these things need to be done to draw the graphics properly.

The most interesting part for our purposes though, is the following line:
Code: [Select]
Unit:Tank.GetTurretGraphic(Unit *baseUnit, char rotation)

This calls a function through the Unit's virtual function table that determines which turret animationIndex to use based on the rotation of the turret. The rotation values ranges from 0-255, where 0 is facing right/east, and it increasing going counterclockwise, and for every 16 units (22.5 degrees), there is a new graphic (animationIndex) for the turret. If you ever look into the .PRT/.BMP graphics, you'll see different groups of images, some of which are just the turrets or vehicles facing various angles.


The shortcut I alluded to earlier is to replace only this function (or a subfunction of this function) so that a different turret is returned. In that case we avoid having to do all the light level and frame index calculations in a replacement function. This function is also a lot smaller, which usually makes it easier to understand.

Now the point of call doesn't exactly tell you what function is getting called, since the destination of a virtual function call isn't known until runtime. In fact there are at least 3 possible functions that can be called from that point. They correspond to specific instances of that function for Lynxes, Panthers, and Tigers. Here is the address of the function for Lynxes as an example:

Code: [Select]
004855C0 > . 8>MOV EDX,DWORD PTR SS:[ESP+8]                     ;  Function: Unit:Tank:Lynx.GetTurretGraphic(Unit *baseUnit, char rotation)  [Returns info in global variables]

If you check out that address in a debugger, you'll see that the function fits nicely onto one screen. This function passes the work off to the UnitTypeInfo object for the Weapon type of the turret. That class is responsible for returning the correct animationIndex for any given rotation value. Note that although a new graphic is only used for every 22.5 degrees, the UnitTypeInfo is the one that determines this. It is free to return whatever graphic it wants for any rotation from 0-255. It just happens to round to the nearest multiple of 16 (22.5 degrees), and use that corresponding graphic. If you wanted to implement smoother rotation, you could override this function to return more graphics (if you also provided those graphics).


Also note that the turret animationIndex is not stored anywhere in the Unit class. This is one reason why we are bothering with all this in the first place. If we could have just changed the value of a variable from the single turret animationIndex to a dual turret animationIndex we'd have done. It would be much simpler. Unless of course we didn't know where to find such a variable if it existed, in which case analysing this function would have told us where it was. Instead it confirms that there isn't such a variable. Also, by the time we do reach any variables of similar nature that may be of interest, they are global to all units and all players. Thus changing them would change the graphics for all units of that type for all players. This doesn't help much if you're trying to selectively "upgrade" units for one player, possibly based on tech research, or add new unit types without removing existing unit types.


Next up will hopefully be a practical example giving sample code to make a Lynx behave and appear to have a dual turret. But, I guess we won't really know until I get the time to write it.
 

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Dual Turret Guard Posts
« Reply #8 on: October 14, 2007, 11:48:40 AM »
Well Sirbomber's been poking me about this thread which I'd sort of forgotten about. So, here's a next part.


Could still use some good graphics for dual turret Guard Posts, plus to use them properly I'd need a bit more info about the lower level graphics drawing functions, which I never quite completed looking into. So, let's do something a little easier, and something we already have graphics for.


Dual Turret Panthers
------------------------

Well, like I suggested in the last post, I replaced the GetTurretAnimationIndex function on the unit class for panthers. I basically rewrote the function, and put in some additional checking so it would test the DualTurret flag in the unit flags field to determine which turret type to display.

I then replaced that one entry in the panther unit's virtual function table. Any panther with this flag set will appear as a dual turret panther, and any with the flag clear will appear as a normal single turret panther. Basically I wrote the function so the graphics now follow the behavior. It also doesn't restrict people to using only the newer dual turret ones in a level, so they are free to mix and match them.


The test level contains two panthers, one with a dual turret, and one with a single turret. It also contains the earlier stuff from the dual turret GP test. (I commented out the code to replace the drawing function, since it was never complete and made the GP invisible). The GP also functions like it's dual turret, but of course the graphics haven't been changed. There are 3 Lynxes that charge the GP location.

Hopefully this might make panthers a little more useful and worth building. They will now have the same offensive power as tigers, but faster moving and with less armour. As a side note, you can easily make the dual turret panther into a dual turret lynx, as the code will work there just as well. But, making the lynxes dual turret seems to unbalance things further, so I figure panthers are a better idea to make dual turret. (You could also potentially make single turret tigers by clearing the dual turret flag and using the replacement function).


Further Work
---------------
Tie this into a tech tree with a research that enables the dual turret units. Currently it's manually done after creating the unit using TethysGame::CreateUnit. This can be done in a number of ways though and at pretty much any time, although I don't currently know of a way to set the dual turret flag on units produced at a vehicle factory the isntant they appear. You could hack something with time triggers, where they were scanned for periodically and upgraded, but it'd be noticable, so that's a really sick hack.


Bugs
------

Just thought of something. I was a bit lazy in testing and getting it working, and it occurs to me now that I never restored the virtual function table when the level ends. (The way I did it in the level modifies the original one). If you try to play another level after this one that uses a different DLL, the game will probably crash.


Source
--------

Here is the source for the dual turret panther test. I have the source split between two files.

Main.cpp
----------
Code: [Select]
#define WIN32_LEAN_AND_MEAN  // Exclude rarely-used stuff from Windows headers
#include <windows.h>

// Include important header files needed to interface with Outpost 2
#include "..\Outpost2DLL\Outpost2DLL.h"


void* SetupDualTurretGP();
void SetDualTurretFlag(int unitIndex);
void SetUnitVtbl(int unitIndex, void* newVtbl);
void SetUnitVtblEntry(int unitIndex, int entryIndex, void* funcAddr);
bool SetVtblEntry(void* vtblAddr, int entryIndex, void* funcAddr);
void __stdcall GetTankTurretAnimationIndex(void* baseUnit, int rotation);
void __stdcall DrawDualTurretGPUnit(void *graphicsObject);


// Required level exports
char MapName[]   = "cm01.map";       // The .map file used for this level
char LevelDesc[]  = "Dual Turret GP Test Level";   // Description appearing in the game list box
char TechtreeName[]  = "Multitek.txt";      // File to use for the tech tree
SDescBlock DescBlock = { Colony, 2, 12, false };    // Important level details (GameType, NumPlayers, MaxTechLevel, UnitOnlyMission)


BOOL APIENTRY DllMain(HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
  DisableThreadLibraryCalls((HMODULE)hModule);
}

    return TRUE;
}


int InitProc()
{
Unit unit;
Unit guardPost;

void* dualTurretGPvftbl = SetupDualTurretGP();

// Make sure graphics look right
Player[0].GoEden();
Player[1].GoEden();

// Create a guard post
TethysGame::CreateUnit(guardPost, mapGuardPost, LOCATION(100,40), 0, mapLaser, 0);
// Create a power plant to power the guard post
TethysGame::CreateUnit(unit, mapTokamak, LOCATION(90, 40), 0, mapNone, 0);

// Set the Dual Turret flag on the guard post
SetDualTurretFlag(guardPost.unitID);
// Set a new vtbl for the Dual Turret GP
//SetUnitVtbl(guardPost.unitID, dualTurretGPvftbl);

// Create some enemy units
TethysGame::CreateUnit(unit, mapLynx, LOCATION(110,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(116,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(122,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in

// A refrence unit for you
TethysGame::CreateUnit(unit, mapLynx, LOCATION(100,20), 0, mapLaser, 0);

// Create two Panthers, one dual turret, one not
TethysGame::CreateUnit(unit, mapPanther, LOCATION(104, 34), 0, mapLaser, 2);
TethysGame::CreateUnit(unit, mapPanther, LOCATION(104, 34), 0, mapLaser, 2);
// Modify the second panther to be dual turret
SetDualTurretFlag(unit.unitID);
// Modify the vtbl (for all panthers) to handle dual turret graphics
SetUnitVtblEntry(unit.unitID, 5, &GetTankTurretAnimationIndex);

// Center the view on the action
Player[0].CenterViewOn(110, 40);

TethysGame::SetDaylightEverywhere(true);

return 1; // return 1 if OK; 0 on failure
}


void AIProc()
{
}

void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = 0; // Pointer to a buffer that needs to be saved
bufDesc.length = 0;   // sizeof(buffer)
}

int StatusProc()
{
return 0; // must return 0
}

SCRIPT_API void NoResponseToTrigger()
{
}


DualTurretUnit.cpp
---------------------
Code: [Select]
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <memory.h>


void* SetupDualTurretGP();
void SetDualTurretFlag(int unitIndex);
void SetUnitVtbl(int unitIndex, void* newVtbl);
void SetUnitVtblEntry(int unitIndex, int entryIndex, void* funcAddr);
bool SetVtblEntry(void* vtblAddr, int entryIndex, void* funcAddr);
void __stdcall GetTankTurretAnimationIndex(void* baseUnit, int rotation);
void __stdcall DrawDualTurretGPUnit(void *graphicsObject);





// New DualTurretGP virtual function table
const int GPVtblSize = 65;
void* dualTurretGPvftbl[GPVtblSize];


void* SetupDualTurretGP()
{
// Copy the vtbl we want to edit
memcpy(&dualTurretGPvftbl, (void*)0x004CFFB0, sizeof(dualTurretGPvftbl));
// Update the DrawUnit function pointer
dualTurretGPvftbl[6] = &DrawDualTurretGPUnit;

// Return the new virtual function table
return dualTurretGPvftbl;
}


void SetDualTurretFlag(int unitIndex)
{
int* unitArray = (int*)0x54F848;
char* unitData = (char*)(((char*)(*unitArray)) + 120 * unitIndex); // Get the Unit address

// Set the Dual Turret flag
*(int*)(unitData + 0x44) |= 0x20;
}


void SetUnitVtbl(int unitIndex, void* newVtbl)
{
int* unitArray = (int*)0x54F848;
int* unitData = (int*)(((char*)(*unitArray)) + 120 * unitIndex); // Get the Unit address

// Set the new vtbl
*unitData = (int)newVtbl;
}


void SetUnitVtblEntry(int unitIndex, int entryIndex, void* funcAddr)
{
int* unitArray = (int*)0x54F848;
int* unitVtbl = (*(int**)(((char*)(*unitArray)) + 120 * unitIndex)); // Load the Unit vtbl pointer

// Modify the unit vtbl entry
SetVtblEntry(unitVtbl, entryIndex, funcAddr);
}


bool SetVtblEntry(void* vtblAddr, int entryIndex, void* funcAddr)
{
DWORD oldProtection;
DWORD dummyProtection;
void* entryAddress = &(((void**)vtblAddr)[entryIndex]);

// Unprotect the memory
if (VirtualProtect(entryAddress, 4, PAGE_READWRITE, &oldProtection))
{
  // Modify the vtbl entry
  ((int*)vtblAddr)[entryIndex] = (int)funcAddr;

  // Reprotect the memory
  VirtualProtect(entryAddress, 4, oldProtection, &dummyProtection);

  return true;
}

return false;
}


// Replacement member function for Unit:Tank
void __stdcall GetTankTurretAnimationIndex(void* weaponUnit, int rotation)
{
// Unit values
void* baseUnit;
__asm MOV [baseUnit], ECX  // Store hidden "this" pointer
int weaponType;
int flags;
int chasisRotation;
int rotationIndex;
// UnitTypeInfo values
int* unitTypeInfoPtrArray = (int*)0x4E1348;
void* weaponUnitTypeInfo;
void* getTurretAnimationFuncAddr;
// Graphics values
void* graphics = (void*)0x4EFD68;
void* getOffsetFuncAddr = (void*)0x404F00;
// Globals
int* animationOffsetX = (int*)0x4EA7BC;
int* animationOffsetY = (int*)0x4EA7C0;
int offsetX;
int offsetY;

// Get the weapon type
weaponType = *(short*)((char*)baseUnit + 0x24);
// Get the flags
flags = *(int*)((char*)baseUnit + 0x44);
// Get the chasis rotation
chasisRotation = *((char*)baseUnit + 0x1C);
rotationIndex = (chasisRotation + 8) / 16;  // Round to nearest multiple of 16 (22.5 degrees)

// Get the weapon UnitTypeInfo
weaponUnitTypeInfo = (void*)unitTypeInfoPtrArray[weaponType];
// Get the weapon type Get<Chasis>TurretAnimationIndex function address
if (flags & 0x20) // Check for double fire rate
{
  // Get the address of GetTigerTurretAnimationIndex
  getTurretAnimationFuncAddr = (void*)(*(int*)((char*)(*(int*)weaponUnitTypeInfo) + 0x20));
}
else
{
  // Get the address of GetPantherTurretAnimationIndex
  getTurretAnimationFuncAddr = (void*)(*(int*)((char*)(*(int*)weaponUnitTypeInfo) + 0x24));
}

// Get the animation index (set into a global)
__asm
{
  MOV EAX, rotation
  MOV EDX, baseUnit
  MOV ECX, weaponUnitTypeInfo   // this*  [UnitTypeInfo:Weapon*]
  PUSH EAX       // Arg2 - int rotation
  PUSH EDX       // Arg1 - Unit* baseUnit
  CALL [getTurretAnimationFuncAddr] // UnitTypeInfo.Get<Chasis>TurretAnimation(Unit* baseUnit, int rotation)
}

// Get the animation offsets of the chasis
__asm
{
  // Get the Chasis animation index
  MOV EDX, rotationIndex
  PUSH EDX     // Arg1 - int rotationIndex
  MOV ECX, baseUnit   // this*  [Unit*]
  MOV EAX, [ECX]    // Unit.vtbl
  CALL [EAX + 0x80]   // Unit.GetChasisAnimationIndex(int rotationIndex)

  // Get the animation offset of the chasis
  PUSH 0      // Arg2 - int frameIndex
  PUSH EAX     // Arg1 - int animationIndex
  MOV ECX, graphics   // this*  [Graphics*]
  CALL [getOffsetFuncAddr] // Graphics.GetAnimationFrameOffsets(int animationIndex)

  // Store to locals
  MOVSX ECX, AL
  MOVSX EAX, AH
  MOV [offsetX], ECX
  MOV [offsetY], EAX
}

// Update the global variable with the offsets
*animationOffsetX += offsetX;
*animationOffsetY += offsetY;
}



// Replacement member function for Unit:GuardPost
void __stdcall DrawDualTurretGPUnit(void *graphicsObject)
{
void *thisGuardPost;

__asm MOV thisGuardPost, ECX;
}



I'll also attach a working DLL for other people that want to play with it.
« Last Edit: October 14, 2007, 12:02:49 PM by Hooman »

Offline Arklon

  • Administrator
  • Hero Member
  • *****
  • Posts: 1269
Dual Turret Guard Posts
« Reply #9 on: December 01, 2007, 04:25:05 PM »
I modified the source a bit so that loading a different mission after loading the dual turret units one will no longer crash the game (assuming the exe doesn't get relocated in memory). I've also turned the reference tiger into a single turret tiger. :P

Main.cpp
--------------
Code: [Select]
#define WIN32_LEAN_AND_MEAN  // Exclude rarely-used stuff from Windows headers
#include <windows.h>

// Include important header files needed to interface with Outpost 2
#include <Outpost2DLL.h>


void* SetupDualTurretGP();
void SetDualTurretFlag(int unitIndex);
void ClearDualTurretFlag(int unitIndex);
void SetUnitVtbl(int unitIndex, void* newVtbl);
void SetUnitVtblEntry(int unitIndex, int entryIndex, void* funcAddr);
bool SetVtblEntry(void* vtblAddr, int entryIndex, void* funcAddr);
void __stdcall GetTankTurretAnimationIndex(void* baseUnit, int rotation);
void __stdcall DrawDualTurretGPUnit(void *graphicsObject);


// Required level exports
char MapName[]   = "cm01.map";       // The .map file used for this level
char LevelDesc[]  = "Dual Turret GP Test Level";   // Description appearing in the game list box
char TechtreeName[]  = "Multitek.txt";      // File to use for the tech tree
SDescBlock DescBlock = { Colony, 2, 12, false };    // Important level details (GameType, NumPlayers, MaxTechLevel, UnitOnlyMission)


BOOL APIENTRY DllMain(HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
 DisableThreadLibraryCalls((HMODULE)hModule);
 SetVtblEntry((int*)0x004D0880, 5, &GetTankTurretAnimationIndex); // Patch lynx vtbl entry to new func addr
 SetVtblEntry((int*)0x004D0938, 5, &GetTankTurretAnimationIndex); // Patch panther vtbl entry to new func addr
 SetVtblEntry((int*)0x004D05C0, 5, &GetTankTurretAnimationIndex); // Patch tiger vtbl entry to new func addr
} else if (ul_reason_for_call == DLL_PROCESS_DETACH) {
 SetVtblEntry((int*)0x004D0880, 5, (void*)0x004855C0); // Patch lynx vtbl entry to original value
 SetVtblEntry((int*)0x004D0938, 5, (void*)0x004857A0); // Patch panther vtbl entry to original value
 SetVtblEntry((int*)0x004D05C0, 5, (void*)0x00485950); // Patch tiger vtbl entry to original value
}

   return TRUE;
}


int InitProc()
{
Unit unit;
Unit guardPost;

void* dualTurretGPvftbl = SetupDualTurretGP();

// Make sure graphics look right
Player[0].GoEden();
Player[1].GoEden();

// Create a guard post
TethysGame::CreateUnit(guardPost, mapGuardPost, LOCATION(100,40), 0, mapLaser, 0);
// Create a power plant to power the guard post
TethysGame::CreateUnit(unit, mapTokamak, LOCATION(90, 40), 0, mapNone, 0);

// Set the Dual Turret flag on the guard post
SetDualTurretFlag(guardPost.unitID);
// Set a new vtbl for the Dual Turret GP
//SetUnitVtbl(guardPost.unitID, dualTurretGPvftbl);

// Create some enemy units
TethysGame::CreateUnit(unit, mapLynx, LOCATION(110,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(116,40), 1, mapLaser, 0);
unit.DoMove(LOCATION(100,40));  // Send it in
TethysGame::CreateUnit(unit, mapLynx, LOCATION(122,40), 1, mapThorsHammer, 0);
// Modify the third lynx to be dual turret
SetDualTurretFlag(unit.unitID);
unit.DoMove(LOCATION(100,40));  // Send it in

// A not really refrence unit for you
TethysGame::CreateUnit(unit, mapTiger, LOCATION(100,20), 0, mapLaser, 0);
// Modify the tiger to be single turret
ClearDualTurretFlag(unit.unitID);

// Create two Panthers, one dual turret, one not
TethysGame::CreateUnit(unit, mapPanther, LOCATION(104, 34), 0, mapThorsHammer, 2);
TethysGame::CreateUnit(unit, mapPanther, LOCATION(104, 34), 0, mapThorsHammer, 2);
// Modify the second panther to be dual turret
SetDualTurretFlag(unit.unitID);

// Center the view on the action
Player[0].CenterViewOn(110, 40);

TethysGame::SetDaylightEverywhere(true);

return 1; // return 1 if OK; 0 on failure
}


void AIProc()
{
}

void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = 0; // Pointer to a buffer that needs to be saved
bufDesc.length = 0;   // sizeof(buffer)
}

int StatusProc()
{
return 0; // must return 0
}

SCRIPT_API void NoResponseToTrigger()
{
}

DualTurretUnit.cpp
--------------
Code: [Select]
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <memory.h>


void* SetupDualTurretGP();
void SetDualTurretFlag(int unitIndex, bool foo);
void ClearDualTurretFlag(int unitIndex);
void SetUnitVtbl(int unitIndex, void* newVtbl);
void SetUnitVtblEntry(int unitIndex, int entryIndex, void* funcAddr);
bool SetVtblEntry(void* vtblAddr, int entryIndex, void* funcAddr);
void __stdcall GetTankTurretAnimationIndex(void* baseUnit, int rotation);
void __stdcall DrawDualTurretGPUnit(void *graphicsObject);



// New DualTurretGP virtual function table
const int GPVtblSize = 65;
void* dualTurretGPvftbl[GPVtblSize];


void* SetupDualTurretGP()
{
// Copy the vtbl we want to edit
memcpy(&dualTurretGPvftbl, (void*)0x004CFFB0, sizeof(dualTurretGPvftbl));
// Update the DrawUnit function pointer
dualTurretGPvftbl[6] = &DrawDualTurretGPUnit;

// Return the new virtual function table
return dualTurretGPvftbl;
}


void SetDualTurretFlag(int unitIndex)
{
int* unitArray = (int*)0x54F848;
char* unitData = (char*)(((char*)(*unitArray)) + 120 * unitIndex); // Get the Unit address

*(int*)(unitData + 0x44) |= 0x20;
}

void ClearDualTurretFlag(int unitIndex)
{
int* unitArray = (int*)0x54F848;
char* unitData = (char*)(((char*)(*unitArray)) + 120 * unitIndex); // Get the Unit address

*(int*)(unitData + 0x44) &= ~0x20;
}


void SetUnitVtbl(int unitIndex, void* newVtbl)
{
int* unitArray = (int*)0x54F848;
int* unitData = (int*)(((char*)(*unitArray)) + 120 * unitIndex); // Get the Unit address

// Set the new vtbl
*unitData = (int)newVtbl;
}


void SetUnitVtblEntry(int unitIndex, int entryIndex, void* funcAddr)
{
int* unitArray = (int*)0x54F848;
int* unitVtbl = (*(int**)(((char*)(*unitArray)) + 120 * unitIndex)); // Load the Unit vtbl pointer

// Modify the unit vtbl entry
SetVtblEntry(unitVtbl, entryIndex, funcAddr);
}


bool SetVtblEntry(void* vtblAddr, int entryIndex, void* funcAddr)
{
DWORD oldProtection;
DWORD dummyProtection;
void* entryAddress = &(((void**)vtblAddr)[entryIndex]);

// Unprotect the memory
if (VirtualProtect(entryAddress, 4, PAGE_READWRITE, &oldProtection))
{
 // Modify the vtbl entry
 ((int*)vtblAddr)[entryIndex] = (int)funcAddr;

 // Reprotect the memory
 VirtualProtect(entryAddress, 4, oldProtection, &dummyProtection);

 return true;
}

return false;
}


// Replacement member function for Unit:Tank
void __stdcall GetTankTurretAnimationIndex(void* weaponUnit, int rotation)
{
// Unit values
void* baseUnit;
__asm MOV [baseUnit], ECX  // Store hidden "this" pointer
int weaponType;
int flags;
int chasisRotation;
int rotationIndex;
// UnitTypeInfo values
int* unitTypeInfoPtrArray = (int*)0x4E1348;
void* weaponUnitTypeInfo;
void* getTurretAnimationFuncAddr;
// Graphics values
void* graphics = (void*)0x4EFD68;
void* getOffsetFuncAddr = (void*)0x404F00;
// Globals
int* animationOffsetX = (int*)0x4EA7BC;
int* animationOffsetY = (int*)0x4EA7C0;
int offsetX;
int offsetY;

// Get the weapon type
weaponType = *(short*)((char*)baseUnit + 0x24);
// Get the flags
flags = *(int*)((char*)baseUnit + 0x44);
// Get the chasis rotation
chasisRotation = *((char*)baseUnit + 0x1C);
rotationIndex = (chasisRotation + 8) / 16;  // Round to nearest multiple of 16 (22.5 degrees)

// Get the weapon UnitTypeInfo
weaponUnitTypeInfo = (void*)unitTypeInfoPtrArray[weaponType];
// Get the weapon type Get<Chasis>TurretAnimationIndex function address
if (flags & 0x20) // Check for double fire rate
{
 // Get the address of GetTigerTurretAnimationIndex
 getTurretAnimationFuncAddr = (void*)(*(int*)((char*)(*(int*)weaponUnitTypeInfo) + 0x20));
}
else
{
 // Get the address of GetPantherTurretAnimationIndex
 getTurretAnimationFuncAddr = (void*)(*(int*)((char*)(*(int*)weaponUnitTypeInfo) + 0x24));
}

// Get the animation index (set into a global)
__asm
{
 MOV EAX, rotation
 MOV EDX, baseUnit
 MOV ECX, weaponUnitTypeInfo   // this*  [UnitTypeInfo:Weapon*]
 PUSH EAX       // Arg2 - int rotation
 PUSH EDX       // Arg1 - Unit* baseUnit
 CALL [getTurretAnimationFuncAddr] // UnitTypeInfo.Get<Chasis>TurretAnimation(Unit* baseUnit, int rotation)
}

// Get the animation offsets of the chasis
__asm
{
 // Get the Chasis animation index
 MOV EDX, rotationIndex
 PUSH EDX     // Arg1 - int rotationIndex
 MOV ECX, baseUnit   // this*  [Unit*]
 MOV EAX, [ECX]    // Unit.vtbl
 CALL [EAX + 0x80]   // Unit.GetChasisAnimationIndex(int rotationIndex)

 // Get the animation offset of the chasis
 PUSH 0      // Arg2 - int frameIndex
 PUSH EAX     // Arg1 - int animationIndex
 MOV ECX, graphics   // this*  [Graphics*]
 CALL [getOffsetFuncAddr] // Graphics.GetAnimationFrameOffsets(int animationIndex)

 // Store to locals
 MOVSX ECX, AL
 MOVSX EAX, AH
 MOV [offsetX], ECX
 MOV [offsetY], EAX
}

// Update the global variable with the offsets
*animationOffsetX += offsetX;
*animationOffsetY += offsetY;
}



// Replacement member function for Unit:GuardPost
void __stdcall DrawDualTurretGPUnit(void *graphicsObject)
{
void *thisGuardPost;

__asm MOV thisGuardPost, ECX;
}
« Last Edit: December 01, 2007, 04:30:51 PM by Arklon »

Offline evecolonycamander

  • Hero Member
  • *****
  • Posts: 602
Dual Turret Guard Posts
« Reply #10 on: May 21, 2010, 07:11:41 AM »
i just figured out how to make the dual turnet GP. take the image of the hover car and switch it with a costem image of a GP base. then have the image of a tiger tunet centerd on top. its that simple(not so unless your hooman :P )
''The blight cant get us up here!''
-famous last words
--------------o0o--------------
Outpost 2: EoM project status: Re-planning

Offline Sirbomber

  • Hero Member
  • *****
  • Posts: 3238
Dual Turret Guard Posts
« Reply #11 on: May 21, 2010, 09:31:48 AM »
I'm assuming by hover car you mean the Small-Capacity Air Transport, which just uses the Scout graphics, so there goes that plan.

Besides, how are you going to replace the graphics in the first place?
"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 evecolonycamander

  • Hero Member
  • *****
  • Posts: 602
Dual Turret Guard Posts
« Reply #12 on: May 21, 2010, 09:49:52 AM »
i thout the op2artveiwer.exe could do that as long as you did not ADD to the number of images.
edit: and ive looked. the scout has nothing to due with those images
« Last Edit: May 21, 2010, 09:51:08 AM by evecolonycamander »
''The blight cant get us up here!''
-famous last words
--------------o0o--------------
Outpost 2: EoM project status: Re-planning