diff --git a/Client/multiplayer_sa/CMultiplayerSA_Streaming.cpp b/Client/multiplayer_sa/CMultiplayerSA_Streaming.cpp index fd8308f7bfd..5b0e97cf4ed 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_Streaming.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_Streaming.cpp @@ -48,6 +48,84 @@ static void __declspec(naked) HOOK_CStreaming__ConvertBufferToObject() // clang-format on } +////////////////////////////////////////////////////////////////////////////////////////// +// +// CStreaming::RetryLoadFile - spin loop timeout +// +// GTA:SA's RetryLoadFile (0x4076B7) has an unbounded while(1) spin loop that +// polls CdStreamGetStatus with no sleep or timeout. If the streaming channel +// stays in an error state (ms_channelError != -1), the loop freezes the game. +// +// This hook intercepts the loop-back decision at 0x40776B (the cmp+jnz that +// checks ms_channelError and jumps back to the loop head) and enforces a +// timeout. On timeout, ms_channelError is forced to -1 so the function exits +// through its normal "error cleared" path. +// +////////////////////////////////////////////////////////////////////////////////////////// +#define HOOKPOS_CStreaming__RetryLoadFileTimeout 0x40776B +#define HOOKSIZE_CStreaming__RetryLoadFileTimeout 9 // cmp (7 bytes) + jnz (2 bytes) +static DWORD RETURN_CStreaming__RetryLoadFileTimeout_Exit = 0x407774; // pop edi; pop esi; jmp CLoadingScreen::Continue +static DWORD RETURN_CStreaming__RetryLoadFileTimeout_LoopBack = 0x4076F4; // Loop head (mov eax, ms_channel[esi].LoadStatus) + +static DWORD s_retryLoopStartTick = 0; +static DWORD s_retryLoopLastCallTick = 0; + +static bool ShouldTimeoutRetryLoop() +{ + constexpr DWORD timeoutMs = 5000; + DWORD now = SharedUtil::GetTickCount32(); + + // Detect new invocation: within the spin loop, consecutive calls are + // microseconds apart. A gap over 100ms means this is a fresh RetryLoadFile + // call, so reset the timer. This also handles the case where a previous + // invocation exited via loc_4077A5 without going through our hook. + if (s_retryLoopStartTick == 0 || (now - s_retryLoopLastCallTick) > 100) + s_retryLoopStartTick = now; + + s_retryLoopLastCallTick = now; + + DWORD elapsed = now - s_retryLoopStartTick; + if (elapsed > timeoutMs) + { + s_retryLoopStartTick = 0; + *(int*)0x8E4B90 = -1; // CStreaming::ms_channelError = -1 (force clear) + AddReportLog(8650, SString("RetryLoadFile spin loop timed out after %ums", elapsed)); + return true; + } + + return false; +} + +static void _declspec(naked) HOOK_CStreaming__RetryLoadFileTimeout() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + pushad + call ShouldTimeoutRetryLoop + test al, al + jnz timeout + + popad + + // Original code: cmp ms_channelError, -1; jnz loc_4076F4 + cmp dword ptr ds:[0x8E4B90], 0FFFFFFFFh + jnz loopback + + jmp RETURN_CStreaming__RetryLoadFileTimeout_Exit + + loopback: + jmp RETURN_CStreaming__RetryLoadFileTimeout_LoopBack + + timeout: + popad + jmp RETURN_CStreaming__RetryLoadFileTimeout_Exit + } + // clang-format on +} + ////////////////////////////////////////////////////////////////////////////////////////// // // CMultiplayerSA::InitHooks_Streaming @@ -58,4 +136,5 @@ static void __declspec(naked) HOOK_CStreaming__ConvertBufferToObject() void CMultiplayerSA::InitHooks_Streaming() { EZHookInstall(CStreaming__ConvertBufferToObject); + EZHookInstall(CStreaming__RetryLoadFileTimeout); } diff --git a/Client/multiplayer_sa/CMultiplayerSA_VehicleDummies.cpp b/Client/multiplayer_sa/CMultiplayerSA_VehicleDummies.cpp index 08d1219f390..c56cbe69613 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_VehicleDummies.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_VehicleDummies.cpp @@ -1083,6 +1083,12 @@ static void __declspec(naked) HOOK_CVehicle_GetPlaneGunsPosition() test eax, eax jz continueWithOriginalCode + // Check if VEH_GUN dummy (offset 0x9C) is uninitialized + mov edx, [eax+9Ch] + or edx, [eax+0A0h] + or edx, [eax+0A4h] + jz continueWithOriginalCode + popad movsx ecx, dx mov eax, vehicleDummiesPositionArray @@ -1130,6 +1136,12 @@ static void __declspec(naked) HOOK_CVehicle_GetPlaneOrdnancePosition() test eax, eax jz continueWithOriginalCode + // Check if VEH_GUN dummy (offset 0x9C) is uninitialized + mov edx, [eax+9Ch] + or edx, [eax+0A0h] + or edx, [eax+0A4h] + jz continueWithOriginalCode + popad mov eax, vehicleDummiesPositionArray add eax, 9Ch diff --git a/Server/mods/deathmatch/logic/net/CNetBuffer.cpp b/Server/mods/deathmatch/logic/net/CNetBuffer.cpp index c0e461f8ce0..f26d0ee9297 100644 --- a/Server/mods/deathmatch/logic/net/CNetBuffer.cpp +++ b/Server/mods/deathmatch/logic/net/CNetBuffer.cpp @@ -738,6 +738,18 @@ void CNetServerBuffer::SetNetOptions(const SNetOptions& options) AddCommandAndWait(pArgs); } +/////////////////////////////////////////////////////////////////////////// +// +// CNetServerBuffer::SetMinClientRequirement +// +// Thread safe +// +/////////////////////////////////////////////////////////////////////////// +void CNetServerBuffer::SetMinClientRequirement(const char* szVersion) +{ + m_pRealNetServer->SetMinClientRequirement(szVersion); +} + /////////////////////////////////////////////////////////////////////////// // // CNetServerBuffer::GenerateRandomData diff --git a/Server/mods/deathmatch/logic/net/CNetBuffer.h b/Server/mods/deathmatch/logic/net/CNetBuffer.h index c2231860391..56898a22d5c 100644 --- a/Server/mods/deathmatch/logic/net/CNetBuffer.h +++ b/Server/mods/deathmatch/logic/net/CNetBuffer.h @@ -110,6 +110,7 @@ class CNetServerBuffer : public CNetServer SFixedString<32>& strVersion); virtual void SetNetOptions(const SNetOptions& options); virtual void GenerateRandomData(void* pOutData, uint uiLength); + virtual void SetMinClientRequirement(const char* szVersion); // // Macros of doom to declare function argument structures diff --git a/Server/sdk/net/CNetServer.h b/Server/sdk/net/CNetServer.h index e1171db035a..089dca54b79 100644 --- a/Server/sdk/net/CNetServer.h +++ b/Server/sdk/net/CNetServer.h @@ -164,4 +164,5 @@ class CNetServer assert(0); return false; } + virtual void SetMinClientRequirement(const char* szVersion) = 0; };