Old Source Engine VPK mounting system
Table of contents
- Introduction
- About the Steam2 Wrapper
- Into the VPK mounting system
- So, how do I mount VPK files?
- A sidenote about the VPK files coming with the game and the vpk(lib) version used
- Small demo
- Offsets/binary information
- Special thanks
- Contact
Introduction
Before the Steampipe update, game content was kept inside GCF archives. Those would get mounted to the Source Engine's virtual file system and the game would access them from the archive itself, without extracting. (Well, for the most part, because GCF archives have a flag that'd make Steam extract necessary files before the game is first ran.) However, after the Steampipe update, the GCF format was scrapped and completely removed from Steam. Alongside GCF, the legacy Steam API (steam.dll) was also scrapped in favor of steam_api.dll and steamclient.dll. To not break old games, Valve decided to make the steam2wrapper.
About the Steam2 Wrapper
The Steam2 wrapper is a steam.dll replacement that acts kind-of as a proxy between the new post-Steampipe Steam and the game. It's the thing that handles the VPK mounting and makes the VGUI friends dialog still work. Note that the version of Steam2 wrapper inside the main Steam directory isn't the one that the old Source games use, instead they come with an old build of it dated Wed Feb 26 2014. The Steam2 wrapper uses an internal Valve library called vpklib to open VPK files and provide it to the game's virtual file system, the library is also used in newer Source Engine builds for the filesystem module and vpk.exe.
Into the VPK mounting system
The VPK mounting process happens entirely in steam.dll, well, at least the "first" part of it because filesystem_steam tries to redo it afterwards as well. The process goes like this:
CFileSystem_Steam::MountSteamContent tries to load steam.dll -> steam.SteamStartup -> sub_1000AA70 (vpk_entry) -> inside vpk_entry app dependencies are enumerated -> sub_1000A960 (mount_vpk)
After it loads steam.dll, it tries to do the process itself again which goes like this:
CFileSystem_Steam::MountSteamContent-> steam.MountFilesytem -> mount_vpk
This behaviour can be also observed from the Windows' debug log through software like DbgView. Enabling debug output from the Steam2 wrapper is later explained further in this article.
00000027 3.56093121 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_215_dir.vpk
00000028 3.56212473 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_208_dir.vpk
00000029 3.56450486 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_207_dir.vpk
00000030 3.56573367 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_206_dir.vpk
00000031 3.56595469 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_212_dir.vpk
00000032 3.56704235 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_213_dir.vpk
00000033 3.56837034 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_308_dir.vpk
00000034 3.56847954 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_309_dir.vpk
00000035 3.56900430 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_381_dir.vpk
00000036 3.57062411 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_421_dir.vpk
00000037 3.57099271 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_422_dir.vpk
00000038 3.57142329 [20780] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_423_dir.vpk
00000039 3.57181120 [20780] SteamMountAppFilesystem
…
00000579 7.42786551 [9408] SteamStartup
00000580 7.42814875 [9408] SteamEnumerateApp appid 320, dependencies 3
00000581 7.42829370 [9408] SteamEnumerateAppDependency AppID 320, depot 321
00000582 7.42840338 [9408] SteamIsAppSubscribed appid 321: yes
00000583 7.42862082 [9408] SteamMountFilesystem uDepotId 321, path ""
00000584 7.42874336 [9408] SteamEnumerateAppDependency AppID 320, depot 320
00000585 7.42884779 [9408] SteamIsAppSubscribed appid 320: yes
00000586 7.42898655 [9408] SteamMountFilesystem uDepotId 320, path ""
00000587 7.42906332 [9408] SteamEnumerateAppDependency AppID 320, depot 232371
00000588 7.42917204 [9408] SteamIsAppSubscribed appid 232371: yes
00000589 7.42936468 [9408] SteamMountFilesystem uDepotId 232371, path ""
...
00000626 7.43344784 [9408] SteamEnumerateAppDependency AppID 215, depot 422
00000627 7.43352175 [9408] SteamIsAppSubscribed appid 422: yes
00000628 7.43381691 [9408] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_422_dir.vpk
00000629 7.43383741 [9408] SteamMountFilesystem uDepotId 422, path ""
00000630 7.43391275 [9408] SteamEnumerateAppDependency AppID 215, depot 423
00000631 7.43398380 [9408] SteamIsAppSubscribed appid 423: yes
00000632 7.43417740 [9408] Mounting d:\steam\steamapps\common\source sdk base\vpks\depot_423_dir.vpk
00000633 7.43419743 [9408] SteamMountFilesystem uDepotId 423, path ""
00000634 7.43427420 [9408] SteamEnumerateAppDependency AppID 215, depot 1004
00000635 7.43434620 [9408] SteamIsAppSubscribed appid 1004: yes
00000636 7.43447208 [9408] SteamMountFilesystem uDepotId 1004, path ""
Upon further investigation of the Steam2 wrapper, I have found out that it tries to read a file called "steam_vpks.vdf"
, which to my knowledge has not yet been documented anywhere online. However, I was able to reverse engineer the format and get it to work. The format looks as follows:
steam_vpks.vdf example
"does_this_even_matter"
{
"spewfileio" "true"
"mount"
{
"vpk" "vpks/depot_206"
"vpk" "vpks/depot_207"
"vpk" "vpks/depot_208"
"vpk" "vpks/depot_212"
"vpk" "vpks/depot_215"
// other vpks can be mounted here as well
}
}
… and here is a snippet of pseudo code generated by IDA. This is a part of the vpk_entry
function:
if ( (unsigned __int8)sub_10011A50(v3, (char)"vpk", (int)hSteamVPKs_idk, 0) )
{
v5 = (_DWORD *)sub_10009BA0(&Str);
SubKey = GetSubKey(v5, "mount", 0);
v7 = (_DWORD *)sub_10009BA0(&Str);
String = (wchar_t *)GetString(v7, "spewfileio");
bDebug = sub_10010010(String, 0, 0);
if ( SubKey )
{
for ( i = (_DWORD *)sub_1000FE10(SubKey); i; i = (_DWORD *)sub_1000FE30(i) )
{
v10 = GetString(i, 0);
v11 = (char *)sub_10010FD0(v10, (int)&unk_100397BA);
mount_vpk(v11);
}
v12 = 1;
}
The steam_vpks.vdf
file disables the automatic dependency enumeration and mounting by taking a different code path, so you have to include the base VPKs in the steam_vpks.vdf
file. The bDebug
variable is also set by the -debugsteam2wrapper
command line parameter, however as you can see, it's also overriden in vpk_entry
if you have the steam_vpks.vdf
file and it's valid. The "debug mode" also logs every find
, stat
, open
and close
call, even the ones which end up reading directly from the filesystem, so keep in mind it's probably a good idea to leave it disabled while you don't need it.
So, how do I mount VPK files?
I've found 3 ways to do it:
SteamMountFilesystem
SteamMountFilesystem is the way filesystem_steam.dll handles it. There isn't much to say about it as just calling the function should work, but I wouldn't call it the best way as it looks for the depot in a very specific path, that is:
<game_path>/vpks/depot_<depot_id>_dir.vpk
Calling mount_vpk
directly
This one is also a rather simple one. Simply call the function with the path and the VPK file should get mounted. The path should be relative to the directory hl2.exe
is in, for example in case of Source SDK Base 2006, the path would be relative to steamapps\common\Source SDK Base
.
The steam_vpks.vdf
file
Start by copying the example steam_vpks.vdf
I provided earlier and replace the example VPKs with the ones that your base game needs to work (such as depots 206, 207 and 208). Then at the end you can add the VPK you want to mount, you should also probably make spewfileio
false to not get your debug log spammed.
Out of the three ways I listed, I only thoroughly tested the last two. If you can't see your content, consider making a fake game directory. As in, structure your VPK file like the following:
- root
- fakegamedir
- maps
- …
- models
- …
- materials
- …
- …
- …
- maps
- fakegamedir
and add it to your gameinfo.txt
as:
Game fake_game_dir
A sidenote about the VPK files coming with the game and the vpk(lib) version used
I deem the VPK files and the vpklib version built into the steam.dll a very specific one. The VPK files coming with all Valve games and licensed games that utilize this mounting system are special, specifically it's pretty much a valid VPK2 file except for the checksums at the end. The tree and chunk checksums have been replaced by something that looks like corruption, which might've been caused by the checksums being uninitialized memory aligned next to each other. Here's how it looks like in the depot_215_dir.vpk
file from Source SDK Base 2006. This pattern can also be seen in the depot_307_dir.vpk
file from Source SDK Base 2007.
{
"PersonaName" "martin
Here is another pattern, this one is found in depot_309_dir.vpk
and depot_308_dir.vpk
from Source SDK Base 2007.
{
"0" "0100000065a3e27eb
As to what the actual cause of the corruption is, I can only speculate. The VPK files might've been made by some internal vpk.exe build, which licensees would also have, because this can be observed in both Valve and licensed games as I have stated before.
Small demo
Offsets/binary information
Hashes of binaries examined
File: FileSystem_Steam.dll
CRC-32: 554bdd04
SHA-1: b4df74a7d66128c7698c45e21ef1bcd802c2e553
SHA-256: 60389fc40694cf54646f03f84471f2d1994af760de76a77498b7c0924b40ed26
SHA-512: 907ebb08faebc9a10941bdb72ac90be6877906ecc2c13208ee9bc7ceb682ab4d7723d9418817326a35478b1db9c5dff64f1025db4b2fc9ad61905a8e30b34cfc
File: steam.dll
CRC-32: cc3c72e4
SHA-1: 7ea23a50f764e3dac18ec2609607469d92088f74
SHA-256: 18ccb20a1c8ab916a2f93aa846d8c9730fd80c0d279cdee0ac7987d8a14a7f71
SHA-512: 676bdc922cffd1228f98a7375339b50d3ddf87cb858031cd1c0a8e871c9b013687b83f78942281c3236ee2b555a1f065c3ce0ff57bca9a6771f321f018256182
Offsets
Binary | Function Name | Function Address/RVA |
---|---|---|
steam.dll | mount_vpk | 0xA960 from base |
steam.dll | vpk_entry | 0xAA70 from base |
filesystem_steam.dll | CFileSystem_Steam::MountSteamContent | 0x16C70 from base |
steam.dll | SteamMountFilesystem | n/a (GetProcAddress) |
Example code
Example code used while making and testing this can be found here. You will need Minhook and Source SDK in order to compile it. If you do not know anything about C++, it's probably not a great idea to do so.
Special thanks
- mv - for providing BGT and The Ship VPK files.
- Nafrayu - for trying to figure this out with me long ago
- DarkOK - for reviewing this
… and everyone else that I've forgotten.
Contact
You can contact me via email here. Feedback is greatly appreciated.