2024-07-07 09:22:24 +02:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Globalization ;
using System.IO ;
using System.Reflection ;
using System.Security.Cryptography ;
using Debugger ;
namespace Transformers2_Launcher
{
public class Transformers2_Launcher
{
private const string MEMORY_DATA_FOLDER = "MemoryData" ;
private const string TARGET_EXE_NAME = @"Transformers2.exe" ;
private int _ProcessId = 0 ;
private Process _Process ;
private IntPtr _Process_MemoryBaseAddress = IntPtr . Zero ;
private IntPtr _ProcessHandle = IntPtr . Zero ;
//Memory Hacks
private UInt32 _DisableCShell_Offset = 0x001E70E4 ;
private UInt32 _RestoreHiddenConfig_Offset = 0x00240C80 ;
private UInt32 _SetWindowedMode_Offset = 0x002409F5 ;
private UInt32 _RemoveOriginalWindowedMode_Offset = 0x00240A1C ;
private UInt32 _ForceResolutionIndex_Offset = 0x00260AD5 ;
private UInt32 _ResolutionTableHd_Offset = 0x008A5008 ;
private UInt32 _DisableLEDBoardCreation_Offset = 0x002EB93C ;
private UInt32 _DisableSAEBoardCreation_Offset = 0x0017A4A2 ;
private UInt32 _CGunMgrForceInputMouse_Offset1 = 0x000F54F9 ;
private UInt32 _CGunMgrForceInputMouse_Offset2 = 0x000F551C ;
private UInt32 _CGunMgrForceInputMouse_Offset3 = 0x000F5534 ;
2024-07-21 13:30:58 +02:00
private UInt32 _RelativePathFix_Ocean_Offset = 0x0010B018 ;
private UInt32 _RelativePathFix_InfiniteOcean_Offset = 0x0010C0B2 ;
private UInt32 _RelativePathFix_SmokeRenderer_Offset = 0x001EDC88 ;
private UInt32 _RelativePathFix_TrackRenderer_Offset = 0x002174E2 ;
private UInt32 _RelativePathFix_WaterDistort_Offset = 0x00111D01 ;
2024-07-07 09:22:24 +02:00
//MD5 check of target binaries, may help to know if it's the wrong version or not compatible
protected Dictionary < string , string > _KnownMd5Prints ;
protected String _TargetProcess_Md5Hash = string . Empty ;
//Config values
private const string LAUNCHER_INI_PATH = @".\Transformers2_Launcher.ini" ;
private INIFile _Launcher_IniFile ;
private UInt32 _Cfg_ScreenWidth = 0 ;
private UInt32 _Cfg_ScreenHeight = 0 ;
private byte _Cfg_Windowed = 0 ;
Debugger . QuickDebugger _Qdb ;
public Transformers2_Launcher ( bool EnableLogs )
{
Logger . InitLogFileName ( ) ;
Logger . IsEnabled = EnableLogs ;
_Launcher_IniFile = new INIFile ( AppDomain . CurrentDomain . BaseDirectory + LAUNCHER_INI_PATH ) ;
if ( ! File . Exists ( AppDomain . CurrentDomain . BaseDirectory + TARGET_EXE_NAME ) )
{
Logger . IsEnabled = true ;
Logger . WriteLog ( "Transformers2_Launcher() => Transformers2.exe not found. Abording..." ) ;
Environment . Exit ( 0 ) ;
}
if ( ! File . Exists ( _Launcher_IniFile . FInfo . FullName ) )
{
Logger . IsEnabled = true ;
Logger . WriteLog ( "Transformers2_Launcher() => No config file found. Abording..." ) ;
Environment . Exit ( 0 ) ;
}
try
{
_Cfg_ScreenWidth = UInt32 . Parse ( _Launcher_IniFile . IniReadValue ( "Video" , "WIDTH" ) ) ;
_Cfg_ScreenHeight = UInt32 . Parse ( _Launcher_IniFile . IniReadValue ( "Video" , "HEIGHT" ) ) ;
if ( _Launcher_IniFile . IniReadValue ( "Video" , "FULLSCREEN" ) . Equals ( "0" ) )
_Cfg_Windowed = 1 ;
else
_Cfg_Windowed = 0 ;
}
catch ( Exception Ex )
{
Logger . IsEnabled = true ;
Logger . WriteLog ( "Transformers2_Launcher() => Error reading config file : " + _Launcher_IniFile . FInfo . FullName ) ;
Logger . WriteLog ( Ex . Message . ToString ( ) ) ;
Logger . WriteLog ( "Abording..." ) ;
Environment . Exit ( 0 ) ;
}
_KnownMd5Prints = new Dictionary < String , String > ( ) ;
_KnownMd5Prints . Add ( "Transformers Shadow Rising v180605 - Original Dump" , "b3b1f4ad6408d6ee946761a00f761455" ) ;
}
/// <summary>
/// Create the process with DEBUG attributes, so that we can stop it to inject the code, and then resume it
/// Inspired from a hand-made debugger https://www.codeproject.com/Articles/43682/Writing-a-basic-Windows-debugger
/// </summary>
public void RunGame ( )
{
_Qdb = new QuickDebugger ( AppDomain . CurrentDomain . BaseDirectory + TARGET_EXE_NAME ) ;
_Qdb . OnDebugEvent + = new Debugger . QuickDebugger . DebugEventHandler ( Qdb_OnDebugEvent ) ;
_Qdb . StartProcess ( ) ;
}
private void Qdb_OnDebugEvent ( object sender , Debugger . DebugEventArgs e )
{
switch ( e . Dbe . dwDebugEventCode )
{
case DebugEventType . CREATE_PROCESS_DEBUG_EVENT :
{
Logger . WriteLog ( "RunGame() => Process created" ) ;
_Qdb . ContinueDebugEvent ( ) ;
} break ;
case DebugEventType . CREATE_THREAD_DEBUG_EVENT :
{
DEBUG_EVENT . CREATE_THREAD_DEBUG_INFO ti = new DEBUG_EVENT . CREATE_THREAD_DEBUG_INFO ( ) ;
ti = e . Dbe . CreateThread ;
Logger . WriteLog ( "Thread 0x" + ti . hThread . ToString ( "X8" ) + " (Id: " + e . Dbe . dwThreadId . ToString ( ) + ") created" ) ;
_Qdb . ContinueDebugEvent ( ) ;
} break ;
//The game has a breakpoint installed at start (!), we can use it to search for information, block the process to insert our code
case DebugEventType . EXCEPTION_DEBUG_EVENT :
{
DEBUG_EVENT . EXCEPTION_DEBUG_INFO Ex = new DEBUG_EVENT . EXCEPTION_DEBUG_INFO ( ) ;
Ex = e . Dbe . Exception ;
if ( Ex . ExceptionRecord . ExceptionCode = = QuickDebugger . STATUS_BREAKPOINT )
{
Logger . WriteLog ( "RunGame() => Breakpoint reached !" ) ;
Process p = Process . GetProcessById ( e . Dbe . dwProcessId ) ;
_Process = p ;
_ProcessId = _Process . Id ;
_ProcessHandle = _Process . Handle ;
_Process_MemoryBaseAddress = _Process . MainModule . BaseAddress ;
Logger . WriteLog ( "RunGame() => Process ID: " + _ProcessId . ToString ( ) ) ;
Logger . WriteLog ( "RunGame() => Process Memory Base Address: 0x" + _Process_MemoryBaseAddress . ToString ( "X8" ) ) ;
Logger . WriteLog ( "RunGame() => Process Handle: 0x" + _ProcessHandle . ToString ( "X8" ) ) ;
CheckExeMd5 ( ) ;
ReadGameDataFromMd5Hash ( ) ;
Apply_Hacks ( ) ;
Logger . WriteLog ( "RunGame() => Hack complete, leaving the game to run on its own now...." ) ;
_Qdb . DetachDebugger ( ) ;
_Qdb . ContinueDebugEvent ( ) ;
}
} break ;
default :
{
_Qdb . ContinueDebugEvent ( ) ;
} break ;
}
}
/// <summary>
/// Creating the Process without DEBUG attributes can allow another debugger to go in and analyse what's going on
/// </summary>
public void Run_Game_Debug ( )
{
FileInfo fi = new FileInfo ( AppDomain . CurrentDomain . BaseDirectory + TARGET_EXE_NAME ) ;
_Process = new Process ( ) ;
_Process . StartInfo . FileName = fi . FullName ;
_Process . Start ( ) ;
try
{
ProcessTools . SuspendProcess ( _Process ) ;
_ProcessId = _Process . Id ;
_ProcessHandle = _Process . Handle ;
_Process_MemoryBaseAddress = _Process . MainModule . BaseAddress ;
Apply_Hacks ( ) ;
ProcessTools . ResumeProcess ( _Process ) ;
}
catch ( InvalidOperationException )
{
}
catch ( Exception )
{
}
}
#region Hacks
public void Apply_Hacks ( )
{
//Disabling the CShell necessity, if not the game is shutting down if no correct Keep-alive reply from the Shell.exe program
WriteByte ( ( UInt32 ) _Process_MemoryBaseAddress + _DisableCShell_Offset , 0x00 ) ;
//GetConfigIniDir() function is stripped. Putting back a fixed name (./App.ini) to restore the possibility to change some settings
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _RestoreHiddenConfig_Offset , new byte [ ] { 0xB8 , 0x86 , 0x0C , 0x64 , 0x00 , 0xC3 , 0x2E , 0x2F , 0x41 , 0x70 , 0x70 , 0x2E , 0x69 , 0x6E , 0x69 , 0x00 } ) ;
//LED Board is connected through COM port
//Setting COM access result to (-1) instead of trying to open COM port for real
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _DisableLEDBoardCreation_Offset , new byte [ ] { 0xB8 , 0xFF , 0xFF , 0xFF , 0xFF , 0xEB , 0x1D , 0x90 , 0x90 , 0x90 , 0x90 } ) ;
//Force not to create SAEBoard
WriteByte ( ( UInt32 ) _Process_MemoryBaseAddress + _DisableSAEBoardCreation_Offset , 0xEB ) ;
//CGunMgr() init input mode by checking first if JVS is enabled, forcing it to false
WriteByte ( ( UInt32 ) _Process_MemoryBaseAddress + _CGunMgrForceInputMouse_Offset1 , 0xEB ) ;
//Then checking if SAEBoard is active, forcing it to false also
WriteByte ( ( UInt32 ) _Process_MemoryBaseAddress + _CGunMgrForceInputMouse_Offset2 , 0xEB ) ;
//Finally forcing input mode to '1' (mouse) for P2 (already 1 for P1 normally)
WriteByte ( ( UInt32 ) _Process_MemoryBaseAddress + _CGunMgrForceInputMouse_Offset3 , 0x1 ) ;
//Replacing the original default value for Windowed mode by our own
WriteByte ( ( UInt32 ) _Process_MemoryBaseAddress + _SetWindowedMode_Offset , _Cfg_Windowed ) ;
//Disabling the default Windowed mode set after reading the "hidden" config file
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _RemoveOriginalWindowedMode_Offset , new byte [ ] { 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 } ) ;
//Screen Size Hack :
//First step is to force the game to choose index 0xF in the resolution table, whatever SWITCH or option is used
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _ForceResolutionIndex_Offset , new byte [ ] { 0x90 , 0x90 , 0xB8 , 0x0F } ) ;
//Second step is to replace Width and Height values for the desires Resolution by our own
for ( uint i = 0 ; i < 3 ; i + + )
{
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _ResolutionTableHd_Offset + ( i * 8 ) , BitConverter . GetBytes ( _Cfg_ScreenWidth ) ) ;
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _ResolutionTableHd_Offset + ( i * 8 ) + 4 , BitConverter . GetBytes ( _Cfg_ScreenHeight ) ) ;
}
// Not needed
//Credits force value to 0 instead of -1 (-1 won't update value) ?????? to confirm when set_credit hack is done
//WriteByte((UInt32)_Process_MemoryBaseAddress + 0x66953, 0x00);
2024-07-21 13:30:58 +02:00
//When trying to load shadders from disks, game sometimes have issues with Absolute path name (based on the current dir) with special characters or length(?)
//Forcing it to load relative path name may fix the issue
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _RelativePathFix_Ocean_Offset , new byte [ ] { 0x90 , 0x90 } ) ;
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _RelativePathFix_InfiniteOcean_Offset , new byte [ ] { 0x90 , 0x90 } ) ;
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _RelativePathFix_SmokeRenderer_Offset , new byte [ ] { 0x90 , 0x90 } ) ;
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _RelativePathFix_TrackRenderer_Offset , new byte [ ] { 0x90 , 0x90 } ) ;
WriteBytes ( ( UInt32 ) _Process_MemoryBaseAddress + _RelativePathFix_WaterDistort_Offset , new byte [ ] { 0x90 , 0x90 } ) ;
2024-07-07 09:22:24 +02:00
}
#endregion
#region MD5 Verification
/// <summary>
/// Compute the MD5 hash of the target executable and compare it to the known list of MD5 Hashes
/// This can be usefull if people are using some unknown dump with different memory,
/// or a wrong version of emulator
/// This is absolutely not blocking, just for debuging with output log
/// </summary>
protected void CheckExeMd5 ( )
{
CheckMd5 ( _Process . MainModule . FileName ) ;
}
protected void CheckMd5 ( String TargetFileName )
{
GetMd5HashAsString ( TargetFileName ) ;
Logger . WriteLog ( "CheckMd5() => MD5 hash of " + TargetFileName + " = " + _TargetProcess_Md5Hash ) ;
String FoundMd5 = String . Empty ;
foreach ( KeyValuePair < String , String > pair in _KnownMd5Prints )
{
if ( pair . Value = = _TargetProcess_Md5Hash )
{
FoundMd5 = pair . Key ;
break ;
}
}
if ( FoundMd5 = = String . Empty )
{
Logger . WriteLog ( @"CheckMd5() => /!\ MD5 Hash unknown, the mod may not work correctly with this target /!\" ) ;
}
else
{
Logger . WriteLog ( "CheckMd5() => MD5 Hash is corresponding to a known target = " + FoundMd5 ) ;
}
}
/// <summary>
/// Compute the MD5 hash from the target file.
/// </summary>
/// <param name="FileName">Full filepath of the targeted executable.</param>
private void GetMd5HashAsString ( String FileName )
{
if ( File . Exists ( FileName ) )
{
using ( var md5 = MD5 . Create ( ) )
{
using ( var stream = File . OpenRead ( FileName ) )
{
var hash = md5 . ComputeHash ( stream ) ;
_TargetProcess_Md5Hash = BitConverter . ToString ( hash ) . Replace ( "-" , "" ) . ToLowerInvariant ( ) ;
}
}
}
}
#endregion
#region MemoryData Loading
/// <summary>
/// Read memory values in .cfg file, whose name depends on the MD5 hash of the targeted exe.
/// Mostly used for PC games
/// </summary>
/// <param name="GameData_Folder"></param>
protected virtual void ReadGameDataFromMd5Hash ( )
{
String ConfigFile = AppDomain . CurrentDomain . BaseDirectory + MEMORY_DATA_FOLDER + @"\" + _TargetProcess_Md5Hash + ".cfg" ;
if ( File . Exists ( ConfigFile ) )
{
Logger . WriteLog ( "ReadGameDataFromMd5Hash() => Reading game memory setting from " + ConfigFile ) ;
using ( StreamReader sr = new StreamReader ( ConfigFile ) )
{
String line ;
String FieldName = String . Empty ;
line = sr . ReadLine ( ) ;
while ( line ! = null )
{
String [ ] buffer = line . Split ( '=' ) ;
if ( buffer . Length > 1 )
{
try
{
FieldName = "_" + buffer [ 0 ] . Trim ( ) ;
if ( buffer [ 0 ] . Contains ( "Nop" ) )
{
NopStruct n = new NopStruct ( buffer [ 1 ] . Trim ( ) ) ;
this . GetType ( ) . GetField ( FieldName , BindingFlags . NonPublic | BindingFlags . Instance | BindingFlags . IgnoreCase ) . SetValue ( this , n ) ;
Logger . WriteLog ( FieldName + " successfully set to following NopStruct : 0x" + n . MemoryOffset . ToString ( "X8" ) + "|" + n . Length . ToString ( ) ) ;
}
else if ( buffer [ 0 ] . Contains ( "Injection" ) )
{
InjectionStruct i = new InjectionStruct ( buffer [ 1 ] . Trim ( ) ) ;
this . GetType ( ) . GetField ( FieldName , BindingFlags . NonPublic | BindingFlags . Instance | BindingFlags . IgnoreCase ) . SetValue ( this , i ) ;
Logger . WriteLog ( FieldName + " successfully set to following InjectionStruct : 0x" + i . Injection_Offset . ToString ( "X8" ) + "|" + i . Length . ToString ( ) ) ;
}
else
{
UInt32 v = UInt32 . Parse ( buffer [ 1 ] . Substring ( 3 ) . Trim ( ) , NumberStyles . HexNumber ) ;
this . GetType ( ) . GetField ( FieldName , BindingFlags . NonPublic | BindingFlags . Instance | BindingFlags . IgnoreCase ) . SetValue ( this , v ) ;
Logger . WriteLog ( FieldName + " successfully set to following value : 0x" + v . ToString ( "X8" ) ) ;
}
}
catch ( Exception ex )
{
Logger . WriteLog ( "ReadGameDataFromMd5Hash() => Error reading game data for " + FieldName + " : " + ex . Message . ToString ( ) ) ;
}
}
line = sr . ReadLine ( ) ;
}
sr . Close ( ) ;
}
}
else
{
Logger . WriteLog ( "ReadGameDataFromMd5Hash() => Memory File not found : " + ConfigFile ) ;
}
}
#endregion
#region Memory Hack x86
/// <summary>
/// Defines how many NOP to write at a given Memory offset
/// </summary>
public struct NopStruct
{
public UInt32 MemoryOffset ;
public UInt32 Length ;
public NopStruct ( UInt32 Offset , UInt32 NopLength )
{
MemoryOffset = Offset ;
Length = NopLength ;
}
public NopStruct ( String OffsetAndNumber )
{
MemoryOffset = 0 ;
Length = 0 ;
if ( OffsetAndNumber ! = null )
{
try
{
Length = UInt32 . Parse ( ( OffsetAndNumber . Split ( '|' ) ) [ 1 ] ) ;
MemoryOffset = UInt32 . Parse ( ( OffsetAndNumber . Split ( '|' ) ) [ 0 ] . Substring ( 2 ) . Trim ( ) , System . Globalization . NumberStyles . HexNumber ) ;
}
catch
{
Logger . WriteLog ( "Impossible to load NopStruct from following String : " + OffsetAndNumber ) ;
}
}
}
}
/// <summary>
/// Defines an injection Memory zone and it's length
/// </summary>
public struct InjectionStruct
{
public UInt32 Injection_Offset ;
public UInt32 Injection_ReturnOffset ;
public UInt32 Length ;
public InjectionStruct ( UInt32 Offset , UInt32 InjectionLength )
{
Injection_Offset = Offset ;
Length = InjectionLength ;
Injection_ReturnOffset = Offset + Length ;
}
public InjectionStruct ( String OffsetAndNumber )
{
Injection_Offset = 0 ;
Length = 0 ;
Injection_ReturnOffset = 0 ;
if ( OffsetAndNumber ! = null )
{
try
{
Length = UInt32 . Parse ( ( OffsetAndNumber . Split ( '|' ) ) [ 1 ] ) ;
Injection_Offset = UInt32 . Parse ( ( OffsetAndNumber . Split ( '|' ) ) [ 0 ] . Substring ( 2 ) . Trim ( ) , System . Globalization . NumberStyles . HexNumber ) ;
Injection_ReturnOffset = Injection_Offset + Length ;
}
catch
{
Logger . WriteLog ( "Impossible to load InjectionStruct from following String : " + OffsetAndNumber ) ;
}
}
}
}
protected Byte ReadByte ( UInt32 Address )
{
byte [ ] Buffer = { 0 } ;
UInt32 bytesRead = 0 ;
if ( ! Win32API . ReadProcessMemory ( _ProcessHandle , Address , Buffer , 1 , ref bytesRead ) )
{
Logger . WriteLog ( "Cannot read memory at address 0x" + Address . ToString ( "X8" ) ) ;
}
return Buffer [ 0 ] ;
}
protected Byte [ ] ReadBytes ( UInt32 Address , UInt32 BytesCount )
{
byte [ ] Buffer = new byte [ BytesCount ] ;
UInt32 bytesRead = 0 ;
if ( ! Win32API . ReadProcessMemory ( _ProcessHandle , Address , Buffer , ( UInt32 ) Buffer . Length , ref bytesRead ) )
{
Logger . WriteLog ( "Cannot read memory at address 0x" + Address . ToString ( "X8" ) ) ;
}
return Buffer ;
}
protected UInt32 ReadPtr ( UInt32 PtrAddress )
{
byte [ ] Buffer = ReadBytes ( PtrAddress , 4 ) ;
return BitConverter . ToUInt32 ( Buffer , 0 ) ;
}
protected UInt32 ReadPtrChain ( UInt32 BaseAddress , UInt32 [ ] Offsets )
{
byte [ ] Buffer = ReadBytes ( BaseAddress , 4 ) ;
UInt32 Ptr = BitConverter . ToUInt32 ( Buffer , 0 ) ;
if ( Ptr = = 0 )
{
return 0 ;
}
else
{
for ( int i = 0 ; i < Offsets . Length ; i + + )
{
Buffer = ReadBytes ( Ptr + Offsets [ i ] , 8 ) ;
Ptr = BitConverter . ToUInt32 ( Buffer , 0 ) ;
if ( Ptr = = 0 )
return 0 ;
}
}
return Ptr ;
}
protected bool WriteByte ( UInt32 Address , byte Value )
{
UInt32 bytesWritten = 0 ;
Byte [ ] Buffer = { Value } ;
if ( Win32API . WriteProcessMemory ( _ProcessHandle , Address , Buffer , 1 , ref bytesWritten ) )
{
if ( bytesWritten = = 1 )
return true ;
else
return false ;
}
else
return false ;
}
protected bool WriteBytes ( UInt32 Address , byte [ ] Buffer )
{
UInt32 bytesWritten = 0 ;
if ( Win32API . WriteProcessMemory ( _ProcessHandle , Address , Buffer , ( UInt32 ) Buffer . Length , ref bytesWritten ) )
{
if ( bytesWritten = = Buffer . Length )
return true ;
else
return false ;
}
else
return false ;
}
protected void SetNops ( UInt32 BaseAddress , NopStruct Nop )
{
for ( UInt32 i = 0 ; i < Nop . Length ; i + + )
{
UInt32 Address = ( UInt32 ) BaseAddress + Nop . MemoryOffset + i ;
if ( ! WriteByte ( Address , 0x90 ) )
{
Logger . WriteLog ( "Impossible to NOP address 0x" + Address . ToString ( "X8" ) ) ;
break ;
}
}
}
#endregion
}
}