Jump to content

» «

[Feature Request] RPF-less mod installation

No replies to this topic
  • delusional

    Player Hater

  • New Members
  • Joined: 28 Jan 2017
  • Denmark


Posted 29 January 2017 - 12:24 AM Edited by delusional, 29 January 2017 - 12:26 AM.

I spent christmas looking into RPF-less modding for GTA V.

I'm not quite there, but with the use of dlcpacks (So proper mod packaging) I've managed to come up with a workable solution, it would be great if the OpenIV developers would pick it up. There's no need for yet another mod that deals with mod loading.


The rest of the post will be directed towards the OpenIV developers, and will therefore assume some familiarity with the GTA V source code. The hex values with denote offsets from the game base (as of 1.0.944.2) unless otherwise noted.


The function at 0x11E0E50 looks up a Packfile to serve a path. It supports multiple archives per path, and supports mapping subpaths with global path fallbacks. By default it maps common.rpf to common:/, update.rpf to update:/, and update.rpf/common to common:/ Some others are mapped as well, but unimportant in this context.


To support RPF-less mod installation we need to load some files from common.rpf and update.rpf.

To load files from disk instead of the rpf file, we simply need to attach some Devices to handle those paths.


Luckily GTA V also uses the function 0x11E0E50 to load HDD paths, meaning it already has a Device that does this. They call it DeviceRelative, and the vTable is located at 0x18512A8, It's a struct with a length of 0x118 bytes.


To initialize the Device relative we need to pass the struct to 0x11E28D8, it has the method signature setBackFsAndOtherStuff(DeviceRelative_rage *this, char *path, char assetType, device_rage path_handler) where passing null to path_handler will result in the program looking up what to mount the relative device to via the aforementioned lookup function. If you want a filesystem path, which we do, it's relative to the game root. I've found that an assetType value of 1 works well.


To make our new device handle the common path we need to call the function at 0x11E3E94, with the prototype of char __fastcall registerArchive(Device_rage *pkgfile, char *path) which will register our Device to the path we give it. To overwrite the common.rpf file we register our device on common:/. The great thing about doing it this way is that the code will automatically fall back if some file isn't in our directory. Allowing up to only copy the files we actually want to modify.


If we try and run the game now we find a problem. Some of the files in common.rpf are loaded via the commoncrc:/ path prefix. If we mount a new DeviceRelative, with the same backing path as the other one, it works.


I've hooked some functions and made a proof of concept. There's a lot of other research in there though, so I wont be posting it. The main procedure of mounting the devices looks like the following (mainload is 0x7B7B4C):

char __fastcall mainLoad_replace()
	char ret = mainLoad_original();
	proxyCommon = static_cast<DeviceRelative*>(malloc(0x118));
	proxyCommon->vTable = reinterpret_cast<void*>(mainModuleBase + 0x18512A8);
	setBackAndOtherStuff(proxyCommon, "modloader/common.rpf/", 1, nullptr);
	registerArchive(proxyCommon, "common:/");

	proxyCommonrc = static_cast<DeviceRelative*>(malloc(0x118));
	proxyCommonrc->vTable = reinterpret_cast<void*>(mainModuleBase + 0x18512A8);
	setBackAndOtherStuff(proxyCommonrc, "modloader/common.rpf/", 1, nullptr);
	registerArchive(proxyCommonrc, "commoncrc:/");

	proxyUpdate = static_cast<DeviceRelative*>(malloc(0x118));
	proxyUpdate->vTable = reinterpret_cast<void*>(mainModuleBase + 0x18512A8);
	setBackAndOtherStuff(proxyUpdate, "modloader/update.rpf/", 1, nullptr);
	registerArchive(proxyUpdate, "update:/");

	DBGPRINT(L"Hooked it");

	return ret;

With the quirk that the game reloads the update rpf file to force it on top of all the other rpfs, requiring me to hook another function and reload my mounts (refreshpriority is 0x8EB790):

char __fastcall refreshPriority_replace()
	DBGPRINT(L"Refreshed it");
	char ret = refreshPriority_original();

	registerArchive(proxyCommon, "common:/");
	registerArchive(proxyCommonrc, "commoncrc:/");
	registerArchive(proxyUpdate, "update:/");

	return ret;

The unregister archive is located at 0x11E71F0.


I have my PoC working, loading a single dlcpack mod, and modifying the root handling.meta, all with stock rpf files!

  • theNGclan likes this

1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users