1. Save Game Analysis
Because searching for unknown values that constantly change is a pain in the ass and annoying, I choose to use other means to locate data such as the timer. In my case, I use the save game files because it tends to be a very helpful location for finding information based on your character and game data.
To start, we play the game through until we get to the first Maintenance Room. Here, we can save the game every time we try to exit the room. So to start, we save once. The file is stored at:
C:\Program Files (x86)\CAPCOM\DINOCRISIS
- SAVENO1.dno
Now that we have 1 save, leave the room, enter it and then exit again allowing us to resave. This time save in the 2nd save slot making the new file:
- SAAVENO2.dno
Keep note of the time shown on the screen for each save spot.
For example my two saves were:
- #1: 29:22
- #2: 31:29
Next we compare the save files to each other. And find the differences between the two of them. In this games case there are a handful of differences since we have to move and such while we are trying to save between games. So another step we can take is converting the timers we have to their storage type.
Now, this step will not work for every game, this is just a guess and in our case it worked out for this game. (CAPCOM seems to do the same for almost all their games) so it works out well for us. So we take the timer we have, for example:
29:22
And we convert this back to the base seconds timer.
- timer = original tick count from the game
- abilityTimer = timer / 60;
- hour = floor( abilityTimer / ( 60 * 60 ) );
- mins = floor( abilityTimer / 60 - hour * 60 );
- secs = floor( abilityTimer - ( mins + hour * 60 ) * 60 );
105700 - 105800
So we can check the differences of the two save games and locate this value.

Which we find is:
105724
2. Memory Analysis
Next after we find the value from the save game file, we can restart the game and go to the load menu. Once there, scan for the value we found in the save game file with a memory scanner such as Cheat Engine.
After searching (in my case only found 1 result) add this item to the cheat list then use Cheat Engines 'Find What Accesses This Address' feature.
Now we will find that the following is reading the data for the load screen:
- 0041D93C - 8B 48 0C - mov ecx,[eax+0C]
- 0041D939 - 8B 45 08 - mov eax,[ebp+08]
- 0041D93C - 8B 48 0C - mov ecx,[eax+0C]
- 0041D93F - 89 4D E4 - mov [ebp-1C],ecx
- 0041D942 - 8B 45 E4 - mov eax,[ebp-1C]
- 0041D945 - 33 D2 - xor edx,edx
- 0041D947 - B9 C04B0300 - mov ecx,00034BC0
- 0041D94C - F7 F1 - div ecx
- 0041D94E - 89 45 E0 - mov [ebp-20],eax
- 0041D951 - 8B 55 E0 - mov edx,[ebp-20]
- 0041D954 - 69 D2 C04B0300 - imul edx,edx,00034BC0
- 0041D95A - 8B 45 E4 - mov eax,[ebp-1C]
- 0041D95D - 2B C2 - sub eax,edx
- 0041D95F - 89 45 E4 - mov [ebp-1C],eax
- 0041D962 - 8B 45 E4 - mov eax,[ebp-1C]
- 0041D965 - 33 D2 - xor edx,edx
- 0041D967 - B9 100E0000 - mov ecx,00000E10
- 0041D96C - F7 F1 - div ecx
- 0041D96E - 89 45 F8 - mov [ebp-08],eax
- 0041D971 - 8B 55 F8 - mov edx,[ebp-08]
- 0041D974 - 69 D2 100E0000 - imul edx,edx,00000E10
- 0041D97A - 8B 45 E4 - mov eax,[ebp-1C]
- 0041D97D - 2B C2 - sub eax,edx
- 0041D97F - 89 45 E4 - mov [ebp-1C],eax
- 0041D982 - 8B 45 E4 - mov eax,[ebp-1C]
- 0041D985 - 33 D2 - xor edx,edx
- 0041D987 - B9 3C000000 - mov ecx,0000003C
- 0041D98C - F7 F1 - div ecx
To do that we take a chunk of the data here and scan for it as an array of bytes. In my case I choose this chunk:
33 D2 B9 10 0E 00 00 F7 F1
- 0041D945 - 33 D2 - xor edx,edx
- 0041D947 - B9 C04B0300 - mov ecx,00034BC0
- 0041D94C - F7 F1 - div ecx
The first one is actually the one we are after luckily.
Inside the first function result, we find:
- 0041D380 - 55 - push ebp
- 0041D381 - 8B EC - mov ebp,esp
- 0041D383 - 81 EC 8C000000 - sub esp,0000008C
- 0041D389 - C7 45 D8 0000801F - mov [ebp-28],1F800000
- 0041D390 - 81 7D D8 0000801F - cmp [ebp-28],1F800000
- 0041D397 - 72 09 - jb 0041D3A2
- 0041D399 - 81 7D D8 0004801F - cmp [ebp-28],1F800400
- 0041D3A0 - 72 08 - jb 0041D3AA
- 0041D3A2 - 8B 45 D8 - mov eax,[ebp-28]
- 0041D3A5 - 89 45 D4 - mov [ebp-2C],eax
- 0041D3A8 - EB 13 - jmp 0041D3BD
- 0041D3AA - 8B 4D D8 - mov ecx,[ebp-28]
- 0041D3AD - 8B 15 08E16700 - mov edx,[0067E108] : [022C6040]
- 0041D3B3 - 8D 84 0A 000080E0 - lea eax,[edx+ecx-1F800000]
- 0041D3BA - 89 45 D4 - mov [ebp-2C],eax
- 0041D3BD - 8B 4D D4 - mov ecx,[ebp-2C]
- 0041D3C0 - 8B 11 - mov edx,[ecx]
- 0041D3C2 - 8B 82 B49A0000 - mov eax,[edx+00009AB4]
- 0041D3C8 - 89 45 E0 - mov [ebp-20],eax
- 0041D3CB - 8B 45 E0 - mov eax,[ebp-20]
- 0041D3CE - 33 D2 - xor edx,edx
- 0041D3D0 - B9 C04B0300 - mov ecx,00034BC0
- 0041D3D5 - F7 F1 - div ecx
- 0041D3AD - 8B 15 08E16700 - mov edx,[0067E108] : [022C6040]
- 0041D3B3 - 8D 84 0A 000080E0 - lea eax,[edx+ecx-1F800000]
- 0041D3BA - 89 45 D4 - mov [ebp-2C],eax
- 0041D3BD - 8B 4D D4 - mov ecx,[ebp-2C]
- 0041D3C0 - 8B 11 - mov edx,[ecx]
- 0041D3C2 - 8B 82 B49A0000 - mov eax,[edx+00009AB4]
- 0041D3C8 - 89 45 E0 - mov [ebp-20],eax
- 0041D3CB - 8B 45 E0 - mov eax,[ebp-20]
Then it is read into, then stored, and read into from the base plus: 00009AB4
So we see that ultimately, the game is reading:
[[[0067E108]+0]+09AB4]
So we follow this and find that its the current in-game timer.


3. Confirming Our Work
To double check and make sure things are correct, we can reset the game and go to the load screen again. With one of our save games we know the timer in the save game file is:
00:31:29 (113368)
From the load screen we scan for our value of: 113368
So we find this value at: dino.exe+2C66AC (6C66AC)
Now select the game, but before selecting a costume, scan for our value again: 113368
We now find more than one address of this value, now run the game as normal. One of those addresses will now be constantly incrementing which is our in-game timer value.
Source Code
Here is some source code to read and display the timer over the game window using simple GDI:
- /**
- * Dino Crisis (PC) - Timer Trainer
- * (c) 2014 atom0s [atom0s@live.com]
- */
- #include <Windows.h>
- #include <iostream>
- #include <TlHelp32.h>
- /**
- * @brief Game Executable Names
- */
- const char g_GameNames[][255] =
- {
- { "DINO.exe" },
- { NULL }
- };
- /**
- * @brief Data structure passed to our window enumeration callback.
- */
- typedef struct _WINDOW_DATA
- {
- DWORD ProcessId;
- HWND WindowHandle;
- } WINDOW_DATA;
- /**
- * @brief Enumerates the running windows on the system.
- *
- * @Param hWnd The current window handle.
- * @Param lParam The custom data passed to this callback.
- *
- * @returN True to keep searching, false otherwise.
- */
- BOOL __stdcall EnumWindowsProc(HWND hWnd, LPARAM lParam)
- {
- auto procId = (DWORD)0;
- auto wd = (WINDOW_DATA*)lParam;
- ::GetWindowThreadProcessId(hWnd, &procId);
- if (procId == wd->ProcessId)
- {
- wd->WindowHandle = hWnd;
- return FALSE;
- }
- return TRUE;
- }
- /**
- * @brief Obtains the main window hwnd of the given process id.
- *
- * @Param dwProcId The process id of our game.
- *
- * @returN The window handle if found, NULL otherwise.
- */
- HWND getResidentEvilWindowHandle(DWORD dwProcId)
- {
- auto wd = new WINDOW_DATA();
- wd->ProcessId = dwProcId;
- ::EnumWindows(EnumWindowsProc, (LPARAM)wd);
- auto hWnd = wd->WindowHandle;
- delete wd;
- return hWnd;
- }
- /**
- * @brief Obtains the process id of our target.
- * @returN The process id if found, 0 otherwise.
- */
- DWORD getGameProcessId(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
- {
- for (auto x = 0; x < _countof(g_GameNames); x++)
- {
- if (g_GameNames[x][0] != NULL && _strnicmp(pe32.szExeFile, g_GameNames[x], strlen(g_GameNames[x])) == 0)
- {
- ::CloseHandle(handle);
- return pe32.th32ProcessID;
- }
- }
- } while (::Process32Next(handle, &pe32));
- ::CloseHandle(handle);
- return 0;
- }
- /**
- * @brief Obtains the process base address of the target process.
- *
- * @Param dwProcId The process id to obtain the base address of.
- * @returN The process base address if found, 0 otherwise.
- */
- DWORD getGameProcessBaseAddress(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 Obtains the process base size of the target process.
- *
- * @Param dwProcId The process id to obtain the base address of.
- * @returN The process base size if found, 0 otherwise.
- */
- DWORD getGameProcessBaseSize(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 me32.modBaseSize;
- }
- /**
- * @brief Compares the given memory data against the given pattern using the desired mask.
- *
- * @Param lpData Pointer to the actual memory to be compared again.
- * @Param lpPattern Pointer to the pattern to scan the memory for.
- * @Param pszMask String containing the mask for the pattern being compared against.
- *
- * @returN True when the pattern is found, false otherwise.
- */
- static bool __stdcall MaskCompare(const unsigned char* lpData, const unsigned char* lpPattern, const char* pszMask)
- {
- for (; *pszMask; ++pszMask, ++lpData, ++lpPattern)
- if (*pszMask == 'x' && *lpData != *lpPattern)
- return false;
- return (*pszMask) == NULL;
- }
- /**
- * @brief Locates the given pattern inside the given data.
- *
- * @Param lpData The data to scan for our pattern within.
- * @Param nDataSize The size of the data block to scan within.
- * @Param lpPattern The pattern to compare the memory against.
- * @Param pszMask String containing the mask for the pattern being compared against.
- *
- * @returN Location of where the pattern was found, 0 otherwise.
- */
- static DWORD __stdcall FindPattern(const unsigned char* lpData, unsigned int nDataSize, const unsigned char* lpPattern, const char* pszMask)
- {
- for (unsigned int x = 0; x < nDataSize; x++)
- if (MaskCompare(lpData + x, lpPattern, pszMask))
- return (DWORD)(lpData + x);
- return 0;
- }
- /**
- * @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[])
- {
- std::cout << "===============================================" << std::endl;
- std::cout << "Dino Crisis (PC) Game Timer" << std::endl;
- std::cout << "by atom0s (c) 2014 [atom0s@live.com]" << std::endl;
- std::cout << "===============================================" << std::endl << std::endl;
- // Get the process id..
- auto procId = getGameProcessId();
- if (procId == 0)
- return -1;
- std::cout << "[*] Found game process!" << std::endl;
- // Get the process base address..
- auto procBase = getGameProcessBaseAddress(procId);
- if (procBase == 0)
- return -1;
- std::cout << "[*] Found game process base!" << std::endl;
- // Get the process base size..
- auto procSize = getGameProcessBaseSize(procId);
- if (procSize == 0)
- return -1;
- std::cout << "[*] Obtained process base size!" << std::endl;
- // Get the window handle..
- auto procHwnd = getResidentEvilWindowHandle(procId);
- // 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;
- std::cout << "[*] Obtained process handle for memory reading!" << std::endl;
- // Dump the process memory..
- auto procDump = new unsigned char[procSize + 1];
- ::ReadProcessMemory(handle, (LPCVOID)procBase, procDump, procSize, NULL);
- // Scan for our signature..
- auto timerPtr = FindPattern(procDump, procSize, (BYTE*)"\x8B\x15\xFF\xFF\xFF\xFF\x8D\x84\x0A\xFF\xFF\xFF\xFF\x89\x45\xD4\x8B\x4D\xD4\x8B\x11\x8B\x82\xFF\xFF\xFF\xFF\x89\x45\xE0\x8B\x45\xE0", "xx????xxx????xxxxxxxxxx????xxxxxx");
- if (timerPtr == NULL)
- {
- ::CloseHandle(handle);
- return 0;
- }
- // Readjust the offsets..
- timerPtr -= (DWORD)procDump;
- std::cout << "[*] Found signatures for timer offsets!" << std::endl << std::endl;
- std::cout << "[*] Game time should now be rendering on the game window!" << std::endl;
- std::cout << "[*] Hold shift and press escape to close this application!" << std::endl;
- // Loop until we want to escape..
- while (true)
- {
- // Allow escape to leave the loop and close the app..
- if ((::GetAsyncKeyState(VK_ESCAPE) & 1) && ::GetAsyncKeyState(VK_SHIFT))
- break;
- // Obtain the main timer value..
- DWORD dwTimerValue1 = 0;
- DWORD dwTimerValue2 = 0;
- ::ReadProcessMemory(handle, (LPCVOID)(procBase + timerPtr + 2), &dwTimerValue1, 4, NULL);
- ::ReadProcessMemory(handle, (LPCVOID)(procBase + timerPtr + 23), &dwTimerValue2, 4, NULL);
- ::ReadProcessMemory(handle, (LPCVOID)(dwTimerValue1), &dwTimerValue1, 4, NULL);
- ::ReadProcessMemory(handle, (LPCVOID)(dwTimerValue1), &dwTimerValue1, 4, NULL);
- ::ReadProcessMemory(handle, (LPCVOID)(dwTimerValue1 + dwTimerValue2), &dwTimerValue1, 4, NULL);
- // Convert the time into a timestamp..
- auto timer = dwTimerValue1;
- 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);
- char szBuffer[1024] = { 0 };
- sprintf_s(szBuffer, "%02i:%02i:%02i\r\n", hour, mins, secs);
- // Get the window dc..
- auto dc = ::GetDC(procHwnd);
- // Get the default UI font..
- LOGFONT logfont = { 0 };
- auto font = ::GetStockObject(DEFAULT_GUI_FONT);
- ::GetObject(font, sizeof(LOGFONT), &logfont);
- // Adjust the font size..
- logfont.lfHeight = -MulDiv(15, ::GetDeviceCaps(dc, LOGPIXELSY), 72);
- // Create the new font..
- font = ::CreateFontIndirect(&logfont);
- // Change the dc font..
- auto oldfont = ::SelectObject(dc, font);
- // Draw our text..
- RECT rectText = { 5, 5, 0, 0 };
- ::SetTextColor(dc, 0x0000FF);
- ::SetBkMode(dc, TRANSPARENT);
- ::DrawText(dc, szBuffer, strlen(szBuffer), &rectText, DT_CALCRECT);
- ::DrawText(dc, szBuffer, strlen(szBuffer), &rectText, 0);
- // Cleanup the font..
- ::SelectObject(dc, oldfont);
- ::DeleteObject(font);
- ::DeleteDC(dc);
- ::Sleep(1);
- }
- // Cleanup..
- ::CloseHandle(handle);
- return ERROR_SUCCESS;
- }
