Writing file templates using the EXE - Ezekial711/MonsterHunterWorldModding GitHub Wiki

Writing file templates using the EXE (by example)

Prerequisites

  • Ghidra/IDA + an analyzed dump of MHW 15.11 (or newer)
  • Basic knowledge on how to use your RE tool of choice (Like how to navigate around, retype/rename variables and functions in the decompiler)
  • Some basic C knowledge

A fully analyzed dump with (almost) all DTI Types added can be found here. The mhw_15.11.rep_11272022.zip is the latest MHW dump.

How this method works

Basically we're looking at how the game deserializes/parses/reads the file, and write the template based on that. The game uses an object called MtDataReader to parse its files, which has specific methods to read all kinds of data types from a file.

I'm gonna be making a rough template for the dtt_cvc files in this guide.

1 Mapping out the object

First we need to map out the object that corresponds to the file we're looking into. Each file type corresponds to a class in the game. A mapping of file extensions to class name can be found here.

For the dtt_cvc files this class is rEmCombatValueChange.

First let's look for it's constructor. If you're using my ghidra dump you can just type rEmCombatValueChange::ctor into the Goto prompt.

Immediately you should see something akin to the following in the decompiler output: 1

There are 4 interesting things here in total:

  1. The assignment of MtArray::vft to field_0xb8 which immediately lets us know that there is an MtArray at 0xB8, so let's change that accordingly. In ghidra, you can click on the member name (field_0xb8) and press Ctrl+L to change its type. Press L to rename it.
  2. The call to nEmDtTune::cDyingValue::ctor which tells us that at 0xD8 there is an object of that type, so let's change the type of field_0xd8 to nEmDtTune::cDyingValue and rename it to mDyingValue.
  3. The call to nEmDtTune::cCombatSignValue::ctor. Let's do the same here as we did for #2.
  4. Finally the assignment of nEmDtTune::cLotRate::vft to field_0x160 which, once again, tells us that field_0x160 is of that type, so let's retype that and rename it to mLotRate.

After making these changes things should look something like this:

2

Not a huge difference but a bit better. It'll help later tho.

2 Finding the deserializer function

Navigate to the virtual function table of rEmCombatValueChange. (GoTo -> rEmCombatValueChange::vft in ghidra)

Offset 0x50 from the VTable of any resource class (the ones starting with a lowercase 'r') is its deserializer. 3

When first looking at the function things might not be very pretty at first and might look something like this: 4

So let's make some adjustments here.

  1. Change the type of param_1 to rEmCombatValueChange * and its name to this.
  2. Change the return value of the function to bool, since all deserializers return true/false based on success.
  3. Optionally change the name of the function to rEmCombatValueChange::Deserialize.

Now things should look a bit cleaner.

3 Figuring out the structure

3.1 Header

5 The first things we see at the start of the function is the game creating an MtDataReader (I called it MtByteReader but whatever), then reading a u32 from the file and comparing it to 0x18091001 which is the common IB signature. If you've dealt with files in this game before you've probable seen it before. Next it reads another u32 and compares it to 0x16 this is the file version. Next it reads the EmHeader which consists of 2 u32s, the monster ID and some unused value.

From this we can gather that the file header looks like this:

typedef struct Header {
    uint signature;
    uint version;
    uint monster;
    uint unused;
} Header;

3.2 Cleanup

The deserializer only continues if signature and version match. The next part seems to clear any existing entries in the MtArray at 0xB8. 6

Line 23 to 30 loops through each entry in the array, line 25 checks if the object is valid, (i.e. not NULL), and then the object gets destructed (and consequently free'd) on line 26 by a call to its destructor.

Lines 32 to 34 clear the existing array buffer by calling its allocators Free method. Then the array buffer and array length are set to 0 to finish the cleanup.

Now comes the actual deserialization process.

3.3 Deserialization

7 At line 38 it reads a u32 from the file which represents the number of entries in the 0xB8 array. On line 42 it then calls MtArray::Reserve to allocate enough space for that many entries.

This means that the very first uint in the file after the header is an entry count.

Then it loops n times (where n is the entry count) and creates a new cEmCombatValueChange object for each entry. First it allocates space for it at line 47. Then it passes the returned pointer to the constructor at line 50.

From that we can gather that the structure there looks something like this:

uint count;
cEmCombatValueChange entries[count];

Then, the code that we're interested in, the deserialization of the object, starts at line 52. It calls the cEmCombatValueChange::Deserialize method on the newly created object. This method is the one we're interested in.

First however we'll take a quick look at the cEmCombatValueChange class. It's constructor looks like this: 8

Like before, we can make some adjustments to make things look cleaner:

  1. Change the type of field_0x18 to MtArray
  2. Change the type of field_0x38 to nEmDtTune::cEmDistanceRate[2] as there seems to be an inlined array of 2 of those objects.
  3. Change the type of field_0xa0 to nEmDtTune::cEmDistanceRate[2] as well.
  4. Change the type of field_0x8 to float.
  5. Change the types of fields 0x8c, 0x90, 0x94 and 0x98 to float.

We'll also quickly take a look at nEmDtTune::cEmDistanceRate::ctor: 9

Not much to do here except change the type of field_0x8 to MtArray.

After making all of those adjustments the ctor of cEmCombatValueChange should look like this: 10

Now let's take a look at the deserializer of cEmCombatValueChange: The game calls cEmCombatValueChange::vft+0x28 so let's go to that function:

11

12

Things don't exactly look pretty here, so let's do some cleanup:

  1. Change the type of param_1 to cEmCombatValueChange * and its name to this.
  2. Change the return value of the function to bool.
  3. Change the type of param_2 to MtByteReader * and its name to reader.

Already things should look a lot more readable.

Now you should see that the game reads 3 floats from the file and stores them in fields 0x8, 0xC and 0x10. These 3 fields are therefore floats.

We can begin the structure of cEmCombatValueChange by adding the following to the template:

typedef struct cEmCombatValueChange {
    float unk0;
    float unk1;
    float unk2;
} cEmCombatValueChange;

Now at lines 23 to 38 we see the already familiar array cleanup again just like in the previous function: 13

Then comes the actual deserialization:

14

On line 39 it reads the number of entries in the array and on line 43 it calls MtArray::Reserve to allocate enough space for that many entries, just like before.

Then it once again loops that many times and creates new objects, this time of the type nEmDtTune::cFindCondition, and then deserializes them on line 48. The rest of the loop is just for inserting the object into the array.

Before we look at the deserializer for nEmDtTune::cFindCondition we'll take a look at the rest of the function so we can finally get some structure into our template: 15

Lines 64 and 65 call the deserializer for nEmDtTune::cEmDistanceRate, just like lines 78 and 79. Lines 66 to 77 read a total of 6 floats from the file and store them in fields 0x88, 0x8c, 0x90, 0x94, 0x98 and 0x9c. These fields are therefore floats. From this we can gather that the structure of cEmCombatValueChange looks like this:

typedef struct cFindCondition {
    // TODO
} cFindCondition;

typedef struct cEmDistanceRate {
    // TODO
} cEmDistanceRate;

typedef struct cEmCombatValueChange {
    float unk0;
    float unk1;
    float unk2;
    uint count;
    cFindCondition mFIndConditions[count];
    cEmDistanceRate mDistanceRates1[2];
    float unk4[6];
    cEmDistanceRate mDistanceRates2[2];
} cEmCombatValueChange;

So we can insert this into our template.

Now let's take a look at the constructor of nEmDtTune::cFindCondition. For that we look at the function call on line 47. 16

puVar2 is the actual object which gets allocated and then initialized. field_0x8 is of type MtEnum and field_0x18 is a float.

Once again let's navigate to the deserializer of nEmDtTune::cFindCondition, which can be found at nEmDtTune::cFindCondition::vft+0x28:

After a little bit of cleanup the function looks like this:

17

We see an int being read, which is the enum value, and then 2 floats. We can therefore conclude that the structure of nEmDtTune::cFindCondition looks like this:

typedef struct cFindCondition {
    int enumValue; // ConditionType
    float unk0;
    float unk1;
} cFindCondition;

That was a short one, so now let's continue by looking at the deserializer for nEmDtTune::cEmDistanceRate (found at nEmDtTune::cEmDistanceRate::vft+0x28).

I'm not gonna show an image as it looks pretty similar to all the previous deserializers:

  1. It clears the array at field_0x8.
  2. It reads an entry count u32.
  3. It loops that many times and deserializes nEmDtTune::cEmDistanceRate::EmDistRate objects.

Which means the structure of nEmDtTune::cEmDistanceRate looks like this:

typedef struct cEmDistanceRate {
    uint count;
    EmDistRate mDistRates[count];
} cEmDistanceRate;

Pretty straight forward. The actual content is inside EmDistRate. The deserializer can once again be found at nEmDtTune::cEmDistanceRate::EmDistRate::vft+0x28.

It merely contains 2 floats:

18

So the structure of nEmDtTune::cEmDistanceRate::EmDistRate looks like this:

typedef struct EmDistRate {
    float unk0;
    float unk1;
} EmDistRate;

Lots of unknowns I know.

And that's cEmCombatValueChange done. The updated structure looks like this:

typedef struct cFindCondition {
    int enumValue; // ConditionType
    float unk0;
    float unk1;
} cFindCondition;

typedef struct EmDistRate {
    float unk0;
    float unk1;
} EmDistRate;

typedef struct cEmDistanceRate {
    uint count;
    EmDistRate mDistRates[count];
} cEmDistanceRate;

typedef struct cEmCombatValueChange {
    float unk0;
    float unk1;
    float unk2;
    uint count;
    cFindCondition mFindConditions[count];
    cEmDistanceRate mDistanceRates1[2];
    float unk3[6];
    cEmDistanceRate mDistanceRates2[2];
} cEmCombatValueChange;

Unfortunately we're not done yet. We still have 3 classes to go through:

  • nEmDtTune::cDyingValue
  • nEmDtTune::cCombatSignValue
  • nEmDtTune::cLotRate

Looking at the deserializer for rEmCombatValueChange we can see that it calls the deserializer for nEmDtTune::cDyingValue at nEmDtTune::cDyingValue::vft+0x28 on line 66: 19

The deserializer looks as follows:

20

We can see that reads 5 floats.

But before that there are 2 calls to MtByteReader::Deserialize<nEmDtTune::cDyingRate>. Let's take a look. 21

It looks very similar to other functions we've seen before.

Lines 12 to 26 clean up the array. Line 27 reads an entry count.

Then it loops and deserializes that many nEmDtTune::cDyingRate objects. So let's quickly create some skeleton template code for nEmDtTune::cDyingValue:

typedef struct cDyingRate {
    // TODO
} cDyingRate;

typedef struct cDyingValue {
    uint count1;
    cDyingRate mDyingRates1[count1];
    uint count2;
    cDyingRate mDyingRates2[count2];
    float unk0;
    float unk1;
    float unk2;
    float unk3;
    float unk4;
} cDyingValue;

Now let's take a look at the deserializer for nEmDtTune::cDyingRate:

22

It simply reads 2 floats and a u32. So the structure of nEmDtTune::cDyingRate looks like this:

typedef struct cDyingRate {
    float unk0;
    uint unk1;
    float unk2;
} cDyingRate;

And that's nEmDtTune::cDyingValue done. Next let's move on to nEmDtTune::cCombatSignValue:

23

You're probably noticing a pattern here by now. This is how a lot of the DtTune classes are structured. We have yet another array here, this time of type nEmDtTune::cCombatSign.

Let's take a look at the deserializer for nEmDtTune::cCombatSignValue:

18

Funnily enough, we've come accross this exact same deserializer before. It's the same as the one for nEmDtTune::cEmDistanceRate::EmDistRate. So we can conclude that the structure of nEmDtTune::cCombatSignValue looks like this:

typedef struct cCombatSign {
    float unk0;
    float unk1;
} cCombatSign;

typedef struct cCombatSignValue {
    uint count;
    cCombatSign mCombatSigns[count];
} cCombatSignValue;

And that's nEmDtTune::cCombatSignValue done. The last class we need to look at is nEmDtTune::cLotRate:

24

Fortunately a very simple function. It simply reads a float and a u32. So the structure of nEmDtTune::cLotRate looks like this:

typedef struct cLotRate {
    float unk0;
    uint unk1;
} cLotRate;

And that's all the classes we need to look at. The final structure of rEmCombatValueChange looks like this:

typedef struct rEmCombatValueChange {
    uint count;
    cEmCombatValueChange mCombatValueChanges[count];
    cDyingValue mDyingValue;
    cCombatSignValue mCombatSignValue;
    cLotRate mLotRate;
} rEmCombatValueChange;

4 Putting it all together

The full template should now look like this:

typedef struct Header {
    uint signature;
    uint version;
    uint monster;
    uint unused;
} Header;

typedef struct cFindCondition {
    int enumValue; // ConditionType
    float unk0;
    float unk1;
} cFindCondition;

typedef struct EmDistRate {
    float unk0;
    float unk1;
} EmDistRate;

typedef struct cEmDistanceRate {
    uint count;
    EmDistRate mDistRates[count]<optimize=false>;
} cEmDistanceRate;

typedef struct cEmCombatValueChange {
    float unk0;
    float unk1;
    float unk2;
    uint count;
    cFindCondition mFindConditions[count]<optimize=false>;
    cEmDistanceRate mDistanceRates1[2]<optimize=false>;
    float unk3[6];
    cEmDistanceRate mDistanceRates2[2]<optimize=false>;
} cEmCombatValueChange;

typedef struct cDyingRate {
    float unk0;
    uint unk1;
    float unk2;
} cDyingRate;

typedef struct cDyingValue {
    uint count1;
    cDyingRate mDyingRates1[count1]<optimize=false>;
    uint count2;
    cDyingRate mDyingRates2[count2]<optimize=false>;
    float unk0;
    float unk1;
    float unk2;
    float unk3;
    float unk4;
} cDyingValue;

typedef struct cCombatSign {
    float unk0;
    float unk1;
} cCombatSign;

typedef struct cCombatSignValue {
    uint count;
    cCombatSign mCombatSigns[count]<optimize=false>;
} cCombatSignValue;

typedef struct cLotRate {
    float unk0;
    uint unk1;
} cLotRate;

typedef struct rEmCombatValueChange {
    uint count;
    cEmCombatValueChange mCombatValueChanges[count]<optimize=false>;
    cDyingValue mDyingValue;
    cCombatSignValue mCombatSignValue;
    cLotRate mLotRate;
} rEmCombatValueChange;

Header header;
rEmCombatValueChange file;

You might have noticed that I added a bunch of <optimize=false> to the template. You can read up on what this does here. But just remember that you should generally use this on any array of structs.

5 Conclusion

And that's it. I realized after starting that I may have chosen a bit of a convoluted file for this guide but I was already too far in to stop so whatever. This approach works for basically every file. Some files may be quite a bit more complex than this one however.

In any case, if you have any questions about this directly you can contact me on discord. My name is Fexty#4696.