Registration Code (Part 1): w%kQ6
Registration Code (Part 2): b<#$1[*(cw~
In order to register on this forum, you must use the codes above. Combine them into one code (copy paste).

Resident Evil 3 - Movie Timing Fix

Topics regarding the Resident Evil game series.
Post Reply
User avatar
atom0s
Site Admin
Posts: 397
Joined: Sun Jan 04, 2015 11:23 pm
Location: 127.0.0.1
Contact:

Resident Evil 3 - Movie Timing Fix

Post by atom0s » Wed Nov 22, 2017 1:47 am

As part of a project I was helping with recently that landed up falling through due to various reasons, here is some information about Resident Evil 3.

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:

Image

(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.
  1. .text:0042FDD0 sub_42FDD0      proc near               ; CODE XREF: sub_42F830+C4p
  2. .text:0042FDD0                                         ; sub_42F830+129p
  3. .text:0042FDD0
  4. .text:0042FDD0 anonymous_0     = byte ptr -804h
  5. .text:0042FDD0 var_800         = byte ptr -800h
  6. .text:0042FDD0 arg_0           = dword ptr  4
  7. .text:0042FDD0
  8. .text:0042FDD0                 mov     eax, [esp+arg_0]
  9. .text:0042FDD4                 mov     edx, dword_A4FD84
  10. .text:0042FDDA                 mov     byte_A4FD55, 0
  11. .text:0042FDE1                 sub     esp, 800h
  12. .text:0042FDE7                 lea     eax, [eax+eax*2]
  13. .text:0042FDEA                 shl     eax, 3
  14. .text:0042FDED                 push    esi
  15. .text:0042FDEE                 mov     cx, word_51B97C[eax]
  16. .text:0042FDF5                 mov     word_A4FD50, cx
  17. .text:0042FDFC                 mov     word ptr [edx+2], 0
  18. .text:0042FE02                 mov     ecx, dword_A4FD84
  19. .text:0042FE08                 mov     word ptr [ecx], 0
  20. .text:0042FE0D                 mov     eax, off_51B978[eax]
  21.  
  22. ;// and more but snipped for post size..
This is the function that is used to read the movie file path, set the frame count, and prepare the video file for playback. A pseudo version of this function:
  1. signed int __cdecl sub_42FDD0(int a1)
  2. {
  3.   char *v1; // eax@1
  4.   int v2; // esi@1
  5.   char i; // cl@1
  6.   int v4; // eax@5
  7.   char v5; // cl@5
  8.   char *v6; // edx@5
  9.   char *v7; // eax@5
  10.   signed int result; // eax@12
  11.   char v9; // [sp+4h] [bp-800h]@5
  12.  
  13.   // Read the frame count of the movie file..
  14.   byte_A4FD55 = 0;
  15.   word_A4FD50 = word_51B97C[12 * a1];
  16.   *(_WORD *)(dword_A4FD84 + 2) = 0;
  17.   *(_WORD *)dword_A4FD84 = 0;
  18.  
  19.   // Read the file path from the table.. (Path is hard coded to the developer location and then remade to the users path.)
  20.   v1 = (&off_51B978)[24 * a1];
  21.  
  22.   // Strip out the developer path upto the file name..
  23.   v2 = (int)v1;
  24.   for ( i = *v1; i; ++v1 )
  25.   {
  26.     if ( i == 47 )
  27.       v2 = (int)(v1 + 1);
  28.     i = v1[1];
  29.   }
  30.  
  31.   // Get the players install path to the game..
  32.   v4 = sub_405ED0();
  33.  
  34.   // Copy the install path into v9.. (strcpy)
  35.   sub_506CE0(&v9, v4);
  36.  
  37.   // Append \ to the file path in v9.. (strcat)
  38.   sub_506CA0(&v9, asc_514948);
  39.  
  40.   // Append the movie file name to the file path in v9.. (strcat)
  41.   sub_506CA0(&v9, v2);
  42.  
  43.   // Find and replace the file extension from .str to .dat
  44.   v5 = v9;
  45.   v6 = 0;
  46.   v7 = &v9;
  47.   if ( v9 )
  48.   {
  49.     do
  50.     {
  51.       if ( v5 == 46 )
  52.         v6 = v7;
  53.       v5 = (v7++)[1];
  54.     }
  55.     while ( v5 );
  56.     if ( v6 )
  57.       sub_506CE0(v6, a_dat);
  58.   }
  59.  
  60.   // Load the movie file.. (COM calls and such to load the movie player.)
  61.   if ( sub_506E30(&v9) )
  62.     result = sub_506E50() == 0;
  63.   else
  64.     result = 1;
  65.   return result;
  66. }

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:
  1.  
  2. /**
  3.  * Patch Specific Variables
  4.  */
  5. uint32_t Patch_FMV_Timing_Address = 0;
  6. uint32_t Patch_FMV_Timing_Index = 0;
  7. uint8_t* Patch_FMV_Timing_OriginalCode = nullptr;
  8. uint32_t Patch_FMV_Timing_ReturnAddress = 0;
  9.  
  10. /**
  11.  * Timing Table Variables
  12.  */
  13. uint32_t Patch_FMV_Timing_LookupTable = 0;
  14. uint32_t Patch_FMV_Timing_Storage = 0;
  15.  
  16. /**
  17.  * Overrides the FMV file timings based on the given index.
  18.  * @param {uint16_t} index - The index being looked up.
  19.  * @returns {uint16_t} The FMV file timing.
  20.  */
  21. uint16_t FmvLookup(uint16_t index)
  22. {
  23.     // Obtain the override map..
  24.     auto overrides = RE3RP::Configurations::instance().GetValues("Patch_FMV_Timings");
  25.  
  26.     // Calculate the real index..
  27.     auto entryIndex = ((index / 12) / 2);
  28.  
  29.     // Look for an override value..
  30.     auto iter = overrides.find(std::to_string(entryIndex));
  31.     if (iter == overrides.end() || strtol(iter->second.c_str(), nullptr, 16) == -1)
  32.     {
  33.         // No override found, use default value..
  34.         return *(uint16_t*)(Patch_FMV_Timing_LookupTable + index);
  35.     }
  36.  
  37.     // Use the override value..
  38.     auto value = (uint16_t)strtol(iter->second.c_str(), nullptr, 16);
  39.     RE3RP::Utils::Log("(DEBUG) FmvLookup - Using override value for index: %d, value: %04X", entryIndex, value);
  40.     return value;
  41. }
  42.  
  43. /**
  44.  * Code cave used to override the FMV timing table values.
  45.  */
  46. __declspec(naked) void Patch_FMV_Timing_Cave(void)
  47. {
  48.     __asm
  49.     {
  50.         // Store the return address..
  51.         pop Patch_FMV_Timing_ReturnAddress;
  52.  
  53.         // Restore the original code..
  54.         lea eax, [eax + eax * 2];
  55.         shl eax, 3;
  56.         push esi;
  57.  
  58.         // Store the current index..
  59.         mov Patch_FMV_Timing_Index, eax;
  60.  
  61.         // Preserve the stack and registers..
  62.         pushad;
  63.         pushfd;
  64.     }
  65.  
  66.     {
  67.         // Manually apply the FMV value..
  68.         *(uint16_t*)(Patch_FMV_Timing_Storage) = FmvLookup(Patch_FMV_Timing_Index);
  69.     }
  70.  
  71.     __asm
  72.     {
  73.         // Restore the stack and registers..
  74.         popfd;
  75.         popad;
  76.  
  77.         // Return to the original function..
  78.         push Patch_FMV_Timing_ReturnAddress;
  79.         ret;
  80.     }
  81. }
  82.  
  83. /**
  84.  * Disables patches to the games FMV timing table.
  85.  * @returns {bool} True on success, false otherwise.
  86.  */
  87. bool Patch_FMV_Timing_Disable(void)
  88. {
  89.     // Ensure there is data to restore..
  90.     if (Patch_FMV_Timing_OriginalCode == nullptr)
  91.         return false;
  92.  
  93.     // Restore the original data..
  94.     if (Patch_FMV_Timing_Address != 0)
  95.         RE3RP::Utils::RestoreCaveCall(Patch_FMV_Timing_Address, &Patch_FMV_Timing_OriginalCode, 0x08);
  96.  
  97.     // Delete the backup data..
  98.     SAFE_DELETE_ARR(Patch_FMV_Timing_OriginalCode);
  99.     Patch_FMV_Timing_Address = 0;
  100.     Patch_FMV_Timing_ReturnAddress = 0;
  101.  
  102.     return true;
  103. }
  104.  
  105. /**
  106.  * Enables patches to the games FMV timing table.
  107.  * @returns {bool} True on success, false otherwise.
  108.  */
  109. bool Patch_FMV_Timing_Enable(void)
  110. {
  111.     // Ensure the patch is enabled..
  112.     auto& config = RE3RP::Configurations::instance();
  113.     auto enabled = config.GetValue("Patch_FMV_Timing", "enabled", false);
  114.     if (!enabled)
  115.         return true;
  116.  
  117.     // Obtain the patch data..
  118.     auto pattern = config.GetString("Patch_FMV_Timing", "pattern");
  119.     auto offset = config.GetValue<int32_t>("Patch_FMV_Timing", "offset", 0);
  120.     auto count = config.GetValue<uint32_t>("Patch_FMV_Timing", "count", 0);
  121.     auto lookupOffset = config.GetValue<uint32_t>("Patch_FMV_Timing", "lookupoffset", 10);
  122.     auto storageOffset = config.GetValue<uint32_t>("Patch_FMV_Timing", "storageoffset", 17);
  123.  
  124.     // Ensure the pattern is valid..
  125.     if (pattern == nullptr)
  126.     {
  127.         RE3RP::Utils::Log("ERROR: Patch_FMV_Timing - Failed to read pattern from ini file. [Patch_FMV_Timing] => pattern");
  128.         return false;
  129.     }
  130.  
  131.     // Find the function..
  132.     Patch_FMV_Timing_Address = RE3RP::Utils::FindPattern((uintptr_t)g_GameModule.modBaseAddr, g_GameModule.modBaseSize, pattern, offset, count);
  133.     if (Patch_FMV_Timing_Address == 0)
  134.     {
  135.         RE3RP::Utils::Log("ERROR: Patch_FMV_Timing - Failed to find required pattern.");
  136.         return false;
  137.     }
  138.  
  139.     // Read the original table and storage addresses before patching..
  140.     Patch_FMV_Timing_LookupTable = *(uint32_t*)(Patch_FMV_Timing_Address + lookupOffset);
  141.     Patch_FMV_Timing_Storage = *(uint32_t*)(Patch_FMV_Timing_Address + storageOffset);
  142.  
  143.     // Ensure the pointers are valid..
  144.     if (Patch_FMV_Timing_LookupTable == 0 || Patch_FMV_Timing_Storage == 0)
  145.     {
  146.         RE3RP::Utils::Log("ERROR: Patch_FMV_Timing - Failed to read required lookup table and storage pointers.");
  147.         return false;
  148.     }
  149.  
  150.     // Create the cave..
  151.     return RE3RP::Utils::CaveCall(Patch_FMV_Timing_Address, (uintptr_t)Patch_FMV_Timing_Cave, 16, &Patch_FMV_Timing_OriginalCode) != 0;
  152. }

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:
  1. [Patch_FMV_Timings]
  2. 0               = 0x054D ; opn.dat
  3. 1               = 0x018B ; ins01.dat
  4. 2               = 0x00E3 ; ins02.dat
  5. 3               = 0x012D ; ins03.dat
  6. 4               = 0x01C2 ; ins04.dat
  7. 5               = 0x00BB ; ins05.dat
  8. 6               = 0x01D6 ; ins06.dat
  9. 7               = 0x0166 ; ins07.dat
  10. 8               = 0x0122 ; ins08.dat
  11. 9               = 0x00F9 ; ins09.dat
  12. 10              = 0x0336 ; enda.dat
  13. 11              = 0x034D ; endb.dat
  14. 12              = 0x00F3 ; roopne.dat
  15. 13              = 0x03BD ; bdino.dat
Derp~
Need a great web host? Check out: AnHonestHost.com


Donations can be made via Paypal:
https://www.paypal.me/atom0s
User avatar
atom0s
Site Admin
Posts: 397
Joined: Sun Jan 04, 2015 11:23 pm
Location: 127.0.0.1
Contact:

Re: Resident Evil 3 - Movie Timing Fix

Post by atom0s » Wed Nov 22, 2017 1:50 am

Movie Timing Debug Info

Here is some debug information about the timing table structure entries. This is mapped by setting single byte breakpoints and walking up the table as things are being read/write to. The size of each entry can be determined by the opcode that is using the entry.
  1. struct FMVEntry
  2. {
  3.     //
  4.     // 0042FE0D | 8B 80 78 B9 51 00        | mov eax,dword ptr ds:[eax+51B978]       | [eax+51B978]:"d:/bio19/data/zmovie/roopne.str"
  5.     //
  6.     uint32_t    NamePointer;    // Pointer to the file name.
  7.  
  8.     //
  9.     // 0042FDEE | 66 8B 88 7C B9 51 00     | mov cx,word ptr ds:[eax+51B97C]         |
  10.     //
  11.     uint16_t    FrameCount;     // Frame Counter / Time
  12.  
  13.     //
  14.     // Unknown (No reference for opening video.)
  15.     // Possibly just byte alignment padding.
  16.     //
  17.     uint16_t    Unknown0000;
  18.  
  19.     //
  20.     // 0042F7AF | 66 8B 4E 08              | mov cx,word ptr ds:[esi+8]              |
  21.     //
  22.     uint16_t    UnknownFlag0;
  23.  
  24.     //
  25.     // 0042F6EA | 66 8B 4E 0A              | mov cx,word ptr ds:[esi+A]              |
  26.     //
  27.     uint16_t    UnknownFlag1;
  28.  
  29.     //
  30.     // (used as 2 and 4 bytes)
  31.     // 0042F715 | 66 8B 46 0C              | mov ax,word ptr ds:[esi+C]              |
  32.     // 0042F71F | 8B 4E 0C                 | mov ecx,dword ptr ds:[esi+C]            |
  33.     //
  34.     uint32_t    UnknownFlag2;
  35.    
  36.     //
  37.     // 0042F697 | 66 8B 0C DD 88 B9 51 00  | mov cx,word ptr ds:[ebx*8+51B988]       |
  38.     //
  39.     uint16_t    UnknownFlag3;
  40.    
  41.     //
  42.     // 0042F630 | 8A 4E 12                 | mov cl,byte ptr ds:[esi+12]             |
  43.     // sub_42F600 (used as bytes)
  44.     // 0042F72C | 66 8B 46 12              | mov ax,word ptr ds:[esi+12]             |
  45.     //
  46.     uint16_t    UnknownFlag4;
  47.    
  48.     //
  49.     // 0042F781 | 66 8B 4E 14              | mov cx,word ptr ds:[esi+14]             |
  50.     //
  51.     uint16_t    UnknownFlag5;
  52.  
  53.     //
  54.     // Unknown (No reference for opening video.)
  55.     // Possibly just byte alignment padding.
  56.     //
  57.     uint8_t     Unknown0001;
  58.     uint8_t     Unknown0002;
  59. };
Derp~
Need a great web host? Check out: AnHonestHost.com


Donations can be made via Paypal:
https://www.paypal.me/atom0s
Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests