Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions Client/multiplayer_sa/CMultiplayerSA_Streaming.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -58,4 +136,5 @@ static void __declspec(naked) HOOK_CStreaming__ConvertBufferToObject()
void CMultiplayerSA::InitHooks_Streaming()
{
EZHookInstall(CStreaming__ConvertBufferToObject);
EZHookInstall(CStreaming__RetryLoadFileTimeout);
}
12 changes: 12 additions & 0 deletions Client/multiplayer_sa/CMultiplayerSA_VehicleDummies.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Server/mods/deathmatch/logic/net/CNetBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Server/mods/deathmatch/logic/net/CNetBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Server/sdk/net/CNetServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,5 @@ class CNetServer
assert(0);
return false;
}
virtual void SetMinClientRequirement(const char* szVersion) = 0;
};
Loading