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:
There are 4 interesting things here in total:
- The assignment of
MtArray::vft
tofield_0xb8
which immediately lets us know that there is anMtArray
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. - 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 offield_0xd8
tonEmDtTune::cDyingValue
and rename it tomDyingValue
. - The call to
nEmDtTune::cCombatSignValue::ctor
. Let's do the same here as we did for #2. - Finally the assignment of
nEmDtTune::cLotRate::vft
tofield_0x160
which, once again, tells us thatfield_0x160
is of that type, so let's retype that and rename it tomLotRate
.
After making these changes things should look something like this:
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.
When first looking at the function things might not be very pretty at first and might look something like this:
So let's make some adjustments here.
- Change the type of
param_1
torEmCombatValueChange *
and its name tothis
. - Change the return value of the function to
bool
, since all deserializers return true/false based on success. - 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
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 u32
s, 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.
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
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:
Like before, we can make some adjustments to make things look cleaner:
- Change the type of
field_0x18
toMtArray
- Change the type of
field_0x38
tonEmDtTune::cEmDistanceRate[2]
as there seems to be an inlined array of 2 of those objects. - Change the type of
field_0xa0
tonEmDtTune::cEmDistanceRate[2]
as well. - Change the type of
field_0x8
to float. - Change the types of fields 0x8c, 0x90, 0x94 and 0x98 to
float
.
We'll also quickly take a look at nEmDtTune::cEmDistanceRate::ctor
:
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:
Now let's take a look at the deserializer of cEmCombatValueChange
:
The game calls cEmCombatValueChange::vft+0x28
so let's go to that function:
Things don't exactly look pretty here, so let's do some cleanup:
- Change the type of
param_1
tocEmCombatValueChange *
and its name tothis
. - Change the return value of the function to
bool
. - Change the type of
param_2
toMtByteReader *
and its name toreader
.
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:
Then comes the actual deserialization:
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:
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.
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:
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:
- It clears the array at
field_0x8
. - It reads an entry count
u32
. - 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:
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:
The deserializer looks as follows:
We can see that reads 5 floats.
But before that there are 2 calls to MtByteReader::Deserialize<nEmDtTune::cDyingRate>
. Let's take a look.
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
:
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
:
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
:
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
:
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.