#include "StdAfx.h" #include "Maths.h" #include "Timer.h" #include "Common.h" #include "Common_ddraw.h" #include "Desktop.h" #include "EntityVC.h" #include "ModelInfoVC.h" #include "VehicleVC.h" #include "SVF.h" #include "RWUtils.hpp" #include "TheFLAUtils.h" #include "ParseUtils.hpp" #include "Random.h" #include #include #include #include #include "Utils/ModuleList.hpp" #include "Utils/Patterns.h" #include "Utils/ScopedUnprotect.hpp" #include "Utils/HookEach.hpp" #include "Utils/DelimStringReader.h" #include "debugmenu_public.h" #pragma comment(lib, "shlwapi.lib") EXTERN_C IMAGE_DOS_HEADER __ImageBase; // ============= Mod compatibility stuff ============= namespace ModCompat { namespace Utils { template HMODULE GetModuleHandleFromAddress( AT address ) { HMODULE result = nullptr; GetModuleHandleEx( GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT|GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, LPCTSTR(address), &result ); return result; } } } struct RsGlobalType { const char* AppName; unsigned int unkWidth, unkHeight; signed int MaximumWidth; signed int MaximumHeight; unsigned int frameLimit; BOOL quit; void* ps; void* keyboard; void* mouse; void* pad; }; DebugMenuAPI gDebugMenuAPI; static RsGlobalType* RsGlobal; static const void* SubtitlesShadowFix_JumpBack; void* (*GetModelInfo)(const char*, int*); // This is actually CBaseModelInfo, but we currently don't have it defined CVehicleModelInfo**& ms_modelInfoPtrs = *hook::get_pattern("8B 15 ? ? ? ? 8D 04 24", 2); int32_t& numModelInfos = *hook::get_pattern("81 FD ? ? ? ? 7C B7", 2); float GetWidthMult() { static float** ResolutionWidthMult = hook::get_pattern("D8 0D ? ? ? ? D1 F8 52", 2); return **ResolutionWidthMult; } float GetHeightMult() { static float** ResolutionHeightMult = hook::get_pattern("D8 0D ? ? ? ? D1 F8 C7 44 24 ? 00 00 00 00 89 04 24", 2); return **ResolutionHeightMult; } static bool bGameInFocus = true; static LRESULT (CALLBACK **OldWndProc)(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK CustomWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch ( uMsg ) { case WM_KILLFOCUS: bGameInFocus = false; break; case WM_SETFOCUS: bGameInFocus = true; break; } return (*OldWndProc)(hwnd, uMsg, wParam, lParam); } static auto* const pCustomWndProc = CustomWndProc; static void (* const RsMouseSetPos)(RwV2d*) = AddressByVersion(0x6030C0, 0x6030A0, 0x602CE0); static void (*orgConstructRenderList)(); void ResetMousePos() { if ( bGameInFocus ) { RwV2d vecPos = { RsGlobal->MaximumWidth * 0.5f, RsGlobal->MaximumHeight * 0.5f }; RsMouseSetPos(&vecPos); } orgConstructRenderList(); } void __stdcall Recalculate(float& fX, float& fY, signed int nShadow) { fX = nShadow * GetWidthMult() * RsGlobal->MaximumWidth; fY = nShadow * GetHeightMult() * RsGlobal->MaximumHeight; } namespace PrintStringShadows { template static const float** margin = reinterpret_cast(Memory::DynBaseAddress(addr)); static void PrintString_Internal(void (*printFn)(float,float,const wchar_t*), float fX, float fY, float fMarginX, float fMarginY, const wchar_t* pText) { printFn(fX - fMarginX + (fMarginX * GetWidthMult() * RsGlobal->MaximumWidth), fY - fMarginY + (fMarginY * GetHeightMult() * RsGlobal->MaximumHeight), pText); } template struct XY { static inline void (*orgPrintString)(float,float,const wchar_t*); static void PrintString(float fX, float fY, const wchar_t* pText) { PrintString_Internal(orgPrintString, fX, fY, **margin, **margin, pText); } static void Hook(uintptr_t addr) { Memory::DynBase::InterceptCall(addr, orgPrintString, PrintString); } }; template struct XYMinus { static inline void (*orgPrintString)(float,float,const wchar_t*); static void PrintString(float fX, float fY, const wchar_t* pText) { PrintString_Internal(orgPrintString, fX, fY, -(**margin), -(**margin), pText); } static void Hook(uintptr_t addr) { Memory::DynBase::InterceptCall(addr, orgPrintString, PrintString); } }; template struct X { static inline void (*orgPrintString)(float,float,const wchar_t*); static void PrintString(float fX, float fY, const wchar_t* pText) { PrintString_Internal(orgPrintString, fX, fY, **margin, 0.0f, pText); } static void Hook(uintptr_t addr) { Memory::DynBase::InterceptCall(addr, orgPrintString, PrintString); } }; template struct XMinus { static inline void (*orgPrintString)(float,float,const wchar_t*); static void PrintString(float fX, float fY, const wchar_t* pText) { PrintString_Internal(orgPrintString, fX, fY, -(**margin), 0.0f, pText); } static void Hook(uintptr_t addr) { Memory::DynBase::InterceptCall(addr, orgPrintString, PrintString); } }; template struct Y { static inline void (*orgPrintString)(float,float,const wchar_t*); static void PrintString(float fX, float fY, const wchar_t* pText) { PrintString_Internal(orgPrintString, fX, fY, 0.0f, **margin, pText); } static void Hook(uintptr_t addr) { Memory::DynBase::InterceptCall(addr, orgPrintString, PrintString); } }; } // ============= Radar position and radardisc shadow ============= namespace RadardiscFixes { static const float RADARDISC_SHRINK = 2.0f; // We are shrinking the radardisc by that template static const float* orgRadarXPos; template static float RadarXPos_Recalculated; template static void RecalculateXPositions(std::index_sequence) { const float multiplier = GetWidthMult() * RsGlobal->MaximumWidth; ((RadarXPos_Recalculated = *orgRadarXPos * multiplier), ...); } template static const float* orgRadarYPos; template static float RadarYPos_Recalculated; template static void RecalculateYPositions(std::index_sequence) { const float multiplier = GetHeightMult() * RsGlobal->MaximumHeight; ((RadarYPos_Recalculated = *orgRadarYPos * multiplier), ...); } template static const float* orgRadarXPos_RadardiscShrink; template static float RadarXPos_Recalculated_RadardiscShrink; template static void RecalculateXPositions_RadardiscShrink(std::index_sequence) { const float multiplier = GetWidthMult() * RsGlobal->MaximumWidth; ((RadarXPos_Recalculated_RadardiscShrink = (*orgRadarXPos_RadardiscShrink - RADARDISC_SHRINK) * multiplier), ...); } template static const float* orgRadarYPos_RadardiscShrink; template static float RadarYPos_Recalculated_RadardiscShrink; template static void RecalculateYPositions_RadardiscShrink(std::index_sequence) { const float multiplier = GetHeightMult() * RsGlobal->MaximumHeight; ((RadarYPos_Recalculated_RadardiscShrink = (*orgRadarYPos_RadardiscShrink - RADARDISC_SHRINK) * multiplier), ...); } static void (*orgDrawMap)(); template static void DrawMap_RecalculatePositions() { RecalculateXPositions(std::make_index_sequence{}); RecalculateYPositions(std::make_index_sequence{}); RecalculateXPositions_RadardiscShrink(std::make_index_sequence{}); RecalculateYPositions_RadardiscShrink(std::make_index_sequence{}); orgDrawMap(); } HOOK_EACH_INIT(CalculateRadarXPos, orgRadarXPos, RadarXPos_Recalculated); HOOK_EACH_INIT(CalculateRadarYPos, orgRadarYPos, RadarYPos_Recalculated); HOOK_EACH_INIT(CalculateRadarXPos_RadardiscShrink, orgRadarXPos_RadardiscShrink, RadarXPos_Recalculated_RadardiscShrink); HOOK_EACH_INIT(CalculateRadarYPos_RadardiscShrink, orgRadarYPos_RadardiscShrink, RadarYPos_Recalculated_RadardiscShrink); static CRect ScaleWidthRect(CRect rect) { // Also account for a smaller radardisc rect.x1 = (rect.x1 + RADARDISC_SHRINK) * GetWidthMult() * RsGlobal->MaximumWidth; return rect; } template static void (__fastcall* orgDrawSprite)(void* obj, void*, const CRect& rect, const CRGBA& col1, const CRGBA& col2, const CRGBA& col3, const CRGBA& col4); template static void __fastcall DrawSprite_Scale(void* obj, void*, const CRect& rect, const CRGBA& col1, const CRGBA& col2, const CRGBA& col3, const CRGBA& col4) { orgDrawSprite(obj, nullptr, ScaleWidthRect(rect), col1, col2, col3, col4); } HOOK_EACH_INIT(DrawRadarDisc, orgDrawSprite, DrawSprite_Scale); } // ============= Fix the onscreen counter bar placement and shadow not scaling to resolution ============= namespace OnscreenCounterBarFixes { template static const float* orgXPos; template static float XPos_Recalculated; template static const float* orgYPos; template static float YPos_Recalculated; template static void RecalculateXPositions(std::index_sequence) { const float multiplier = GetWidthMult() * RsGlobal->MaximumWidth; ((XPos_Recalculated = *orgXPos * multiplier), ...); } template static void RecalculateYPositions(std::index_sequence) { const float multiplier = GetHeightMult() * RsGlobal->MaximumHeight; ((YPos_Recalculated = *orgYPos * multiplier), ...); } static int (*orgAtoi)(const char* str); template static int atoi_RecalculatePositions(const char* str) { RecalculateXPositions(std::make_index_sequence{}); RecalculateYPositions(std::make_index_sequence{}); return orgAtoi(str); } HOOK_EACH_INIT(XPos, orgXPos, XPos_Recalculated); HOOK_EACH_INIT(YPos, orgYPos, YPos_Recalculated); } // ============= Fix the radar trace blip outline not scaling to resolution ============= namespace RadarTraceOutlineFixes { template static const float* orgXPos; template static float XPos_Recalculated; template static const float* orgYPos; template static float YPos_Recalculated; template static void RecalculateXPositions(std::index_sequence) { const float multiplier = GetWidthMult() * RsGlobal->MaximumWidth; ((XPos_Recalculated = *orgXPos * multiplier), ...); } template static void RecalculateYPositions(std::index_sequence) { const float multiplier = GetHeightMult() * RsGlobal->MaximumHeight; ((YPos_Recalculated = *orgYPos * multiplier), ...); } template struct PositionRecalculator { static void RecalculatePositions() { RecalculateXPositions(std::make_index_sequence{}); RecalculateYPositions(std::make_index_sequence{}); } template static void (*orgShowRadarTraceWithHeight)(float, float, unsigned int, unsigned char, unsigned char, unsigned char, unsigned char, unsigned char); template static void ShowRadarTraceWithHeight_RecalculatePositions(float a1, float a2, unsigned int a3, unsigned char a4, unsigned char a5, unsigned char a6, unsigned char a7, unsigned char a8) { RecalculatePositions(); orgShowRadarTraceWithHeight(a1, a2, a3, a4, a5, a6, a7, a8); } HOOK_EACH_INIT(ShowRadarTraceWithHeight, orgShowRadarTraceWithHeight, ShowRadarTraceWithHeight_RecalculatePositions); }; HOOK_EACH_INIT(XPos, orgXPos, XPos_Recalculated); HOOK_EACH_INIT(YPos, orgYPos, YPos_Recalculated); } // ============= Fix the loading bar outline not scaling to resolution ============= namespace LoadingBarOutlineFixes { template static const float* orgXPos; template static float XPos_Recalculated; template static const float* orgYPos; template static float YPos_Recalculated; template static void RecalculateXPositions(std::index_sequence) { const float multiplier = GetWidthMult() * RsGlobal->MaximumWidth; ((XPos_Recalculated = *orgXPos * multiplier), ...); } template static void RecalculateYPositions(std::index_sequence) { const float multiplier = GetHeightMult() * RsGlobal->MaximumHeight; ((YPos_Recalculated = *orgYPos * multiplier), ...); } static CRGBA* (__fastcall* orgRGBACtor)(CRGBA* obj, void*, unsigned char r, unsigned char g, unsigned char b, unsigned char a); template static CRGBA* __fastcall RGBACtor_RecalculatePositions(CRGBA* obj, void*, unsigned char r, unsigned char g, unsigned char b, unsigned char a) { RecalculateXPositions(std::make_index_sequence{}); RecalculateYPositions(std::make_index_sequence{}); return orgRGBACtor(obj, nullptr, r, g, b, a); } HOOK_EACH_INIT(XPos, orgXPos, XPos_Recalculated); HOOK_EACH_INIT(YPos, orgYPos, YPos_Recalculated); } // ============= Fix credits not scaling to resolution ============= namespace CreditsScalingFixes { static const unsigned int FIXED_RES_HEIGHT_SCALE = 448; template static void (*orgPrintString)(float,float,const wchar_t*); template static void PrintString_ScaleY(float fX, float fY, const wchar_t* pText) { orgPrintString(fX, fY * GetHeightMult() * RsGlobal->MaximumHeight, pText); } static void (*orgSetScale)(float X, float Y); static void SetScale_ScaleToRes(float X, float Y) { orgSetScale(X * GetWidthMult() * RsGlobal->MaximumWidth, Y * GetHeightMult() * RsGlobal->MaximumHeight); } HOOK_EACH_INIT(PrintString, orgPrintString, PrintString_ScaleY); } // ============= Fix some big messages staying on screen longer at high resolutions due to a cut sliding text feature ============= namespace SlidingTextsScalingFixes { static const unsigned int FIXED_RES_WIDTH_SCALE = 640; static std::array* pBigMessageX; static float* pOddJob2XOffset; template struct BigMessageSlider { static inline bool bSlidingEnabled = false; template static void (*orgPrintString)(float,float,const wchar_t*); template static void PrintString_Slide(float fX, float fY, const wchar_t* pText) { // We divide by a constant 640.0, because the X position is meant to slide across the entire screen orgPrintString(bSlidingEnabled ? (*pBigMessageX)[BigMessageIndex] * RsGlobal->MaximumWidth / 640.0f : fX, fY, pText); } template static void (*orgSetRightJustifyWrap)(float wrap); template static void SetRightJustifyWrap_Slide(float wrap) { orgSetRightJustifyWrap(bSlidingEnabled ? -500.0f * GetWidthMult() * RsGlobal->MaximumWidth : wrap); } HOOK_EACH_INIT(PrintString, orgPrintString, PrintString_Slide); HOOK_EACH_INIT(RightJustifyWrap, orgSetRightJustifyWrap, SetRightJustifyWrap_Slide); }; struct OddJob2Slider { static inline bool bSlidingEnabled = false; template static void (*orgPrintString)(float,float,const wchar_t*); template static void PrintString_Slide(float fX, float fY, const wchar_t* pText) { // We divide by a constant 640.0, because the X position is meant to slide across the entire screen if (bSlidingEnabled) { fX -= *pOddJob2XOffset * RsGlobal->MaximumWidth / 640.0f; } orgPrintString(fX, fY, pText); } HOOK_EACH_INIT(PrintString, orgPrintString, PrintString_Slide); }; } // ============= Minimal HUD changes ============= namespace MinimalHUD { // Wanted level stars static void (*orgSetDropShadowPosition)(short); static void (*orgSetDropColor)(const CRGBA&); static int8_t* pDropShadowSize; static int8_t* pDropShadowR; static int8_t* pDropShadowG; static int8_t* pDropShadowB; static void (*orgSetColor)(const CRGBA&); static void SetColor_SetShadow(const CRGBA& color) { orgSetDropShadowPosition(*pDropShadowSize); orgSetDropColor(CRGBA(*pDropShadowR, *pDropShadowG, *pDropShadowB, color.a)); orgSetColor(color); } // Show the energy values when losing armor static uint8_t* pPlayerInFocus; static uint32_t* pTimeLastArmorLoss; static uint32_t LastTimeArmorLost; static float (*orgDrawFadeState)(int state, int force); static float DrawFadeState_CheckArmor(int state, int force) { // This is a bit hacky, but we don't necessarily need better right now if (pTimeLastArmorLoss[92 * *pPlayerInFocus] != LastTimeArmorLost) { force = 1; LastTimeArmorLost = pTimeLastArmorLoss[92 * *pPlayerInFocus]; } return orgDrawFadeState(state, force); } // Fade the weapon icon - fix the render state and pass the alpha parameter static void (*orgRenderOneXLUSprite)(float, float, float, float, float, uint8_t, uint8_t, uint8_t, int16_t, float, uint8_t); static void RenderOneXLUSprite_FloatAlpha(float arg1, float arg2, float arg3, float arg4, float arg5, uint8_t red, uint8_t green, uint8_t blue, int16_t mult, float arg10, float alpha) { RwScopedRenderState vertexAlpha; RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void*)TRUE); orgRenderOneXLUSprite(arg1, arg2, arg3, arg4, arg5, red, green, blue, mult, arg10, static_cast(alpha)); } } float FixedRefValue() { return 1.0f; } void __declspec(naked) SubtitlesShadowFix() { _asm { mov [esp], eax fild [esp] push eax lea eax, [esp+20h-18h] push eax lea eax, [esp+24h-14h] push eax call Recalculate jmp SubtitlesShadowFix_JumpBack } } void __declspec(naked) CreateInstance_BikeFix() { _asm { push eax mov ecx, ebp call CVehicleModelInfo::GetExtrasFrame retn } } extern char** ppUserFilesDir = AddressByVersion(0x6022AA, 0x60228A, 0x601ECA); static LARGE_INTEGER FrameTime; __declspec(safebuffers) int32_t GetTimeSinceLastFrame() { LARGE_INTEGER curTime; QueryPerformanceCounter(&curTime); return int32_t(curTime.QuadPart - FrameTime.QuadPart); } static int (*RsEventHandler)(int, void*); int NewFrameRender(int nEvent, void* pParam) { QueryPerformanceCounter(&FrameTime); return RsEventHandler(nEvent, pParam); } static void (*orgPickNextNodeToChaseCar)(void*, float, float, void*); static float PickNextNodeToChaseCarZ = 0.0f; static void PickNextNodeToChaseCarXYZ( void* vehicle, const CVector& vec, void* chaseTarget ) { PickNextNodeToChaseCarZ = vec.z; orgPickNextNodeToChaseCar( vehicle, vec.x, vec.y, chaseTarget ); PickNextNodeToChaseCarZ = 0.0f; } static char aNoDesktopMode[64]; unsigned int __cdecl AutoPilotTimerCalculation_VC(unsigned int nTimer, int nScaleFactor, float fScaleCoef) { return nTimer - static_cast(nScaleFactor * fScaleCoef); } void __declspec(naked) AutoPilotTimerFix_VC() { _asm { push dword ptr[esp + 0xC] push dword ptr[ebx + 0x10] push eax call AutoPilotTimerCalculation_VC add esp, 0xC mov [ebx + 0xC], eax add esp, 0x30 pop ebp pop ebx retn 4 } } namespace ZeroAmmoFix { template static void (__fastcall *orgGiveWeapon)(void* ped, void*, unsigned int weapon, unsigned int ammo, bool flag); template static void __fastcall GiveWeapon_SP(void* ped, void*, unsigned int weapon, unsigned int ammo, bool flag) { orgGiveWeapon(ped, nullptr, weapon, std::max(1u, ammo), flag); } HOOK_EACH_INIT(GiveWeapon, orgGiveWeapon, GiveWeapon_SP); } // ============= Credits! ============= namespace Credits { static void (*PrintCreditText)(float scaleX, float scaleY, const wchar_t* text, unsigned int& pos, float timeOffset); static void (*PrintCreditText_Hooked)(float scaleX, float scaleY, const wchar_t* text, unsigned int& pos, float timeOffset); static void PrintCreditSpace( float scale, unsigned int& pos ) { pos += static_cast( scale * 25.0f ); } constexpr wchar_t xvChar(const wchar_t ch) { constexpr uint8_t xv = SILENTPATCH_REVISION_ID; return ch ^ xv; } constexpr wchar_t operator "" _xv(const char ch) { return xvChar(ch); } static void PrintSPCredits( float scaleX, float scaleY, const wchar_t* text, unsigned int& pos, float timeOffset ) { // Original text we intercepted PrintCreditText_Hooked( scaleX, scaleY, text, pos, timeOffset ); PrintCreditSpace( 1.5f, pos ); { wchar_t spText[] = { 'A'_xv, 'N'_xv, 'D'_xv, '\0'_xv }; for ( auto& ch : spText ) ch = xvChar(ch); PrintCreditText( 1.1f, 0.8f, spText, pos, timeOffset ); } PrintCreditSpace( 1.5f, pos ); { wchar_t spText[] = { 'A'_xv, 'D'_xv, 'R'_xv, 'I'_xv, 'A'_xv, 'N'_xv, ' '_xv, '\''_xv, 'S'_xv, 'I'_xv, 'L'_xv, 'E'_xv, 'N'_xv, 'T'_xv, '\''_xv, ' '_xv, 'Z'_xv, 'D'_xv, 'A'_xv, 'N'_xv, 'O'_xv, 'W'_xv, 'I'_xv, 'C'_xv, 'Z'_xv, '\0'_xv }; for ( auto& ch : spText ) ch = xvChar(ch); PrintCreditText( 1.1f, 1.1f, spText, pos, timeOffset ); } } } // ============= Keyboard latency input fix ============= namespace KeyboardInputFix { static void* NewKeyState; static void* OldKeyState; static void* TempKeyState; static constexpr size_t objSize = 0x270; static void (__fastcall *orgClearSimButtonPressCheckers)(void*); void __fastcall ClearSimButtonPressCheckers(void* pThis) { memcpy( OldKeyState, NewKeyState, objSize ); memcpy( NewKeyState, TempKeyState, objSize ); orgClearSimButtonPressCheckers(pThis); } } namespace Localization { static int8_t forcedUnits = -1; // 0 - metric, 1 - imperial bool IsMetric_LocaleBased() { if ( forcedUnits != -1 ) return forcedUnits == 0; unsigned int LCData; if ( GetLocaleInfo( LOCALE_USER_DEFAULT, LOCALE_IMEASURE|LOCALE_RETURN_NUMBER, reinterpret_cast(&LCData), sizeof(LCData) / sizeof(TCHAR) ) != 0 ) { return LCData == 0; } // If fails, default to metric. Hopefully never fails though return true; } static void (__thiscall* orgUpdateCompareFlag_IsMetric)(void* pThis, uint8_t flag); void __fastcall UpdateCompareFlag_IsMetric(void* pThis, void*, uint8_t) { std::invoke( orgUpdateCompareFlag_IsMetric, pThis, IsMetric_LocaleBased() ); } uint32_t PrefsLanguage_IsMetric() { return IsMetric_LocaleBased(); } } // ============= Corrected FBI Washington sirens sound ============= namespace SirenSwitchingFix { void __declspec(naked) IsFBIRanchOrFBICar() { _asm { mov dword ptr [esi+1Ch], 1Ch // al = 0 - high pitched siren // al = 1 - normal siren cmp dword ptr [ebp+14h], 90 // fbiranch je IsFBIRanchOrFBICar_HighPitchSiren cmp dword ptr [ebp+14h], 17 // fbicar setne al retn IsFBIRanchOrFBICar_HighPitchSiren: xor al, al retn } } } // ============= Corrected siren corona placement for FBI cars and Vice Cheetah ============= namespace FBISirenCoronaFix { bool overridePosition; CVector vecOverridePosition; // True - don't display siren // False - display siren bool SetUpFBISiren( const CVehicle* vehicle ) { SVF::Feature foundFeature = SVF::Feature::NO_FEATURE; SVF::ForAllModelFeatures( vehicle->GetModelIndex(), [&]( SVF::Feature f ) { if ( f >= SVF::Feature::FBI_RANCHER_SIREN && f <= SVF::Feature::VICE_CHEETAH_SIREN ) { foundFeature = f; return false; } return true; } ); if ( foundFeature != SVF::Feature::NO_FEATURE ) { if ( foundFeature != SVF::Feature::VICE_CHEETAH_SIREN ) { constexpr CVector FBICAR_SIREN_POS(0.4f, 0.8f, 0.25f); constexpr CVector FBIRANCH_SIREN_POS(0.5f, 1.12f, 0.5f); overridePosition = true; vecOverridePosition = foundFeature == SVF::Feature::FBI_WASHINGTON_SIREN ? FBICAR_SIREN_POS : FBIRANCH_SIREN_POS; } else { overridePosition = false; } return false; } return true; } CVector& __fastcall SetUpVector( CVector& out, void*, float X, float Y, float Z ) { if ( overridePosition ) { out = vecOverridePosition; } else { out = CVector(X, Y, Z); } return out; } } // ============= Fixed vehicles exploding twice if the driver leaves the car while it's exploding ============= namespace RemoveDriverStatusFix { __declspec(naked) void RemoveDriver_SetStatus() { // if (m_nStatus != STATUS_WRECKED) // m_nStatus = STATUS_ABANDONED; _asm { mov cl, [ebx+50h] mov al, cl and cl, 0F8h cmp cl, 28h je DontSetStatus and al, 7 or al, 20h DontSetStatus: retn } } } // ============= Apply the environment mapping on extra components ============= namespace EnvMapsOnExtras { static void RemoveSpecularityFromAtomic(RpAtomic* atomic) { RpGeometry* geometry = RpAtomicGetGeometry(atomic); if (geometry != nullptr) { RpGeometryForAllMaterials(geometry, [](RpMaterial* material) { bool bRemoveSpecularity = false; // Only remove specularity from the body materials, keep glass intact. // This is only done on a best-effort basis, as mods can fine-tune it better // and just remove the model from the exceptions list RwTexture* texture = RpMaterialGetTexture(material); if (texture != nullptr) { if (strstr(RwTextureGetName(texture), "glass") == nullptr && strstr(RwTextureGetMaskName(texture), "glass") == nullptr) { bRemoveSpecularity = true; } } if (bRemoveSpecularity) { RpMaterialGetSurfaceProperties(material)->specular = 0.0f; } return material; }); } } static RpClump* (*orgRpClumpForAllAtomics)(RpClump* clump, RpAtomicCallBack callback, void* data); static RpClump* RpClumpForAllAtomics_ExtraComps(CVehicleModelInfo* modelInfo, RpAtomicCallBack callback, void* data) { RpClump* result = orgRpClumpForAllAtomics(modelInfo->m_clump, callback, data); const int32_t modelID = std::distance(ms_modelInfoPtrs, std::find(ms_modelInfoPtrs, ms_modelInfoPtrs+numModelInfos, modelInfo)); const bool bRemoveSpecularity = ExtraCompSpecularity::SpecularityExcluded(modelID); for (int32_t i = 0; i < modelInfo->m_numComps; i++) { if (bRemoveSpecularity) { RemoveSpecularityFromAtomic(modelInfo->m_comps[i]); } callback(modelInfo->m_comps[i], data); CVehicleModelInfo::AttachCarPipeToRwObject(reinterpret_cast(modelInfo->m_comps[i])); } return result; } } // ============= Null terminate read lines in CPlane::LoadPath ============= namespace NullTerminatedLines { static void* orgSscanf_LoadPath; __declspec(naked) static void sscanf1_LoadPath_Terminate() { _asm { mov eax, [esp+4] mov byte ptr [eax+ecx], 0 jmp [orgSscanf_LoadPath] } } } // ============= Don't reset mouse sensitivity on New Game ============= namespace MouseSensNewGame { static float DefaultHorizontalAccel; static float* fMouseAccelHorzntl; static void (*orgSetDirMyDocuments)(); static void SetDirMyDocuments_ResetMouse() { orgSetDirMyDocuments(); *fMouseAccelHorzntl = DefaultHorizontalAccel; } } // ============= Fixed pickup effects colors ============= namespace PickupEffectsFixes { __declspec(naked) static void PickUpEffects_BigDollarColor() { _asm { mov byte ptr [esp+184h-170h], 0 mov dword ptr [esp+184h-174h], 37 retn } } __declspec(naked) static void PickUpEffects_Minigun2Glow() { _asm { cmp ecx, 294 // minigun2 jnz NotMinigun2 mov byte ptr [esp+184h-170h], 0 xor eax, eax jmp Return NotMinigun2: lea eax, [ecx+1] Return: mov ebx, ecx retn } } } // ============= Fixed IS_PLAYER_TARGETTING_CHAR incorrectly detecting targetting in Classic controls ============= // ============= when the player is not aiming ============= // ============= By Wesser ============= namespace IsPlayerTargettingCharFix { static bool* bUseMouse3rdPerson; static void* TheCamera; static bool (__fastcall* Using1stPersonWeaponMode)(); __declspec(naked) static void IsPlayerTargettingChar_ExtraChecks() { // After this extra block of code, there is a jz Return, so set ZF to 0 here if this path is to be entered _asm { test bl, bl jnz ReturnToUpdateCompareFlag mov eax, [bUseMouse3rdPerson] cmp byte ptr [eax], 0 jne CmpAndReturn mov ecx, [TheCamera] call [Using1stPersonWeaponMode] test al, al jz ReturnToUpdateCompareFlag CmpAndReturn: cmp byte ptr [esp+11Ch-10Ch], 0 retn ReturnToUpdateCompareFlag: xor al, al retn } } } // ============= Resetting stats and variables on New Game ============= namespace VariableResets { static auto TimerInitialise = reinterpret_cast(hook::get_pattern("83 E4 F8 68 ? ? ? ? E8", -6)); using VarVariant = std::variant< bool*, int* >; std::vector GameVariablesToReset; static void ReInitOurVariables() { for ( const auto& var : GameVariablesToReset ) { std::visit( []( auto&& v ) { *v = {}; }, var ); } // Functions that should have been called by the game but aren't... TimerInitialise(); } template static void (*orgReInitGameObjectVariables)(); template void ReInitGameObjectVariables() { // First reinit "our" variables in case stock ones rely on those during resetting ReInitOurVariables(); orgReInitGameObjectVariables(); } HOOK_EACH_INIT(ReInitGameObjectVariables, orgReInitGameObjectVariables, ReInitGameObjectVariables); static void (*orgGameInitialise)(const char*); void GameInitialise(const char* path) { ReInitOurVariables(); orgGameInitialise(path); } } // ============= Disabled backface culling on detached car parts, peds and specific models ============= namespace SelectableBackfaceCulling { void ReadDrawBackfacesExclusions(const wchar_t* pPath) { constexpr size_t SCRATCH_PAD_SIZE = 32767; WideDelimStringReader reader(SCRATCH_PAD_SIZE); GetPrivateProfileSectionW(L"DrawBackfaces", reader.GetBuffer(), reader.GetSize(), pPath); while (const wchar_t* str = reader.GetString()) { auto modelID = ParseUtils::TryParseInt(str); if (modelID) SVF::RegisterFeature(*modelID, SVF::Feature::DRAW_BACKFACES); else SVF::RegisterFeature(ParseUtils::ParseString(str), SVF::Feature::DRAW_BACKFACES); } } // Only the parts of CObject we need struct Object { std::byte __pad[364]; uint8_t m_objectCreatedBy; bool bObjectFlag0 : 1; bool bObjectFlag1 : 1; bool bObjectFlag2 : 1; bool bObjectFlag3 : 1; bool bObjectFlag4 : 1; bool bObjectFlag5 : 1; bool m_bIsExploded : 1; bool bUseVehicleColours : 1; std::byte __pad2[22]; FLAUtils::int16 m_wCarPartModelIndex; }; static void* EntityRender_Prologue_JumpBack; __declspec(naked) static void __fastcall EntityRender_Original(CEntity*) { _asm { push ebx mov ebx, ecx cmp dword ptr [ebx+4Ch], 0 jmp [EntityRender_Prologue_JumpBack] } } static bool ShouldDisableBackfaceCulling(const CEntity* entity) { const uint8_t entityType = entity->m_nType; // Vehicles disable BFC elsewhere already if (entityType == 2) { return false; } // Always disable BFC on peds if (entityType == 3) { return true; } // For objects, do extra checks if (entityType == 4) { const Object* object = reinterpret_cast(entity); if (object->m_wCarPartModelIndex.Get() != -1 && object->m_objectCreatedBy == TEMP_OBJECT && object->bUseVehicleColours) { return true; } } // For everything else, check the exclusion list return SVF::ModelHasFeature(entity->m_modelIndex.Get(), SVF::Feature::DRAW_BACKFACES); } // If CEntity::Render is re-routed by another mod, we overwrite this later static void (__fastcall *orgEntityRender)(CEntity* obj) = &EntityRender_Original; static void __fastcall EntityRender_BackfaceCulling(CEntity* obj) { RwScopedRenderState cullState; if (ShouldDisableBackfaceCulling(obj)) { RwRenderStateSet(rwRENDERSTATECULLMODE, reinterpret_cast(rwCULLMODECULLNONE)); } orgEntityRender(obj); } } // ============= Fix the construction site LOD losing its HQ model and showing at all times ============= namespace ConstructionSiteLODFix { static bool bActivateConstructionSiteFix = false; static int32_t MI_BLDNGST2MESH, MI_BLDNGST2MESHDAM; static CSimpleModelInfo* Bldngst2mesh_ModelInfo; static CSimpleModelInfo* Bldngst2meshDam_ModelInfo; static CSimpleModelInfo* LODngst2mesh_ModelInfo; void MatchModelIndices() { CSimpleModelInfo* Bldngst2mesh = reinterpret_cast(GetModelInfo("bldngst2mesh", &MI_BLDNGST2MESH)); CSimpleModelInfo* Bldngst2meshDam = reinterpret_cast(GetModelInfo("bldngst2meshdam", &MI_BLDNGST2MESHDAM)); CSimpleModelInfo* LODngst2mesh = reinterpret_cast(GetModelInfo("LODngst2mesh", nullptr)); CSimpleModelInfo* LODngst2meshDam = reinterpret_cast(GetModelInfo("LODngst2meshdam", nullptr)); const bool bHasBldngst2mesh = Bldngst2mesh != nullptr; const bool bHasBldngst2meshDam = Bldngst2meshDam != nullptr; const bool bHasLODngst2mesh = LODngst2mesh != nullptr; const bool bHasLODngst2meshDam = LODngst2meshDam != nullptr; // LODngst2meshdam doesn't exist in the vanilla game, so if it exists - a mod to fix this issue via // the map modifications has been installed. bActivateConstructionSiteFix = bHasBldngst2mesh && bHasBldngst2meshDam && bHasLODngst2mesh && !bHasLODngst2meshDam; Bldngst2mesh_ModelInfo = Bldngst2mesh; Bldngst2meshDam_ModelInfo = Bldngst2meshDam; LODngst2mesh_ModelInfo = LODngst2mesh; } static void FixConstructionSiteModel(int oldModelID, int newModelID) { if (!bActivateConstructionSiteFix) { return; } if (oldModelID == MI_BLDNGST2MESH && newModelID == MI_BLDNGST2MESHDAM) { LODngst2mesh_ModelInfo->m_atomics[2] = Bldngst2meshDam_ModelInfo; } else if (oldModelID == MI_BLDNGST2MESHDAM && newModelID == MI_BLDNGST2MESH) { LODngst2mesh_ModelInfo->m_atomics[2] = Bldngst2mesh_ModelInfo; } } template static void (__fastcall *orgReplaceWithNewModel)(CEntity* building, void*, int newModelID); template static void __fastcall ReplaceWithNewModel_ConstructionSiteFix(CEntity* building, void*, int newModelID) { const int oldModelID = building->m_modelIndex.Get(); orgReplaceWithNewModel(building, nullptr, newModelID); FixConstructionSiteModel(oldModelID, newModelID); } HOOK_EACH_INIT(ReplaceWithNewModel, orgReplaceWithNewModel, ReplaceWithNewModel_ConstructionSiteFix); } namespace ModelIndicesReadyHook { static void (*orgInitialiseObjectData)(const char*); static void InitialiseObjectData_ReadySVF(const char* path) { orgInitialiseObjectData(path); SVF::MarkModelNamesReady(); ConstructionSiteLODFix::MatchModelIndices(); // This is a bit dirty, but whatever // Tooled Up in North Point Mall needs a "draw last" flag, or else our BFC changes break it very badly // AmmuNation and other stores already have that flag, this one does not void* model = GetModelInfo("mall_hardware", nullptr); if (model != nullptr) { uint16_t* flags = reinterpret_cast(static_cast(model) + 0x42); *flags |= 0x40; } } } // ============= Fix the outro splash flickering for a frame when fading in ============= namespace OutroSplashFix { struct RGBA { uint8_t r, g, b, a; }; static RGBA* (__thiscall *orgRGBASet)(RGBA*, uint8_t, uint8_t, uint8_t, uint8_t); static RGBA* __fastcall RGBASet_Clamp(RGBA* rgba, void*, int r, int g, int b, int a) { return orgRGBASet(rgba, static_cast(r), static_cast(g), static_cast(b), static_cast(std::clamp(a, 0, 255))); } } // ============= Fix Tommy not shaking his fists with brass knuckles (in all cases) and most post-GTA III weapons (when cars slow down for him) ============= namespace TommyFistShakeWithWeapons { struct WeaponInfo { std::byte __pad[0x60]; uint32_t m_weaponSlot; }; static WeaponInfo* (*GetWeaponInfo)(uint32_t weaponID); constexpr uint32_t WEAPON_CHAINSAW = 11; static WeaponInfo DUMMY_INFO = [] { WeaponInfo dummy; dummy.m_weaponSlot = 99; return dummy; }(); static bool WeaponProhibitsFistShake(uint32_t weaponID) { const uint32_t weaponSlot = GetWeaponInfo(weaponID)->m_weaponSlot; const bool bWeaponAllowsFistShake = (weaponSlot == 0 || weaponSlot == 1 || weaponSlot == 3 || weaponSlot == 5) && weaponID != WEAPON_CHAINSAW; return !bWeaponAllowsFistShake; } static __declspec(naked) void CheckWeaponGroupHook() { _asm { push dword ptr [eax] call WeaponProhibitsFistShake add esp, 4 test al, al retn } } template static WeaponInfo* (*orgGetWeaponInfo)(uint32_t weaponID); template static WeaponInfo* gGetWeaponInfo_ExcludeChainsaw(uint32_t weaponID) { if (weaponID == WEAPON_CHAINSAW) { return &DUMMY_INFO; } return orgGetWeaponInfo(weaponID); } HOOK_EACH_INIT(ExcludeChainsaw, orgGetWeaponInfo, gGetWeaponInfo_ExcludeChainsaw); } void InjectDelayedPatches_VC_Common( bool bHasDebugMenu, const wchar_t* wcModulePath ) { using namespace Memory; using namespace hook::txn; const ModuleList moduleList; const HMODULE hGameModule = GetModuleHandle(nullptr); const HMODULE skygfxModule = moduleList.Get(L"skygfx"); if (skygfxModule != nullptr) { auto attachCarPipe = reinterpret_cast(GetProcAddress(skygfxModule, "AttachCarPipeToRwObject")); if (attachCarPipe != nullptr) { CVehicleModelInfo::AttachCarPipeToRwObject = attachCarPipe; } } auto PatchFloat = [](float** address, const float*& org, float& replaced) { org = *address; Patch(address, &replaced); }; // Locale based metric/imperial system INI/debug menu { using namespace Localization; forcedUnits = static_cast(GetPrivateProfileIntW(L"SilentPatch", L"Units", -1, wcModulePath)); if ( bHasDebugMenu ) { static const char * const str[] = { "Default", "Metric", "Imperial" }; DebugMenuEntry *e = DebugMenuAddVar( "SilentPatch", "Forced units", &forcedUnits, nullptr, 1, -1, 1, str ); DebugMenuEntrySetWrap(e, true); } } // Corrected siren corona placement for emergency vehicles if ( GetPrivateProfileIntW(L"SilentPatch", L"EnableVehicleCoronaFixes", -1, wcModulePath) == 1 ) { // Other mods might be touching it, so only patch specific vehicles if their code has not been touched at all try { auto firetruck1 = pattern("8D 8C 24 24 09 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); auto firetruck2 = pattern("8D 8C 24 30 09 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); static const CVector FIRETRUCK_SIREN_POS(0.95f, 3.2f, 1.4f); static const float FIRETRUCK_SIREN_MINUS_X = -FIRETRUCK_SIREN_POS.x; Patch( firetruck1.get( 7 + 2 ), &FIRETRUCK_SIREN_POS.z ); Patch( firetruck1.get( 7 + 2 + (6*1) ), &FIRETRUCK_SIREN_POS.y ); Patch( firetruck1.get( 7 + 2 + (6*2) ), &FIRETRUCK_SIREN_POS.x ); Patch( firetruck2.get( 7 + 2 ), &FIRETRUCK_SIREN_POS.z ); Patch( firetruck2.get( 7 + 2 + (6*1) ), &FIRETRUCK_SIREN_POS.y ); Patch( firetruck2.get( 7 + 2 + (6*2) ), &FIRETRUCK_SIREN_MINUS_X ); } TXN_CATCH(); try { auto ambulan1 = pattern("8D 8C 24 0C 09 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); auto ambulan2 = pattern("8D 8C 24 18 09 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); static const CVector AMBULANCE_SIREN_POS(0.7f, 0.65f, 1.55f); static const float AMBULANCE_SIREN_MINUS_X = -AMBULANCE_SIREN_POS.x; Patch( ambulan1.get( 7 + 2 ), &AMBULANCE_SIREN_POS.z ); Patch( ambulan1.get( 7 + 2 + (6*1) ), &AMBULANCE_SIREN_POS.y ); Patch( ambulan1.get( 7 + 2 + (6*2) ), &AMBULANCE_SIREN_POS.x ); Patch( ambulan2.get( 7 + 2 ), &AMBULANCE_SIREN_POS.z ); Patch( ambulan2.get( 7 + 2 + (6*1) ), &AMBULANCE_SIREN_POS.y ); Patch( ambulan2.get( 7 + 2 + (6*2) ), &AMBULANCE_SIREN_MINUS_X ); } TXN_CATCH(); try { auto police1 = pattern("8D 8C 24 DC 08 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); auto police2 = pattern("8D 8C 24 E8 08 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); static const CVector POLICE_SIREN_POS(0.55f, -0.4f, 0.95f); static const float POLICE_SIREN_MINUS_X = -POLICE_SIREN_POS.x; Patch( police1.get( 7 + 2 ), &POLICE_SIREN_POS.z ); Patch( police1.get( 7 + 2 + (6*1) ), &POLICE_SIREN_POS.y ); Patch( police1.get( 7 + 2 + (6*2) ), &POLICE_SIREN_POS.x ); Patch( police2.get( 7 + 2 ), &POLICE_SIREN_POS.z ); Patch( police2.get( 7 + 2 + (6*1) ), &POLICE_SIREN_POS.y ); Patch( police2.get( 7 + 2 + (6*2) ), &POLICE_SIREN_MINUS_X ); } TXN_CATCH(); try { auto enforcer1 = pattern("8D 8C 24 F4 08 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); auto enforcer2 = pattern("8D 8C 24 00 09 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35").get_one(); static const CVector ENFORCER_SIREN_POS(0.6f, 1.05f, 1.4f); static const float ENFORCER_SIREN_MINUS_X = -ENFORCER_SIREN_POS.x; Patch( enforcer1.get( 7 + 2 ), &ENFORCER_SIREN_POS.z ); Patch( enforcer1.get( 7 + 2 + (6*1) ), &ENFORCER_SIREN_POS.y ); Patch( enforcer1.get( 7 + 2 + (6*2) ), &ENFORCER_SIREN_POS.x ); Patch( enforcer2.get( 7 + 2 ), &ENFORCER_SIREN_POS.z ); Patch( enforcer2.get( 7 + 2 + (6*1) ), &ENFORCER_SIREN_POS.y ); Patch( enforcer2.get( 7 + 2 + (6*2) ), &ENFORCER_SIREN_MINUS_X ); } TXN_CATCH(); { try { auto chopper1 = pattern("C7 44 24 44 00 00 E0 40 50 C7 44 24 4C 00 00 00 00").get_one(); // Front light constexpr CVector CHOPPER_SEARCH_LIGHT_POS(0.0f, 3.0f, -1.0f); // Same as in III Aircraft (not implemented there yet!) Patch( chopper1.get( 4 ), CHOPPER_SEARCH_LIGHT_POS.y ); Patch( chopper1.get( 9 + 4 ), CHOPPER_SEARCH_LIGHT_POS.z ); } TXN_CATCH(); try { auto chopper2 = pattern("C7 44 24 6C 00 00 10 C1 8D 44 24 5C C7 44 24 70 00 00 00 00").get_one(); // Tail light constexpr CVector CHOPPER_RED_LIGHT_POS(0.0f, -7.5f, 2.5f); // Same as in III Aircraft Patch( chopper2.get( 4 ), CHOPPER_RED_LIGHT_POS.y ); Patch( chopper2.get( 12 + 4 ), CHOPPER_RED_LIGHT_POS.z ); } TXN_CATCH(); } try { using namespace FBISirenCoronaFix; auto viceCheetah = pattern("8D 8C 24 CC 09 00 00 FF 35 ? ? ? ? FF 35 ? ? ? ? FF 35 ? ? ? ? E8").get_one(); // Siren pos try { auto hasFBISiren = pattern("83 E9 04 0F 84 87 0A 00 00 83 E9 10").get_one(); // Predicate for showing FBI/Vice Squad siren Patch( hasFBISiren.get(), 0x55 ); // push ebp InjectHook( hasFBISiren.get( 1 ), SetUpFBISiren, HookType::Call ); Patch( hasFBISiren.get( 1 + 5 ), { 0x83, 0xC4, 0x04, 0x84, 0xC0, 0x90 } ); // add esp, 4 / test al, al / nop InjectHook( viceCheetah.get( 0x19 ), SetUpVector ); } TXN_CATCH(); static const float VICE_CHEETAH_SIREN_POS_Z = 0.25f; Patch( viceCheetah.get( 7 + 2 ), &VICE_CHEETAH_SIREN_POS_Z ); } TXN_CATCH(); } bool HasModelInfo = false; // Register CBaseModelInfo::GetModelInfo for SVF so we can resolve model names try { using namespace ModelIndicesReadyHook; auto initialiseObjectData = get_pattern("E8 ? ? ? ? 59 E8 ? ? ? ? E8 ? ? ? ? 31 DB"); auto getModelInfo = (void*(*)(const char*, int*))get_pattern("57 31 FF 55 8B 6C 24 14", -6); GetModelInfo = getModelInfo; InterceptCall(initialiseObjectData, orgInitialiseObjectData, InitialiseObjectData_ReadySVF); SVF::RegisterGetModelInfoCB(getModelInfo); HasModelInfo = true; } TXN_CATCH(); // Fix the construction site LOD losing its HQ model and showing at all times if (HasModelInfo) try { using namespace ConstructionSiteLODFix; std::array replaceWithNewModel = { get_pattern("E8 ? ? ? ? C7 85 ? ? ? ? 00 00 00 00 83 8D ? ? ? ? FF"), get_pattern("DD D8 E8 ? ? ? ? 56", 2), get_pattern("E8 ? ? ? ? FF 44 24 0C 83 C5 0C"), }; HookEach_ReplaceWithNewModel(replaceWithNewModel, InterceptCall); } TXN_CATCH(); // Fix the radar disc shadow scaling and radar X position try { using namespace RadardiscFixes; auto draw_entity_coord_blip = pattern("D8 0D ? ? ? ? D8 05 ? ? ? ? DE C1 D9 5C 24 18").count(2); auto draw_radar_disc1 = pattern("D8 25 ? ? ? ? DD DB D9 C2 D9 9C 24 ? ? ? ? DB 05 ? ? ? ? D8 0D ? ? ? ? D8 0D ? ? ? ? D8 05 ? ? ? ? D8 05").count(2); auto draw_radar_disc2 = pattern("D8 C1 D8 05 ? ? ? ? D9 9C 24 ? ? ? ? DE D9 DD D8").count(2); std::array radarXPos = { get_pattern("D8 05 ? ? ? ? DE C1 D9 5C 24 28", 2), get_pattern("D8 05 ? ? ? ? DE C1 D9 5C EC 30", 2), get_pattern("D8 0D ? ? ? ? D8 05 ? ? ? ? DE C1 D9 9C C4", 6 + 2), get_pattern("D8 0D ? ? ? ? D8 05 ? ? ? ? DE C1 D9 5C 24 08", 6 + 2), get_pattern("D8 0D ? ? ? ? D8 05 ? ? ? ? DE C1 DD D9 DB 44 24 18", 6 + 2), get_pattern("D8 0D ? ? ? ? D8 05 ? ? ? ? DE C1 DD DB DB 44 24 18", 6 + 2), draw_entity_coord_blip.get(0).get(6 + 2), draw_entity_coord_blip.get(1).get(6 + 2), }; std::array radarXPos_RadardiscShrink = { draw_radar_disc1.get(0).get(35 + 2), draw_radar_disc1.get(0).get(35 + 6 + 2), draw_radar_disc1.get(1).get(35 + 2), draw_radar_disc1.get(1).get(35 + 6 + 2), }; std::array radarYPos_RadardiscShrink = { draw_radar_disc1.get(0).get(2), draw_radar_disc1.get(1).get(2), draw_radar_disc2.get(0).get(2 + 2), draw_radar_disc2.get(1).get(2 + 2), }; auto drawMap = get_pattern("59 E8 ? ? ? ? 83 3D ? ? ? ? ? 0F 84", 1); auto drawRadarDiscSprite = pattern("D8 05 ? ? ? ? D9 9C 24 ? ? ? ? DE D9 DD D8 E8").count(2); std::array spriteDraw = { drawRadarDiscSprite.get(0).get(17), drawRadarDiscSprite.get(1).get(17), }; // Undo the damage caused by IVRadarScaling from the widescreen fix moving the radar way too far to the right // It's moved from 40.0f to 71.0f, which is way too much now that we're scaling the horizontal placement correctly! // This is removed from the most up-to-date widescreen fix, but keep it so we don't break with older builds. try { // Use exactly the same patterns as widescreen fix float* radarPos = *get_pattern("D8 05 ? ? ? ? DE C1 D9 5C 24 28", 2); // No need to undo CRadar::DrawYouAreHereSprite, as wsfix keeps it as 40.0f // This hardcodes a patched constant inside so the pattern will fail to match without IV radar scaling auto radarRing1 = pattern("C7 84 24 ? ? ? ? 00 00 82 42").count(2); auto radarRing2 = pattern("D8 05 ? ? ? ? D8 05 ? ? ? ? D9 9C 24").count(2); // This + radarRing1 succeeding is enough proof that IVRadarScaling is in use if (hGameModule == ModCompat::Utils::GetModuleHandleFromAddress(radarPos) && *radarPos == (40.0f + 31.0f)) { *radarPos = 40.0f; radarRing1.for_each_result([](pattern_match match) { Patch(match.get(7), 34.0f); }); radarRing2.for_each_result([](pattern_match match) { static float STOCK_RADAR_POS = 40.0f; Patch(match.get(2), &STOCK_RADAR_POS); }); } } TXN_CATCH(); HookEach_CalculateRadarXPos(radarXPos, PatchFloat); HookEach_CalculateRadarXPos_RadardiscShrink(radarXPos_RadardiscShrink, PatchFloat); HookEach_CalculateRadarYPos_RadardiscShrink(radarYPos_RadardiscShrink, PatchFloat); HookEach_DrawRadarDisc(spriteDraw, InterceptCall); InterceptCall(drawMap, orgDrawMap, DrawMap_RecalculatePositions); } TXN_CATCH(); // Fix the onscreen counter bar placement and shadow not scaling to resolution try { using namespace OnscreenCounterBarFixes; auto atoiWrap = get_pattern("E8 ? ? ? ? D9 EE DB 05 ? ? ? ? 89 C7"); auto shadow1 = pattern("D8 05 ? ? ? ? D9 9C 24 ? ? ? ? D9 05 ? ? ? ? D8 44 24 50").get_one(); auto shadow2 = pattern("D9 05 ? ? ? ? D8 C1 D9 9C 24 ? ? ? ? D9 05").get_one(); auto fill1 = pattern("D8 05 ? ? ? ? D9 9C 24 ? ? ? ? D9 05 ? ? ? ? 8D 84 24").get_one(); std::array XPositions = { shadow1.get(2), shadow2.get(2), fill1.get(2), fill1.get(13 + 2), get_pattern("D9 05 ? ? ? ? D8 C1 D9 9C 24 ? ? ? ? DB 44 24 6C", 2), get_pattern("D8 05 ? ? ? ? D9 9C 24 ? ? ? ? DE D9 E8 ? ? ? ? 59", 2), }; std::array YPositions = { shadow1.get(13 + 2), shadow2.get(15 + 2), }; HookEach_XPos(XPositions, PatchFloat); HookEach_YPos(YPositions, PatchFloat); InterceptCall(atoiWrap, orgAtoi, atoi_RecalculatePositions); } TXN_CATCH(); // Fix the radar trace blip shadow not scaling to resolution try { using namespace RadarTraceOutlineFixes; auto triangle1 = pattern("D8 05 ? ? ? ? DD D9 D9 44 24 68").count(2); auto triangle2 = pattern("D8 05 ? ? ? ? DD DA D9 44 24 58").count(2); auto showRadarTraceWithHeight = pattern("E8 ? ? ? ? 83 C4 20 80 3D ? ? ? ? ? 0F 84 ? ? ? ? 30 D2").count(2); std::array XPositions = { triangle1.get(0).get(2), triangle1.get(1).get(2), triangle2.get(0).get(2), triangle2.get(1).get(2), get_pattern("D8 05 ? ? ? ? DD D9 D9 44 24 50", 2), }; std::array YPositions = { get_pattern("D9 05 ? ? ? ? D8 C1 D8 6C 24 58 DD DA", 2), get_pattern("D9 05 ? ? ? ? D8 C1 D8 44 24 60", 2), get_pattern("D8 05 ? ? ? ? DD D9 D9 44 24 58", 2), get_pattern("D9 05 ? ? ? ? D8 C1 D8 6C 24 58 89 D0", 2), get_pattern("D9 05 ? ? ? ? D8 C1 D8 44 24 64", 2), get_pattern("D8 05 ? ? ? ? 89 D8", 2), }; std::array showRadarTraceWithHeight_Patches = { showRadarTraceWithHeight.get(0).get(), showRadarTraceWithHeight.get(1).get(), }; HookEach_XPos(XPositions, PatchFloat); HookEach_YPos(YPositions, PatchFloat); PositionRecalculator::HookEach_ShowRadarTraceWithHeight(showRadarTraceWithHeight_Patches, InterceptCall); } TXN_CATCH(); // Fix the loading bar outline not scaling to resolution try { using namespace LoadingBarOutlineFixes; auto rgbaCtor = get_pattern("6A 00 E8 ? ? ? ? 8B 0D ? ? ? ? A1", 2); std::array XPositions = { get_pattern("D9 C1 D8 25 ? ? ? ? DD DC", 2 + 2), get_pattern(" D8 C2 D8 05 ? ? ? ? D9 5C 24 38", 2 + 2), }; std::array YPositions = { get_pattern("DD D1 D8 25 ? ? ? ? D9 5C 24 3C", 2 + 2), get_pattern("D8 05 ? ? ? ? D9 5C 24 34 DE D9", 2), }; HookEach_XPos(XPositions, PatchFloat); HookEach_YPos(YPositions, PatchFloat); InterceptCall(rgbaCtor, orgRGBACtor, RGBACtor_RecalculatePositions); } TXN_CATCH(); // Fix credits not scaling to resolution try { using namespace CreditsScalingFixes; std::array creditPrintString = { get_pattern("E8 ? ? ? ? 83 C4 0C 8D 4C 24 14"), get_pattern("E8 ? ? ? ? 83 C4 0C 8B 03"), }; auto setScale = get_pattern("E8 ? ? ? ? 59 59 8D 4C 24 10"); // Fix the credits cutting off on the bottom early, they don't do that in III // but it regressed in VC and SA auto positionOffset = pattern("D8 1D ? ? ? ? DF E0 F6 C4 45 0F 85 ? ? ? ? 89 4C 24 08 DB 44 24 08 D8 25").get_one(); // As we now scale everything on PrintString time, the resolution height checks need to be unscaled. void* resHeightScales[] = { get_pattern("8B 0D ? ? ? ? 8B 03", 2), get_pattern("8B 0D ? ? ? ? C7 44 24 ? ? ? ? ? 03 4C 24 14 ", 2), }; static const float floatStorage[2] = { 1.0f, -(**positionOffset.get(0x19 + 2)) }; Patch(positionOffset.get(2), &floatStorage[0]); Patch(positionOffset.get(0x19 + 2), &floatStorage[0]); HookEach_PrintString(creditPrintString, InterceptCall); InterceptCall(setScale, orgSetScale, SetScale_ScaleToRes); for (void* addr : resHeightScales) { Patch(addr, &FIXED_RES_HEIGHT_SCALE); } } TXN_CATCH(); // Minimal HUD if (const int INIoption = GetPrivateProfileIntW(L"SilentPatch", L"MinimalHUD", -1, wcModulePath); INIoption != -1) try { using namespace MinimalHUD; // Fix original bugs // Wanted level stars losing their shadow if health/armour counters are off auto setColor_WantedStars = get_pattern("E8 ? ? ? ? DB 05 ? ? ? ? 8D 84 24 ? ? ? ? 59 50 50 D8 0D ? ? ? ? D8 0D ? ? ? ? D9 1C 24 FF 74 24 24"); // Get SetDropShadowPosition and SetDropColor with their corresponding parameters straight from the armor counter, // to preserve the current behaviour auto setDropShadow = pattern("6A ? E8 ? ? ? ? 59 8D 8C 24 ? ? ? ? 68 ? ? ? ? 6A ? 6A ? 6A ? E8 ? ? ? ? 8D 84 24 ? ? ? ? 50 E8").get_one(); // Show the energy values when losing armor auto drawFadeState_Energy = get_pattern("6A 01 E8 ? ? ? ? DD D8", 2); auto timeLastArmorLoss = pattern("0F B6 15 ? ? ? ? A1 ? ? ? ? 6B D2 2E 89 04 D5 ? ? ? ? D9 83").get_one(); // Fade the weapon icon - fix the render state and pass the alpha parameter auto drawWeaponIconAlphaPush = get_pattern("68 FF 00 00 00 D8 0D ? ? ? ? FF 35"); auto renderOneXLUSprite = get_pattern("E8 ? ? ? ? 83 C4 2C 6A 00 6A 08"); // Stuff to let us (re)initialize static void (*HUDReInitialise)() = static_cast(get_pattern("31 C0 53 0F EF C0 C6 05")); // This pattern has 5 hits - first 2 are in Reinitialise, the rest is in Initialise auto reinitialise1 = pattern("C7 05 ? ? ? ? 05 00 00 00 66 C7 05 ? ? ? ? 00 00 C7 05 ? ? ? ? 00 00 00 00").count(5); // This one covers the rest of Reinitialise auto reinitialise2 = pattern("C7 05 ? ? ? ? 05 00 00 00 C6 05 ? ? ? ? 00 C7 05 ? ? ? ? 00 00 00 00").count(2); // This one we touch only once, no need for static const std::array hudInitialiseVariables = { reinitialise1.get(2).get(6), reinitialise1.get(3).get(6), reinitialise1.get(4).get(6), get_pattern("8B 83 ? ? ? ? C7 05 ? ? ? ? 05 00 00 00", 6 + 6), }; static const std::array hudReinitialiseVariables = { reinitialise1.get(0).get(6), reinitialise1.get(1).get(6), reinitialise2.get(0).get(6), reinitialise2.get(1).get(6), }; ReadCall(setDropShadow.get(2), orgSetDropShadowPosition); ReadCall(setDropShadow.get(39), orgSetDropColor); pDropShadowSize = setDropShadow.get(1); pDropShadowB = setDropShadow.get(20 + 1); pDropShadowG = setDropShadow.get(22 + 1); pDropShadowR = setDropShadow.get(24 + 1); InterceptCall(setColor_WantedStars, orgSetColor, SetColor_SetShadow); pPlayerInFocus = *timeLastArmorLoss.get(3); pTimeLastArmorLoss = *timeLastArmorLoss.get(0xF + 3); InterceptCall(drawFadeState_Energy, orgDrawFadeState, DrawFadeState_CheckArmor); // push 0FFh -> push dword ptr [esp+520h+var_4E0] Patch(drawWeaponIconAlphaPush, { 0x90, 0xFF, 0x74, 0x24, 0x40 }); InterceptCall(renderOneXLUSprite, orgRenderOneXLUSprite, RenderOneXLUSprite_FloatAlpha); if (INIoption != 0) { for (uint32_t* var : hudInitialiseVariables) { Patch(var, 0); } for (uint32_t* var : hudReinitialiseVariables) { Patch(var, 0); } } if (bHasDebugMenu) { static bool bMinimalHUDEnabled = INIoption != 0; DebugMenuAddVar("SilentPatch", "Minimal HUD", &bMinimalHUDEnabled, [] { if (bMinimalHUDEnabled) { for (uint32_t* var : hudReinitialiseVariables) { Memory::VP::Patch(var, 0); } } else { for (uint32_t* var : hudReinitialiseVariables) { Memory::VP::Patch(var, 5); } } // Call CHud::ReInitialise HUDReInitialise(); }); } } TXN_CATCH(); // Fix some big messages staying on screen longer at high resolutions due to a cut sliding text feature // Also since we're touching it, optionally allow to re-enable this feature. try { using namespace SlidingTextsScalingFixes; // "Unscale" text sliding thresholds, so texts don't stay on screen longer at high resolutions void* scalingThreshold[] = { get_pattern("A1 ? ? ? ? 59 83 C0 EC", 1), get_pattern("59 A1 ? ? ? ? 83 C0 EC", 1 + 1) }; for (void* addr : scalingThreshold) { Patch(addr, &FIXED_RES_WIDTH_SCALE); } // Optional sliding texts if (const int INIoption = GetPrivateProfileIntW(L"SilentPatch", L"SlidingMissionTitleText", -1, wcModulePath); INIoption != -1) try { pBigMessageX = *get_pattern*>("DB 44 24 68 D8 1D ? ? ? ? DF E0", 4 + 2); std::array slidingMessage1 = { get_pattern("E8 ? ? ? ? 83 C4 0C EB 0A C7 05 ? ? ? ? ? ? ? ? 83 C4 60"), }; std::array textWrapFix = { get_pattern("E8 ? ? ? ? 59 E8 ? ? ? ? 6A 00 E8 ? ? ? ? 59 8B 0D"), }; BigMessageSlider<1>::bSlidingEnabled = INIoption != 0; BigMessageSlider<1>::HookEach_PrintString(slidingMessage1, InterceptCall); BigMessageSlider<1>::HookEach_RightJustifyWrap(textWrapFix, InterceptCall); if (bHasDebugMenu) { DebugMenuAddVar("SilentPatch", "Sliding mission title text", &BigMessageSlider<1>::bSlidingEnabled, nullptr); } } TXN_CATCH(); if (const int INIoption = GetPrivateProfileIntW(L"SilentPatch", L"SlidingOddJobText", -1, wcModulePath); INIoption != -1) try { pOddJob2XOffset = *get_pattern("D9 05 ? ? ? ? D8 E1 D9 1D ? ? ? ? E9", 2); std::array slidingOddJob2 = { get_pattern("E8 ? ? ? ? 83 C4 0C 66 83 3D ? ? ? ? ? 0F 84"), }; OddJob2Slider::bSlidingEnabled = INIoption != 0; OddJob2Slider::HookEach_PrintString(slidingOddJob2, InterceptCall); if (bHasDebugMenu) { DebugMenuAddVar("SilentPatch", "Sliding odd job text", &OddJob2Slider::bSlidingEnabled, nullptr); } } TXN_CATCH(); } TXN_CATCH(); FLAUtils::Init(moduleList); } void InjectDelayedPatches_VC_Common() { std::unique_ptr Protect = ScopedUnprotect::UnprotectSectionOrFullModule( GetModuleHandle( nullptr ), ".text" ); // Obtain a path to the ASI wchar_t wcModulePath[MAX_PATH]; GetModuleFileNameW(reinterpret_cast(&__ImageBase), wcModulePath, _countof(wcModulePath) - 3); // Minus max required space for extension PathRenameExtensionW(wcModulePath, L".ini"); const bool hasDebugMenu = DebugMenuLoad(); SelectableBackfaceCulling::ReadDrawBackfacesExclusions(wcModulePath); InjectDelayedPatches_VC_Common( hasDebugMenu, wcModulePath ); Common::Patches::III_VC_DelayedCommon( hasDebugMenu, wcModulePath ); } void Patch_VC_10(uint32_t width, uint32_t height) { using namespace Memory::DynBase; RsGlobal = *(RsGlobalType**)DynBaseAddress(0x602D32); SubtitlesShadowFix_JumpBack = (void*)DynBaseAddress(0x551701); InjectHook(0x5433BD, FixedRefValue); InjectHook(0x5516FC, SubtitlesShadowFix, HookType::Jump); Patch(0x5517C4, 0xD9); Patch(0x5517DF, 0xD9); Patch(0x551832, 0xD9); Patch(0x551848, 0xD9); Patch(0x5517E2, 0x34-0x14); Patch(0x55184B, 0x34-0x14); Patch(0x5517C7, 0x28-0x18); Patch(0x551835, 0x24-0x18); Patch(0x5516FB, 0x90); { using namespace PrintStringShadows; XY<0x5FA1F6, 0x5FA1D5>::Hook(0x5FA1FD); XMinus<0x544727/*, 0x544727*/>::Hook(0x54474D); // Don't patch Y as we're doing it in the credits scale fix } // Mouse fucking fix! Patch(0x601740, 0xC3C030); // (Hopefully) more precise frame limiter ReadCall( 0x6004A2, RsEventHandler ); InjectHook(0x6004A2, NewFrameRender); InjectHook(0x600449, GetTimeSinceLastFrame); // RsMouseSetPos call (SA style fix) ReadCall( 0x4A5E45, orgConstructRenderList ); InjectHook(0x4A5E45, ResetMousePos); // New wndproc OldWndProc = *(LRESULT (CALLBACK***)(HWND, UINT, WPARAM, LPARAM))DynBaseAddress(0x601727); Patch(0x601727, &pCustomWndProc); // Y axis sensitivity fix // By ThirteenAG float* sens = *(float**)DynBaseAddress(0x4796E5); Patch(0x479410 + 0x2E0 + 0x2, sens); Patch(0x47A20E + 0x27D + 0x2, sens); Patch(0x47AE27 + 0x1CC + 0x2, sens); Patch(0x47BE8F + 0x22E + 0x2, sens); Patch(0x481AB3 + 0x4FE + 0x2, sens); // Don't lock mouse Y axis during fadeins Patch(0x47C11E, 0xEB); Patch(0x47CD94, 0xEB); Nop(0x47C15A, 2); // Scan for A/B drives looking for audio files Patch(0x5D7941, 'A'); Patch(0x5D7B04, 'A'); // ~x~ as cyan blip // Shared with GInput Patch(0x550481, 0); Patch(0x550488, 255); Patch(0x55048F, 255); Patch(0x5505FF, 0); Patch(0x550603, 255); Patch(0x550607, 255); // Corrected crime codes Patch(0x5FDDDB, 0xC5); // Fixed ammo for melee weapons in cheats Patch(0x4AED14+1, 1); // katana Patch(0x4AEB74+1, 1); // chainsaw // Fixed crash related to autopilot timing calculations InjectHook(0x418FAE, AutoPilotTimerFix_VC, HookType::Jump); Common::Patches::DDraw_VC_10( width, height, aNoDesktopMode ); } void Patch_VC_11(uint32_t width, uint32_t height) { using namespace Memory::DynBase; RsGlobal = *(RsGlobalType**)DynBaseAddress(0x602D12); SubtitlesShadowFix_JumpBack = (void*)DynBaseAddress(0x551721); InjectHook(0x5433DD, FixedRefValue); InjectHook(0x55171C, SubtitlesShadowFix, HookType::Jump); Patch(0x5517E4, 0xD9); Patch(0x5517FF, 0xD9); Patch(0x551852, 0xD9); Patch(0x551868, 0xD9); Patch(0x551802, 0x34-0x14); Patch(0x55186B, 0x34-0x14); Patch(0x5517E7, 0x28-0x18); Patch(0x551855, 0x24-0x18); Patch(0x55171B, 0x90); { using namespace PrintStringShadows; XY<0x5FA216, 0x5FA1F5>::Hook(0x5FA21D); XMinus<0x544747/*, 0x544747*/>::Hook(0x54476D); // Don't patch Y as we're doing it in the credits scale fix } // Mouse fucking fix! Patch(0x601770, 0xC3C030); // (Hopefully) more precise frame limiter ReadCall( 0x6004C2, RsEventHandler ); InjectHook(0x6004C2, NewFrameRender); InjectHook(0x600469, GetTimeSinceLastFrame); // RsMouseSetPos call (SA style fix) ReadCall( 0x4A5E65, orgConstructRenderList ); InjectHook(0x4A5E65, ResetMousePos); // New wndproc OldWndProc = *(LRESULT (CALLBACK***)(HWND, UINT, WPARAM, LPARAM))DynBaseAddress(0x601757); Patch(0x601757, &pCustomWndProc); // Y axis sensitivity fix // By ThirteenAG float* sens = *(float**)DynBaseAddress(0x4796E5); Patch(0x479410 + 0x2E0 + 0x2, sens); Patch(0x47A20E + 0x27D + 0x2, sens); Patch(0x47AE27 + 0x1CC + 0x2, sens); Patch(0x47BE8F + 0x22E + 0x2, sens); Patch(0x481AB3 + 0x4FE + 0x2, sens); // Don't lock mouse Y axis during fadeins Patch(0x47C11E, 0xEB); Patch(0x47CD94, 0xEB); Nop(0x47C15A, 2); // Scan for A/B drives looking for audio files Patch(0x5D7961, 'A'); Patch(0x5D7B24, 'A'); // ~x~ as cyan blip // Shared with GInput Patch(0x5504A1, 0); Patch(0x5504A8, 255); Patch(0x5504AF, 255); Patch(0x55061F, 0); Patch(0x550623, 255); Patch(0x550627, 255); // Corrected crime codes Patch(0x5FDDFB, 0xC5); // Fixed ammo for melee weapons in cheats Patch(0x4AED34+1, 1); // katana Patch(0x4AEB94+1, 1); // chainsaw // Fixed crash related to autopilot timing calculations InjectHook(0x418FAE, AutoPilotTimerFix_VC, HookType::Jump); Common::Patches::DDraw_VC_11( width, height, aNoDesktopMode ); } void Patch_VC_Steam(uint32_t width, uint32_t height) { using namespace Memory::DynBase; RsGlobal = *(RsGlobalType**)DynBaseAddress(0x602952); SubtitlesShadowFix_JumpBack = (void*)DynBaseAddress(0x5515F1); InjectHook(0x5432AD, FixedRefValue); InjectHook(0x5515EC, SubtitlesShadowFix, HookType::Jump); Patch(0x5516B4, 0xD9); Patch(0x5516CF, 0xD9); Patch(0x551722, 0xD9); Patch(0x551738, 0xD9); Patch(0x5516D2, 0x34-0x14); Patch(0x55173B, 0x34-0x14); Patch(0x5516B7, 0x28-0x18); Patch(0x551725, 0x24-0x18); Patch(0x5515EB, 0x90); { using namespace PrintStringShadows; XY<0x5F9E56, 0x5F9E35>::Hook(0x5F9E5D); XMinus<0x544617/*, 0x544617*/>::Hook(0x54463D); // Don't patch Y as we're doing it in the credits scale fix } // Mouse fucking fix! Patch(0x6013B0, 0xC3C030); // (Hopefully) more precise frame limiter ReadCall( 0x600102, RsEventHandler ); InjectHook(0x600102, NewFrameRender); InjectHook(0x6000A9, GetTimeSinceLastFrame); // RsMouseSetPos call (SA style fix) ReadCall( 0x4A5D15, orgConstructRenderList ); InjectHook(0x4A5D15, ResetMousePos); // New wndproc OldWndProc = *(LRESULT (CALLBACK***)(HWND, UINT, WPARAM, LPARAM))DynBaseAddress(0x601397); Patch(0x601397, &pCustomWndProc); // Y axis sensitivity fix // By ThirteenAG float* sens = *(float**)DynBaseAddress(0x4795C5); Patch(0x4792F0 + 0x2E0 + 0x2, sens); Patch(0x47A0EE + 0x27D + 0x2, sens); Patch(0x47AD07 + 0x1CC + 0x2, sens); Patch(0x47BD6F + 0x22E + 0x2, sens); Patch(0x481993 + 0x4FE + 0x2, sens); // Don't lock mouse Y axis during fadeins Patch(0x47BFFE, 0xEB); Patch(0x47CC74, 0xEB); Nop(0x47C03A, 2); // Scan for A/B drives looking for audio files Patch(0x5D7764, 'A'); // ~x~ as cyan blip // Shared with GInput Patch(0x550371, 0); Patch(0x550378, 255); Patch(0x55037F, 255); Patch(0x5504EF, 0); Patch(0x5504F3, 255); Patch(0x5504F7, 255); // Corrected crime codes Patch(0x5FDA3B, 0xC5); // Fixed ammo for melee weapons in cheats Patch(0x4AEA44+1, 1); // katana Patch(0x4AEBE4+1, 1); // chainsaw // Fixed crash related to autopilot timing calculations InjectHook(0x418FAE, AutoPilotTimerFix_VC, HookType::Jump); Common::Patches::DDraw_VC_Steam( width, height, aNoDesktopMode ); } void Patch_VC_JP() { using namespace Memory::DynBase; // Y axis sensitivity fix // By ThirteenAG Patch(0x4797E7 + 0x2E0 + 0x2, 0x94ABD8); Patch(0x47A5E5 + 0x27D + 0x2, 0x94ABD8); Patch(0x47B1FE + 0x1CC + 0x2, 0x94ABD8); Patch(0x47C266 + 0x22E + 0x2, 0x94ABD8); Patch(0x481E8A + 0x4FE + 0x2, 0x94ABD8); } void Patch_VC_Common() { using namespace Memory; using namespace hook::txn; const HMODULE hGameModule = GetModuleHandle(nullptr); // New timers fix try { auto hookPoint = pattern( "83 E4 F8 89 44 24 08 C7 44 24 0C 00 00 00 00 DF 6C 24 08" ).get_one(); auto jmpPoint = get_pattern( "DD D8 E9 31 FF FF FF" ); InjectHook( hookPoint.get( 0x21 ), CTimer::Update_SilentPatch, HookType::Call ); InjectHook( hookPoint.get( 0x21 + 5 ), jmpPoint, HookType::Jump ); } TXN_CATCH(); // Alt+F4 try { auto addr = pattern( "59 59 31 C0 83 C4 70 5D 5F 5E 5B C2 10 00" ).count(2); auto dest = get_pattern( "53 55 56 FF B4 24 90 00 00 00 FF 15" ); addr.for_each_result( [&]( pattern_match match ) { InjectHook( match.get( 2 ), dest, HookType::Jump ); }); } TXN_CATCH(); // Proper panels damage try { auto addr = pattern( "C6 41 09 03 C6 41 0A 03 C6 41 0B 03" ).get_one(); // or dword ptr[ecx+14], 3300000h // nop Patch( addr.get( 0x18 ), { 0x81, 0x49, 0x14, 0x00, 0x00, 0x30, 0x03 } ); Nop( addr.get( 0x18 + 7 ), 13 ); Nop( addr.get( 0x33 ), 7 ); } TXN_CATCH(); // Proper metric-imperial conversion constants try { static const float METERS_TO_MILES = 0.0006213711922f; auto addr = pattern( "75 ? D9 05 ? ? ? ? D8 0D ? ? ? ? 6A 00 6A 00 D9 9C 24" ).count(6); auto sum = get_pattern( "D9 9C 24 A8 00 00 00 8D 84 24 A8 00 00 00 50", -6 + 2 ); addr.for_each_result( [&]( pattern_match match ) { Patch( match.get( 0x8 + 2 ), &METERS_TO_MILES ); }); Patch( sum, &METERS_TO_MILES ); } TXN_CATCH(); // Improved pathfinding in PickNextNodeAccordingStrategy - PickNextNodeToChaseCar with XYZ coords try { auto addr = pattern( "E8 ? ? ? ? 50 8D 44 24 10 50 E8" ).get_one(); ReadCall( addr.get( 0x25 ), orgPickNextNodeToChaseCar ); const uintptr_t funcAddr = (uintptr_t)get_pattern( "8B 9C 24 BC 00 00 00 66 8B B3 A6 01 00 00 66 85 F6", -0xA ); InjectHook( funcAddr - 5, PickNextNodeToChaseCarXYZ, HookType::Jump ); // For plugin-sdk // push PickNextNodeToChaseCarZ instead of 0.0f // mov ecx, [PickNextNodeToChaseCarZ] // mov [esp+0B8h+var_2C], ecx Patch( funcAddr + 0x5D, { 0x8B, 0x0D } ); Patch( funcAddr + 0x5D + 2, &PickNextNodeToChaseCarZ ); Patch( funcAddr + 0x5D + 6, { 0x89, 0x8C, 0x24, 0x8C, 0x00, 0x00, 0x00 } ); // lea eax, [ecx+edx*4] -> lea eax, [edx+edx*4] Patch( funcAddr + 0x6E + 2, 0x92 ); // lea eax, [esp+20h+var_10] // push eax // nop... Patch( addr.get( 0x10 ), { 0x83, 0xC4, 0x04, 0x8D, 0x44, 0x24, 0x10, 0x50, 0xEB, 0x0A } ); InjectHook( addr.get( 0x25 ), PickNextNodeToChaseCarXYZ ); Patch( addr.get( 0x2A + 2 ), 0xC ); // push edx // nop... Patch( addr.get( 0x3E ), 0x52 ); Nop( addr.get( 0x3E + 1 ), 6 ); InjectHook( addr.get( 0x46 ), PickNextNodeToChaseCarXYZ ); Patch( addr.get( 0x4B + 2 ), 0xC ); } TXN_CATCH(); // No censorships try { auto addr = get_pattern( "8B 43 50 85 C0 8B 53 50 74 2B 83 E8 01" ); Patch( addr, { 0x83, 0xC4, 0x08, 0x5B, 0xC3 } ); // add esp, 8 \ pop ebx \ retn } TXN_CATCH(); // 014C cargen counter fix (by spaceeinstein) try { auto do_processing = pattern( "0F B7 43 28 83 F8 FF 7D 04 66 FF 4B 28" ).get_one(); Patch( do_processing.get(1), 0xBF ); // movzx eax, word ptr [ebx+28h] -> movsx eax, word ptr [ebx+28h] Patch( do_processing.get(7), 0x74 ); // jge -> jz } TXN_CATCH(); // Fixed ammo from SCM try { using namespace ZeroAmmoFix; std::array give_weapon = { get_pattern( "6B C0 2E 6A 01 56 8B 3C", 0x15 ), get_pattern( "89 F9 6A 01 55 50 E8", 6 ), }; HookEach_GiveWeapon(give_weapon, InterceptCall); } TXN_CATCH(); // Extras working correctly on bikes try { auto createInstance = get_pattern( "89 C1 8B 41 04" ); InjectHook( createInstance, CreateInstance_BikeFix, HookType::Call ); } TXN_CATCH(); // Credits =) try { auto renderCredits = pattern( "8D 44 24 28 83 C4 14 50 FF 35 ? ? ? ? E8 ? ? ? ? 8D 44 24 1C 59 59 50 FF 35 ? ? ? ? E8 ? ? ? ? 59 59" ).get_one(); ReadCall( renderCredits.get( -50 ), Credits::PrintCreditText ); ReadCall( renderCredits.get( -5 ), Credits::PrintCreditText_Hooked ); InjectHook( renderCredits.get( -5 ), Credits::PrintSPCredits ); } TXN_CATCH(); // Decreased keyboard input latency try { using namespace KeyboardInputFix; auto updatePads = pattern( "66 8B 42 1A" ).get_one(); void* jmpDest = get_pattern( "66 A3 ? ? ? ? 5F", 6 ); void* simButtonCheckers = get_pattern( "56 57 B3 01", 0x16 ); NewKeyState = *updatePads.get( 0x27 + 1 ); OldKeyState = *updatePads.get( 4 + 1 ); TempKeyState = *updatePads.get( 0x270 + 1 ); ReadCall( simButtonCheckers, orgClearSimButtonPressCheckers ); InjectHook( simButtonCheckers, ClearSimButtonPressCheckers ); InjectHook( updatePads.get( 9 ), jmpDest, HookType::Jump ); } TXN_CATCH(); // Locale based metric/imperial system try { using namespace Localization; void* updateCompareFlag = get_pattern( "89 D9 6A 00 E8 ? ? ? ? 30 C0 83 C4 70 5D 5F 5E 5B C2 04 00", 4 ); auto constructStatLine = pattern( "85 C0 74 11 83 E8 01 83 F8 03" ).get_one(); ReadCall( updateCompareFlag, orgUpdateCompareFlag_IsMetric ); InjectHook( updateCompareFlag, UpdateCompareFlag_IsMetric ); // Stats Nop( constructStatLine.get( -11 ), 1 ); InjectHook( constructStatLine.get( -11 + 1 ), PrefsLanguage_IsMetric, HookType::Call ); Nop( constructStatLine.get( -2 ), 2 ); } TXN_CATCH(); // Corrected FBI Washington sirens sound // Primary siren lower pitched like in FBI Rancher and secondary siren higher pitched try { using namespace SirenSwitchingFix; // Other mods might be touching it, so only patch specific vehicles if their code has not been touched at all auto sirenPitch = pattern( "83 F8 17 74 32" ).get_one(); InjectHook( sirenPitch.get( 5 ), IsFBIRanchOrFBICar, HookType::Call ); Patch( sirenPitch.get( 5 + 5 ), { 0x84, 0xC0 } ); // test al, al Nop( sirenPitch.get( 5 + 5 + 2 ), 4 ); // Pitch shift FBI Washington primary siren try { struct tVehicleSampleData { int m_nAccelerationSampleIndex; char m_bEngineSoundType; int m_nHornSample; int m_nHornFrequency; char m_nSirenOrAlarmSample; int m_nSirenOrAlarmFrequency; char m_bDoorType; }; tVehicleSampleData* dataTable = *get_pattern( "8B 04 95 ? ? ? ? 89 43 1C", 3 ); // Only pitch shift if table hasn't been relocated elsewhere if ( hGameModule == ModCompat::Utils::GetModuleHandleFromAddress(dataTable) ) { // fbicar frequency = fbiranch frequency dataTable[17].m_nSirenOrAlarmFrequency = dataTable[90].m_nSirenOrAlarmFrequency; } } TXN_CATCH(); } TXN_CATCH(); // Allow extra6 to be picked with component rule 4 (any) try { void* extraMult6 = get_pattern( "D8 0D ? ? ? ? D9 7C 24 04 8B 44 24 04 80 4C 24 05 0C D9 6C 24 04 89 44 24 04 DB 5C 24 08 D9 6C 24 04 8B 44 24 08 83 C4 10 5D", 2 ); static const float MULT_6 = 6.0f; Patch( extraMult6, &MULT_6 ); } TXN_CATCH(); // Make drive-by one shot sounds owned by the driver instead of the car // Fixes incorrect weapon sound being used for drive-by try { auto getDriverOneShot = pattern( "FF 35 ? ? ? ? 6A 37 50 E8 ? ? ? ? 83 7E 08 00" ).get_one(); // nop // mov ecx, ebx // call CVehicle::GetOneShotOwnerID Patch( getDriverOneShot.get( -8 ), { 0x90, 0x89, 0xD9 } ); InjectHook( getDriverOneShot.get( -5 ), &CVehicle::GetOneShotOwnerID_SilentPatch, HookType::Call ); } TXN_CATCH(); // Fixed vehicles exploding twice if the driver leaves the car while it's exploding try { using namespace RemoveDriverStatusFix; auto removeDriver = pattern("8A 43 50 24 07 0C 20 88 43 50 E8").get_one(); auto processCommands1 = get_pattern("88 42 50 8B 33"); auto processCommands2 = get_pattern("88 42 50 8B AE"); auto removeThisPed = get_pattern("88 42 50 8B 85"); auto pedSetOutCar = get_pattern("0C 20 88 47 50 8B 85", 2); Nop(removeDriver.get(), 2); InjectHook(removeDriver.get(2), RemoveDriver_SetStatus, HookType::Call); // CVehicle::RemoveDriver already sets the status to STATUS_ABANDONED, these are redundant Nop(processCommands1, 3); Nop(processCommands2, 3); Nop(removeThisPed, 3); Nop(pedSetOutCar, 3); } TXN_CATCH(); // Apply the environment mapping on extra components try { using namespace EnvMapsOnExtras; auto forAllAtomics = pattern("50 E8 ? ? ? ? 66 8B 4B 44").get_one(); // push eax -> push ebx Patch(forAllAtomics.get(), 0x53); InterceptCall(forAllAtomics.get(1), orgRpClumpForAllAtomics, RpClumpForAllAtomics_ExtraComps); } TXN_CATCH(); // Fix probabilities in CVehicle::InflictDamage incorrectly assuming a random range from 0 to 100.000 try { auto probability = get_pattern("66 81 7B 5A ? ? 73 50", 4); Patch(probability, 35000u / 2u); } TXN_CATCH(); // Null terminate read lines in CPlane::LoadPath try { using namespace NullTerminatedLines; auto loadPath = get_pattern("DD D8 45 E8", 3); InterceptCall(loadPath, orgSscanf_LoadPath, sscanf1_LoadPath_Terminate); } TXN_CATCH(); // Don't reset mouse sensitivity on New Game try { using namespace MouseSensNewGame; auto cameraInit = pattern("C7 85 14 09 00 00 00 00 00 00 C7 05 ? ? ? ? ? ? ? ? C7 05").get_one(); auto setDirMyDocuments = get_pattern("89 CD E8 ? ? ? ? 68", 2); DefaultHorizontalAccel = *cameraInit.get(20 + 2 + 4); fMouseAccelHorzntl = *cameraInit.get(20 + 2); Nop(cameraInit.get(20), 10); InterceptCall(setDirMyDocuments, orgSetDirMyDocuments, SetDirMyDocuments_ResetMouse); } TXN_CATCH(); // Fixed pickup effects try { using namespace PickupEffectsFixes; // Give money pickups color ID 37, like most other "generic" pickups // Coincidentally, it's also the most likely color to be "randomly" assigned to them now auto bigDollarColor = get_pattern("C6 44 24 ? 00 E9 ? ? ? ? 8D 80 00 00 00 00 0F B7 1D ? ? ? ? 39 CB 75 0C"); // Remove the glow from minigun2 auto minigun2Glow = get_pattern("8D 41 01 89 CB"); InjectHook(bigDollarColor, &PickUpEffects_BigDollarColor, HookType::Call); InjectHook(minigun2Glow, &PickUpEffects_Minigun2Glow, HookType::Call); } TXN_CATCH(); // Fixed the muzzle flash facing the wrong direction // By Wesser try { auto fireInstantHit = pattern("D9 44 24 50 D8 44 24 44").get_one(); // Replace fld [esp].vecSource with fldz, as vecEnd is already absolute Patch(fireInstantHit.get(), { 0xD9, 0xEE, 0x90, 0x90 }); Patch(fireInstantHit.get(15), { 0xD9, 0xEE, 0x90, 0x90 }); Patch(fireInstantHit.get(30), { 0xD9, 0xEE, 0x90, 0x90 }); } TXN_CATCH(); // Fixed IS_PLAYER_TARGETTING_CHAR incorrectly detecting targetting in Classic controls // when the player is not aiming // By Wesser try { using namespace IsPlayerTargettingCharFix; auto isPlayerTargettingChar = pattern("83 7C 24 ? ? A3 ? ? ? ? 0F 84").get_one(); auto using1stPersonWeaponMode = static_cast(get_pattern("66 83 F8 07 74 18", -7)); bool* useMouse3rdPerson = *get_pattern("80 3D ? ? ? ? ? 75 09 66 C7 05 ? ? ? ? ? ? 8B 35", 2); void* theCamera = *get_pattern("B9 ? ? ? ? 31 DB E8", 1); Using1stPersonWeaponMode = using1stPersonWeaponMode; bUseMouse3rdPerson = useMouse3rdPerson; TheCamera = theCamera; // Move mov ds:dword_784030, eax one instruction earlier so we don't need // to include it in the patched routine memmove(isPlayerTargettingChar.get(), isPlayerTargettingChar.get(5), 5); InjectHook(isPlayerTargettingChar.get(5), IsPlayerTargettingChar_ExtraChecks, HookType::Call); } TXN_CATCH(); // Use PS2 randomness for Rosenberg audio to hopefully bring the odds closer to PS2 // The functionality was never broken on PC - but the random distribution seemingly made it looks as if it was try { using namespace ConsoleRandomness; auto busted_audio_rand = get_pattern("80 BB 48 01 00 00 00 0F 85 ? ? ? ? E8 ? ? ? ? 25 FF FF 00 00", 13); InjectHook(busted_audio_rand, rand15); } TXN_CATCH(); // Reset variables on New Game try { using namespace VariableResets; auto game_initialise = get_pattern("6A 00 E8 ? ? ? ? 83 C4 0C 68 ? ? ? ? E8 ? ? ? ? 59 C3", 15); std::array reinit_game_object_variables = { get_pattern("74 05 E8 ? ? ? ? E8 ? ? ? ? 80 3D", 7), get_pattern("C6 05 ? ? ? ? ? E8 ? ? ? ? C7 05", 7) }; InterceptCall(game_initialise, orgGameInitialise, GameInitialise); HookEach_ReInitGameObjectVariables(reinit_game_object_variables, InterceptCall); // Variables to reset GameVariablesToReset.emplace_back(*get_pattern("7D 09 80 3D ? ? ? ? ? 74 32", 2 + 2)); // Free resprays GameVariablesToReset.emplace_back(*get_pattern("7D 78 A1 ? ? ? ? 05", 2 + 1)); // LastTimeAmbulanceCreated GameVariablesToReset.emplace_back(*get_pattern("A1 ? ? ? ? 05 ? ? ? ? 39 05 ? ? ? ? 0F 86 ? ? ? ? 8B 15", 1)); // LastTimeFireTruckCreated } TXN_CATCH(); // Ped speech fix // Based off Sergenaur's fix try { // Remove the artificial 6s delay between any ped speech samples auto delay_check = get_pattern("80 BE ? ? ? ? ? 0F 85 ? ? ? ? B9", 7); auto comment_delay_id1 = get_pattern("0F B7 C2 DD D8 C1 E0 04"); auto comment_delay_id2 = pattern("0F B7 95 DA 05 00 00 D9 6C 24 04").get_one(); Nop(delay_check, 6); // movzx eax, dx -> movzx eax, bx Patch(comment_delay_id1, { 0x0F, 0xB7, 0xC3 }); // movzx edx, word ptr [ebp+5DAh] -> movzx edx, bx \ nop Patch(comment_delay_id2.get(), { 0x0F, 0xB7, 0xD3 }); Nop(comment_delay_id2.get(3), 4); } TXN_CATCH(); // Disabled backface culling on detached car parts, peds and specific models try { using namespace SelectableBackfaceCulling; auto entity_render = pattern("56 75 06 5E 5B C3").get_one(); EntityRender_Prologue_JumpBack = entity_render.get(); // Check if CEntity::Render is already re-routed by something else if (*entity_render.get(-7) == 0xE9) { ReadCall(entity_render.get(-7), orgEntityRender); } InjectHook(entity_render.get(-7), EntityRender_BackfaceCulling, HookType::Jump); } TXN_CATCH(); // Correct the duration of the outro splash to 2.5 seconds // The outro splash displays for 150 ticks from the moment it fully fades in, with the tick cpimt supposedly incrementing every 10ms // However, since the game is locked to 30FPS, the tick count actually increments every 33.3ms, so the splash takes around 5s // Correct the "tick rate" to 33ms and the "tick count" to 75, so it is more or less 2.5s at 30FPS, // and similarly long when running at 60FPS or uncapped. The original code hints at 1.5s, // but that makes it hard to read the splash before it vanishes // // Also fix the splash flickering for a frame when fading in try { using namespace OutroSplashFix; auto outro_tick_rate = get_pattern("83 F8 0A 76 10", 2); auto outro_tick_count = get_pattern("81 3D ? ? ? ? 96 00 00 00", 6); auto splash_rgba = get_pattern("E8 ? ? ? ? DB 05 ? ? ? ? 8D 54 24 2C"); auto alpha_clamp = get_pattern("8A 83 ? ? ? ? 8D 4C 24 2C"); // Ideally, we want (time - lastTime) >= 33, but we can express the same with > 32 Patch(outro_tick_rate, 32); Patch(outro_tick_count, 75); InterceptCall(splash_rgba, orgRGBASet, RGBASet_Clamp); // al -> eax Patch(alpha_clamp, 0x8B); } TXN_CATCH(); // Fix Tommy not shaking his fists with brass knuckles (in all cases) // and most post-GTA III weapons (when cars slow down for him) try { using namespace TommyFistShakeWithWeapons; auto weapon_group_1a = pattern("8B 40 60 59 83 F8 01 75 43").get_one(); auto weapon_group_1b = pattern("8B 40 60 59 83 F8 01 0F 85 ? ? ? ? 8A 85").get_one(); auto slow_car_down_for_peds = pattern("89 C7 8B 17 85 D2 74 19").get_one(); auto else_jump = get_pattern("0F 85 ? ? ? ? 8A 83 ? ? ? ? 24 FE"); std::array exclude_chainsaw = { weapon_group_1a.get(-5), weapon_group_1b.get(-5), }; ReadCall(weapon_group_1a.get(-5), GetWeaponInfo); // jnz -> ja Patch(weapon_group_1a.get(7), 0x77); Patch(weapon_group_1b.get(7 + 1), 0x87); Nop(slow_car_down_for_peds.get(), 1); InjectHook(slow_car_down_for_peds.get(1), &CheckWeaponGroupHook, HookType::Call); InjectHook(slow_car_down_for_peds.get(8), else_jump, HookType::Jump); HookEach_ExcludeChainsaw(exclude_chainsaw, InterceptCall); } TXN_CATCH(); // Fix the screwdriver not making sounds on impact try { void** pedAttackJumpTable = *get_pattern("83 F8 05 77 77 FF 24 85", 5 + 3); // Only make changes if the table hasn't been relocated if (hGameModule == ModCompat::Utils::GetModuleHandleFromAddress(pedAttackJumpTable)) { // Give ASSOCGRP_SCREWDRIVER the same case as ASSOCGRP_KNIFE and others pedAttackJumpTable[1] = pedAttackJumpTable[2]; } } TXN_CATCH(); // Allow the tear gas to damage anyone (including the player), like on PS2 try { auto set_peds_choking = get_pattern("0F 84 ? ? ? ? 8D 4B 34 D9 41 08"); Nop(set_peds_choking, 6); } TXN_CATCH(); } BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { UNREFERENCED_PARAMETER(hinstDLL); UNREFERENCED_PARAMETER(lpvReserved); if ( fdwReason == DLL_PROCESS_ATTACH ) { const auto [width, height] = GetDesktopResolution(); sprintf_s(aNoDesktopMode, "Cannot find %ux%ux32 video mode", width, height); // This scope is mandatory so Protect goes out of scope before rwcseg gets fixed { std::unique_ptr Protect = ScopedUnprotect::UnprotectSectionOrFullModule( GetModuleHandle( nullptr ), ".text" ); const int8_t version = Memory::GetVersion().version; if ( version == 0 ) Patch_VC_10(width, height); else if ( version == 1 ) Patch_VC_11(width, height); else if ( version == 2 ) Patch_VC_Steam(width, height); // Y axis sensitivity only else if (*(DWORD*)Memory::DynBaseAddress(0x601048) == 0x5E5F5D60) Patch_VC_JP(); Patch_VC_Common(); Common::Patches::III_VC_Common(); Common::Patches::DDraw_Common(); Common::Patches::III_VC_SetDelayedPatchesFunc( InjectDelayedPatches_VC_Common ); } Common::Patches::FixRwcseg_Patterns(); } return TRUE; } extern "C" __declspec(dllexport) uint32_t GetBuildNumber() { return (SILENTPATCH_REVISION_ID << 8) | SILENTPATCH_BUILD_ID; }