top of page
viewport_error_material_for_aspect.png

The original Valve Source engine has a handler for missing model assets. It replaces the studio and hardware pointer of the model asset with that of the classic "ERROR" model shown in the picture above (found in all Source games) .

​

In the Respawn Entertainment version of the engine, this handler is stripped, as the assets are stored in a robust way were missing assets is only possible due to file corruption, or modding. Any missing static and dynamic (non scripted) models will result in an "Engine Error" message box explaining the error and closing the application.

​

Since we develop mods for the game, and also create new pak files for maps, weapons, props, etc... we tend to forget including a material or model. Instead of erroring the engine (or crashing), I reverse engineered the modified datacache system and rebuild the error handler by swapping the bad studio data out for the "mdl/error.rmdl" studio pointer). I also created some logging which will print the missing model and material names to the console, and the asset it gets replaced with, which is useful for rebaking the pak files.

​

INTRODUCTION

PROGRAMMING

The assembly above loads [rdi - 5] into r11 (rdi get added with the pointer in rax, the result is -5 + ptr to the end of the model string that is being requested to be loaded, which will point to the start of the file extension.

 

For example: we got a string "mdl/beacon/mendoko_wire_cluster_04.rmdl", in the assembly above, rax will point to the last character of "mdl/beacon/mendoko_wire_cluster_04.rmdl" before the null character, we then subtract 5 bytes, which will make the pointer point to the extension delimiter, resulting in: ".rmdl".

 

We compare this string pointer against the following pointers to string literals in read only memory

with the call to V_Stringcmp (this has been renamed from a memory address). If the model's extension does not equal ".rmdl", ".rrig" or ".rpak", we error with a "Attempted to load old model \"%s\"; replace with rmdl" message.

​

But instead of erroring, we can actually return the fallback model's studio pointer, so the engine will display the classic 3D "ERROR" model in place of deprecated assets in the world!

The assembly above is a condition that comes later down the same function, this condition is fired when the model does use the newer extensions but does not exist in the asset catalogue. Here we deref the studiodata pointer and obtain the member at offset 0x8 ([rsi+8]). After some live reversing and memory analysis, I found that [rsi+8] is the pointer to the studio header (studiohdr_t in Source SDK). If the header field is null, the zero flag gets set and the conditional jump to address "1401E78C1" is taken, which calls the error wrapper displaying an error message box with "Model %s not found\n".

​

However, instead of calling Engine Error, we can set the studiodata structure field at 0x8 to the address of our fallback "mdl/error.rmdl" studiohdr pointer, and return that in the rax register.

​

To do so, we start of by creating a simple structure that contains pointers and handles to our fallback models:

We then hook the model caching function using a trampoline, and start rebuilding it in the SDK by first catching the studio pointers and model handles for the error model into our structure:

The error and empty models are the first to be loaded by the engine, which are stored in "common.rpak". 

The reason I used a reinterpret_cast with a double deref on pStudioData here, is because I don't have the studiodata_t structure fully reverse engineered yet, but I do know the first pointer points to another structure, in which the first pointer points to the pointer of the studio header (studiohdr_t).

​

Now we can actually start testing pointers and replacing if necessary:

This is the reversed engineered assembly code, but besides reversing it, the behavior has also been changed.

Instead of considering old model files critical (terminating the process):

  • We try to obtain the studio pointer of "mdl/error.rmdl" and return this pointer to the caller.

  • We check if we did not encounter this model handle before and print the missing model to the console, so we do not create duplicate logs.

  • We call the critical Engine Error function, terminating the process if we do not have anything to fall back to (mdl/error.rmdl was not loaded).

This is the reversed second condition found later in the assembler of the function we are hooking, that gets fired if the model features the new extensions but is not found in the asset catalogue. Here we also perform the same operation as above to recover from the error.

console_error_replacement.png

After compiling the dll and running the engine, we can see the console now prints the newly implemented errors, indicating which models are missing and replaced. Despite the missing models (both static and dynamic), the engine initiated the server game dll and script vm. Our system has succeeded so far.

viewport_error_no_studiohw.png

There is one problem however, the "ERROR" model is not being rendered, even though it has been replaced.

After poking around a bit deeper in the data cache system, I noticed there were some traces where the engine sets "0xDEADFEEDDEADFEED" as 'invalid' pointer value for "pStudioData->m_MDLCache".

 

I think the debug builds of this engine has sanity checks for these, something like "if (pStudioData->m_MDLCache) == BAD_STUDIO_CACHE" then assert and replace with "ERROR" model. Since the retail version doesn't have these, I crashed in a function which seems to return the studio hardware for the model, which after some reverse engineering, seems fundamental for having the fallback models to be rendered:

With the structures partially reversed, and the function itself reversed as well (similar to the first function), I created another trampoline with the following code:

Instead of returning nullptr, we attempt to return the studio hardware of the "ERROR" model, which defines the root LOD, the num LOD's, the LOD's pointer, and the mesh groups/data. If we fail (rare), we return nullptr.

viewport_error_material_for_aspect.png

The "ERROR" models are now being rendered at the origin and angles of each model that was not loaded.

​

The engine also replaces missing material/texture assets with a bright checkered material, showing the blue/orange color. This system was not stripped from the retail engine, so we did not have to rebuild this.

​

The warning/error prints have been stripped though, so I created a small trampoline which calls 2 virtual methods within the engine (IMaterial::GetName and IMaterialInternal::IsErrorMaterial), which we use to determine whether or not the material is an ERROR material, and to retrieve the name for the print:

The missing/replaced materials are now printed to the console as well:

console_error_material_for_aspect.png

Using the new error handler, we can simply read out (or parse) the missing assets, and rebake an RPak with all missing assets included. The classic one by one, trial and error technique is no longer required!

viewport_error_noerror.png

Remark:

  • The code is mostly based on the assembly of the original functions, which in most cases, is not pretty to read.

  • We have to use all inline methods and variables used by the original functions; this includes the mutex shared across the entire data cache system. Resolving pointers to these makes the job a lot harder.

  • I used padding and pre-processor macros to make the code portable between other versions of the engine, which includes versions that have been compiled with different compiler optimization flags.

bottom of page