Movie Timing Fix
File: ResidentEvil3.exe
CRC32: 0x929F6CAE
Type: Hook
Purpose: Fixes the timing (frame count) of the game videos to extend the playtime further allowing the video to not look cut-off.
Pattern: 81EC????????8D0440C1E0035666
An issue that is easily seen with the videos of Resident Evil 3 is the load time of the video and the desync of the audio. For example, a movie may take 3-5 seconds to load but the audio track will start playing immediately. So you will hear things but not see any video for a few seconds. This can be really annoying on videos that may have situational information displayed that is missed. This also causes the videos to cut out earlier than they should making it look like the video is canceled abruptly.
The FMV video information is hardcoded into the game. This is a table of entries for each video file. The easiest way to find the timing information handler is to set a breakpoint on the intro video's table entry. You can set a 4 byte / 8 byte breakpoint on the start of the entry which looks like this:

(At the time I wrote the struct mapping in the pic above, I did not finish reversing the structure fully. Since the project fell through, I stopped working on this and did not complete the structure. The other fields contain flags about the movie file as well as the movie resolution and some other details. See below for information about each element in an entry via debug info.)
This will allow us to trace back to the timing function that loads the videos frame count. The framecount is at +4 in the structure.
- .text:0042FDD0 sub_42FDD0 proc near ; CODE XREF: sub_42F830+C4p
- .text:0042FDD0 ; sub_42F830+129p
- .text:0042FDD0
- .text:0042FDD0 anonymous_0 = byte ptr -804h
- .text:0042FDD0 var_800 = byte ptr -800h
- .text:0042FDD0 arg_0 = dword ptr 4
- .text:0042FDD0
- .text:0042FDD0 mov eax, [esp+arg_0]
- .text:0042FDD4 mov edx, dword_A4FD84
- .text:0042FDDA mov byte_A4FD55, 0
- .text:0042FDE1 sub esp, 800h
- .text:0042FDE7 lea eax, [eax+eax*2]
- .text:0042FDEA shl eax, 3
- .text:0042FDED push esi
- .text:0042FDEE mov cx, word_51B97C[eax]
- .text:0042FDF5 mov word_A4FD50, cx
- .text:0042FDFC mov word ptr [edx+2], 0
- .text:0042FE02 mov ecx, dword_A4FD84
- .text:0042FE08 mov word ptr [ecx], 0
- .text:0042FE0D mov eax, off_51B978[eax]
- ;// and more but snipped for post size..
- signed int __cdecl sub_42FDD0(int a1)
- {
- char *v1; // eax@1
- int v2; // esi@1
- char i; // cl@1
- int v4; // eax@5
- char v5; // cl@5
- char *v6; // edx@5
- char *v7; // eax@5
- signed int result; // eax@12
- char v9; // [sp+4h] [bp-800h]@5
- // Read the frame count of the movie file..
- byte_A4FD55 = 0;
- word_A4FD50 = word_51B97C[12 * a1];
- *(_WORD *)(dword_A4FD84 + 2) = 0;
- *(_WORD *)dword_A4FD84 = 0;
- // Read the file path from the table.. (Path is hard coded to the developer location and then remade to the users path.)
- v1 = (&off_51B978)[24 * a1];
- // Strip out the developer path upto the file name..
- v2 = (int)v1;
- for ( i = *v1; i; ++v1 )
- {
- if ( i == 47 )
- v2 = (int)(v1 + 1);
- i = v1[1];
- }
- // Get the players install path to the game..
- v4 = sub_405ED0();
- // Copy the install path into v9.. (strcpy)
- sub_506CE0(&v9, v4);
- // Append \ to the file path in v9.. (strcat)
- sub_506CA0(&v9, asc_514948);
- // Append the movie file name to the file path in v9.. (strcat)
- sub_506CA0(&v9, v2);
- // Find and replace the file extension from .str to .dat
- v5 = v9;
- v6 = 0;
- v7 = &v9;
- if ( v9 )
- {
- do
- {
- if ( v5 == 46 )
- v6 = v7;
- v5 = (v7++)[1];
- }
- while ( v5 );
- if ( v6 )
- sub_506CE0(v6, a_dat);
- }
- // Load the movie file.. (COM calls and such to load the movie player.)
- if ( sub_506E30(&v9) )
- result = sub_506E50() == 0;
- else
- result = 1;
- return result;
- }
a1 is the file index to read from the FMV table. The files frame count is set, then the file path is rebuilt and loaded.
We hook this function to override the frame count that is being loaded based on the current file index. An example of how that looks would be:
- /**
- * Patch Specific Variables
- */
- uint32_t Patch_FMV_Timing_Address = 0;
- uint32_t Patch_FMV_Timing_Index = 0;
- uint8_t* Patch_FMV_Timing_OriginalCode = nullptr;
- uint32_t Patch_FMV_Timing_ReturnAddress = 0;
- /**
- * Timing Table Variables
- */
- uint32_t Patch_FMV_Timing_LookupTable = 0;
- uint32_t Patch_FMV_Timing_Storage = 0;
- /**
- * Overrides the FMV file timings based on the given index.
- * @param {uint16_t} index - The index being looked up.
- * @returns {uint16_t} The FMV file timing.
- */
- uint16_t FmvLookup(uint16_t index)
- {
- // Obtain the override map..
- auto overrides = RE3RP::Configurations::instance().GetValues("Patch_FMV_Timings");
- // Calculate the real index..
- auto entryIndex = ((index / 12) / 2);
- // Look for an override value..
- auto iter = overrides.find(std::to_string(entryIndex));
- if (iter == overrides.end() || strtol(iter->second.c_str(), nullptr, 16) == -1)
- {
- // No override found, use default value..
- return *(uint16_t*)(Patch_FMV_Timing_LookupTable + index);
- }
- // Use the override value..
- auto value = (uint16_t)strtol(iter->second.c_str(), nullptr, 16);
- RE3RP::Utils::Log("(DEBUG) FmvLookup - Using override value for index: %d, value: %04X", entryIndex, value);
- return value;
- }
- /**
- * Code cave used to override the FMV timing table values.
- */
- __declspec(naked) void Patch_FMV_Timing_Cave(void)
- {
- __asm
- {
- // Store the return address..
- pop Patch_FMV_Timing_ReturnAddress;
- // Restore the original code..
- lea eax, [eax + eax * 2];
- shl eax, 3;
- push esi;
- // Store the current index..
- mov Patch_FMV_Timing_Index, eax;
- // Preserve the stack and registers..
- pushad;
- pushfd;
- }
- {
- // Manually apply the FMV value..
- *(uint16_t*)(Patch_FMV_Timing_Storage) = FmvLookup(Patch_FMV_Timing_Index);
- }
- __asm
- {
- // Restore the stack and registers..
- popfd;
- popad;
- // Return to the original function..
- push Patch_FMV_Timing_ReturnAddress;
- ret;
- }
- }
- /**
- * Disables patches to the games FMV timing table.
- * @returns {bool} True on success, false otherwise.
- */
- bool Patch_FMV_Timing_Disable(void)
- {
- // Ensure there is data to restore..
- if (Patch_FMV_Timing_OriginalCode == nullptr)
- return false;
- // Restore the original data..
- if (Patch_FMV_Timing_Address != 0)
- RE3RP::Utils::RestoreCaveCall(Patch_FMV_Timing_Address, &Patch_FMV_Timing_OriginalCode, 0x08);
- // Delete the backup data..
- SAFE_DELETE_ARR(Patch_FMV_Timing_OriginalCode);
- Patch_FMV_Timing_Address = 0;
- Patch_FMV_Timing_ReturnAddress = 0;
- return true;
- }
- /**
- * Enables patches to the games FMV timing table.
- * @returns {bool} True on success, false otherwise.
- */
- bool Patch_FMV_Timing_Enable(void)
- {
- // Ensure the patch is enabled..
- auto& config = RE3RP::Configurations::instance();
- auto enabled = config.GetValue("Patch_FMV_Timing", "enabled", false);
- if (!enabled)
- return true;
- // Obtain the patch data..
- auto pattern = config.GetString("Patch_FMV_Timing", "pattern");
- auto offset = config.GetValue<int32_t>("Patch_FMV_Timing", "offset", 0);
- auto count = config.GetValue<uint32_t>("Patch_FMV_Timing", "count", 0);
- auto lookupOffset = config.GetValue<uint32_t>("Patch_FMV_Timing", "lookupoffset", 10);
- auto storageOffset = config.GetValue<uint32_t>("Patch_FMV_Timing", "storageoffset", 17);
- // Ensure the pattern is valid..
- if (pattern == nullptr)
- {
- RE3RP::Utils::Log("ERROR: Patch_FMV_Timing - Failed to read pattern from ini file. [Patch_FMV_Timing] => pattern");
- return false;
- }
- // Find the function..
- Patch_FMV_Timing_Address = RE3RP::Utils::FindPattern((uintptr_t)g_GameModule.modBaseAddr, g_GameModule.modBaseSize, pattern, offset, count);
- if (Patch_FMV_Timing_Address == 0)
- {
- RE3RP::Utils::Log("ERROR: Patch_FMV_Timing - Failed to find required pattern.");
- return false;
- }
- // Read the original table and storage addresses before patching..
- Patch_FMV_Timing_LookupTable = *(uint32_t*)(Patch_FMV_Timing_Address + lookupOffset);
- Patch_FMV_Timing_Storage = *(uint32_t*)(Patch_FMV_Timing_Address + storageOffset);
- // Ensure the pointers are valid..
- if (Patch_FMV_Timing_LookupTable == 0 || Patch_FMV_Timing_Storage == 0)
- {
- RE3RP::Utils::Log("ERROR: Patch_FMV_Timing - Failed to read required lookup table and storage pointers.");
- return false;
- }
- // Create the cave..
- return RE3RP::Utils::CaveCall(Patch_FMV_Timing_Address, (uintptr_t)Patch_FMV_Timing_Cave, 16, &Patch_FMV_Timing_OriginalCode) != 0;
- }
The opening movie in the English version of the game is "roopne.dat" which is at index 12. By default this file frame count is 0x00E7. To make up for the delay/lag of the video, we can add some frames to the files frame count. Depending on the system playing the video, the timing values can vary. Some systems with an SSD instead of a normal harddrive may not experience the delay or have really small delay times, while someone with a normal harddrive may see upwards to 5seconds of delay. A range of 8-24 additional frames per video is usually enough to make up for the delay.
So the timing table patches that the above code used based on public info/research came up with:
- [Patch_FMV_Timings]
- 0 = 0x054D ; opn.dat
- 1 = 0x018B ; ins01.dat
- 2 = 0x00E3 ; ins02.dat
- 3 = 0x012D ; ins03.dat
- 4 = 0x01C2 ; ins04.dat
- 5 = 0x00BB ; ins05.dat
- 6 = 0x01D6 ; ins06.dat
- 7 = 0x0166 ; ins07.dat
- 8 = 0x0122 ; ins08.dat
- 9 = 0x00F9 ; ins09.dat
- 10 = 0x0336 ; enda.dat
- 11 = 0x034D ; endb.dat
- 12 = 0x00F3 ; roopne.dat
- 13 = 0x03BD ; bdino.dat