Cheat Engine Forum Index Cheat Engine
The Official Site of Cheat Engine
 
 FAQFAQ   SearchSearch   MemberlistMemberlist   UsergroupsUsergroups   RegisterRegister 
 ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in 


Help with a semi-basic C++ program (To track in-game timer)

 
Post new topic   Reply to topic    Cheat Engine Forum Index -> General programming
View previous topic :: View next topic  
Author Message
Hetsig
Newbie cheater
Reputation: 1

Joined: 17 Mar 2014
Posts: 11

PostPosted: Mon Mar 17, 2014 12:39 pm    Post subject: Help with a semi-basic C++ program (To track in-game timer) Reply with quote

I would love to have some help for a project i'm doing as of now.
So i'm speedrunning Resident Evil 3 on PC just now, and i've seen a in-game timer program for Resident Evil 2 on PC but not on the third installment.

What i like the program to do: Fetch the current in-game time from the game.
I know this might be easier than it sounds to make but i'm not that good in C++.

(A screenshot on the Resident Evil 2 program is included)
I only want the timer in a simple .exe to show in my stream Smile.

Why this is needed
In professional speedrunning in Resident Evil 1-3, the in-game timer is the MAIN timer that's being considered if you've made a World Record or not. As you only can see the total time when you've completed the game it's VERY useful to have this program to keep track of it AND to question if it's worth completing the run or not.
There's split programs that does this BUT in professional play the in-game time is what counts and sometimes the split time is not accurate with the in-game time.

You will do the Resident Evil speedrunning community a big favor and i will credit you as much as possible!

EDIT There's i a special .exe version of the game that i would like to have this timer work on. You can PM me to get it, or information about it.



ss (2014-03-17 at 06.12.08).png
 Description:
 Filesize:  21.64 KB
 Viewed:  6870 Time(s)

ss (2014-03-17 at 06.12.08).png




Last edited by Hetsig on Thu Mar 20, 2014 1:07 pm; edited 1 time in total
Back to top
View user's profile Send private message
atom0s
Moderator
Reputation: 205

Joined: 25 Jan 2006
Posts: 8587
Location: 127.0.0.1

PostPosted: Wed Mar 19, 2014 7:50 pm    Post subject: Reply with quote

I'd start with the save game file to locate things.

Save a game, copy the save file to your desktop, save again about a minute later. Then compare the files byte by byte in a hex editor of your choice.

For example:
Code:

D2 5E 01 00 00 00 00 00 01 00 00 00 85 10 A8 B1
00 00 53 B4 46 00 00 00 02 00 00 03 00 00 04 00
FF FF FF FF FF FF 00 00 00 00 05 00 01 00 03 00
00 00 00 00 00 00 01 00 00 00 00 00 08 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
17 00 01 00 14 01 02 00 00 00 4E CE 10 01 00 00
00 00 C8 00 01 00 BC 02 00 00 00 00 00 00 08 00
00 00 03 00 00 00 00 00 94 02 90 87 00 80 00 04


This chunk of my save file contains the save count and some other data.
The first 4 bytes appear that they could be the timer.

The second row where the 00 02 00 is, that is the save count.

So now we can scan for this when we are in-game. Load up the game, scan for the time in the saved file before loading the game. (Sit at the load menu with the game hovered but not actually in the game.)

I found a few results. Using breakpoints lead me to this function:
Code:

0043AE63 - C7 05 F8C3A700 00000000 - mov [00A7C3F8],00000000
0043AE6D - EB 27                 - jmp 0043AE96
0043AE6F - 6A FF                 - push -01
0043AE71 - E8 AAA50D00           - call 00515420
0043AE76 - 8B 15 E46DA700        - mov edx,[00A76DE4] : [00001993]
0043AE7C - 8B C8                 - mov ecx,eax
0043AE7E - 2B CA                 - sub ecx,edx
0043AE80 - 8B 15 F8C3A700        - mov edx,[00A7C3F8] : [00018396] ; Time currently in our saved file.
0043AE86 - 83 C4 04              - add esp,04
0043AE89 - 03 D1                 - add edx,ecx
0043AE8B - 89 15 F8C3A700        - mov [00A7C3F8],edx
0043AE91 - A3 E46DA700           - mov [00A76DE4],eax


The call here is:
Code:

00515420 - 8B 0D 44CC5400        - mov ecx,[0054CC44] : [04AE8890]
00515426 - 8B 54 24 04           - mov edx,[esp+04]
0051542A - 8B 01                 - mov eax,[ecx]
0051542C - 52                    - push edx ; our current timer from the saved file...
0051542D - FF 50 78              - call dword ptr [eax+78]


Following this call takes us to: [[[0054CC44]]+78]
Code:

00518630 - 8B 44 24 04           - mov eax,[esp+04]
00518634 - 85 C0                 - test eax,eax
00518636 - 56                    - push esi
00518637 - 8B F1                 - mov esi,ecx
...


Reading through this, we will see:
Code:

00518643 - 8B 86 AC050000        - mov eax,[esi+000005AC]


This is using the class pointer (this) +0x5AC which points to the increased time since you last saved.

So from the looks of it:
ResidentEvil3.exe+67C3F8 = The time that was last saved.
[0054CC44]+5AC = The time since you last saved.

_________________
- Retired.
Back to top
View user's profile Send private message Visit poster's website
atom0s
Moderator
Reputation: 205

Joined: 25 Jan 2006
Posts: 8587
Location: 127.0.0.1

PostPosted: Wed Mar 19, 2014 7:57 pm    Post subject: Reply with quote

Also if you want it here is the full timer function:
Code:

int __thiscall sub_518630(int this, signed int a2)
{
  int v2; // esi@1
  __int64 v3; // qax@2
  int v4; // eax@4
  int v5; // ecx@5
  int v6; // ecx@8
  int v7; // edi@13
  int v8; // eax@14
  int v9; // ecx@14
  int v10; // edi@17
  int v11; // esi@17
  double v12; // st7@17
  int v13; // edi@17
  int v14; // ecx@18

  v2 = this;
  if ( a2 < 0 )
  {
    (*(void (**)(void))(**(_DWORD **)(this + 4) + 16))();
    LODWORD(v3) = *(_DWORD *)(v2 + 1452);
    return v3;
  }
  if ( !a2 )
  {
    v4 = *(_DWORD *)(this + 4);
    if ( v4 )
      v5 = *(_DWORD *)(v4 + 44);
    else
      v5 = 0;
    if ( (unsigned int)(*(int (**)(void))(*(_DWORD *)v5 + 8))() <= *(_DWORD *)(v2 + 1460) )
    {
      (*(void (__thiscall **)(int))(*(_DWORD *)v2 + 176))(v2);
      (*(void (**)(void))(**(_DWORD **)(v2 + 4) + 16))();
    }
    else
    {
      v6 = *(_DWORD *)(v2 + 4);
      ++*(_DWORD *)(v2 + 0x5B8);
      (*(void (**)(void))(*(_DWORD *)v6 + 16))();
    }
LABEL_16:
    *(_DWORD *)(v2 + 0x5B0) = *(_DWORD *)(v2 + 0x5AC);
    goto LABEL_17;
  }
  if ( a2 == 1 )
  {
    (*(void (**)(void))(**(_DWORD **)(this + 4) + 16))();
  }
  else
  {
    if ( a2 > 1 )
    {
      v7 = a2 + *(_DWORD *)(this + 1456);
      if ( *(_DWORD *)(this + 1452) < (unsigned int)v7 )
      {
        do
        {
          (*(void (__thiscall **)(int))(*(_DWORD *)v2 + 176))(v2);
          (*(void (**)(void))(**(_DWORD **)(v2 + 4) + 16))();
        }
        while ( *(_DWORD *)(v2 + 1452) < (unsigned int)v7 );
      }
      else
      {
        v8 = *(_DWORD *)(this + 1464);
        v9 = *(_DWORD *)(this + 4);
        *(_DWORD *)(v2 + 1464) = v8 + 1;
        (*(void (**)(void))(*(_DWORD *)v9 + 16))();
      }
      goto LABEL_16;
    }
  }
LABEL_17:
  v12 = (double)*(unsigned int *)(v2 + 1456);
  v13 = *(_DWORD *)(v2 + 1448);
  v11 = *(_DWORD *)(v2 + 4);
  v10 = v13 - (unsigned __int64)(v12 * -16.66666666666667);
  if ( v11 )
    v14 = *(_DWORD *)(v11 + 44);
  else
    v14 = 0;
  return (unsigned __int64)((double)(unsigned int)((*(int (**)(void))(*(_DWORD *)v14 + 8))() - v10) * 15.78);
}


The main important part here is this:
Code:

  if ( a2 < 0 )
  {
    (*(void (**)(void))(**(_DWORD **)(this + 4) + 0x10))();
    LODWORD(v3) = *(_DWORD *)(v2 + 0x5AC);
    return v3;
  }


When you save the game, the 2nd param (a2) is always -1, so this first chunk is whats important to us.

So now with that info we have this:

Code:

//
// Call the timer function I pasted above..
//
0043AE6F - 6A FF                 - push -01
0043AE71 - E8 AAA50D00           - call 00515420

//
// Calculate the difference in time..
//
0043AE76 - 8B 15 E46DA700        - mov edx,[00A76DE4] : [00000593]
0043AE7C - 8B C8                 - mov ecx,eax
0043AE7E - 2B CA                 - sub ecx,edx
0043AE80 - 8B 15 F8C3A700        - mov edx,[00A7C3F8] : [00016F96]
0043AE86 - 83 C4 04              - add esp,04
0043AE89 - 03 D1                 - add edx,ecx

//
// Store the new time offsets..
//
0043AE8B - 89 15 F8C3A700        - mov [00A7C3F8],edx
0043AE91 - A3 E46DA700           - mov [00A76DE4],eax


Which looks like this:
Code:

      v0 = sub_515420(-1); // Call to the timer function..
      dword_A7C3F8 += v0 - dword_A76DE4;
      dword_A76DE4 = v0;


So in the end we have this:
A7C3F8 += [0054CC44]+05AC - A76DE4

_________________
- Retired.
Back to top
View user's profile Send private message Visit poster's website
atom0s
Moderator
Reputation: 205

Joined: 25 Jan 2006
Posts: 8587
Location: 127.0.0.1

PostPosted: Wed Mar 19, 2014 8:41 pm    Post subject: Reply with quote

And now onto code, here is a simple example in a console of how to read and calculate the timer:

Code:


/**
 * Resident Evil 3 - Game Timer
 * (c) 2014 atom0s [[email protected]]
 */

#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

/**
 * @brief Timer information offsets.
 */
#define TIMER_OFFSET1   0x67C3F8 // The first offset of the timer calculation.
#define TIMER_OFFSET2   0x676DE4 // The second offset of the timer calculation.
#define TIMER_CLASSPTR  0x14CC44 // The pointer offset to the timer class object.

/**
 * @brief Obtains the process id of Residen Evil 3.
 * @return The process id if found, 0 otherwise.
 */
DWORD getResidentEvilProcessId(void)
{
    PROCESSENTRY32 pe32{ sizeof(PROCESSENTRY32) };

    auto handle = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (handle == INVALID_HANDLE_VALUE)
        return 0;

    if (!::Process32First(handle, &pe32))
    {
        ::CloseHandle(handle);
        return 0;
    }

    do
    {
        if (_strnicmp(pe32.szExeFile, "ResidentEvil3.exe", 17) == 0)
        {
            ::CloseHandle(handle);
            return pe32.th32ProcessID;
        }
    } while (::Process32Next(handle, &pe32));

    ::CloseHandle(handle);
    return 0;
}

/**
 * @brief Obtains the process base address of Residen Evil 3.
 *
 * @param dwProcId  The process id to obtain the base address of.
 * @return The process base address if found, 0 otherwise.
 */
DWORD getResidentEvilProcessBase(DWORD dwProcId)
{
    MODULEENTRY32 me32{ sizeof(MODULEENTRY32) };

    auto handle = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcId);
    if (handle == INVALID_HANDLE_VALUE)
        return 0;

    if (!::Module32First(handle, &me32))
    {
        ::CloseHandle(handle);
        return 0;
    }

    ::CloseHandle(handle);
    return (DWORD)me32.modBaseAddr;
}

/**
 * @brief The main application entry point.
 *
 * @param argc  The number of arguments passed to this program.
 * @param argv  The arguments passed to this program.
 *
 * @return Error code on succss of application.
 */
int __cdecl main(int argc, char* argv[])
{
    // Get the process id..
    auto procId = getResidentEvilProcessId();
    if (procId == 0)
        return -1;

    // Get the process base address..
    auto procBase = getResidentEvilProcessBase(procId);
    if (procBase == 0)
        return -1;

    // Open the process for reading..
    auto handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ, FALSE, procId);
    if (handle == INVALID_HANDLE_VALUE)
        return -1;

    while (true)
    {
        // Allow escape to leave the loop and close the app..
        if (GetAsyncKeyState(VK_ESCAPE) & 1)
            break;

        DWORD dwTimerObject = 0;
        DWORD dwTimerValue1 = 0;
        DWORD dwTimerValue2 = 0;
        DWORD dwTimerValue3 = 0;

        // Read the timer value from the timer class..
        ::ReadProcessMemory(handle, (LPCVOID)(procBase + TIMER_CLASSPTR), &dwTimerObject, 4, NULL);
        ::ReadProcessMemory(handle, (LPCVOID)(dwTimerObject + 0x00005AC), &dwTimerValue1, 4, NULL);

        // Read the other two timer values..
        ::ReadProcessMemory(handle, (LPCVOID)(procBase + TIMER_OFFSET1), &dwTimerValue2, 4, NULL);
        ::ReadProcessMemory(handle, (LPCVOID)(procBase + TIMER_OFFSET2), &dwTimerValue3, 4, NULL);

        // Calculate the full timer..
        dwTimerValue2 += dwTimerValue1 - dwTimerValue3;

        // Convert the time into a timestamp..
        auto timer = dwTimerValue2;
        timer /= 60;

        auto hour = (int)floor(timer / (60 * 60));
        auto mins = (int)floor(timer / 60 - hour * 60);
        auto secs = (int)floor(timer - (mins + hour * 60) * 60);

        printf_s("%02i:%02i:%02i\r\n", hour, mins, secs);
       
        ::Sleep(1000);
    }

    // Cleanup..
    ::CloseHandle(handle);
    return ERROR_SUCCESS;
}


Download:
https://www.dropbox.com/s/eviwivcm9pyeg9y/atom0s%21re3timer.7z

Important Note

Keep in mind, when you start the game, the timer is already ticking up.
However when you create a new game, as soon as the intro movie and junk are done and you bust through the door to start playing, the timer resets and the game timer is then "true" time to the game.

No idea why they coded it that way but yea.. it is that way lol.

_________________
- Retired.
Back to top
View user's profile Send private message Visit poster's website
atom0s
Moderator
Reputation: 205

Joined: 25 Jan 2006
Posts: 8587
Location: 127.0.0.1

PostPosted: Thu Mar 20, 2014 6:05 pm    Post subject: Reply with quote

Talked with Hetsig on Steam, he needed the timer for the JP version of the game and not the English version. So here is an updated archive containing src for both versions of the game now:
https://www.dropbox.com/s/eviwivcm9pyeg9y/atom0s%21re3timer.7z

Use the define at the top of the src to change the game version you want to use.

#define USE_ENGLISH_GAME_OFFSETS 0

0 = JP (Japanese)
1 = NA (English)

=======

Another small update, if you use the SourceNext version, here are the offsets for that:
Code:
#define TIMER_OFFSET1   0x667338 // The first offset of the timer calculation.
#define TIMER_OFFSET2   0x661D24 // The second offset of the timer calculation.
#define TIMER_CLASSPTR  0x139BBC // The pointer offset to the timer class object.

_________________
- Retired.


Last edited by atom0s on Thu Mar 20, 2014 8:00 pm; edited 1 time in total
Back to top
View user's profile Send private message Visit poster's website
atom0s
Moderator
Reputation: 205

Joined: 25 Jan 2006
Posts: 8587
Location: 127.0.0.1

PostPosted: Thu Mar 20, 2014 9:44 pm    Post subject: Reply with quote

And last update, this version is designed to work for any version of the game:
https://www.dropbox.com/s/eviwivcm9pyeg9y/atom0s%21re3timer.7z

_________________
- Retired.
Back to top
View user's profile Send private message Visit poster's website
Hetsig
Newbie cheater
Reputation: 1

Joined: 17 Mar 2014
Posts: 11

PostPosted: Fri Mar 21, 2014 7:34 am    Post subject: Reply with quote

This is amazing atom0s, thank you very much for the help!
It works flawless Very Happy
Back to top
View user's profile Send private message
Display posts from previous:   
Post new topic   Reply to topic    Cheat Engine Forum Index -> General programming All times are GMT - 6 Hours
Page 1 of 1

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You can download files in this forum


Powered by phpBB © 2001, 2005 phpBB Group

CE Wiki   IRC (#CEF)   Twitter
Third party websites