Making a persistent loop plugin - Ezekial711/MonsterHunterWorldModding GitHub Wiki

If you don't know what a Persistent Loop Plugin is you can check it out on the introductory page.

In this guide you will learn how to read and write simple values from and to memory via pointers.

Table of contents

Requirements

To follow this guide you should know

Setting a goal

First we need to actually know what we want to do in our plugin.

For this example, I will be making a plugin to automatically get rid of Fireblight once the player gets affected by it.

We can do that by simply checking if the Fireblight timer is greater than 0, and if it is, we set it back to 0. This is because the game automatically applies buff/debuff effects if their timers are > 0.

In pseudocode:

START LOOP:
    IF FIREBLIGHT_TIMER > 0:
        FIREBLIGHT_TIMER = 0
    END IF
END LOOP

Grabbing a pointer

Now that we have a goal, we of course need a pointer to this fireblight timer. There are many ways of going about this, but the most straight forward way is using Cheat Engine. If you don't know how to find values/pointers using CE, there are lots of tutorials on the internet that you can find with a quick google search. If you don't know where to start, here are 2 pretty good videos to get you started:

However for the prupose of this guide, I already have my pointer here:

MonsterHunterWorld.exe+0x506D270
+0x80
+0x7D20
+0x5EC

If you have absolutely no idea what you're looking at, I recommend watching one (or both) of the two videos mentioned above.

This is a 4 level deep pointer. Few things to take note of here: MonsterHunterWorld.exe+0x506D270 is the static base-address of the pointer. MonsterHunterWorld.exe represents the image base address of the process. Which will always be 0x140000000.

So this address would translate to 0x140000000 + 0x506D270 = 0x14506D270.

0x80, 0x7D20, etc. are further offsets. A common beginner mistake is that you just add these offsets to one another and that's your value, but this is not the case.

The way multi-level pointers work, is that you need to go to the base address, dereference it as a 64bit integer. This gives you yet another pointer.

Now you add the first offset to that new pointer. Let's say we read 0xFC503A0 from this pointer. Now we add 0x80 to that and dereference there again. We continue this until we have added our final offset, where we will dereference as a float, to get our timer value.

Implementation

Now all that above would be a big headache to do manually each time, so how about we write a function to take care of that for us.

This is where external and internal start to deviate. I will always provide examples for both approaches.

Internal

Internally, dereferencing a pointer is pretty straight forward. If our pointer is stored in a variable ptr, all we need to do to dereference is to cast it to the correct pointer type and then dereference like this:

int value = *(int*)ptr;

If this doesn't make any sense to you, you should read up on pointers in the C language.

Now the function for reading multi-level pointers could look like this:

template<class T>
T* ReadMultiLevelPointer(void* base_address, const std::vector<uintptr_t>& offsets)
{
    uintptr_t addr = (uintptr_t)base_address;
    for (const auto& offset : offsets)
    {
        addr = *(uintptr_t*)addr;
        addr += offset;
    }

    return (T*)addr;
}

Continue to Using the function.

External

The external function looks pretty similar, but we replace any dereferences by a call to ReadProcessMemory:

template<class T>
T* ReadMultiLevelPointer(HANDLE process_handle, void* base_address, const std::vector<uintptr_t>& offsets)
{
    uintptr_t addr = (uintptr_t)base_address;
    for (const auto& offset : offsets)
    {
        ReadProcessMemory(process_handle, (void*)addr, &addr, sizeof(addr), NULL);
        addr += offset;
    }

    return (T*)addr;
}

As you might have guessed, ReadProcessMemory can be used to read the memory of an external process. There is also an equivalent function to write: WriteProcessMemory.

Using the function

Internally, all you need to do is call the function as is. So taking our pointer from above as an example, it would look like this:

float* timer = ReadMultiLevelPointer<float>((void*)0x14506D270, { 0x80, 0x7D20, 0x5EC });

You can now continue to Preventing Fireblight.

Now for external, you might've noticed that I've added an additional parameter process_handle to the function. This is because reading memory from an external process requires some extra setup work. We first need to get a handle to the game process.

To do so, we need to get a window handle, use that to get the process id of the game, and finally open the process using appropriate access rights. That will allow us to read and write to the game's memory.

So first we need to get the game's window handle using the function FindWindowA:

HWND window = FindWindowA(NULL, "MONSTER HUNTER: WORLD<buildNo>");

Next we need to get the process id of the game. We can do so using the function GetWindowThreadProcessId:

DWORD pid;
GetWindowThreadProcessId(window, &pid);

Here we pass the pid variable as a pointer. You can easily see how parameters need to be passed by checking out the documentation for each of these functions on MSDN.

Finally we use the process id to get a handle to the process using OpenProcess:

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);

Keep in mind you only need to do this once at the start of your program, and then simply store the process handle.

Now we can call our function:

float* timer = ReadMultiLevelPointer<float>(hProcess, (void*)0x14506D270, { 0x80, 0x7D20, 0x5EC });

Preventing Fireblight

Now we need to make sure we have our own thread for this, since we'll be checking this timer periodically in a loop. If we were to run our loop on the game's thread we would stall it forever.

So, we need to launch a thread from our DllMain. And inside our thread function we need to start off with setup code. This is, once again, different between internal and external.

Internal

For internal, since our dll is loaded upon boot, we need to delay our pointer reading until the Object containing our times is created.

To do so we can simply check for nullpointers each dereference step. Let's create a function that waits until it can read proper pointers.

template<class T>
T* ReadMultiLevelPointerSafe(void* base_address, const std::vector<uintptr_t>& offsets)
{
    uintptr_t addr = (uintptr_t)base_address;
    for (const auto& offset : offsets)
    {
        while (*(uintptr_t*)addr == nullptr) Sleep(30);
        addr = *(uintptr_t*)addr;
        addr += offset;
    }

    return (T*)addr;
}

This function will now return only once it has successfully read this pointer. This is definitely not the best solution to this problem, but it'll work for now.

So instead of calling ReadMultiLevelPointer we now instead call ReadMultiLevelPointerSafe to get the pointer to our timer.

External

For external the setup code is

  1. What was explained in Using the function
  2. Optionally the delayed pointer reading function

The pointer reading function for external would look like this:

template<class T>
T* ReadMultiLevelPointerSafe(HANDLE process_handle, void* base_address, const std::vector<uintptr_t>& offsets)
{
    uintptr_t addr = (uintptr_t)base_address;
    for (const auto& offset : offsets)
    {
        uintptr_t tmp = nullptr;
        do {
            Sleep(30);
            ReadProcessMemory(process_handle, (void*)addr, &tmp, sizeof(tmp), NULL);
        } while (tmp == nullptr);

        addr = tmp;
        addr += offset;
    }

    return (T*)addr;
}

So at the end of our handle obtaining setup code, we call our new function:

float* timer = ReadMultiLevelPointerSafe<float>(hProcess, (void*)0x14506D270, { 0x80, 0x7D20, 0x5EC });

Now we have our main loop (identical for internal/external):

while (true)
{
    // ...
    Sleep(10);
}

Now we need to check the timer each loop iteration and if it's greater than 0, we set it back to 0:

Internally, this is as simple as this:

if (*timer > 0.0f)
{
    *timer = 0.0f;
}

As for external, we need a little more as usual:

float timerval = 0.0f;

// Read timer value into 'timerval' variable
ReadProcessMemory(hProcess, (void*)timer, &timerval, sizeof(timerval), 0);

// Check if timer is > 0
if (timerval > 0.0f)
{
    // Reset timer value back to 0
    timerval = 0.0f;

    // Write value back to game memory
    WriteProcessMemory(hProcess, (void*)timer, &timerval, sizeof(timerval), 0);
}

We just put this if statement in our while loop, and there we go. We're done! Now all we have to do is build the dll and move it into our plugins folder.

And there you go, you have successfully created a plugin that automatically removes fireblight.

⚠️ **GitHub.com Fallback** ⚠️