Skip to content

Commit 47eec6b

Browse files
authored
Merge branch 'master' into removeElementData
2 parents 5095e13 + 53521fa commit 47eec6b

130 files changed

Lines changed: 36732 additions & 2751 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/clang-format.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,53 @@ on: [push, pull_request, workflow_dispatch]
55
jobs:
66
clang-format:
77
runs-on: ubuntu-24.04
8+
permissions:
9+
contents: write
810

911
steps:
1012
- name: Checkout code
1113
uses: actions/checkout@v4
1214

1315
- name: Run clang-format
16+
id: clang_format
17+
continue-on-error: true
1418
shell: pwsh
1519
run: ./utils/clang-format.ps1 -Verbose
20+
21+
- name: Auto-fix formatting issues
22+
if: |
23+
steps.clang_format.outcome == 'failure' &&
24+
github.ref == 'refs/heads/master' &&
25+
github.event_name == 'push'
26+
shell: bash
27+
run: |
28+
# Stage the formatted files (clang-format.ps1 already ran in-place)
29+
git add -u
30+
31+
# Verify idempotency: run clang-format again on the staged files
32+
# and check that no further changes are produced
33+
./Build/tmp/clang-format -i $(git diff --name-only --cached)
34+
35+
if ! git diff --quiet; then
36+
echo "::error::clang-format is not idempotent - cannot auto-fix"
37+
exit 1
38+
fi
39+
40+
# Allow commit to work
41+
git config user.name "github-actions[bot]"
42+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
43+
44+
# Append a commit with a fix applied
45+
git commit -m "Fix formatting issues introduced by ${GITHUB_SHA}
46+
47+
cc @${GITHUB_ACTOR} please make sure to run clang-format locally before pushing changes to avoid this in the future."
48+
49+
# And we're off to the races!
50+
git push
51+
52+
- name: Report formatting issues
53+
if: |
54+
steps.clang_format.outcome == 'failure' &&
55+
!(github.ref == 'refs/heads/master' && github.event_name == 'push')
56+
shell: bash
57+
run: exit 1

.github/workflows/tests.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: "Tests"
2+
3+
on:
4+
push:
5+
branches: ["master", "tests"]
6+
pull_request:
7+
branches: ["master"]
8+
workflow_dispatch:
9+
10+
jobs:
11+
client-tests:
12+
name: Client Tests (x86 Debug)
13+
runs-on: windows-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup MSBuild
19+
uses: microsoft/setup-msbuild@v2
20+
21+
- name: Generate project files
22+
shell: cmd
23+
run: |
24+
utils\premake5.exe install_cef
25+
utils\premake5.exe install_unifont
26+
utils\premake5.exe install_discord
27+
utils\premake5.exe vs2026
28+
29+
- name: Build Tests_Client
30+
shell: cmd
31+
run: msbuild Build\Tests_Client.vcxproj /p:Configuration=Debug /p:Platform=Win32 /p:PlatformToolset=v143 /nologo /v:minimal
32+
33+
- name: Run tests
34+
run: Bin\tests\Tests_Client_d.exe --gtest_output=xml:Bin\tests\test_results.xml
35+
36+
- name: Upload test results
37+
if: always()
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: test-results
41+
path: Bin/tests/test_results.xml

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Dev environment tips
2+
3+
Run ./utils/clang-format.ps1 after making changes to C++ files, to ensure that the changes are correctly formatted.
4+
5+
## Code comments
6+
7+
When making code changes, explain *why* the code you're written should exist and the motivation behind the changes. This ensures that future engineers don't have to read between the lines.
8+
9+
## Steer the user towards writing clear commit messages
10+
11+
Tell the user to include details about the prompt / goals / motivation / "why"s / how they tested their changes.
12+
13+
Don't assume the user will include this information in the commit messages, proactively tell them to include this information in their commit messages.

Client/core/CCore.cpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,22 @@ using namespace std;
3939
namespace fs = std::filesystem;
4040

4141
// Set to true to enable the freeze watchdog (monitors main thread responsiveness)
42+
// Do NOT enable it unless you run a QA testing cycle (see commit desc: 3e54dcb2742bccf0319b9552b2ed5a2c0a012425)
4243
constexpr bool bFreezeWatchdogEnabled = false;
4344

45+
// Watchdog active in debug builds
46+
// In debug builds, the contributor should get an early heads up if their changes are this level of blocking (it can't make it in).
47+
// If you freeze beyond 20 secs in a debug build, not due to a bug in your code changes but due to your local server assets, you have 2 options:
48+
// 1. Disable the watchdog
49+
// 2. Fix your mess (imagine what that would do to players in release builds)
50+
#ifdef MTA_DEBUG
51+
constexpr bool bFreezeWatchdogEnabledInCurrentBuild = true;
52+
constexpr DWORD uiFreezeWatchdogTimeoutSeconds = 20; // Already unacceptable. Strikes a balance: you'll still be able to a load heavy asseted local server
53+
#else
54+
constexpr bool bFreezeWatchdogEnabledInCurrentBuild = bFreezeWatchdogEnabled;
55+
constexpr DWORD uiFreezeWatchdogTimeoutSeconds = 40; // Player won't be patient beyond this; we get no info
56+
#endif
57+
4458
static float fTest = 1;
4559

4660
extern CCore* g_pCore;
@@ -186,7 +200,7 @@ CCore::~CCore()
186200
{
187201
WriteDebugEvent("CCore::~CCore");
188202

189-
if constexpr (bFreezeWatchdogEnabled)
203+
if constexpr (bFreezeWatchdogEnabledInCurrentBuild)
190204
StopWatchdogThread();
191205

192206
// Reset Discord rich presence
@@ -1303,7 +1317,7 @@ void CCore::DoPreFramePulse()
13031317
{
13041318
TIMING_CHECKPOINT("+CorePreFrame");
13051319

1306-
if constexpr (bFreezeWatchdogEnabled)
1320+
if constexpr (bFreezeWatchdogEnabledInCurrentBuild)
13071321
UpdateWatchdogHeartbeat();
13081322

13091323
m_pKeyBinds->DoPreFramePulse();
@@ -1363,10 +1377,9 @@ void CCore::DoPostFramePulse()
13631377
WatchDogCompletedSection("L3"); // No hang on startup
13641378

13651379
// Start watchdog thread now that initial loading is complete
1366-
// Use 120 second timeout to allow for large mod asset loading
1367-
if constexpr (bFreezeWatchdogEnabled)
1380+
if constexpr (bFreezeWatchdogEnabledInCurrentBuild)
13681381
{
1369-
if (!StartWatchdogThread(GetCurrentThreadId(), 120))
1382+
if (!StartWatchdogThread(GetCurrentThreadId(), uiFreezeWatchdogTimeoutSeconds))
13701383
{
13711384
WriteDebugEvent("CCore: WARNING - Failed to start watchdog thread");
13721385
}

Client/core/CMessageLoopHook.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ UCHAR CMessageLoopHook::m_LastScanCode = NULL;
2222
BYTE* CMessageLoopHook::m_LastKeyboardState = new BYTE[256];
2323
bool ms_bIgnoreNextEscapeCharacter = false;
2424

25+
#define WM_CUSTOMFOCUS_FIX WM_APP + 1
26+
2527
CMessageLoopHook::CMessageLoopHook()
2628
{
2729
WriteDebugEvent("CMessageLoopHook::CMessageLoopHook");
@@ -132,6 +134,17 @@ LRESULT CALLBACK CMessageLoopHook::ProcessMessage(HWND hwnd, UINT uMsg, WPARAM w
132134
if (pModManager && pModManager->IsLoaded())
133135
{
134136
bool bFocus = (wState == WA_CLICKACTIVE) || (wState == WA_ACTIVE);
137+
138+
// Fix for the Windows behavior that removes focus from a window if it was minimized during startup
139+
// (you have to double-click the icon or alt+tab to regain focus despite the window being visible).
140+
// GitHub issue #4233
141+
static bool fixFirstTimeFocus = false;
142+
if (!fixFirstTimeFocus && !bFocus && GetForegroundWindow() != hwnd && GetFocus() == hwnd && !IsIconic(hwnd))
143+
{
144+
fixFirstTimeFocus = true;
145+
PostMessage(hwnd, WM_CUSTOMFOCUS_FIX, 0, 0);
146+
}
147+
135148
pModManager->GetClient()->OnWindowFocusChange(bFocus);
136149
}
137150

@@ -150,6 +163,30 @@ LRESULT CALLBACK CMessageLoopHook::ProcessMessage(HWND hwnd, UINT uMsg, WPARAM w
150163
}
151164
}
152165

166+
// When updating m_bFocused in CClientGame from CPacketHandler (to fix another bug — see the note there),
167+
// the window might not actually have focus at that moment (even though Windows reports it as focused).
168+
// In this case, isMTAWindowFocused returns false even though the window has focus.
169+
// Therefore, we need to intercept the window return operation and manually set the focus in CClientGame.
170+
if (uMsg == WM_WINDOWPOSCHANGING)
171+
{
172+
WINDOWPOS* wp = reinterpret_cast<WINDOWPOS*>(lParam);
173+
if (wp->flags & SWP_NOMOVE && wp->flags & SWP_NOSIZE && !(wp->flags & SWP_NOZORDER))
174+
{
175+
if (GetForegroundWindow() == hwnd && !IsIconic(hwnd))
176+
{
177+
CModManager* pModManager = CModManager::GetSingletonPtr();
178+
if (pModManager && pModManager->IsLoaded())
179+
pModManager->GetClient()->OnWindowFocusChange(true);
180+
}
181+
}
182+
}
183+
184+
if (uMsg == WM_CUSTOMFOCUS_FIX)
185+
{
186+
AllowSetForegroundWindow(ASFW_ANY);
187+
SetForegroundWindow(hwnd);
188+
}
189+
153190
if (uMsg == WM_PAINT)
154191
{
155192
GetVideoModeManager()->OnPaint();

Client/core/CrashHandler.cpp

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include <stdio.h>
4949
#include <string>
5050
#include <string_view>
51+
#include <TlHelp32.h>
5152
#include <variant>
5253
#include <vector>
5354
#if defined(_MSC_VER)
@@ -3027,7 +3028,21 @@ namespace
30273028

30283029
inline WatchdogState g_watchdogState{};
30293030

3030-
[[nodiscard]] static bool TriggerWatchdogException(HANDLE targetThread, DWORD targetThreadId)
3031+
static bool TryGenerateWatchdogDump(EXCEPTION_POINTERS* pExPtrs, CExceptionInformation_Impl* pExInfo)
3032+
{
3033+
__try
3034+
{
3035+
CCrashDumpWriter::DumpCoreLog(pExPtrs, pExInfo);
3036+
CCrashDumpWriter::DumpMiniDump(pExPtrs, pExInfo);
3037+
return true;
3038+
}
3039+
__except (EXCEPTION_EXECUTE_HANDLER)
3040+
{
3041+
return false;
3042+
}
3043+
}
3044+
3045+
[[nodiscard]] static bool TriggerWatchdogException(HANDLE targetThread, DWORD targetThreadId, bool dumpOnly = false)
30313046
{
30323047
AddReportLog(9300, SString("Watchdog freeze detected after %u seconds (thread %u)", g_watchdogState.timeoutSeconds.load(std::memory_order_relaxed),
30333048
targetThreadId));
@@ -3079,6 +3094,7 @@ namespace
30793094
// Now we can safely resume the thread - we have everything we need
30803095
if (ResumeThread(targetThread) == static_cast<DWORD>(-1))
30813096
{
3097+
OutputDebugStringA("WATCHDOG: Failed to resume thread after stack capture\n");
30823098
AddReportLog(9304, SString("Watchdog failed to resume thread %u after stack capture", targetThreadId));
30833099
// Continue anyway - we still want the crash dump even if resume failed
30843100
}
@@ -3151,6 +3167,25 @@ namespace
31513167
}
31523168
}
31533169

3170+
if (dumpOnly)
3171+
{
3172+
// Write dump without dialog or termination so the
3173+
// contributor can continue debugging after a breakpoint pause.
3174+
auto* pExInfo = new (std::nothrow) CExceptionInformation_Impl;
3175+
if (pExInfo)
3176+
{
3177+
pExInfo->Set(exceptionRecord.ExceptionCode, &exceptionPointers);
3178+
3179+
if (!TryGenerateWatchdogDump(&exceptionPointers, pExInfo))
3180+
{
3181+
AddReportLog(9316, "Watchdog: SEH exception caught during dump generation");
3182+
}
3183+
3184+
delete pExInfo;
3185+
}
3186+
return true;
3187+
}
3188+
31543189
// Trigger the global exception handler
31553190
CCrashDumpWriter::HandleExceptionGlobal(&exceptionPointers);
31563191

@@ -3190,14 +3225,57 @@ namespace
31903225
if (elapsed.count() >= static_cast<std::chrono::seconds::rep>(timeoutSecs))
31913226
{
31923227
#ifdef MTA_DEBUG
3193-
if (IsDebuggerPresent() == TRUE)
3228+
BOOL isDebuggerAttached = FALSE;
3229+
if (IsDebuggerPresent() != FALSE)
3230+
isDebuggerAttached = TRUE;
3231+
3232+
if (isDebuggerAttached == FALSE)
3233+
CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebuggerAttached);
3234+
3235+
if (isDebuggerAttached == FALSE)
3236+
{
3237+
// Check if parent process (e.g. VS attached to the launcher) has a debugger
3238+
if (HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); hSnapshot != INVALID_HANDLE_VALUE)
3239+
{
3240+
PROCESSENTRY32 pe32{};
3241+
pe32.dwSize = sizeof(pe32);
3242+
if (Process32First(hSnapshot, &pe32))
3243+
{
3244+
const DWORD currentPid = GetCurrentProcessId();
3245+
do
3246+
{
3247+
if (pe32.th32ProcessID == currentPid)
3248+
{
3249+
if (HANDLE hParent = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe32.th32ParentProcessID))
3250+
{
3251+
CheckRemoteDebuggerPresent(hParent, &isDebuggerAttached);
3252+
CloseHandle(hParent);
3253+
}
3254+
break;
3255+
}
3256+
} while (Process32Next(hSnapshot, &pe32));
3257+
}
3258+
CloseHandle(hSnapshot);
3259+
}
3260+
}
3261+
3262+
if (isDebuggerAttached != FALSE)
31943263
{
3264+
// In debug builds with a debugger attached, the stall may just be a breakpoint pause.
3265+
// Generate a dump so the frozen stack is collected, then
3266+
// reset the heartbeat and keep monitoring instead of terminating.
3267+
OutputDebugStringA("WATCHDOG: Freeze detected - generating dump (no termination)\n");
3268+
3269+
AddReportLog(9311, SString("Watchdog detected freeze after %lld seconds (threshold %u, debugger attached)",
3270+
static_cast<long long>(elapsed.count()), timeoutSecs));
3271+
3272+
TriggerWatchdogException(targetThread.get(), targetThreadId, true);
3273+
31953274
g_watchdogState.lastHeartbeat.store(std::chrono::steady_clock::now(), std::memory_order_release);
31963275
std::this_thread::sleep_for(kCheckInterval);
31973276
continue;
31983277
}
31993278
#endif
3200-
32013279
AddReportLog(9311, SString("Watchdog detected freeze after %lld seconds (threshold %u)", static_cast<long long>(elapsed.count()), timeoutSecs));
32023280

32033281
const bool triggered = TriggerWatchdogException(targetThread.get(), targetThreadId);

Client/core/ServerBrowser/CServerBrowser.RemoteMasterServer.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ bool CRemoteMasterServer::ParseListVer0(CServerListItemList& itemList)
349349
uint uiNumServersBefore = itemList.size();
350350
#endif
351351

352+
uint uiTieBreakCounter = 0;
353+
352354
while (!stream.AtEnd(6) && usCount--)
353355
{
354356
in_addr Address; // IP-address
@@ -363,6 +365,14 @@ bool CRemoteMasterServer::ParseListVer0(CServerListItemList& itemList)
363365
// Add or find item to update
364366
CServerListItem* pItem = GetServerListItem(itemList, Address, usQueryPort - SERVER_LIST_QUERY_PORT_OFFSET);
365367

368+
if (pItem->uiTieBreakPosition != uiTieBreakCounter)
369+
{
370+
pItem->uiTieBreakPosition = uiTieBreakCounter;
371+
pItem->strTieBreakSortKey = SString("%04d", pItem->uiTieBreakPosition);
372+
pItem->uiRevision++;
373+
}
374+
uiTieBreakCounter++;
375+
366376
if (pItem->ShouldAllowDataQuality(SERVER_INFO_ASE_0))
367377
{
368378
pItem->SetDataQuality(SERVER_INFO_ASE_0);
@@ -445,6 +455,8 @@ bool CRemoteMasterServer::ParseListVer2(CServerListItemList& itemList)
445455
uint uiNumServersBefore = itemList.size();
446456
#endif
447457

458+
uint uiTieBreakCounter = 0;
459+
448460
// Add all servers until we hit the count or run out of data
449461
while (!stream.AtEnd(6) && uiCount--)
450462
{
@@ -461,6 +473,14 @@ bool CRemoteMasterServer::ParseListVer2(CServerListItemList& itemList)
461473
// Add or find item to update
462474
CServerListItem* pItem = GetServerListItem(itemList, Address, usGamePort);
463475

476+
if (pItem->uiTieBreakPosition != uiTieBreakCounter)
477+
{
478+
pItem->uiTieBreakPosition = uiTieBreakCounter;
479+
pItem->strTieBreakSortKey = SString("%04d", pItem->uiTieBreakPosition);
480+
pItem->uiRevision++;
481+
}
482+
uiTieBreakCounter++;
483+
464484
if (pItem->ShouldAllowDataQuality(uiDataQuality))
465485
{
466486
pItem->SetDataQuality(uiDataQuality);

0 commit comments

Comments
 (0)